Parcourir la source

Merge pull request #445 from Joystream/development

Constantinople Release
shamil-gadelshin il y a 4 ans
Parent
commit
b171fcc7f2
100 fichiers modifiés avec 15937 ajouts et 2353 suppressions
  1. 2 1
      .dockerignore
  2. 9 0
      .gitignore
  3. 32 24
      .travis.yml
  4. 231 115
      Cargo.lock
  5. 3 0
      Cargo.toml
  6. 16 8
      README.md
  7. 11 0
      cli/.editorconfig
  8. 1 0
      cli/.eslintignore
  9. 6 0
      cli/.eslintrc
  10. 8 0
      cli/.gitignore
  11. 263 0
      cli/README.md
  12. 5 0
      cli/bin/run
  13. 3 0
      cli/bin/run.cmd
  14. 4671 0
      cli/package-lock.json
  15. 90 0
      cli/package.json
  16. 114 0
      cli/src/Api.ts
  17. 14 0
      cli/src/ExitCodes.ts
  18. 63 0
      cli/src/Types.ts
  19. 217 0
      cli/src/base/AccountsCommandBase.ts
  20. 28 0
      cli/src/base/ApiCommandBase.ts
  21. 15 0
      cli/src/base/DefaultCommandBase.ts
  22. 115 0
      cli/src/base/StateAwareCommandBase.ts
  23. 25 0
      cli/src/commands/account/choose.ts
  24. 47 0
      cli/src/commands/account/create.ts
  25. 41 0
      cli/src/commands/account/current.ts
  26. 73 0
      cli/src/commands/account/export.ts
  27. 29 0
      cli/src/commands/account/forget.ts
  28. 46 0
      cli/src/commands/account/import.ts
  29. 68 0
      cli/src/commands/account/transferTokens.ts
  30. 12 0
      cli/src/commands/api/getUri.ts
  31. 277 0
      cli/src/commands/api/inspect.ts
  32. 28 0
      cli/src/commands/api/setUri.ts
  33. 57 0
      cli/src/commands/council/info.ts
  34. 33 0
      cli/src/helpers/display.ts
  35. 19 0
      cli/src/helpers/validation.ts
  36. 1 0
      cli/src/index.ts
  37. 11 0
      cli/test/commands/council/info.test.ts
  38. 5 0
      cli/test/mocha.opts
  39. 9 0
      cli/test/tsconfig.json
  40. 15 0
      cli/tsconfig.json
  41. 7 0
      devops/dockerfiles/node-and-runtime/Dockerfile
  42. 1 1
      devops/dockerfiles/rust-builder/Dockerfile
  43. 5 0
      devops/git-hooks/pre-commit
  44. 10 0
      devops/git-hooks/pre-push
  45. 1 1
      node/Cargo.toml
  46. 55 27
      node/src/chain_spec.rs
  47. 1 1
      node/src/cli.rs
  48. 9 4
      node/src/forum_config/from_serialized.rs
  49. 1 1
      node/src/forum_config/mod.rs
  50. 9 3
      node/src/service.rs
  51. 21 0
      package.json
  52. 0 1
      runtime-modules/common/src/currency.rs
  53. 1 0
      runtime-modules/common/src/lib.rs
  54. 5 0
      runtime-modules/common/src/origin_validator.rs
  55. 3 3
      runtime-modules/content-working-group/src/genesis.rs
  56. 199 151
      runtime-modules/content-working-group/src/lib.rs
  57. 7 2
      runtime-modules/content-working-group/src/mock.rs
  58. 92 6
      runtime-modules/content-working-group/src/tests.rs
  59. 91 268
      runtime-modules/forum/src/lib.rs
  60. 21 14
      runtime-modules/forum/src/mock.rs
  61. 12 1
      runtime-modules/governance/Cargo.toml
  62. 209 11
      runtime-modules/governance/src/council.rs
  63. 173 89
      runtime-modules/governance/src/election.rs
  64. 48 0
      runtime-modules/governance/src/election_params.rs
  65. 1 1
      runtime-modules/governance/src/lib.rs
  66. 10 2
      runtime-modules/governance/src/mock.rs
  67. 0 1572
      runtime-modules/governance/src/proposals.rs
  68. 0 2
      runtime-modules/hiring/src/lib.rs
  69. 1 1
      runtime-modules/membership/Cargo.toml
  70. 1 1
      runtime-modules/membership/src/lib.rs
  71. 17 12
      runtime-modules/membership/src/members.rs
  72. 6 0
      runtime-modules/membership/src/role_types.rs
  73. 184 0
      runtime-modules/proposals/codex/Cargo.toml
  74. 1090 0
      runtime-modules/proposals/codex/src/lib.rs
  75. 148 0
      runtime-modules/proposals/codex/src/proposal_types/mod.rs
  76. 127 0
      runtime-modules/proposals/codex/src/proposal_types/parameters.rs
  77. 296 0
      runtime-modules/proposals/codex/src/tests/mock.rs
  78. 1147 0
      runtime-modules/proposals/codex/src/tests/mod.rs
  79. 94 0
      runtime-modules/proposals/discussion/Cargo.toml
  80. 364 0
      runtime-modules/proposals/discussion/src/lib.rs
  81. 145 0
      runtime-modules/proposals/discussion/src/tests/mock.rs
  82. 417 0
      runtime-modules/proposals/discussion/src/tests/mod.rs
  83. 102 0
      runtime-modules/proposals/discussion/src/types.rs
  84. 106 0
      runtime-modules/proposals/engine/Cargo.toml
  85. 827 0
      runtime-modules/proposals/engine/src/lib.rs
  86. 33 0
      runtime-modules/proposals/engine/src/tests/mock/balance_manager.rs
  87. 186 0
      runtime-modules/proposals/engine/src/tests/mock/mod.rs
  88. 18 0
      runtime-modules/proposals/engine/src/tests/mock/proposals.rs
  89. 66 0
      runtime-modules/proposals/engine/src/tests/mock/stakes.rs
  90. 1581 0
      runtime-modules/proposals/engine/src/tests/mod.rs
  91. 793 0
      runtime-modules/proposals/engine/src/types/mod.rs
  92. 197 0
      runtime-modules/proposals/engine/src/types/proposal_statuses.rs
  93. 247 0
      runtime-modules/proposals/engine/src/types/stakes.rs
  94. 7 1
      runtime-modules/recurring-reward/src/lib.rs
  95. 1 1
      runtime-modules/roles/Cargo.toml
  96. 14 6
      runtime-modules/roles/src/actors.rs
  97. 0 1
      runtime-modules/roles/src/traits.rs
  98. 1 1
      runtime-modules/service-discovery/src/discovery.rs
  99. 9 13
      runtime-modules/stake/src/lib.rs
  100. 7 7
      runtime-modules/stake/src/tests.rs

+ 2 - 1
.dockerignore

@@ -1 +1,2 @@
-**target*
+**target*
+**node_modules*

+ 9 - 0
.gitignore

@@ -8,6 +8,12 @@
 # runtime built with docker build script
 joystream_runtime.wasm
 
+# Node modules directory
+**/node_modules
+
+# Generated by yarn
+yarn*
+
 # JetBrains IDEs
 .idea
 
@@ -16,3 +22,6 @@ joystream_runtime.wasm
 
 # Visual Studio Code
 .vscode
+
+# Compiled WASM code
+*.wasm

+ 32 - 24
.travis.yml

@@ -1,7 +1,7 @@
 language: rust
 
 rust:
-  - 1.42.0
+  - 1.43.0
 
 matrix:
   include:
@@ -12,10 +12,15 @@ matrix:
       services: docker
     - os: osx
       env: TARGET=x86_64-apple-darwin
+    - os: linux
+      env: TARGET=wasm-blob
+      services: docker
 
 before_install:
   - rustup component add rustfmt
   - cargo fmt --all -- --check
+  - rustup component add clippy
+  - BUILD_DUMMY_WASM_BINARY=1 cargo clippy -- -D warnings
   - rustup default stable
   - rustup update nightly
   - rustup target add wasm32-unknown-unknown --toolchain nightly
@@ -37,52 +42,55 @@ script:
           joystream/rust-raspberry \
         build --release
       sudo chmod a+r ${TRAVIS_BUILD_DIR}/target/${TARGET}/release/joystream-node
+    elif [ "$TARGET" = "wasm-blob" ]
+    then
+      docker build --tag joystream/node \
+        --file ./devops/dockerfiles/node-and-runtime/Dockerfile \
+        .
+      docker create --name temp-container-joystream-node joystream/node
+      docker cp temp-container-joystream-node:/joystream/runtime.compact.wasm joystream_runtime.wasm
+      docker rm temp-container-joystream-node
     else
       cargo build --release --target=${TARGET}
     fi
 
 before_deploy:
-  - cp ./target/${TARGET}/release/joystream-node .
   - |
-    if [ "$TARGET" = "arm-unknown-linux-gnueabihf" ]
+    if [ "$TARGET" = "wasm-blob" ]
     then
-      export FILENAME="joystream-node-armv7-linux-gnueabihf"
+      export ASSET="joystream_runtime.wasm"
     else
-      export FILENAME=`./joystream-node --version | sed -e "s/ /-/g"`
+      cp ./target/${TARGET}/release/joystream-node .
+      if [ "$TARGET" = "arm-unknown-linux-gnueabihf" ]
+      then
+        export FILENAME="joystream-node-armv7-linux-gnueabihf"
+      else
+        export FILENAME=`./joystream-node --version | sed -e "s/ /-/g"`
+      fi
+      tar -cf ${FILENAME}.tar ./joystream-node
+      gzip ${FILENAME}.tar
+      export ASSET=${FILENAME}.tar.gz
     fi
-  - tar -cf ${FILENAME}.tar ./joystream-node
-  - gzip ${FILENAME}.tar
 
 deploy:
   - provider: releases
     api_key:
-      secure: QTna4XzKmPrXNA5KnYfLsH8cAKxESLdFbQ5HJ6nvB9reE10SVtg8lZ+ShL+no7TACNBUNt09Qv9HNgs6JcNRJ9QMHEJHKIbMyjplhBtZ+W3l0k+6TL0yeKHZ/OvddDF+vDbpN+y4xBfOf0xqZcNH3lZJTms/NPBn/KT5DpQ3JZ8bibdMP2HSCazfvHLwj38OuLX6VWbFcmN2RAmUR9AXYvk5wWYVw8g1VDzTCxjH1G+dGWH1L9+ZDgFfv7BNSNhPc6V9GghgLVZ+37r/STzTTAQ/gPv+yruglEWUhSAngFfVYUegvTmIeQLi/V+g0tKUS+l7eNX08xz6eZcn0+/32V7P+oEN/dhU84E0kgmiOsiUEGI/KFM+qw9TyX3GtD67UmG5TZrD7OUMIu1qCuPSetsTOK2kvpwlYAn+j5iFB30Uz4hXhOH5dib2zz2I7cYHi1kvzeNQqQOPNDCmaO48bcbRIaeqMAHdsb6scGzh/+CD2V2HOmHlhd+4o1PpX6hAMwmOXAu3bMDi4zlB9Hb1cSZnsYNBHawkD6y45QGepFKpGW/6u5VRPeMK62Gm9wu815C36B4mVg6CVqtZMbk0WYPIk6zfoTft3i04YthKbRO96a5VD9LssVbiSYnudXuZJjSllSZVCi9AKS8JVIS2jC2z+tWkquAesSrwztriRcs=
-    file: ${FILENAME}.tar.gz
+      secure: FfxZGQexxAGT0Skbctl1FuqmEvNHejPDPtNG8Du1ACSGjS7Y+M6o/aPqE6HL158AmddOgndsIPR+HM7VfMDAUMkLTbOhv3nMpDBZu1h25vwk+jHOM65tm5LWUu/ROWBpaAQiG7NKrvtfkNfbNBSETsEbWBt/DPrhlIfSbgsXBFDiid7uRrCiwvDUJ097/EUOJ9OVUrk+O4ebSzfIfKPGPtRU2rQQ0eNX7yX3TCm3jbQm/kplkQNRL9mnAJNxtKuvuko4LqZ6jN4XLoLTHUMjO7E0r6wXVB4GVjA4HA214eLlQD6BhgTbWMDxKgWyuKzPG+2GLKyluSSn0RurSl8tYryXKxKxuN3H1FX9r23a8AzGtpRACJtIePC2YmPuQRSnz2Bw8jlSP2WPLJtXGD036J/wVMj6W9TROm7IBigiC7QlqAqCYNByOnoKyhRCgYyAJZb0Jpa3qWaFhA6b6gCGhyH85QCcrc0q6JAB3oqH8Wfm/K2HVzBobmKaSFu5DpwInNnUXnLWGVzhSt3oCq6ld773izReGdLJtLC2vaJ9rZVaVw29s9M662EEuAGgaVLO/sinZJFeIIaCF4i4zUXwXSLIdfKXGOR0ZibkyT2FS6qPGvl/lLN5IREzD7v/rV8htGMLmw4jpPLNskvRjCHX42ewRRYdMvZzQQOAvSlWcsw=
+    file: ${ASSET}
     on:
       tags: true
-      repo: Joystream/substrate-node-joystream
+      repo: Joystream/joystream
     draft: true
     overwrite: true
     skip_cleanup: true
   - provider: releases
     api_key:
-      secure: QTna4XzKmPrXNA5KnYfLsH8cAKxESLdFbQ5HJ6nvB9reE10SVtg8lZ+ShL+no7TACNBUNt09Qv9HNgs6JcNRJ9QMHEJHKIbMyjplhBtZ+W3l0k+6TL0yeKHZ/OvddDF+vDbpN+y4xBfOf0xqZcNH3lZJTms/NPBn/KT5DpQ3JZ8bibdMP2HSCazfvHLwj38OuLX6VWbFcmN2RAmUR9AXYvk5wWYVw8g1VDzTCxjH1G+dGWH1L9+ZDgFfv7BNSNhPc6V9GghgLVZ+37r/STzTTAQ/gPv+yruglEWUhSAngFfVYUegvTmIeQLi/V+g0tKUS+l7eNX08xz6eZcn0+/32V7P+oEN/dhU84E0kgmiOsiUEGI/KFM+qw9TyX3GtD67UmG5TZrD7OUMIu1qCuPSetsTOK2kvpwlYAn+j5iFB30Uz4hXhOH5dib2zz2I7cYHi1kvzeNQqQOPNDCmaO48bcbRIaeqMAHdsb6scGzh/+CD2V2HOmHlhd+4o1PpX6hAMwmOXAu3bMDi4zlB9Hb1cSZnsYNBHawkD6y45QGepFKpGW/6u5VRPeMK62Gm9wu815C36B4mVg6CVqtZMbk0WYPIk6zfoTft3i04YthKbRO96a5VD9LssVbiSYnudXuZJjSllSZVCi9AKS8JVIS2jC2z+tWkquAesSrwztriRcs=
-    file: ${FILENAME}.tar.gz
+      secure: FfxZGQexxAGT0Skbctl1FuqmEvNHejPDPtNG8Du1ACSGjS7Y+M6o/aPqE6HL158AmddOgndsIPR+HM7VfMDAUMkLTbOhv3nMpDBZu1h25vwk+jHOM65tm5LWUu/ROWBpaAQiG7NKrvtfkNfbNBSETsEbWBt/DPrhlIfSbgsXBFDiid7uRrCiwvDUJ097/EUOJ9OVUrk+O4ebSzfIfKPGPtRU2rQQ0eNX7yX3TCm3jbQm/kplkQNRL9mnAJNxtKuvuko4LqZ6jN4XLoLTHUMjO7E0r6wXVB4GVjA4HA214eLlQD6BhgTbWMDxKgWyuKzPG+2GLKyluSSn0RurSl8tYryXKxKxuN3H1FX9r23a8AzGtpRACJtIePC2YmPuQRSnz2Bw8jlSP2WPLJtXGD036J/wVMj6W9TROm7IBigiC7QlqAqCYNByOnoKyhRCgYyAJZb0Jpa3qWaFhA6b6gCGhyH85QCcrc0q6JAB3oqH8Wfm/K2HVzBobmKaSFu5DpwInNnUXnLWGVzhSt3oCq6ld773izReGdLJtLC2vaJ9rZVaVw29s9M662EEuAGgaVLO/sinZJFeIIaCF4i4zUXwXSLIdfKXGOR0ZibkyT2FS6qPGvl/lLN5IREzD7v/rV8htGMLmw4jpPLNskvRjCHX42ewRRYdMvZzQQOAvSlWcsw=
+    file: ${ASSET}
     on:
       branch: development
-      repo: Joystream/substrate-node-joystream
+      repo: Joystream/joystream
     draft: true
     prerelease: true
     overwrite: true
     skip_cleanup: true
-  - provider: releases
-    api_key:
-      secure: ZoEXp8g+yZOEG8JZ1Fg6tWnW3aYDfupFbZflEejYaAdXhj1nw7G9N10ZX5VDdb/O1iFx8BhfFymQxk0ynxNC8c52LzOjKIhXEporxgvEPdnoPS/N1JhfsOUV0ragwZDLv2tFVi2AT0K4w8WJFJDzrK4qHOMMQgVbVQZtFmDL1whHdfBD5FyFyKmMdZdWBtTGy4s7X0LnmxjNB4/3AMa540T3LowZ5H66MYZkQmAbtg8ib93WomVanhS23vbjNaH9x1Kmzxd2B3pCSgI8uaxBrpmzINvAeSusYVJQt0EF/cAPXmq0+JmGoocvcS1ecg/SNZoKUNmeElB4ns/obg/QAyE+fyQtyl+iDYBilhFLm5xRMUnqkpyeUUD3u824i/Z+/tfLvtm5Egg1QAiXtIIJMeAj1nN8OIeSlHR4phnSTA3jl2PZw9QYidtV9WCqHC0qxtpkYSKkC8ItaefScPB1AuvOvVx8xvnIxfR/tXvL8Y3Y2BvhiLgpky9JkbdMln1b0m0E5c4vyGCEVqHqpbxM63VJkpct8sVx0atGvipWEelVjz5XpkxW2PYbgg4EKUzl3FiYcXwf5Y/ykxaZNZt7I4gv9nz2KkVwUCCPqdwWF7ww1shFWW5tCoCmJuUESOdPFx0jQ7LVWz7SDLDsqvvaW2c2OPxG6DIx9BiTeAE4qIQ=
-    file: "${FILENAME}.tar.gz"
-    skip_cleanup: true
-    draft: true
-    prerelease: true
-    overwrite: true
-    on:
-      repo: mnaamani/substrate-node-joystream
-      branch: deploy

+ 231 - 115
Cargo.lock

@@ -131,7 +131,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0d0864d84b8e07b145449be9a8537db86bf9de5ce03b913214694643b4743502"
 dependencies = [
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
 ]
 
 [[package]]
@@ -159,9 +159,9 @@ checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
 
 [[package]]
 name = "backtrace"
-version = "0.3.45"
+version = "0.3.46"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad235dabf00f36301792cfe82499880ba54c6486be094d1047b02bacb67c14e8"
+checksum = "b1e692897359247cc6bb902933361652380af0f1b7651ae5c5013407f30e109e"
 dependencies = [
  "backtrace-sys",
  "cfg-if",
@@ -171,9 +171,9 @@ dependencies = [
 
 [[package]]
 name = "backtrace-sys"
-version = "0.1.34"
+version = "0.1.35"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca797db0057bae1a7aa2eef3283a874695455cecf08a43bfb8507ee0ebc1ed69"
+checksum = "7de8aba10a69c8e8d7622c5710229485ec32e9d55fdad160ea559c086fdcd118"
 dependencies = [
  "cc",
  "libc",
@@ -230,9 +230,13 @@ checksum = "5da9b3d9f6f585199287a473f4f8dfab6566cf827d15c00c219f53c645687ead"
 
 [[package]]
 name = "bitvec"
-version = "0.15.2"
+version = "0.17.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a993f74b4c99c1908d156b8d2e0fb6277736b0ecbd833982fd1241d39b2766a6"
+checksum = "41262f11d771fd4a61aa3ce019fca363b4b6c282fca9da2a31186d3965a47a5c"
+dependencies = [
+ "either",
+ "radium",
+]
 
 [[package]]
 name = "blake2"
@@ -309,9 +313,9 @@ dependencies = [
 
 [[package]]
 name = "bumpalo"
-version = "3.2.0"
+version = "3.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f359dc14ff8911330a51ef78022d376f25ed00248912803b58f00cb1c27f742"
+checksum = "12ae9db68ad7fac5fe51304d20f016c911539251075a214f8e663babefa35187"
 
 [[package]]
 name = "byte-slice-cast"
@@ -459,7 +463,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2f1af9ac737b2dd2d577701e59fd09ba34822f6f2ebdb30a7647405d9e55e16a"
 dependencies = [
  "const-random-macro",
- "proc-macro-hack 0.5.12",
+ "proc-macro-hack 0.5.15",
 ]
 
 [[package]]
@@ -469,7 +473,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "25e4c606eb459dd29f7c57b2e0879f2b6f14ee130918c2b78ccb58a9624e6c7a"
 dependencies = [
  "getrandom",
- "proc-macro-hack 0.5.12",
+ "proc-macro-hack 0.5.15",
 ]
 
 [[package]]
@@ -640,6 +644,17 @@ version = "2.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "11c0346158a19b3627234e15596f5e465c360fcdb97d817bcb255e0510f5a788"
 
+[[package]]
+name = "derivative"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1eae4d76b7cefedd1b4f8cc24378b2fbd1ac1b66e3bbebe8e2192d3be81cb355"
+dependencies = [
+ "proc-macro2 1.0.10",
+ "quote 1.0.3",
+ "syn 1.0.17",
+]
+
 [[package]]
 name = "derive_more"
 version = "0.14.1"
@@ -776,9 +791,9 @@ checksum = "516aa8d7a71cb00a1c4146f0798549b93d083d4f189b3ced8f3de6b8f11ee6c4"
 
 [[package]]
 name = "erased-serde"
-version = "0.3.10"
+version = "0.3.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd7d80305c9bd8cd78e3c753eb9fb110f83621e5211f1a3afffcc812b104daf9"
+checksum = "d88b6d1705e16a4d62e05ea61cc0496c2bd190f4fa8e5c1f11ce747be6bcf3d1"
 dependencies = [
  "serde",
 ]
@@ -809,9 +824,9 @@ version = "0.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "030a733c8287d6213886dd487564ff5c8f6aae10278b3588ed177f9d18f8d231"
 dependencies = [
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
  "synstructure",
 ]
 
@@ -1045,10 +1060,10 @@ version = "0.3.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9a5081aa3de1f7542a794a397cde100ed903b0630152d0973479018fd85423a7"
 dependencies = [
- "proc-macro-hack 0.5.12",
- "proc-macro2 1.0.9",
+ "proc-macro-hack 0.5.15",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
 ]
 
 [[package]]
@@ -1108,7 +1123,7 @@ dependencies = [
  "futures-task",
  "memchr",
  "pin-utils",
- "proc-macro-hack 0.5.12",
+ "proc-macro-hack 0.5.15",
  "proc-macro-nested",
  "slab",
 ]
@@ -1269,9 +1284,9 @@ dependencies = [
 
 [[package]]
 name = "hermit-abi"
-version = "0.1.8"
+version = "0.1.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1010591b26bbfe835e9faeabeb11866061cc7dcebffd56ad7d0942d0e61aefd8"
+checksum = "725cf19794cf90aa94e65050cb4191ff5d8fa87a498383774c47b332e3af952e"
 dependencies = [
  "libc",
 ]
@@ -1305,7 +1320,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "961de220ec9a91af2e1e5bd80d02109155695e516771762381ef8581317066e0"
 dependencies = [
  "hex-literal-impl 0.2.1",
- "proc-macro-hack 0.5.12",
+ "proc-macro-hack 0.5.15",
 ]
 
 [[package]]
@@ -1323,7 +1338,7 @@ version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9d4c5c844e2fee0bf673d54c2c177f1713b3d2af2ff6e666b49cb7572e6cf42d"
 dependencies = [
- "proc-macro-hack 0.5.12",
+ "proc-macro-hack 0.5.15",
 ]
 
 [[package]]
@@ -1487,9 +1502,9 @@ version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7ef5550a42e3740a0e71f909d4c861056a284060af885ae7aa6242820f920d9d"
 dependencies = [
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
 ]
 
 [[package]]
@@ -1554,7 +1569,7 @@ dependencies = [
 
 [[package]]
 name = "joystream-node"
-version = "2.1.3"
+version = "2.2.0"
 dependencies = [
  "ctrlc",
  "derive_more 0.14.1",
@@ -1599,7 +1614,7 @@ dependencies = [
 
 [[package]]
 name = "joystream-node-runtime"
-version = "6.8.1"
+version = "6.13.0"
 dependencies = [
  "parity-scale-codec",
  "safe-mix",
@@ -1641,6 +1656,9 @@ dependencies = [
  "substrate-memo-module",
  "substrate-offchain-primitives",
  "substrate-primitives",
+ "substrate-proposals-codex-module",
+ "substrate-proposals-discussion-module",
+ "substrate-proposals-engine-module",
  "substrate-recurring-reward-module",
  "substrate-roles-module",
  "substrate-service-discovery-module",
@@ -1655,9 +1673,9 @@ dependencies = [
 
 [[package]]
 name = "js-sys"
-version = "0.3.36"
+version = "0.3.37"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1cb931d43e71f560c81badb0191596562bafad2be06a3f9025b845c847c60df5"
+checksum = "6a27d435371a2fa5b6d2b028a74bbdb1234f308da363226a2854ca3ff8ba7055"
 dependencies = [
  "wasm-bindgen",
 ]
@@ -1720,9 +1738,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8609af8f63b626e8e211f52441fcdb6ec54f1a446606b10d5c89ae9bf8a20058"
 dependencies = [
  "proc-macro-crate",
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
 ]
 
 [[package]]
@@ -2357,8 +2375,8 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e37c5d4cd9473c5f4c9c111f033f15d4df9bd378fdf615944e360a4f55a05f0b"
 dependencies = [
- "proc-macro2 1.0.9",
- "syn 1.0.16",
+ "proc-macro2 1.0.10",
+ "syn 1.0.17",
  "synstructure",
 ]
 
@@ -2504,9 +2522,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d5a615a1ad92048ad5d9633251edb7492b8abc057d7a679a9898476aef173935"
 dependencies = [
  "cfg-if",
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
 ]
 
 [[package]]
@@ -2642,6 +2660,28 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "num_enum"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca565a7df06f3d4b485494f25ba05da1435950f4dc263440eda7a6fa9b8e36e4"
+dependencies = [
+ "derivative",
+ "num_enum_derive",
+]
+
+[[package]]
+name = "num_enum_derive"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffa5a33ddddfee04c0283a7653987d634e880347e96b5b2ed64de07efb59db9d"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2 1.0.10",
+ "quote 1.0.3",
+ "syn 1.0.17",
+]
+
 [[package]]
 name = "ole32-sys"
 version = "0.2.0"
@@ -2755,9 +2795,9 @@ dependencies = [
 
 [[package]]
 name = "parity-scale-codec"
-version = "1.2.0"
+version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f509c5e67ca0605ee17dcd3f91ef41cadd685c75a298fb6261b781a5acb3f910"
+checksum = "329c8f7f4244ddb5c37c103641027a76c530e65e8e4b8240b29f81ea40508b17"
 dependencies = [
  "arrayvec 0.5.1",
  "bitvec",
@@ -2773,9 +2813,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5a0ec292e92e8ec7c58e576adacc1e3f399c597c8f263c42f18420abe58e7245"
 dependencies = [
  "proc-macro-crate",
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
 ]
 
 [[package]]
@@ -2926,24 +2966,24 @@ dependencies = [
 
 [[package]]
 name = "paste"
-version = "0.1.7"
+version = "0.1.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "63e1afe738d71b1ebab5f1207c055054015427dbfc7bbe9ee1266894156ec046"
+checksum = "ab4fb1930692d1b6a9cfabdde3d06ea0a7d186518e2f4d67660d8970e2fa647a"
 dependencies = [
  "paste-impl",
- "proc-macro-hack 0.5.12",
+ "proc-macro-hack 0.5.15",
 ]
 
 [[package]]
 name = "paste-impl"
-version = "0.1.7"
+version = "0.1.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d4dc4a7f6f743211c5aab239640a65091535d97d43d92a52bca435a640892bb"
+checksum = "a62486e111e571b1e93b710b61e8f493c0013be39629b714cb166bdb06aa5a8a"
 dependencies = [
- "proc-macro-hack 0.5.12",
- "proc-macro2 1.0.9",
+ "proc-macro-hack 0.5.15",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
 ]
 
 [[package]]
@@ -3063,9 +3103,9 @@ version = "0.2.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "aeccfe4d5d8ea175d5f0e4a2ad0637e0f4121d63bd99d356fb1f39ab2e7c6097"
 dependencies = [
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
 ]
 
 [[package]]
@@ -3079,14 +3119,9 @@ dependencies = [
 
 [[package]]
 name = "proc-macro-hack"
-version = "0.5.12"
+version = "0.5.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f918f2b601f93baa836c1c2945faef682ba5b6d4828ecb45eeb7cc3c71b811b4"
-dependencies = [
- "proc-macro2 1.0.9",
- "quote 1.0.3",
- "syn 1.0.16",
-]
+checksum = "0d659fe7c6d27f25e9d80a1a094c223f5246f6a6596453e09d7229bf42750b63"
 
 [[package]]
 name = "proc-macro-hack-impl"
@@ -3111,9 +3146,9 @@ dependencies = [
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.9"
+version = "1.0.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c09721c6781493a2a492a96b5a5bf19b65917fe6728884e7c44dd0c60ca3435"
+checksum = "df246d292ff63439fea9bc8c0a270bed0e390d5ebd4db4ba15aba81111b5abe3"
 dependencies = [
  "unicode-xid 0.2.0",
 ]
@@ -3197,9 +3232,15 @@ version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2bdc6c187c65bca4260c9011c9e3132efe4909da44726bad24cf7572ae338d7f"
 dependencies = [
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
 ]
 
+[[package]]
+name = "radium"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "def50a86306165861203e7f84ecffbbdfdea79f0e51039b33de1e952358c47ac"
+
 [[package]]
 name = "rand"
 version = "0.3.23"
@@ -3424,9 +3465,9 @@ checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84"
 
 [[package]]
 name = "regex"
-version = "1.3.5"
+version = "1.3.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8900ebc1363efa7ea1c399ccc32daed870b4002651e0bed86e72d501ebbe0048"
+checksum = "7f6946991529684867e47d86474e3a6d0c0ab9b82d5821e314b1ede31fa3a4b3"
 dependencies = [
  "aho-corasick",
  "memchr",
@@ -3451,9 +3492,9 @@ dependencies = [
 
 [[package]]
 name = "ring"
-version = "0.16.11"
+version = "0.16.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "741ba1704ae21999c00942f9f5944f801e977f54302af346b596287599ad1862"
+checksum = "1ba5a8ec64ee89a76c98c549af81ff14813df09c3e6dc4766c3856da48597a0c"
 dependencies = [
  "cc",
  "lazy_static",
@@ -3606,29 +3647,29 @@ checksum = "a0eddf2e8f50ced781f288c19f18621fa72a3779e3cb58dbf23b07469b0abeb4"
 
 [[package]]
 name = "serde"
-version = "1.0.104"
+version = "1.0.106"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "414115f25f818d7dfccec8ee535d76949ae78584fc4f79a6f45a904bf8ab4449"
+checksum = "36df6ac6412072f67cf767ebbde4133a5b2e88e76dc6187fa7104cd16f783399"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.104"
+version = "1.0.106"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "128f9e303a5a29922045a830221b8f78ec74a5f544944f3d5984f8ec3895ef64"
+checksum = "9e549e3abf4fb8621bd1609f11dfc9f5e50320802273b12f3811a67e6716ea6c"
 dependencies = [
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
 ]
 
 [[package]]
 name = "serde_json"
-version = "1.0.48"
+version = "1.0.51"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9371ade75d4c2d6cb154141b9752cf3781ec9c05e0e5cf35060e1e70ee7b9c25"
+checksum = "da07b57ee2623368351e9a0488bb0b261322a15a6e0ae53e243cbdc0f4208da9"
 dependencies = [
  "itoa",
  "ryu",
@@ -3706,7 +3747,7 @@ dependencies = [
 [[package]]
 name = "slog-async"
 version = "2.3.0"
-source = "git+https://github.com/paritytech/slog-async#107848e7ded5e80dc43f6296c2b96039eb92c0a5"
+source = "git+https://github.com/paritytech/slog-async#0329dc74feb3afe93d0cd2533a472b7ceab44aaf"
 dependencies = [
  "crossbeam-channel",
  "slog",
@@ -3810,9 +3851,9 @@ source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a1232
 dependencies = [
  "blake2-rfc",
  "proc-macro-crate",
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
 ]
 
 [[package]]
@@ -4129,9 +4170,9 @@ version = "2.0.0"
 source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
 dependencies = [
  "proc-macro-crate",
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
 ]
 
 [[package]]
@@ -4174,11 +4215,11 @@ name = "srml-support-procedural"
 version = "2.0.0"
 source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
 dependencies = [
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
  "sr-api-macros",
  "srml-support-procedural-tools",
- "syn 1.0.16",
+ "syn 1.0.17",
 ]
 
 [[package]]
@@ -4187,10 +4228,10 @@ version = "2.0.0"
 source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
 dependencies = [
  "proc-macro-crate",
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
  "srml-support-procedural-tools-derive",
- "syn 1.0.16",
+ "syn 1.0.17",
 ]
 
 [[package]]
@@ -4198,9 +4239,9 @@ name = "srml-support-procedural-tools-derive"
 version = "2.0.0"
 source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
 dependencies = [
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
 ]
 
 [[package]]
@@ -4323,9 +4364,9 @@ checksum = "ea692d40005b3ceba90a9fe7a78fa8d4b82b0ce627eebbffc329aab850f3410e"
 dependencies = [
  "heck",
  "proc-macro-error",
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
 ]
 
 [[package]]
@@ -4441,9 +4482,9 @@ version = "2.0.0"
 source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
 dependencies = [
  "proc-macro-crate",
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
 ]
 
 [[package]]
@@ -4678,9 +4719,9 @@ name = "substrate-debug-derive"
 version = "2.0.0"
 source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
 dependencies = [
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
 ]
 
 [[package]]
@@ -4796,6 +4837,8 @@ dependencies = [
  "substrate-common-module",
  "substrate-membership-module",
  "substrate-primitives",
+ "substrate-recurring-reward-module",
+ "substrate-token-mint-module",
 ]
 
 [[package]]
@@ -4870,7 +4913,7 @@ dependencies = [
 
 [[package]]
 name = "substrate-membership-module"
-version = "1.0.0"
+version = "1.0.1"
 dependencies = [
  "parity-scale-codec",
  "serde",
@@ -5057,6 +5100,79 @@ dependencies = [
  "substrate-debug-derive",
 ]
 
+[[package]]
+name = "substrate-proposals-codex-module"
+version = "2.0.0"
+dependencies = [
+ "num_enum",
+ "parity-scale-codec",
+ "serde",
+ "sr-io",
+ "sr-primitives",
+ "sr-staking-primitives",
+ "sr-std",
+ "srml-balances",
+ "srml-staking",
+ "srml-staking-reward-curve",
+ "srml-support",
+ "srml-system",
+ "srml-timestamp",
+ "substrate-common-module",
+ "substrate-content-working-group-module",
+ "substrate-governance-module",
+ "substrate-hiring-module",
+ "substrate-membership-module",
+ "substrate-primitives",
+ "substrate-proposals-discussion-module",
+ "substrate-proposals-engine-module",
+ "substrate-recurring-reward-module",
+ "substrate-roles-module",
+ "substrate-stake-module",
+ "substrate-token-mint-module",
+ "substrate-versioned-store",
+ "substrate-versioned-store-permissions-module",
+]
+
+[[package]]
+name = "substrate-proposals-discussion-module"
+version = "2.0.0"
+dependencies = [
+ "num_enum",
+ "parity-scale-codec",
+ "serde",
+ "sr-io",
+ "sr-primitives",
+ "sr-std",
+ "srml-balances",
+ "srml-support",
+ "srml-system",
+ "srml-timestamp",
+ "substrate-common-module",
+ "substrate-membership-module",
+ "substrate-primitives",
+]
+
+[[package]]
+name = "substrate-proposals-engine-module"
+version = "2.0.0"
+dependencies = [
+ "mockall",
+ "num_enum",
+ "parity-scale-codec",
+ "serde",
+ "sr-io",
+ "sr-primitives",
+ "sr-std",
+ "srml-balances",
+ "srml-support",
+ "srml-system",
+ "srml-timestamp",
+ "substrate-common-module",
+ "substrate-membership-module",
+ "substrate-primitives",
+ "substrate-stake-module",
+]
+
 [[package]]
 name = "substrate-recurring-reward-module"
 version = "1.0.1"
@@ -5080,7 +5196,7 @@ dependencies = [
 
 [[package]]
 name = "substrate-roles-module"
-version = "1.0.0"
+version = "1.0.1"
 dependencies = [
  "parity-scale-codec",
  "serde",
@@ -5477,11 +5593,11 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "1.0.16"
+version = "1.0.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "123bd9499cfb380418d509322d7a6d52e5315f064fe4b3ad18a53d6b92c07859"
+checksum = "0df0eb663f387145cab623dea85b09c2c5b4b0aef44e945d928e682fce71bb03"
 dependencies = [
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
  "unicode-xid 0.2.0",
 ]
@@ -5492,9 +5608,9 @@ version = "0.12.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545"
 dependencies = [
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
  "unicode-xid 0.2.0",
 ]
 
@@ -6071,9 +6187,9 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
 
 [[package]]
 name = "wasm-bindgen"
-version = "0.2.59"
+version = "0.2.60"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3557c397ab5a8e347d434782bcd31fc1483d927a6826804cec05cc792ee2519d"
+checksum = "2cc57ce05287f8376e998cbddfb4c8cb43b84a7ec55cf4551d7c00eef317a47f"
 dependencies = [
  "cfg-if",
  "wasm-bindgen-macro",
@@ -6081,16 +6197,16 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-backend"
-version = "0.2.59"
+version = "0.2.60"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0da9c9a19850d3af6df1cb9574970b566d617ecfaf36eb0b706b6f3ef9bd2f8"
+checksum = "d967d37bf6c16cca2973ca3af071d0a2523392e4a594548155d89a678f4237cd"
 dependencies = [
  "bumpalo",
  "lazy_static",
  "log",
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
  "wasm-bindgen-shared",
 ]
 
@@ -6109,9 +6225,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro"
-version = "0.2.59"
+version = "0.2.60"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f6fde1d36e75a714b5fe0cffbb78978f222ea6baebb726af13c78869fdb4205"
+checksum = "8bd151b63e1ea881bb742cd20e1d6127cef28399558f3b5d415289bc41eee3a4"
 dependencies = [
  "quote 1.0.3",
  "wasm-bindgen-macro-support",
@@ -6119,22 +6235,22 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro-support"
-version = "0.2.59"
+version = "0.2.60"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25bda4168030a6412ea8a047e27238cadf56f0e53516e1e83fec0a8b7c786f6d"
+checksum = "d68a5b36eef1be7868f668632863292e37739656a80fc4b9acec7b0bd35a4931"
 dependencies = [
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
  "wasm-bindgen-backend",
  "wasm-bindgen-shared",
 ]
 
 [[package]]
 name = "wasm-bindgen-shared"
-version = "0.2.59"
+version = "0.2.60"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc9f36ad51f25b0219a3d4d13b90eb44cd075dff8b6280cca015775d7acaddd8"
+checksum = "daf76fe7d25ac79748a37538b7daeed1c7a6867c92d3245c12c6222e4a20d639"
 
 [[package]]
 name = "wasm-timer"
@@ -6175,9 +6291,9 @@ dependencies = [
 
 [[package]]
 name = "web-sys"
-version = "0.3.36"
+version = "0.3.37"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "721c6263e2c66fd44501cc5efbfa2b7dfa775d13e4ea38c46299646ed1f9c70a"
+checksum = "2d6f51648d8c56c366144378a33290049eafdd784071077f6fe37dae64c1c4cb"
 dependencies = [
  "js-sys",
  "wasm-bindgen",
@@ -6251,9 +6367,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
 
 [[package]]
 name = "winapi-util"
-version = "0.1.3"
+version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ccfbf554c6ad11084fb7517daca16cfdcaccbdadba4fc336f032a8b12c2ad80"
+checksum = "fa515c5163a99cc82bab70fd3bfdd36d827be85de63737b40fcef2ce084a436e"
 dependencies = [
  "winapi 0.3.8",
 ]
@@ -6353,8 +6469,8 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "de251eec69fc7c1bc3923403d18ececb929380e016afe103da75f396704f8ca2"
 dependencies = [
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
  "synstructure",
 ]

+ 3 - 0
Cargo.toml

@@ -1,6 +1,9 @@
 [workspace]
 members = [
 	"runtime",
+	"runtime-modules/proposals/engine",
+	"runtime-modules/proposals/codex",
+	"runtime-modules/proposals/discussion",
 	"runtime-modules/common",
 	"runtime-modules/content-working-group",
 	"runtime-modules/forum",

+ 16 - 8
README.md

@@ -1,4 +1,4 @@
-# Joystream [![Build Status](https://travis-ci.org/Joystream/substrate-runtime-joystream.svg?branch=master)](https://travis-ci.org/Joystream/substrate-runtime-joystream)
+# Joystream [![Build Status](https://travis-ci.org/Joystream/joystream.svg?branch=master)](https://travis-ci.org/Joystream/joystream)
 
 This is the main code reposity for all joystream software. It will  house the substrate chain project, the full node and runtime and all reusable substrate runtime modules that make up the joystream runtime. In addition to all front-end apps and infrastructure servers necessary for operating the network.
 
@@ -6,9 +6,9 @@ The repository is currently just a cargo workspace, but eventually will also con
 
 ## Build Status
 
-Development [![Development Branch Build Status](https://travis-ci.org/Joystream/substrate-runtime-joystream.svg?branch=development)](https://travis-ci.org/Joystream/substrate-runtime-joystream)
+Development [![Development Branch Build Status](https://travis-ci.org/Joystream/joystream.svg?branch=development)](https://travis-ci.org/Joystream/joystream)
 
-More detailed build history on [Travis CI](https://travis-ci.org/github/Joystream/substrate-runtime-joystream/builds)
+More detailed build history on [Travis CI](https://travis-ci.org/github/Joystream/joystream/builds)
 
 ## Overview
 
@@ -26,7 +26,7 @@ To setup a full node and validator review the [advanced guide from the helpdesk]
 
 ###  Pre-built Binaries
 
-The latest pre-built binaries can be downloads from the [releases](https://github.com/Joystream/substrate-runtime-joystream/releases) page.
+The latest pre-built binaries can be downloaded from the [releases](https://github.com/Joystream/joystream/releases) page.
 
 
 ### Building from source
@@ -34,9 +34,9 @@ The latest pre-built binaries can be downloads from the [releases](https://githu
 Clone the repository and install build tools:
 
 ```bash
-git clone https://github.com/Joystream/substrate-runtime-joystream.git
+git clone https://github.com/Joystream/joystream.git
 
-cd substrate-runtime-joystream/
+cd joystream/
 
 ./setup.sh
 ```
@@ -55,7 +55,7 @@ Run the node and connect to the public testnet.
 cargo run --release -- --chain ./rome-tesnet.json
 ```
 
-The `rome-testnet.json` chain file can be ontained from the [release page](https://github.com/Joystream/substrate-runtime-joystream/releases/tag/v6.8.0)
+The `rome-testnet.json` chain file can be obtained from the [releases page](https://github.com/Joystream/joystream/releases/tag/v6.8.0)
 
 
 ### Installing a release build
@@ -85,6 +85,14 @@ This will build and run a fresh new local development chain purging existing one
 cargo test
 ```
 
+### API integration tests
+
+```bash
+./scripts/run-dev-chain.sh
+yarn test
+```
+
+To run the integration tests with a different chain, you can omit step running the local development chain and set the node URL using `NODE_URL` environment variable.
 
 ## Joystream Runtime
 
@@ -111,7 +119,7 @@ Deploying the compiled runtime on a live system can be done in one of two ways:
 ### Versioning the runtime
 
 Versioning of the runtime is set in `runtime/src/lib.rs`
-For detailed information about how to set correct version numbers when developing a new runtime, [see this](https://github.com/Joystream/substrate-runtime-joystream/issues/1)
+For detailed information about how to set correct version numbers when developing a new runtime, [see this](https://github.com/Joystream/joystream/issues/1)
 
 
 ## Coding style

+ 11 - 0
cli/.editorconfig

@@ -0,0 +1,11 @@
+root = true
+
+[*]
+indent_style = space
+indent_size = 4
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false

+ 1 - 0
cli/.eslintignore

@@ -0,0 +1 @@
+/lib

+ 6 - 0
cli/.eslintrc

@@ -0,0 +1,6 @@
+{
+  "extends": [
+    "oclif",
+    "oclif-typescript"
+  ]
+}

+ 8 - 0
cli/.gitignore

@@ -0,0 +1,8 @@
+*-debug.log
+*-error.log
+/.nyc_output
+/dist
+/lib
+/tmp
+/yarn.lock
+node_modules

+ 263 - 0
cli/README.md

@@ -0,0 +1,263 @@
+joystream-cli
+=============
+
+Command Line Interface for Joystream community and governance activities
+
+[![oclif](https://img.shields.io/badge/cli-oclif-brightgreen.svg)](https://oclif.io)
+[![Version](https://img.shields.io/npm/v/joystream-cli.svg)](https://npmjs.org/package/joystream-cli)
+[![Downloads/week](https://img.shields.io/npm/dw/joystream-cli.svg)](https://npmjs.org/package/joystream-cli)
+[![License](https://img.shields.io/npm/l/joystream-cli.svg)](https://github.com/Joystream/cli/blob/master/package.json)
+
+<!-- toc -->
+* [Development](#development)
+* [Usage](#usage)
+* [Commands](#commands)
+<!-- tocstop -->
+
+# Development
+<!-- development -->
+To run a command in developemnt environment (without installing the package):
+
+1. Navigate into the CLI root directory
+1. Either execute any command like this:
+
+    ```
+        $ ./bin/run COMMAND
+    ```
+
+    Or use:
+
+    ```
+        $ npm link
+    ```
+
+    And then execute any command like this:
+
+    ```
+        $ joystream-cli COMMAND
+    ```
+<!-- development -->
+
+# Usage
+<!-- usage -->
+```sh-session
+$ npm install -g joystream-cli
+$ joystream-cli COMMAND
+running command...
+$ joystream-cli (-v|--version|version)
+joystream-cli/0.0.0 linux-x64 node-v13.12.0
+$ joystream-cli --help [COMMAND]
+USAGE
+  $ joystream-cli COMMAND
+...
+```
+<!-- usagestop -->
+# Commands
+<!-- commands -->
+* [`joystream-cli account:choose`](#joystream-cli-accountchoose)
+* [`joystream-cli account:create NAME`](#joystream-cli-accountcreate-name)
+* [`joystream-cli account:current`](#joystream-cli-accountcurrent)
+* [`joystream-cli account:export PATH`](#joystream-cli-accountexport-path)
+* [`joystream-cli account:forget`](#joystream-cli-accountforget)
+* [`joystream-cli account:import BACKUPFILEPATH`](#joystream-cli-accountimport-backupfilepath)
+* [`joystream-cli account:transferTokens RECIPIENT AMOUNT`](#joystream-cli-accounttransfertokens-recipient-amount)
+* [`joystream-cli api:getUri`](#joystream-cli-apigeturi)
+* [`joystream-cli api:inspect`](#joystream-cli-apiinspect)
+* [`joystream-cli api:setUri URI`](#joystream-cli-apiseturi-uri)
+* [`joystream-cli council:info`](#joystream-cli-councilinfo)
+* [`joystream-cli help [COMMAND]`](#joystream-cli-help-command)
+
+## `joystream-cli account:choose`
+
+Choose default account to use in the CLI
+
+```
+USAGE
+  $ joystream-cli account:choose
+```
+
+_See code: [src/commands/account/choose.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/account/choose.ts)_
+
+## `joystream-cli account:create NAME`
+
+Create new account
+
+```
+USAGE
+  $ joystream-cli account:create NAME
+
+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)_
+
+## `joystream-cli account:current`
+
+Display information about currently choosen default account
+
+```
+USAGE
+  $ joystream-cli account:current
+
+ALIASES
+  $ joystream-cli account:info
+  $ 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)_
+
+## `joystream-cli account:export PATH`
+
+Export account(s) to given location
+
+```
+USAGE
+  $ joystream-cli account:export PATH
+
+ARGUMENTS
+  PATH  Path where the exported files should be placed
+
+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)_
+
+## `joystream-cli account:forget`
+
+Forget (remove) account from the list of available accounts
+
+```
+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)_
+
+## `joystream-cli account:import BACKUPFILEPATH`
+
+Import account using JSON backup file
+
+```
+USAGE
+  $ joystream-cli account:import BACKUPFILEPATH
+
+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)_
+
+## `joystream-cli account:transferTokens RECIPIENT AMOUNT`
+
+Transfer tokens from currently choosen account
+
+```
+USAGE
+  $ joystream-cli account:transferTokens RECIPIENT AMOUNT
+
+ARGUMENTS
+  RECIPIENT  Address of the transfer recipient
+  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)_
+
+## `joystream-cli api:getUri`
+
+Get current api WS provider uri
+
+```
+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)_
+
+## `joystream-cli api:inspect`
+
+Lists available node API modules/methods and/or their description(s), or calls one of the API methods (depending on provided arguments and flags)
+
+```
+USAGE
+  $ joystream-cli api:inspect
+
+OPTIONS
+  -M, --module=module
+      Specifies the api module, ie. "system", "staking" etc.
+      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. 
+      "-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 
+      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 
+      default)
+
+  -m, --method=method
+      Specifies the api method to call/describe.
+
+  -t, --type=type
+      Specifies the type/category of the inspected request (ie. "query", "consts" etc.).
+      If no "--module" flag is provided then all available modules in that type will be listed.
+      If this flag is not provided then all available types will be listed.
+
+EXAMPLES
+  $ api:inspect
+  $ api:inspect -t=query
+  $ api:inspect -t=query -M=members
+  $ api:inspect -t=query -M=members -m=memberProfile
+  $ api:inspect -t=query -M=members -m=memberProfile -e
+  $ api:inspect -t=query -M=members -m=memberProfile -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)_
+
+## `joystream-cli api:setUri URI`
+
+Set api WS provider uri
+
+```
+USAGE
+  $ joystream-cli api:setUri URI
+
+ARGUMENTS
+  URI  Uri of the node api WS provider
+```
+
+_See code: [src/commands/api/setUri.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/api/setUri.ts)_
+
+## `joystream-cli council:info`
+
+Get current council and council elections information
+
+```
+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)_
+
+## `joystream-cli help [COMMAND]`
+
+display help for joystream-cli
+
+```
+USAGE
+  $ joystream-cli help [COMMAND]
+
+ARGUMENTS
+  COMMAND  command to show help for
+
+OPTIONS
+  --all  see all commands in CLI
+```
+
+_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v2.2.3/src/commands/help.ts)_
+<!-- commandsstop -->

+ 5 - 0
cli/bin/run

@@ -0,0 +1,5 @@
+#!/usr/bin/env node
+
+require('@oclif/command').run()
+.then(require('@oclif/command/flush'))
+.catch(require('@oclif/errors/handle'))

+ 3 - 0
cli/bin/run.cmd

@@ -0,0 +1,3 @@
+@echo off
+
+node "%~dp0\run" %*

+ 4671 - 0
cli/package-lock.json

@@ -0,0 +1,4671 @@
+{
+  "name": "joystream-cli",
+  "version": "0.0.0",
+  "lockfileVersion": 1,
+  "requires": true,
+  "dependencies": {
+    "@babel/code-frame": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz",
+      "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==",
+      "dev": true,
+      "requires": {
+        "@babel/highlight": "^7.8.3"
+      }
+    },
+    "@babel/generator": {
+      "version": "7.9.4",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.9.4.tgz",
+      "integrity": "sha512-rjP8ahaDy/ouhrvCoU1E5mqaitWrxwuNGU+dy1EpaoK48jZay4MdkskKGIMHLZNewg8sAsqpGSREJwP0zH3YQA==",
+      "dev": true,
+      "requires": {
+        "@babel/types": "^7.9.0",
+        "jsesc": "^2.5.1",
+        "lodash": "^4.17.13",
+        "source-map": "^0.5.0"
+      }
+    },
+    "@babel/helper-function-name": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz",
+      "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-get-function-arity": "^7.8.3",
+        "@babel/template": "^7.8.3",
+        "@babel/types": "^7.8.3"
+      }
+    },
+    "@babel/helper-get-function-arity": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz",
+      "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==",
+      "dev": true,
+      "requires": {
+        "@babel/types": "^7.8.3"
+      }
+    },
+    "@babel/helper-split-export-declaration": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz",
+      "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==",
+      "dev": true,
+      "requires": {
+        "@babel/types": "^7.8.3"
+      }
+    },
+    "@babel/helper-validator-identifier": {
+      "version": "7.9.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz",
+      "integrity": "sha512-6G8bQKjOh+of4PV/ThDm/rRqlU7+IGoJuofpagU5GlEl29Vv0RGqqt86ZGRV8ZuSOY3o+8yXl5y782SMcG7SHw==",
+      "dev": true
+    },
+    "@babel/highlight": {
+      "version": "7.9.0",
+      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz",
+      "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-validator-identifier": "^7.9.0",
+        "chalk": "^2.0.0",
+        "js-tokens": "^4.0.0"
+      }
+    },
+    "@babel/parser": {
+      "version": "7.9.4",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.4.tgz",
+      "integrity": "sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==",
+      "dev": true
+    },
+    "@babel/runtime": {
+      "version": "7.9.2",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.2.tgz",
+      "integrity": "sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q==",
+      "requires": {
+        "regenerator-runtime": "^0.13.4"
+      }
+    },
+    "@babel/template": {
+      "version": "7.8.6",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz",
+      "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "^7.8.3",
+        "@babel/parser": "^7.8.6",
+        "@babel/types": "^7.8.6"
+      }
+    },
+    "@babel/traverse": {
+      "version": "7.9.0",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.9.0.tgz",
+      "integrity": "sha512-jAZQj0+kn4WTHO5dUZkZKhbFrqZE7K5LAQ5JysMnmvGij+wOdr+8lWqPeW0BcF4wFwrEXXtdGO7wcV6YPJcf3w==",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "^7.8.3",
+        "@babel/generator": "^7.9.0",
+        "@babel/helper-function-name": "^7.8.3",
+        "@babel/helper-split-export-declaration": "^7.8.3",
+        "@babel/parser": "^7.9.0",
+        "@babel/types": "^7.9.0",
+        "debug": "^4.1.0",
+        "globals": "^11.1.0",
+        "lodash": "^4.17.13"
+      }
+    },
+    "@babel/types": {
+      "version": "7.9.0",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.0.tgz",
+      "integrity": "sha512-BS9JKfXkzzJl8RluW4JGknzpiUV7ZrvTayM6yfqLTVBEnFtyowVIOu6rqxRd5cVO6yGoWf4T8u8dgK9oB+GCng==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-validator-identifier": "^7.9.0",
+        "lodash": "^4.17.13",
+        "to-fast-properties": "^2.0.0"
+      }
+    },
+    "@joystream/types": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/@joystream/types/-/types-0.6.0.tgz",
+      "integrity": "sha512-b+6U36GHJLlBPxVqMVQRTZzVxu7BGsjqlC/XJfl/vdx8TOy3P8TIB/3olLU64EPB3cVNadg2p9jqYSsvh9XVAQ==",
+      "requires": {
+        "@polkadot/types": "^0.96.1",
+        "@types/vfile": "^4.0.0",
+        "ajv": "^6.11.0"
+      }
+    },
+    "@nodelib/fs.scandir": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz",
+      "integrity": "sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==",
+      "dev": true,
+      "requires": {
+        "@nodelib/fs.stat": "2.0.3",
+        "run-parallel": "^1.1.9"
+      }
+    },
+    "@nodelib/fs.stat": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz",
+      "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==",
+      "dev": true
+    },
+    "@nodelib/fs.walk": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz",
+      "integrity": "sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==",
+      "dev": true,
+      "requires": {
+        "@nodelib/fs.scandir": "2.1.3",
+        "fastq": "^1.6.0"
+      }
+    },
+    "@oclif/command": {
+      "version": "1.5.19",
+      "resolved": "https://registry.npmjs.org/@oclif/command/-/command-1.5.19.tgz",
+      "integrity": "sha512-6+iaCMh/JXJaB2QWikqvGE9//wLEVYYwZd5sud8aLoLKog1Q75naZh2vlGVtg5Mq/NqpqGQvdIjJb3Bm+64AUQ==",
+      "requires": {
+        "@oclif/config": "^1",
+        "@oclif/errors": "^1.2.2",
+        "@oclif/parser": "^3.8.3",
+        "@oclif/plugin-help": "^2",
+        "debug": "^4.1.1",
+        "semver": "^5.6.0"
+      }
+    },
+    "@oclif/config": {
+      "version": "1.14.0",
+      "resolved": "https://registry.npmjs.org/@oclif/config/-/config-1.14.0.tgz",
+      "integrity": "sha512-KsOP/mx9lzTah+EtGqLUXN3PDL0J3zb9/dTneFyiUK2K6T7vFEGhV6OasmqTh4uMZHGYTGrNPV8x/Yw6qZNL6A==",
+      "requires": {
+        "@oclif/errors": "^1.0.0",
+        "@oclif/parser": "^3.8.0",
+        "debug": "^4.1.1",
+        "tslib": "^1.9.3"
+      }
+    },
+    "@oclif/dev-cli": {
+      "version": "1.22.2",
+      "resolved": "https://registry.npmjs.org/@oclif/dev-cli/-/dev-cli-1.22.2.tgz",
+      "integrity": "sha512-c7633R37RxrQIpwqPKxjNRm6/jb1yuG8fd16hmNz9Nw+/MUhEtQtKHSCe9ScH8n5M06l6LEo4ldk9LEGtpaWwA==",
+      "dev": true,
+      "requires": {
+        "@oclif/command": "^1.5.13",
+        "@oclif/config": "^1.12.12",
+        "@oclif/errors": "^1.2.2",
+        "@oclif/plugin-help": "^2.1.6",
+        "cli-ux": "^5.2.1",
+        "debug": "^4.1.1",
+        "fs-extra": "^7.0.1",
+        "github-slugger": "^1.2.1",
+        "lodash": "^4.17.11",
+        "normalize-package-data": "^2.5.0",
+        "qqjs": "^0.3.10",
+        "tslib": "^1.9.3"
+      }
+    },
+    "@oclif/errors": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/@oclif/errors/-/errors-1.2.2.tgz",
+      "integrity": "sha512-Eq8BFuJUQcbAPVofDxwdE0bL14inIiwt5EaKRVY9ZDIG11jwdXZqiQEECJx0VfnLyUZdYfRd/znDI/MytdJoKg==",
+      "requires": {
+        "clean-stack": "^1.3.0",
+        "fs-extra": "^7.0.0",
+        "indent-string": "^3.2.0",
+        "strip-ansi": "^5.0.0",
+        "wrap-ansi": "^4.0.0"
+      }
+    },
+    "@oclif/linewrap": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@oclif/linewrap/-/linewrap-1.0.0.tgz",
+      "integrity": "sha512-Ups2dShK52xXa8w6iBWLgcjPJWjais6KPJQq3gQ/88AY6BXoTX+MIGFPrWQO1KLMiQfoTpcLnUwloN4brrVUHw=="
+    },
+    "@oclif/parser": {
+      "version": "3.8.4",
+      "resolved": "https://registry.npmjs.org/@oclif/parser/-/parser-3.8.4.tgz",
+      "integrity": "sha512-cyP1at3l42kQHZtqDS3KfTeyMvxITGwXwH1qk9ktBYvqgMp5h4vHT+cOD74ld3RqJUOZY/+Zi9lb4Tbza3BtuA==",
+      "requires": {
+        "@oclif/linewrap": "^1.0.0",
+        "chalk": "^2.4.2",
+        "tslib": "^1.9.3"
+      }
+    },
+    "@oclif/plugin-help": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/@oclif/plugin-help/-/plugin-help-2.2.3.tgz",
+      "integrity": "sha512-bGHUdo5e7DjPJ0vTeRBMIrfqTRDBfyR5w0MP41u0n3r7YG5p14lvMmiCXxi6WDaP2Hw5nqx3PnkAIntCKZZN7g==",
+      "requires": {
+        "@oclif/command": "^1.5.13",
+        "chalk": "^2.4.1",
+        "indent-string": "^4.0.0",
+        "lodash.template": "^4.4.0",
+        "string-width": "^3.0.0",
+        "strip-ansi": "^5.0.0",
+        "widest-line": "^2.0.1",
+        "wrap-ansi": "^4.0.0"
+      },
+      "dependencies": {
+        "indent-string": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+          "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="
+        },
+        "string-width": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+          "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+          "requires": {
+            "emoji-regex": "^7.0.1",
+            "is-fullwidth-code-point": "^2.0.0",
+            "strip-ansi": "^5.1.0"
+          }
+        }
+      }
+    },
+    "@oclif/screen": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/@oclif/screen/-/screen-1.0.4.tgz",
+      "integrity": "sha512-60CHpq+eqnTxLZQ4PGHYNwUX572hgpMHGPtTWMjdTMsAvlm69lZV/4ly6O3sAYkomo4NggGcomrDpBe34rxUqw=="
+    },
+    "@oclif/test": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/@oclif/test/-/test-1.2.5.tgz",
+      "integrity": "sha512-8Y+Ix4A3Zhm87aL0ldVonDK7vFWyLfnFHzP3goYaLyIeh/60KL37lMxfmbp/kBN6/Y0Ru17iR1pdDi/hTDClLQ==",
+      "dev": true,
+      "requires": {
+        "fancy-test": "^1.4.3"
+      }
+    },
+    "@polkadot/api": {
+      "version": "0.96.1",
+      "resolved": "https://registry.npmjs.org/@polkadot/api/-/api-0.96.1.tgz",
+      "integrity": "sha512-FeYyMfJL0NACJBIuG7C7mp7f9J/WOGUERF/hUP3RlIz4Ld2X0vRjEoOgiG0VIS89I4K31XaNmSjIchH244WtHg==",
+      "requires": {
+        "@babel/runtime": "^7.7.1",
+        "@polkadot/api-derive": "^0.96.1",
+        "@polkadot/api-metadata": "^0.96.1",
+        "@polkadot/keyring": "^1.7.0-beta.5",
+        "@polkadot/rpc-core": "^0.96.1",
+        "@polkadot/rpc-provider": "^0.96.1",
+        "@polkadot/types": "^0.96.1",
+        "@polkadot/util-crypto": "^1.7.0-beta.5"
+      }
+    },
+    "@polkadot/api-derive": {
+      "version": "0.96.1",
+      "resolved": "https://registry.npmjs.org/@polkadot/api-derive/-/api-derive-0.96.1.tgz",
+      "integrity": "sha512-PGWdUvlD2acUKOgaJcYWuMTfSuQKUpwgwjer5SomHLFn4ZPOz8iDa7mYtrgmxQctRv1zsuck2X01uhxdEdtJZw==",
+      "requires": {
+        "@babel/runtime": "^7.7.1",
+        "@polkadot/api": "^0.96.1",
+        "@polkadot/types": "^0.96.1"
+      }
+    },
+    "@polkadot/api-metadata": {
+      "version": "0.96.1",
+      "resolved": "https://registry.npmjs.org/@polkadot/api-metadata/-/api-metadata-0.96.1.tgz",
+      "integrity": "sha512-I9F3twpSCgx4ny25a3moGrhf2vHKFnjooO3W9NaAxIj/us4q4Gqo4+czQajqt8vaJqrNMq/PE7lzVz1NhYDrZQ==",
+      "requires": {
+        "@babel/runtime": "^7.7.1",
+        "@polkadot/types": "^0.96.1",
+        "@polkadot/util": "^1.7.0-beta.5",
+        "@polkadot/util-crypto": "^1.7.0-beta.5"
+      }
+    },
+    "@polkadot/jsonrpc": {
+      "version": "0.96.1",
+      "resolved": "https://registry.npmjs.org/@polkadot/jsonrpc/-/jsonrpc-0.96.1.tgz",
+      "integrity": "sha512-UHpcUGIvkG4dJ5gUhDyfJ1xfr/VcBlJ5lIlGamGsnNacMuIVmmEsftgxtPlJLWHuoA1EBEHY4cbPSv9CUJ0IFw==",
+      "requires": {
+        "@babel/runtime": "^7.7.1"
+      }
+    },
+    "@polkadot/keyring": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/@polkadot/keyring/-/keyring-1.8.1.tgz",
+      "integrity": "sha512-KeDbfP8biY3bXEhMv1ANp9d3kCuXj2oxseuDK0jvxRo7CehVME9UwAMGQK3Y9NCUuYWd+xTO2To0ZOqR7hdmuQ==",
+      "requires": {
+        "@babel/runtime": "^7.7.7",
+        "@polkadot/util": "^1.8.1",
+        "@polkadot/util-crypto": "^1.8.1"
+      }
+    },
+    "@polkadot/rpc-core": {
+      "version": "0.96.1",
+      "resolved": "https://registry.npmjs.org/@polkadot/rpc-core/-/rpc-core-0.96.1.tgz",
+      "integrity": "sha512-ygSaJpz/QPEq1p35wYRzONuP2PCtkAJ9eS8swQqUIezTo2ZPUOyBhmnJ3nxj11R8YnQClq4Id0QdsJmH1ClYgw==",
+      "requires": {
+        "@babel/runtime": "^7.7.1",
+        "@polkadot/jsonrpc": "^0.96.1",
+        "@polkadot/rpc-provider": "^0.96.1",
+        "@polkadot/types": "^0.96.1",
+        "@polkadot/util": "^1.7.0-beta.5",
+        "rxjs": "^6.5.3"
+      }
+    },
+    "@polkadot/rpc-provider": {
+      "version": "0.96.1",
+      "resolved": "https://registry.npmjs.org/@polkadot/rpc-provider/-/rpc-provider-0.96.1.tgz",
+      "integrity": "sha512-cUhp8FMCYHrXrBTbxZrok/hPIgtOXEUhIXn5/zrffg1Qpbzju/y/bXx7c1Kxl1JF7Bg0vSBRZEGJTn/x0irWRQ==",
+      "requires": {
+        "@babel/runtime": "^7.7.1",
+        "@polkadot/api-metadata": "^0.96.1",
+        "@polkadot/util": "^1.7.0-beta.5",
+        "@polkadot/util-crypto": "^1.7.0-beta.5",
+        "eventemitter3": "^4.0.0",
+        "isomorphic-fetch": "^2.2.1",
+        "websocket": "^1.0.30"
+      }
+    },
+    "@polkadot/ts": {
+      "version": "0.1.91",
+      "resolved": "https://registry.npmjs.org/@polkadot/ts/-/ts-0.1.91.tgz",
+      "integrity": "sha512-UB8zOFZXb/ih03izzAQ1r1DRpiUXBofxAlXjcx4530jopfiNsiU1LZ2J/uS3dVV1QXaGRhkgm8SIJDLsSMRYIQ==",
+      "dev": true,
+      "requires": {
+        "@types/chrome": "^0.0.92"
+      }
+    },
+    "@polkadot/types": {
+      "version": "0.96.1",
+      "resolved": "https://registry.npmjs.org/@polkadot/types/-/types-0.96.1.tgz",
+      "integrity": "sha512-b8AZBNmMjB0+34Oxue3AYc0gIjDHYCdVGtDpel0omHkLMcEquSvrCniLm+p7g4cfArICiZPFmS9In/OWWdRUVA==",
+      "requires": {
+        "@babel/runtime": "^7.7.1",
+        "@polkadot/util": "^1.7.0-beta.5",
+        "@polkadot/util-crypto": "^1.7.0-beta.5",
+        "@types/memoizee": "^0.4.3",
+        "memoizee": "^0.4.14"
+      }
+    },
+    "@polkadot/util": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/@polkadot/util/-/util-1.8.1.tgz",
+      "integrity": "sha512-sFpr+JLCG9d+epjboXsmJ1qcKa96r8ZYzXmVo8+aPzI/9jKKyez6Unox/dnfnpKppZB2nJuLcsxQm6nocp2Caw==",
+      "requires": {
+        "@babel/runtime": "^7.7.7",
+        "@types/bn.js": "^4.11.6",
+        "bn.js": "^4.11.8",
+        "camelcase": "^5.3.1",
+        "chalk": "^3.0.0",
+        "ip-regex": "^4.1.0",
+        "moment": "^2.24.0"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "4.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
+          "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
+          "requires": {
+            "@types/color-name": "^1.1.1",
+            "color-convert": "^2.0.1"
+          }
+        },
+        "chalk": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+          "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+          "requires": {
+            "ansi-styles": "^4.1.0",
+            "supports-color": "^7.1.0"
+          }
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+          "requires": {
+            "color-name": "~1.1.4"
+          }
+        },
+        "color-name": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+        },
+        "has-flag": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
+        },
+        "supports-color": {
+          "version": "7.1.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
+          "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
+          "requires": {
+            "has-flag": "^4.0.0"
+          }
+        }
+      }
+    },
+    "@polkadot/util-crypto": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/@polkadot/util-crypto/-/util-crypto-1.8.1.tgz",
+      "integrity": "sha512-ypUs10hV1HPvYc0ZsEu+LTGSEh0rkr0as/FUh7+Z9v3Bxibn3aO+EOxJPQuDbZZ59FSMRmc9SeOSa0wn9ddrnw==",
+      "requires": {
+        "@babel/runtime": "^7.7.7",
+        "@polkadot/util": "^1.8.1",
+        "@polkadot/wasm-crypto": "^0.14.1",
+        "@types/bip39": "^2.4.2",
+        "@types/bs58": "^4.0.0",
+        "@types/pbkdf2": "^3.0.0",
+        "@types/secp256k1": "^3.5.0",
+        "@types/xxhashjs": "^0.2.1",
+        "base-x": "3.0.5",
+        "bip39": "^2.5.0",
+        "blakejs": "^1.1.0",
+        "bs58": "^4.0.1",
+        "js-sha3": "^0.8.0",
+        "secp256k1": "^3.8.0",
+        "tweetnacl": "^1.0.1",
+        "xxhashjs": "^0.2.2"
+      }
+    },
+    "@polkadot/wasm-crypto": {
+      "version": "0.14.1",
+      "resolved": "https://registry.npmjs.org/@polkadot/wasm-crypto/-/wasm-crypto-0.14.1.tgz",
+      "integrity": "sha512-Xng7L2Z8TNZa/5g6pot4O06Jf0ohQRZdvfl8eQL+E/L2mcqJYC1IjkMxJBSBuQEV7hisWzh9mHOy5WCcgPk29Q=="
+    },
+    "@types/bip39": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/@types/bip39/-/bip39-2.4.2.tgz",
+      "integrity": "sha512-Vo9lqOIRq8uoIzEVrV87ZvcIM0PN9t0K3oYZ/CS61fIYKCBdOIM7mlWzXuRvSXrDtVa1uUO2w1cdfufxTC0bzg==",
+      "requires": {
+        "@types/node": "*"
+      }
+    },
+    "@types/bn.js": {
+      "version": "4.11.6",
+      "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.6.tgz",
+      "integrity": "sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==",
+      "requires": {
+        "@types/node": "*"
+      }
+    },
+    "@types/bs58": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/@types/bs58/-/bs58-4.0.1.tgz",
+      "integrity": "sha512-yfAgiWgVLjFCmRv8zAcOIHywYATEwiTVccTLnRp6UxTNavT55M9d/uhK3T03St/+8/z/wW+CRjGKUNmEqoHHCA==",
+      "requires": {
+        "base-x": "^3.0.6"
+      },
+      "dependencies": {
+        "base-x": {
+          "version": "3.0.8",
+          "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.8.tgz",
+          "integrity": "sha512-Rl/1AWP4J/zRrk54hhlxH4drNxPJXYUaKffODVI53/dAsV4t9fBxyxYKAVPU1XBHxYwOWP9h9H0hM2MVw4YfJA==",
+          "requires": {
+            "safe-buffer": "^5.0.1"
+          }
+        }
+      }
+    },
+    "@types/chai": {
+      "version": "4.2.11",
+      "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.11.tgz",
+      "integrity": "sha512-t7uW6eFafjO+qJ3BIV2gGUyZs27egcNRkUdalkud+Qa3+kg//f129iuOFivHDXQ+vnU3fDXuwgv0cqMCbcE8sw==",
+      "dev": true
+    },
+    "@types/chrome": {
+      "version": "0.0.92",
+      "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.92.tgz",
+      "integrity": "sha512-bTv1EljZ03bexRJwS5FwSZmrudtw+QNbzwUY2sxVtXWgtxk752G4I2owhZ+Mlzbf3VKvG+rBYSw/FnvzuZ4xOA==",
+      "dev": true,
+      "requires": {
+        "@types/filesystem": "*"
+      }
+    },
+    "@types/color-name": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
+      "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ=="
+    },
+    "@types/eslint-visitor-keys": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
+      "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==",
+      "dev": true
+    },
+    "@types/events": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
+      "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==",
+      "dev": true
+    },
+    "@types/filesystem": {
+      "version": "0.0.29",
+      "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.29.tgz",
+      "integrity": "sha512-85/1KfRedmfPGsbK8YzeaQUyV1FQAvMPMTuWFQ5EkLd2w7szhNO96bk3Rh/SKmOfd9co2rCLf0Voy4o7ECBOvw==",
+      "dev": true,
+      "requires": {
+        "@types/filewriter": "*"
+      }
+    },
+    "@types/filewriter": {
+      "version": "0.0.28",
+      "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.28.tgz",
+      "integrity": "sha1-wFTor02d11205jq8dviFFocU1LM=",
+      "dev": true
+    },
+    "@types/glob": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz",
+      "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==",
+      "dev": true,
+      "requires": {
+        "@types/events": "*",
+        "@types/minimatch": "*",
+        "@types/node": "*"
+      }
+    },
+    "@types/inquirer": {
+      "version": "6.5.0",
+      "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-6.5.0.tgz",
+      "integrity": "sha512-rjaYQ9b9y/VFGOpqBEXRavc3jh0a+e6evAbI31tMda8VlPaSy0AZJfXsvmIe3wklc7W6C3zCSfleuMXR7NOyXw==",
+      "requires": {
+        "@types/through": "*",
+        "rxjs": "^6.4.0"
+      }
+    },
+    "@types/json-schema": {
+      "version": "7.0.4",
+      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz",
+      "integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==",
+      "dev": true
+    },
+    "@types/lodash": {
+      "version": "4.14.149",
+      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.149.tgz",
+      "integrity": "sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ==",
+      "dev": true
+    },
+    "@types/memoizee": {
+      "version": "0.4.3",
+      "resolved": "https://registry.npmjs.org/@types/memoizee/-/memoizee-0.4.3.tgz",
+      "integrity": "sha512-N6QT0c9ZbEKl33n1wyoTxZs4cpN+YXjs0Aqy5Qim8ipd9PBNIPqOh/p5Pixc4601tqr5GErsdxUbfqviDfubNw=="
+    },
+    "@types/minimatch": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
+      "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
+      "dev": true
+    },
+    "@types/mocha": {
+      "version": "5.2.7",
+      "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz",
+      "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==",
+      "dev": true
+    },
+    "@types/node": {
+      "version": "10.17.18",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.18.tgz",
+      "integrity": "sha512-DQ2hl/Jl3g33KuAUOcMrcAOtsbzb+y/ufakzAdeK9z/H/xsvkpbETZZbPNMIiQuk24f5ZRMCcZIViAwyFIiKmg=="
+    },
+    "@types/pbkdf2": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@types/pbkdf2/-/pbkdf2-3.0.0.tgz",
+      "integrity": "sha512-6J6MHaAlBJC/eVMy9jOwj9oHaprfutukfW/Dyt0NEnpQ/6HN6YQrpvLwzWdWDeWZIdenjGHlbYDzyEODO5Z+2Q==",
+      "requires": {
+        "@types/node": "*"
+      }
+    },
+    "@types/proper-lockfile": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/@types/proper-lockfile/-/proper-lockfile-4.1.1.tgz",
+      "integrity": "sha512-HAjVfDa73pFgivViHyDu8HHHcds+W4MgOuZZAdyFJrHS8ngtCXmhl4hc2YXqSOwO6Bsa+iF2Sgxb2+gv874VOQ==",
+      "requires": {
+        "@types/retry": "*"
+      }
+    },
+    "@types/retry": {
+      "version": "0.12.0",
+      "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
+      "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="
+    },
+    "@types/secp256k1": {
+      "version": "3.5.3",
+      "resolved": "https://registry.npmjs.org/@types/secp256k1/-/secp256k1-3.5.3.tgz",
+      "integrity": "sha512-NGcsPDR0P+Q71O63e2ayshmiZGAwCOa/cLJzOIuhOiDvmbvrCIiVtEpqdCJGogG92Bnr6tw/6lqVBsRMEl15OQ==",
+      "requires": {
+        "@types/node": "*"
+      }
+    },
+    "@types/sinon": {
+      "version": "9.0.0",
+      "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-9.0.0.tgz",
+      "integrity": "sha512-v2TkYHkts4VXshMkcmot/H+ERZ2SevKa10saGaJPGCJ8vh3lKrC4u663zYEeRZxep+VbG6YRDtQ6gVqw9dYzPA==",
+      "dev": true,
+      "requires": {
+        "@types/sinonjs__fake-timers": "*"
+      }
+    },
+    "@types/sinonjs__fake-timers": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz",
+      "integrity": "sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA==",
+      "dev": true
+    },
+    "@types/slug": {
+      "version": "0.9.1",
+      "resolved": "https://registry.npmjs.org/@types/slug/-/slug-0.9.1.tgz",
+      "integrity": "sha512-zR/u8WFQ4/6uCIikjI00a5uB084XjgEGNRAvM4a1BL39Bw9yEiDQFiPS2DgJ8lPDkR2Qd/vZ26dCR9XqlKbDqQ=="
+    },
+    "@types/through": {
+      "version": "0.0.30",
+      "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.30.tgz",
+      "integrity": "sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg==",
+      "requires": {
+        "@types/node": "*"
+      }
+    },
+    "@types/unist": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz",
+      "integrity": "sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ=="
+    },
+    "@types/vfile": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@types/vfile/-/vfile-4.0.0.tgz",
+      "integrity": "sha512-eleP0/Cz8uVWxARDLi3Axq2+fDdN4ibAXoC6Pv8p6s7znXaUL7XvhgeIhjCiNMnvlLNP+tmCLd+RuCryGgmtEg==",
+      "requires": {
+        "vfile": "*"
+      }
+    },
+    "@types/xxhashjs": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/@types/xxhashjs/-/xxhashjs-0.2.1.tgz",
+      "integrity": "sha512-Akm13wkwsQylVnBokl/aiKLtSxndSjfgTjdvmSxXNehYy4NymwdfdJHwGhpV54wcYfmOByOp3ak8AGdUlvp0sA==",
+      "requires": {
+        "@types/node": "*"
+      }
+    },
+    "@typescript-eslint/eslint-plugin": {
+      "version": "2.26.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.26.0.tgz",
+      "integrity": "sha512-4yUnLv40bzfzsXcTAtZyTjbiGUXMrcIJcIMioI22tSOyAxpdXiZ4r7YQUU8Jj6XXrLz9d5aMHPQf5JFR7h27Nw==",
+      "dev": true,
+      "requires": {
+        "@typescript-eslint/experimental-utils": "2.26.0",
+        "functional-red-black-tree": "^1.0.1",
+        "regexpp": "^3.0.0",
+        "tsutils": "^3.17.1"
+      },
+      "dependencies": {
+        "regexpp": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz",
+          "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==",
+          "dev": true
+        }
+      }
+    },
+    "@typescript-eslint/experimental-utils": {
+      "version": "2.26.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.26.0.tgz",
+      "integrity": "sha512-RELVoH5EYd+JlGprEyojUv9HeKcZqF7nZUGSblyAw1FwOGNnmQIU8kxJ69fttQvEwCsX5D6ECJT8GTozxrDKVQ==",
+      "dev": true,
+      "requires": {
+        "@types/json-schema": "^7.0.3",
+        "@typescript-eslint/typescript-estree": "2.26.0",
+        "eslint-scope": "^5.0.0",
+        "eslint-utils": "^2.0.0"
+      },
+      "dependencies": {
+        "eslint-scope": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz",
+          "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==",
+          "dev": true,
+          "requires": {
+            "esrecurse": "^4.1.0",
+            "estraverse": "^4.1.1"
+          }
+        },
+        "eslint-utils": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.0.0.tgz",
+          "integrity": "sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA==",
+          "dev": true,
+          "requires": {
+            "eslint-visitor-keys": "^1.1.0"
+          }
+        }
+      }
+    },
+    "@typescript-eslint/parser": {
+      "version": "2.26.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.26.0.tgz",
+      "integrity": "sha512-+Xj5fucDtdKEVGSh9353wcnseMRkPpEAOY96EEenN7kJVrLqy/EVwtIh3mxcUz8lsFXW1mT5nN5vvEam/a5HiQ==",
+      "dev": true,
+      "requires": {
+        "@types/eslint-visitor-keys": "^1.0.0",
+        "@typescript-eslint/experimental-utils": "2.26.0",
+        "@typescript-eslint/typescript-estree": "2.26.0",
+        "eslint-visitor-keys": "^1.1.0"
+      }
+    },
+    "@typescript-eslint/typescript-estree": {
+      "version": "2.26.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.26.0.tgz",
+      "integrity": "sha512-3x4SyZCLB4zsKsjuhxDLeVJN6W29VwBnYpCsZ7vIdPel9ZqLfIZJgJXO47MNUkurGpQuIBALdPQKtsSnWpE1Yg==",
+      "dev": true,
+      "requires": {
+        "debug": "^4.1.1",
+        "eslint-visitor-keys": "^1.1.0",
+        "glob": "^7.1.6",
+        "is-glob": "^4.0.1",
+        "lodash": "^4.17.15",
+        "semver": "^6.3.0",
+        "tsutils": "^3.17.1"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "6.3.0",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+          "dev": true
+        }
+      }
+    },
+    "acorn": {
+      "version": "6.4.1",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz",
+      "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==",
+      "dev": true
+    },
+    "acorn-jsx": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz",
+      "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==",
+      "dev": true
+    },
+    "ajv": {
+      "version": "6.12.0",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz",
+      "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==",
+      "requires": {
+        "fast-deep-equal": "^3.1.1",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
+      }
+    },
+    "ansi-escapes": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz",
+      "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ=="
+    },
+    "ansi-regex": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+      "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
+    },
+    "ansi-styles": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+      "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+      "requires": {
+        "color-convert": "^1.9.0"
+      }
+    },
+    "ansicolors": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz",
+      "integrity": "sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk="
+    },
+    "append-transform": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz",
+      "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==",
+      "dev": true,
+      "requires": {
+        "default-require-extensions": "^2.0.0"
+      }
+    },
+    "archy": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz",
+      "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=",
+      "dev": true
+    },
+    "arg": {
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+      "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+      "dev": true
+    },
+    "argparse": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+      "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+      "requires": {
+        "sprintf-js": "~1.0.2"
+      }
+    },
+    "array-union": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+      "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+      "dev": true
+    },
+    "assertion-error": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
+      "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
+      "dev": true
+    },
+    "astral-regex": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz",
+      "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==",
+      "dev": true
+    },
+    "balanced-match": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+      "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
+      "dev": true
+    },
+    "base-x": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.5.tgz",
+      "integrity": "sha512-C3picSgzPSLE+jW3tcBzJoGwitOtazb5B+5YmAxZm2ybmTi9LNgAtDO/jjVEBZwHoXmDBZ9m/IELj3elJVRBcA==",
+      "requires": {
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "base64-js": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
+      "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==",
+      "dev": true
+    },
+    "bindings": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+      "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+      "requires": {
+        "file-uri-to-path": "1.0.0"
+      }
+    },
+    "bip39": {
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/bip39/-/bip39-2.6.0.tgz",
+      "integrity": "sha512-RrnQRG2EgEoqO24ea+Q/fftuPUZLmrEM3qNhhGsA3PbaXaCW791LTzPuVyx/VprXQcTbPJ3K3UeTna8ZnVl2sg==",
+      "requires": {
+        "create-hash": "^1.1.0",
+        "pbkdf2": "^3.0.9",
+        "randombytes": "^2.0.1",
+        "safe-buffer": "^5.0.1",
+        "unorm": "^1.3.3"
+      }
+    },
+    "bip66": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz",
+      "integrity": "sha1-AfqHSHhcpwlV1QESF9GzE5lpyiI=",
+      "requires": {
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "bl": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz",
+      "integrity": "sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ==",
+      "dev": true,
+      "requires": {
+        "buffer": "^5.5.0",
+        "inherits": "^2.0.4",
+        "readable-stream": "^3.4.0"
+      }
+    },
+    "blakejs": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.1.0.tgz",
+      "integrity": "sha1-ad+S75U6qIylGjLfarHFShVfx6U="
+    },
+    "bn.js": {
+      "version": "4.11.8",
+      "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
+      "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="
+    },
+    "brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "requires": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "braces": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+      "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+      "dev": true,
+      "requires": {
+        "fill-range": "^7.0.1"
+      }
+    },
+    "brorand": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
+      "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8="
+    },
+    "browser-stdout": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+      "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
+      "dev": true
+    },
+    "browserify-aes": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
+      "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
+      "requires": {
+        "buffer-xor": "^1.0.3",
+        "cipher-base": "^1.0.0",
+        "create-hash": "^1.1.0",
+        "evp_bytestokey": "^1.0.3",
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "bs58": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
+      "integrity": "sha1-vhYedsNU9veIrkBx9j806MTwpCo=",
+      "requires": {
+        "base-x": "^3.0.2"
+      }
+    },
+    "buffer": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.5.0.tgz",
+      "integrity": "sha512-9FTEDjLjwoAkEwyMGDjYJQN2gfRgOKBKRfiglhvibGbpeeU/pQn1bJxQqm32OD/AIeEuHxU9roxXxg34Byp/Ww==",
+      "dev": true,
+      "requires": {
+        "base64-js": "^1.0.2",
+        "ieee754": "^1.1.4"
+      }
+    },
+    "buffer-from": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
+      "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
+      "dev": true
+    },
+    "buffer-xor": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
+      "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk="
+    },
+    "caching-transform": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-3.0.2.tgz",
+      "integrity": "sha512-Mtgcv3lh3U0zRii/6qVgQODdPA4G3zhG+jtbCWj39RXuUFTMzH0vcdMtaJS1jPowd+It2Pqr6y3NJMQqOqCE2w==",
+      "dev": true,
+      "requires": {
+        "hasha": "^3.0.0",
+        "make-dir": "^2.0.0",
+        "package-hash": "^3.0.0",
+        "write-file-atomic": "^2.4.2"
+      },
+      "dependencies": {
+        "make-dir": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
+          "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
+          "dev": true,
+          "requires": {
+            "pify": "^4.0.1",
+            "semver": "^5.6.0"
+          }
+        },
+        "write-file-atomic": {
+          "version": "2.4.3",
+          "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz",
+          "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==",
+          "dev": true,
+          "requires": {
+            "graceful-fs": "^4.1.11",
+            "imurmurhash": "^0.1.4",
+            "signal-exit": "^3.0.2"
+          }
+        }
+      }
+    },
+    "callsites": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+      "dev": true
+    },
+    "camelcase": {
+      "version": "5.3.1",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+      "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
+    },
+    "cardinal": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz",
+      "integrity": "sha1-fMEFXYItISlU0HsIXeolHMe8VQU=",
+      "requires": {
+        "ansicolors": "~0.3.2",
+        "redeyed": "~2.1.0"
+      }
+    },
+    "chai": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz",
+      "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==",
+      "dev": true,
+      "requires": {
+        "assertion-error": "^1.1.0",
+        "check-error": "^1.0.2",
+        "deep-eql": "^3.0.1",
+        "get-func-name": "^2.0.0",
+        "pathval": "^1.1.0",
+        "type-detect": "^4.0.5"
+      }
+    },
+    "chalk": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+      "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+      "requires": {
+        "ansi-styles": "^3.2.1",
+        "escape-string-regexp": "^1.0.5",
+        "supports-color": "^5.3.0"
+      }
+    },
+    "chardet": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
+      "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
+    },
+    "check-error": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
+      "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=",
+      "dev": true
+    },
+    "chownr": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+      "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+      "dev": true
+    },
+    "cipher-base": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
+      "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
+      "requires": {
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "clean-regexp": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz",
+      "integrity": "sha1-jffHquUf02h06PjQW5GAvBGj/tc=",
+      "dev": true,
+      "requires": {
+        "escape-string-regexp": "^1.0.5"
+      }
+    },
+    "clean-stack": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-1.3.0.tgz",
+      "integrity": "sha1-noIVAa6XmYbEax1m0tQy2y/UrjE="
+    },
+    "cli-cursor": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
+      "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+      "requires": {
+        "restore-cursor": "^3.1.0"
+      }
+    },
+    "cli-progress": {
+      "version": "3.6.1",
+      "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.6.1.tgz",
+      "integrity": "sha512-OVRgcyeI0viJW47MnyS10Jw/0RTpk7wwNbrCOPyXT0TVi2o3Q/u+Os8vQUFYhvkdXSbguSdFvMv1ia+UuwgIQQ==",
+      "requires": {
+        "colors": "^1.1.2",
+        "string-width": "^4.2.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+          "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg=="
+        },
+        "emoji-regex": {
+          "version": "8.0.0",
+          "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+          "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+        },
+        "is-fullwidth-code-point": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+          "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
+        },
+        "string-width": {
+          "version": "4.2.0",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
+          "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
+          "requires": {
+            "emoji-regex": "^8.0.0",
+            "is-fullwidth-code-point": "^3.0.0",
+            "strip-ansi": "^6.0.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+          "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+          "requires": {
+            "ansi-regex": "^5.0.0"
+          }
+        }
+      }
+    },
+    "cli-ux": {
+      "version": "5.4.5",
+      "resolved": "https://registry.npmjs.org/cli-ux/-/cli-ux-5.4.5.tgz",
+      "integrity": "sha512-5A6FuU0wPUlfCWUjtizUvNIbXElp6jN9QUJsDibs6F9cVX1kTgaMR3m6KT0R3iriEXpMrmPKV6yYS8XICNuQ6Q==",
+      "requires": {
+        "@oclif/command": "^1.5.1",
+        "@oclif/errors": "^1.2.1",
+        "@oclif/linewrap": "^1.0.0",
+        "@oclif/screen": "^1.0.3",
+        "ansi-escapes": "^3.1.0",
+        "ansi-styles": "^3.2.1",
+        "cardinal": "^2.1.1",
+        "chalk": "^2.4.1",
+        "clean-stack": "^2.0.0",
+        "cli-progress": "^3.4.0",
+        "extract-stack": "^1.0.0",
+        "fs-extra": "^7.0.1",
+        "hyperlinker": "^1.0.0",
+        "indent-string": "^4.0.0",
+        "is-wsl": "^1.1.0",
+        "js-yaml": "^3.13.1",
+        "lodash": "^4.17.11",
+        "natural-orderby": "^2.0.1",
+        "password-prompt": "^1.1.2",
+        "semver": "^5.6.0",
+        "string-width": "^3.1.0",
+        "strip-ansi": "^5.1.0",
+        "supports-color": "^5.5.0",
+        "supports-hyperlinks": "^1.0.1",
+        "treeify": "^1.1.0",
+        "tslib": "^1.9.3"
+      },
+      "dependencies": {
+        "clean-stack": {
+          "version": "2.2.0",
+          "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
+          "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="
+        },
+        "indent-string": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+          "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="
+        },
+        "string-width": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+          "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+          "requires": {
+            "emoji-regex": "^7.0.1",
+            "is-fullwidth-code-point": "^2.0.0",
+            "strip-ansi": "^5.1.0"
+          }
+        }
+      }
+    },
+    "cli-width": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz",
+      "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk="
+    },
+    "cliui": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
+      "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
+      "dev": true,
+      "requires": {
+        "string-width": "^3.1.0",
+        "strip-ansi": "^5.2.0",
+        "wrap-ansi": "^5.1.0"
+      },
+      "dependencies": {
+        "string-width": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+          "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+          "dev": true,
+          "requires": {
+            "emoji-regex": "^7.0.1",
+            "is-fullwidth-code-point": "^2.0.0",
+            "strip-ansi": "^5.1.0"
+          }
+        },
+        "wrap-ansi": {
+          "version": "5.1.0",
+          "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
+          "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^3.2.0",
+            "string-width": "^3.0.0",
+            "strip-ansi": "^5.0.0"
+          }
+        }
+      }
+    },
+    "color-convert": {
+      "version": "1.9.3",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+      "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+      "requires": {
+        "color-name": "1.1.3"
+      }
+    },
+    "color-name": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+      "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
+    },
+    "colors": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
+      "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="
+    },
+    "commander": {
+      "version": "2.15.1",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
+      "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==",
+      "dev": true
+    },
+    "commondir": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
+      "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
+      "dev": true
+    },
+    "concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+      "dev": true
+    },
+    "content-type": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
+      "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==",
+      "dev": true
+    },
+    "convert-source-map": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz",
+      "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==",
+      "dev": true,
+      "requires": {
+        "safe-buffer": "~5.1.1"
+      },
+      "dependencies": {
+        "safe-buffer": {
+          "version": "5.1.2",
+          "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+          "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+          "dev": true
+        }
+      }
+    },
+    "cp-file": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/cp-file/-/cp-file-6.2.0.tgz",
+      "integrity": "sha512-fmvV4caBnofhPe8kOcitBwSn2f39QLjnAnGq3gO9dfd75mUytzKNZB1hde6QHunW2Rt+OwuBOMc3i1tNElbszA==",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.1.2",
+        "make-dir": "^2.0.0",
+        "nested-error-stacks": "^2.0.0",
+        "pify": "^4.0.1",
+        "safe-buffer": "^5.0.1"
+      },
+      "dependencies": {
+        "make-dir": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
+          "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
+          "dev": true,
+          "requires": {
+            "pify": "^4.0.1",
+            "semver": "^5.6.0"
+          }
+        }
+      }
+    },
+    "create-hash": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
+      "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
+      "requires": {
+        "cipher-base": "^1.0.1",
+        "inherits": "^2.0.1",
+        "md5.js": "^1.3.4",
+        "ripemd160": "^2.0.1",
+        "sha.js": "^2.4.0"
+      }
+    },
+    "create-hmac": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
+      "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
+      "requires": {
+        "cipher-base": "^1.0.3",
+        "create-hash": "^1.1.0",
+        "inherits": "^2.0.1",
+        "ripemd160": "^2.0.0",
+        "safe-buffer": "^5.0.1",
+        "sha.js": "^2.4.8"
+      }
+    },
+    "cross-spawn": {
+      "version": "6.0.5",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
+      "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+      "requires": {
+        "nice-try": "^1.0.4",
+        "path-key": "^2.0.1",
+        "semver": "^5.5.0",
+        "shebang-command": "^1.2.0",
+        "which": "^1.2.9"
+      }
+    },
+    "cuint": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz",
+      "integrity": "sha1-QICG1AlVDCYxFVYZ6fp7ytw7mRs="
+    },
+    "d": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
+      "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==",
+      "requires": {
+        "es5-ext": "^0.10.50",
+        "type": "^1.0.1"
+      }
+    },
+    "debug": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+      "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+      "requires": {
+        "ms": "^2.1.1"
+      }
+    },
+    "decamelize": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+      "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
+      "dev": true
+    },
+    "deep-eql": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz",
+      "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==",
+      "dev": true,
+      "requires": {
+        "type-detect": "^4.0.0"
+      }
+    },
+    "deep-is": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
+      "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
+      "dev": true
+    },
+    "default-require-extensions": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz",
+      "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=",
+      "dev": true,
+      "requires": {
+        "strip-bom": "^3.0.0"
+      },
+      "dependencies": {
+        "strip-bom": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+          "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
+          "dev": true
+        }
+      }
+    },
+    "detect-indent": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.0.0.tgz",
+      "integrity": "sha512-oSyFlqaTHCItVRGK5RmrmjB+CmaMOW7IaNA/kdxqhoa6d17j/5ce9O9eWXmV/KEdRwqpQA+Vqe8a8Bsybu4YnA==",
+      "dev": true
+    },
+    "diff": {
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
+      "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
+      "dev": true
+    },
+    "dir-glob": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+      "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+      "dev": true,
+      "requires": {
+        "path-type": "^4.0.0"
+      }
+    },
+    "doctrine": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+      "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+      "dev": true,
+      "requires": {
+        "esutils": "^2.0.2"
+      }
+    },
+    "drbg.js": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz",
+      "integrity": "sha1-Pja2xCs3BDgjzbwzLVjzHiRFSAs=",
+      "requires": {
+        "browserify-aes": "^1.0.6",
+        "create-hash": "^1.1.2",
+        "create-hmac": "^1.1.4"
+      }
+    },
+    "elliptic": {
+      "version": "6.5.2",
+      "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz",
+      "integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==",
+      "requires": {
+        "bn.js": "^4.4.0",
+        "brorand": "^1.0.1",
+        "hash.js": "^1.0.0",
+        "hmac-drbg": "^1.0.0",
+        "inherits": "^2.0.1",
+        "minimalistic-assert": "^1.0.0",
+        "minimalistic-crypto-utils": "^1.0.0"
+      }
+    },
+    "emoji-regex": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
+      "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA=="
+    },
+    "encoding": {
+      "version": "0.1.12",
+      "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
+      "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
+      "requires": {
+        "iconv-lite": "~0.4.13"
+      }
+    },
+    "end-of-stream": {
+      "version": "1.4.4",
+      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+      "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+      "dev": true,
+      "requires": {
+        "once": "^1.4.0"
+      }
+    },
+    "error-ex": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+      "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+      "dev": true,
+      "requires": {
+        "is-arrayish": "^0.2.1"
+      }
+    },
+    "es5-ext": {
+      "version": "0.10.53",
+      "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz",
+      "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==",
+      "requires": {
+        "es6-iterator": "~2.0.3",
+        "es6-symbol": "~3.1.3",
+        "next-tick": "~1.0.0"
+      },
+      "dependencies": {
+        "next-tick": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
+          "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw="
+        }
+      }
+    },
+    "es6-error": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
+      "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
+      "dev": true
+    },
+    "es6-iterator": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
+      "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=",
+      "requires": {
+        "d": "1",
+        "es5-ext": "^0.10.35",
+        "es6-symbol": "^3.1.1"
+      }
+    },
+    "es6-symbol": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz",
+      "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==",
+      "requires": {
+        "d": "^1.0.1",
+        "ext": "^1.1.2"
+      }
+    },
+    "es6-weak-map": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz",
+      "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==",
+      "requires": {
+        "d": "1",
+        "es5-ext": "^0.10.46",
+        "es6-iterator": "^2.0.3",
+        "es6-symbol": "^3.1.1"
+      }
+    },
+    "escape-string-regexp": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
+    },
+    "eslint": {
+      "version": "5.16.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz",
+      "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "^7.0.0",
+        "ajv": "^6.9.1",
+        "chalk": "^2.1.0",
+        "cross-spawn": "^6.0.5",
+        "debug": "^4.0.1",
+        "doctrine": "^3.0.0",
+        "eslint-scope": "^4.0.3",
+        "eslint-utils": "^1.3.1",
+        "eslint-visitor-keys": "^1.0.0",
+        "espree": "^5.0.1",
+        "esquery": "^1.0.1",
+        "esutils": "^2.0.2",
+        "file-entry-cache": "^5.0.1",
+        "functional-red-black-tree": "^1.0.1",
+        "glob": "^7.1.2",
+        "globals": "^11.7.0",
+        "ignore": "^4.0.6",
+        "import-fresh": "^3.0.0",
+        "imurmurhash": "^0.1.4",
+        "inquirer": "^6.2.2",
+        "js-yaml": "^3.13.0",
+        "json-stable-stringify-without-jsonify": "^1.0.1",
+        "levn": "^0.3.0",
+        "lodash": "^4.17.11",
+        "minimatch": "^3.0.4",
+        "mkdirp": "^0.5.1",
+        "natural-compare": "^1.4.0",
+        "optionator": "^0.8.2",
+        "path-is-inside": "^1.0.2",
+        "progress": "^2.0.0",
+        "regexpp": "^2.0.1",
+        "semver": "^5.5.1",
+        "strip-ansi": "^4.0.0",
+        "strip-json-comments": "^2.0.1",
+        "table": "^5.2.3",
+        "text-table": "^0.2.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+          "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+          "dev": true
+        },
+        "cli-cursor": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
+          "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=",
+          "dev": true,
+          "requires": {
+            "restore-cursor": "^2.0.0"
+          }
+        },
+        "figures": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
+          "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=",
+          "dev": true,
+          "requires": {
+            "escape-string-regexp": "^1.0.5"
+          }
+        },
+        "ignore": {
+          "version": "4.0.6",
+          "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
+          "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
+          "dev": true
+        },
+        "inquirer": {
+          "version": "6.5.2",
+          "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz",
+          "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==",
+          "dev": true,
+          "requires": {
+            "ansi-escapes": "^3.2.0",
+            "chalk": "^2.4.2",
+            "cli-cursor": "^2.1.0",
+            "cli-width": "^2.0.0",
+            "external-editor": "^3.0.3",
+            "figures": "^2.0.0",
+            "lodash": "^4.17.12",
+            "mute-stream": "0.0.7",
+            "run-async": "^2.2.0",
+            "rxjs": "^6.4.0",
+            "string-width": "^2.1.0",
+            "strip-ansi": "^5.1.0",
+            "through": "^2.3.6"
+          },
+          "dependencies": {
+            "ansi-regex": {
+              "version": "4.1.0",
+              "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+              "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+              "dev": true
+            },
+            "strip-ansi": {
+              "version": "5.2.0",
+              "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+              "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+              "dev": true,
+              "requires": {
+                "ansi-regex": "^4.1.0"
+              }
+            }
+          }
+        },
+        "mimic-fn": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
+          "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==",
+          "dev": true
+        },
+        "mute-stream": {
+          "version": "0.0.7",
+          "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
+          "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=",
+          "dev": true
+        },
+        "onetime": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz",
+          "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=",
+          "dev": true,
+          "requires": {
+            "mimic-fn": "^1.0.0"
+          }
+        },
+        "restore-cursor": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
+          "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=",
+          "dev": true,
+          "requires": {
+            "onetime": "^2.0.0",
+            "signal-exit": "^3.0.2"
+          }
+        },
+        "strip-ansi": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+          "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^3.0.0"
+          }
+        }
+      }
+    },
+    "eslint-ast-utils": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/eslint-ast-utils/-/eslint-ast-utils-1.1.0.tgz",
+      "integrity": "sha512-otzzTim2/1+lVrlH19EfQQJEhVJSu0zOb9ygb3iapN6UlyaDtyRq4b5U1FuW0v1lRa9Fp/GJyHkSwm6NqABgCA==",
+      "dev": true,
+      "requires": {
+        "lodash.get": "^4.4.2",
+        "lodash.zip": "^4.2.0"
+      }
+    },
+    "eslint-config-oclif": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/eslint-config-oclif/-/eslint-config-oclif-3.1.0.tgz",
+      "integrity": "sha512-Tqgy43cNXsSdhTLWW4RuDYGFhV240sC4ISSv/ZiUEg/zFxExSEUpRE6J+AGnkKY9dYwIW4C9b2YSUVv8z/miMA==",
+      "dev": true,
+      "requires": {
+        "eslint-config-xo-space": "^0.20.0",
+        "eslint-plugin-mocha": "^5.2.0",
+        "eslint-plugin-node": "^7.0.1",
+        "eslint-plugin-unicorn": "^6.0.1"
+      }
+    },
+    "eslint-config-oclif-typescript": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/eslint-config-oclif-typescript/-/eslint-config-oclif-typescript-0.1.0.tgz",
+      "integrity": "sha512-BjXNJcH2F02MdaSFml9vJskviUFVkLHbTPGM5tinIt98H6klFNKP7/lQ+fB/Goc2wB45usEuuw6+l/fwAv9i7g==",
+      "dev": true,
+      "requires": {
+        "@typescript-eslint/eslint-plugin": "^2.6.1",
+        "@typescript-eslint/parser": "^2.6.1",
+        "eslint-config-oclif": "^3.1.0",
+        "eslint-config-xo-space": "^0.20.0",
+        "eslint-plugin-mocha": "^5.2.0",
+        "eslint-plugin-node": "^7.0.1",
+        "eslint-plugin-unicorn": "^6.0.1"
+      }
+    },
+    "eslint-config-xo": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/eslint-config-xo/-/eslint-config-xo-0.24.2.tgz",
+      "integrity": "sha512-ivQ7qISScW6gfBp+p31nQntz1rg34UCybd3uvlngcxt5Utsf4PMMi9QoAluLFcPUM5Tvqk4JGraR9qu3msKPKQ==",
+      "dev": true
+    },
+    "eslint-config-xo-space": {
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/eslint-config-xo-space/-/eslint-config-xo-space-0.20.0.tgz",
+      "integrity": "sha512-bOsoZA8M6v1HviDUIGVq1fLVnSu3mMZzn85m2tqKb73tSzu4GKD4Jd2Py4ZKjCgvCbRRByEB5HPC3fTMnnJ1uw==",
+      "dev": true,
+      "requires": {
+        "eslint-config-xo": "^0.24.0"
+      }
+    },
+    "eslint-plugin-es": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-1.4.1.tgz",
+      "integrity": "sha512-5fa/gR2yR3NxQf+UXkeLeP8FBBl6tSgdrAz1+cF84v1FMM4twGwQoqTnn+QxFLcPOrF4pdKEJKDB/q9GoyJrCA==",
+      "dev": true,
+      "requires": {
+        "eslint-utils": "^1.4.2",
+        "regexpp": "^2.0.1"
+      }
+    },
+    "eslint-plugin-mocha": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-5.3.0.tgz",
+      "integrity": "sha512-3uwlJVLijjEmBeNyH60nzqgA1gacUWLUmcKV8PIGNvj1kwP/CTgAWQHn2ayyJVwziX+KETkr9opNwT1qD/RZ5A==",
+      "dev": true,
+      "requires": {
+        "ramda": "^0.26.1"
+      }
+    },
+    "eslint-plugin-node": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-7.0.1.tgz",
+      "integrity": "sha512-lfVw3TEqThwq0j2Ba/Ckn2ABdwmL5dkOgAux1rvOk6CO7A6yGyPI2+zIxN6FyNkp1X1X/BSvKOceD6mBWSj4Yw==",
+      "dev": true,
+      "requires": {
+        "eslint-plugin-es": "^1.3.1",
+        "eslint-utils": "^1.3.1",
+        "ignore": "^4.0.2",
+        "minimatch": "^3.0.4",
+        "resolve": "^1.8.1",
+        "semver": "^5.5.0"
+      },
+      "dependencies": {
+        "ignore": {
+          "version": "4.0.6",
+          "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
+          "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
+          "dev": true
+        }
+      }
+    },
+    "eslint-plugin-unicorn": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-6.0.1.tgz",
+      "integrity": "sha512-hjy9LhTdtL7pz8WTrzS0CGXRkWK3VAPLDjihofj8JC+uxQLfXm0WwZPPPB7xKmcjRyoH+jruPHOCrHNEINpG/Q==",
+      "dev": true,
+      "requires": {
+        "clean-regexp": "^1.0.0",
+        "eslint-ast-utils": "^1.0.0",
+        "import-modules": "^1.1.0",
+        "lodash.camelcase": "^4.1.1",
+        "lodash.kebabcase": "^4.0.1",
+        "lodash.snakecase": "^4.0.1",
+        "lodash.upperfirst": "^4.2.0",
+        "safe-regex": "^1.1.0"
+      }
+    },
+    "eslint-scope": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz",
+      "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==",
+      "dev": true,
+      "requires": {
+        "esrecurse": "^4.1.0",
+        "estraverse": "^4.1.1"
+      }
+    },
+    "eslint-utils": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz",
+      "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==",
+      "dev": true,
+      "requires": {
+        "eslint-visitor-keys": "^1.1.0"
+      }
+    },
+    "eslint-visitor-keys": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz",
+      "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==",
+      "dev": true
+    },
+    "espree": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz",
+      "integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==",
+      "dev": true,
+      "requires": {
+        "acorn": "^6.0.7",
+        "acorn-jsx": "^5.0.0",
+        "eslint-visitor-keys": "^1.0.0"
+      }
+    },
+    "esprima": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+      "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
+    },
+    "esquery": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.2.0.tgz",
+      "integrity": "sha512-weltsSqdeWIX9G2qQZz7KlTRJdkkOCTPgLYJUz1Hacf48R4YOwGPHO3+ORfWedqJKbq5WQmsgK90n+pFLIKt/Q==",
+      "dev": true,
+      "requires": {
+        "estraverse": "^5.0.0"
+      },
+      "dependencies": {
+        "estraverse": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.0.0.tgz",
+          "integrity": "sha512-j3acdrMzqrxmJTNj5dbr1YbjacrYgAxVMeF0gK16E3j494mOe7xygM/ZLIguEQ0ETwAg2hlJCtHRGav+y0Ny5A==",
+          "dev": true
+        }
+      }
+    },
+    "esrecurse": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz",
+      "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==",
+      "dev": true,
+      "requires": {
+        "estraverse": "^4.1.0"
+      }
+    },
+    "estraverse": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+      "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+      "dev": true
+    },
+    "esutils": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+      "dev": true
+    },
+    "event-emitter": {
+      "version": "0.3.5",
+      "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
+      "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=",
+      "requires": {
+        "d": "1",
+        "es5-ext": "~0.10.14"
+      }
+    },
+    "eventemitter3": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz",
+      "integrity": "sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg=="
+    },
+    "evp_bytestokey": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
+      "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
+      "requires": {
+        "md5.js": "^1.3.4",
+        "safe-buffer": "^5.1.1"
+      }
+    },
+    "execa": {
+      "version": "0.10.0",
+      "resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz",
+      "integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==",
+      "dev": true,
+      "requires": {
+        "cross-spawn": "^6.0.0",
+        "get-stream": "^3.0.0",
+        "is-stream": "^1.1.0",
+        "npm-run-path": "^2.0.0",
+        "p-finally": "^1.0.0",
+        "signal-exit": "^3.0.0",
+        "strip-eof": "^1.0.0"
+      },
+      "dependencies": {
+        "get-stream": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
+          "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=",
+          "dev": true
+        }
+      }
+    },
+    "ext": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz",
+      "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==",
+      "requires": {
+        "type": "^2.0.0"
+      },
+      "dependencies": {
+        "type": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/type/-/type-2.0.0.tgz",
+          "integrity": "sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow=="
+        }
+      }
+    },
+    "external-editor": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
+      "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
+      "requires": {
+        "chardet": "^0.7.0",
+        "iconv-lite": "^0.4.24",
+        "tmp": "^0.0.33"
+      },
+      "dependencies": {
+        "tmp": {
+          "version": "0.0.33",
+          "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
+          "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
+          "requires": {
+            "os-tmpdir": "~1.0.2"
+          }
+        }
+      }
+    },
+    "extract-stack": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/extract-stack/-/extract-stack-1.0.0.tgz",
+      "integrity": "sha1-uXrK+UQe6iMyUpYktzL8WhyBZfo="
+    },
+    "fancy-test": {
+      "version": "1.4.7",
+      "resolved": "https://registry.npmjs.org/fancy-test/-/fancy-test-1.4.7.tgz",
+      "integrity": "sha512-drgNrpNbvXXbPAz0rn7jvzjoEihDKpm1fFF+aZ+FVLatjE3jZSc6WwfgC5x7N/+nhmentMx4TXPQ0OkS0SElVQ==",
+      "dev": true,
+      "requires": {
+        "@types/chai": "*",
+        "@types/lodash": "*",
+        "@types/mocha": "*",
+        "@types/node": "*",
+        "@types/sinon": "*",
+        "lodash": "^4.17.13",
+        "mock-stdin": "^0.3.1",
+        "stdout-stderr": "^0.1.9"
+      }
+    },
+    "fast-deep-equal": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz",
+      "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA=="
+    },
+    "fast-glob": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.2.tgz",
+      "integrity": "sha512-UDV82o4uQyljznxwMxyVRJgZZt3O5wENYojjzbaGEGZgeOxkLFf+V4cnUD+krzb2F72E18RhamkMZ7AdeggF7A==",
+      "dev": true,
+      "requires": {
+        "@nodelib/fs.stat": "^2.0.2",
+        "@nodelib/fs.walk": "^1.2.3",
+        "glob-parent": "^5.1.0",
+        "merge2": "^1.3.0",
+        "micromatch": "^4.0.2",
+        "picomatch": "^2.2.1"
+      }
+    },
+    "fast-json-stable-stringify": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
+    },
+    "fast-levenshtein": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+      "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
+      "dev": true
+    },
+    "fastq": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.7.0.tgz",
+      "integrity": "sha512-YOadQRnHd5q6PogvAR/x62BGituF2ufiEA6s8aavQANw5YKHERI4AREboX6KotzP8oX2klxYF2wcV/7bn1clfQ==",
+      "dev": true,
+      "requires": {
+        "reusify": "^1.0.4"
+      }
+    },
+    "figures": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
+      "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
+      "requires": {
+        "escape-string-regexp": "^1.0.5"
+      }
+    },
+    "file-entry-cache": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz",
+      "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==",
+      "dev": true,
+      "requires": {
+        "flat-cache": "^2.0.1"
+      }
+    },
+    "file-uri-to-path": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+      "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="
+    },
+    "fill-range": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+      "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+      "dev": true,
+      "requires": {
+        "to-regex-range": "^5.0.1"
+      }
+    },
+    "find-cache-dir": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz",
+      "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==",
+      "dev": true,
+      "requires": {
+        "commondir": "^1.0.1",
+        "make-dir": "^2.0.0",
+        "pkg-dir": "^3.0.0"
+      },
+      "dependencies": {
+        "find-up": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+          "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+          "dev": true,
+          "requires": {
+            "locate-path": "^3.0.0"
+          }
+        },
+        "locate-path": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+          "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+          "dev": true,
+          "requires": {
+            "p-locate": "^3.0.0",
+            "path-exists": "^3.0.0"
+          }
+        },
+        "make-dir": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
+          "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
+          "dev": true,
+          "requires": {
+            "pify": "^4.0.1",
+            "semver": "^5.6.0"
+          }
+        },
+        "p-locate": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+          "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+          "dev": true,
+          "requires": {
+            "p-limit": "^2.0.0"
+          }
+        },
+        "path-exists": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+          "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+          "dev": true
+        },
+        "pkg-dir": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz",
+          "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==",
+          "dev": true,
+          "requires": {
+            "find-up": "^3.0.0"
+          }
+        }
+      }
+    },
+    "find-up": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+      "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+      "dev": true,
+      "requires": {
+        "locate-path": "^5.0.0",
+        "path-exists": "^4.0.0"
+      }
+    },
+    "flat-cache": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz",
+      "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==",
+      "dev": true,
+      "requires": {
+        "flatted": "^2.0.0",
+        "rimraf": "2.6.3",
+        "write": "1.0.3"
+      },
+      "dependencies": {
+        "rimraf": {
+          "version": "2.6.3",
+          "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
+          "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
+          "dev": true,
+          "requires": {
+            "glob": "^7.1.3"
+          }
+        }
+      }
+    },
+    "flatted": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz",
+      "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==",
+      "dev": true
+    },
+    "foreground-child": {
+      "version": "1.5.6",
+      "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-1.5.6.tgz",
+      "integrity": "sha1-T9ca0t/elnibmApcCilZN8svXOk=",
+      "dev": true,
+      "requires": {
+        "cross-spawn": "^4",
+        "signal-exit": "^3.0.0"
+      },
+      "dependencies": {
+        "cross-spawn": {
+          "version": "4.0.2",
+          "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz",
+          "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=",
+          "dev": true,
+          "requires": {
+            "lru-cache": "^4.0.1",
+            "which": "^1.2.9"
+          }
+        }
+      }
+    },
+    "fs-constants": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+      "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+      "dev": true
+    },
+    "fs-extra": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
+      "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
+      "requires": {
+        "graceful-fs": "^4.1.2",
+        "jsonfile": "^4.0.0",
+        "universalify": "^0.1.0"
+      }
+    },
+    "fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+      "dev": true
+    },
+    "functional-red-black-tree": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
+      "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
+      "dev": true
+    },
+    "get-caller-file": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+      "dev": true
+    },
+    "get-func-name": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
+      "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=",
+      "dev": true
+    },
+    "get-stream": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz",
+      "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==",
+      "dev": true,
+      "requires": {
+        "pump": "^3.0.0"
+      }
+    },
+    "github-slugger": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.3.0.tgz",
+      "integrity": "sha512-gwJScWVNhFYSRDvURk/8yhcFBee6aFjye2a7Lhb2bUyRulpIoek9p0I9Kt7PT67d/nUlZbFu8L9RLiA0woQN8Q==",
+      "dev": true,
+      "requires": {
+        "emoji-regex": ">=6.0.0 <=6.1.1"
+      },
+      "dependencies": {
+        "emoji-regex": {
+          "version": "6.1.1",
+          "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.1.1.tgz",
+          "integrity": "sha1-xs0OwbBkLio8Z6ETfvxeeW2k+I4=",
+          "dev": true
+        }
+      }
+    },
+    "glob": {
+      "version": "7.1.6",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
+      "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
+      "dev": true,
+      "requires": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.0.4",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      }
+    },
+    "glob-parent": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
+      "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
+      "dev": true,
+      "requires": {
+        "is-glob": "^4.0.1"
+      }
+    },
+    "globals": {
+      "version": "11.12.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
+      "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+      "dev": true
+    },
+    "globby": {
+      "version": "10.0.2",
+      "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.2.tgz",
+      "integrity": "sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==",
+      "dev": true,
+      "requires": {
+        "@types/glob": "^7.1.1",
+        "array-union": "^2.1.0",
+        "dir-glob": "^3.0.1",
+        "fast-glob": "^3.0.3",
+        "glob": "^7.1.3",
+        "ignore": "^5.1.1",
+        "merge2": "^1.2.3",
+        "slash": "^3.0.0"
+      }
+    },
+    "graceful-fs": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz",
+      "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ=="
+    },
+    "growl": {
+      "version": "1.10.5",
+      "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
+      "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==",
+      "dev": true
+    },
+    "has-flag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+      "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
+    },
+    "hash-base": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz",
+      "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=",
+      "requires": {
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "hash.js": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
+      "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
+      "requires": {
+        "inherits": "^2.0.3",
+        "minimalistic-assert": "^1.0.1"
+      }
+    },
+    "hasha": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/hasha/-/hasha-3.0.0.tgz",
+      "integrity": "sha1-UqMvq4Vp1BymmmH/GiFPjrfIvTk=",
+      "dev": true,
+      "requires": {
+        "is-stream": "^1.0.1"
+      }
+    },
+    "he": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
+      "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=",
+      "dev": true
+    },
+    "hmac-drbg": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
+      "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
+      "requires": {
+        "hash.js": "^1.0.3",
+        "minimalistic-assert": "^1.0.0",
+        "minimalistic-crypto-utils": "^1.0.1"
+      }
+    },
+    "hosted-git-info": {
+      "version": "2.8.8",
+      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
+      "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
+      "dev": true
+    },
+    "html-escaper": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+      "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+      "dev": true
+    },
+    "http-call": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/http-call/-/http-call-5.3.0.tgz",
+      "integrity": "sha512-ahwimsC23ICE4kPl9xTBjKB4inbRaeLyZeRunC/1Jy/Z6X8tv22MEAjK+KBOMSVLaqXPTTmd8638waVIKLGx2w==",
+      "dev": true,
+      "requires": {
+        "content-type": "^1.0.4",
+        "debug": "^4.1.1",
+        "is-retry-allowed": "^1.1.0",
+        "is-stream": "^2.0.0",
+        "parse-json": "^4.0.0",
+        "tunnel-agent": "^0.6.0"
+      },
+      "dependencies": {
+        "is-stream": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz",
+          "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==",
+          "dev": true
+        }
+      }
+    },
+    "hyperlinker": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/hyperlinker/-/hyperlinker-1.0.0.tgz",
+      "integrity": "sha512-Ty8UblRWFEcfSuIaajM34LdPXIhbs1ajEX/BBPv24J+enSVaEVY63xQ6lTO9VRYS5LAoghIG0IDJ+p+IPzKUQQ=="
+    },
+    "iconv-lite": {
+      "version": "0.4.24",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+      "requires": {
+        "safer-buffer": ">= 2.1.2 < 3"
+      }
+    },
+    "ieee754": {
+      "version": "1.1.13",
+      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
+      "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==",
+      "dev": true
+    },
+    "ignore": {
+      "version": "5.1.4",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz",
+      "integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==",
+      "dev": true
+    },
+    "import-fresh": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz",
+      "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==",
+      "dev": true,
+      "requires": {
+        "parent-module": "^1.0.0",
+        "resolve-from": "^4.0.0"
+      }
+    },
+    "import-modules": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/import-modules/-/import-modules-1.1.0.tgz",
+      "integrity": "sha1-dI23nFzEK7lwHvq0JPiU5yYA6dw=",
+      "dev": true
+    },
+    "imurmurhash": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+      "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
+      "dev": true
+    },
+    "indent-string": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz",
+      "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok="
+    },
+    "inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+      "dev": true,
+      "requires": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+    },
+    "inquirer": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz",
+      "integrity": "sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==",
+      "requires": {
+        "ansi-escapes": "^4.2.1",
+        "chalk": "^3.0.0",
+        "cli-cursor": "^3.1.0",
+        "cli-width": "^2.0.0",
+        "external-editor": "^3.0.3",
+        "figures": "^3.0.0",
+        "lodash": "^4.17.15",
+        "mute-stream": "0.0.8",
+        "run-async": "^2.4.0",
+        "rxjs": "^6.5.3",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0",
+        "through": "^2.3.6"
+      },
+      "dependencies": {
+        "ansi-escapes": {
+          "version": "4.3.1",
+          "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz",
+          "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==",
+          "requires": {
+            "type-fest": "^0.11.0"
+          }
+        },
+        "ansi-regex": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+          "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg=="
+        },
+        "ansi-styles": {
+          "version": "4.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
+          "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
+          "requires": {
+            "@types/color-name": "^1.1.1",
+            "color-convert": "^2.0.1"
+          }
+        },
+        "chalk": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+          "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+          "requires": {
+            "ansi-styles": "^4.1.0",
+            "supports-color": "^7.1.0"
+          }
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+          "requires": {
+            "color-name": "~1.1.4"
+          }
+        },
+        "color-name": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+        },
+        "emoji-regex": {
+          "version": "8.0.0",
+          "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+          "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+        },
+        "has-flag": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
+        },
+        "is-fullwidth-code-point": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+          "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
+        },
+        "string-width": {
+          "version": "4.2.0",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
+          "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
+          "requires": {
+            "emoji-regex": "^8.0.0",
+            "is-fullwidth-code-point": "^3.0.0",
+            "strip-ansi": "^6.0.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+          "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+          "requires": {
+            "ansi-regex": "^5.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "7.1.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
+          "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
+          "requires": {
+            "has-flag": "^4.0.0"
+          }
+        },
+        "type-fest": {
+          "version": "0.11.0",
+          "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz",
+          "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ=="
+        }
+      }
+    },
+    "ip-regex": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.1.0.tgz",
+      "integrity": "sha512-pKnZpbgCTfH/1NLIlOduP/V+WRXzC2MOz3Qo8xmxk8C5GudJLgK5QyLVXOSWy3ParAH7Eemurl3xjv/WXYFvMA=="
+    },
+    "is-arrayish": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+      "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
+      "dev": true
+    },
+    "is-buffer": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz",
+      "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A=="
+    },
+    "is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+      "dev": true
+    },
+    "is-fullwidth-code-point": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+      "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
+    },
+    "is-glob": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
+      "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
+      "dev": true,
+      "requires": {
+        "is-extglob": "^2.1.1"
+      }
+    },
+    "is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "dev": true
+    },
+    "is-plain-obj": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
+      "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
+      "dev": true
+    },
+    "is-promise": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz",
+      "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o="
+    },
+    "is-retry-allowed": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz",
+      "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==",
+      "dev": true
+    },
+    "is-stream": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
+      "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
+    },
+    "is-typedarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+      "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
+    },
+    "is-wsl": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz",
+      "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0="
+    },
+    "isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
+    },
+    "isomorphic-fetch": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz",
+      "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=",
+      "requires": {
+        "node-fetch": "^1.0.1",
+        "whatwg-fetch": ">=0.10.0"
+      }
+    },
+    "istanbul-lib-coverage": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz",
+      "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==",
+      "dev": true
+    },
+    "istanbul-lib-hook": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-2.0.7.tgz",
+      "integrity": "sha512-vrRztU9VRRFDyC+aklfLoeXyNdTfga2EI3udDGn4cZ6fpSXpHLV9X6CHvfoMCPtggg8zvDDmC4b9xfu0z6/llA==",
+      "dev": true,
+      "requires": {
+        "append-transform": "^1.0.0"
+      }
+    },
+    "istanbul-lib-instrument": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz",
+      "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==",
+      "dev": true,
+      "requires": {
+        "@babel/generator": "^7.4.0",
+        "@babel/parser": "^7.4.3",
+        "@babel/template": "^7.4.0",
+        "@babel/traverse": "^7.4.3",
+        "@babel/types": "^7.4.0",
+        "istanbul-lib-coverage": "^2.0.5",
+        "semver": "^6.0.0"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "6.3.0",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+          "dev": true
+        }
+      }
+    },
+    "istanbul-lib-report": {
+      "version": "2.0.8",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz",
+      "integrity": "sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==",
+      "dev": true,
+      "requires": {
+        "istanbul-lib-coverage": "^2.0.5",
+        "make-dir": "^2.1.0",
+        "supports-color": "^6.1.0"
+      },
+      "dependencies": {
+        "make-dir": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
+          "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
+          "dev": true,
+          "requires": {
+            "pify": "^4.0.1",
+            "semver": "^5.6.0"
+          }
+        },
+        "supports-color": {
+          "version": "6.1.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
+          "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^3.0.0"
+          }
+        }
+      }
+    },
+    "istanbul-lib-source-maps": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz",
+      "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==",
+      "dev": true,
+      "requires": {
+        "debug": "^4.1.1",
+        "istanbul-lib-coverage": "^2.0.5",
+        "make-dir": "^2.1.0",
+        "rimraf": "^2.6.3",
+        "source-map": "^0.6.1"
+      },
+      "dependencies": {
+        "make-dir": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
+          "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
+          "dev": true,
+          "requires": {
+            "pify": "^4.0.1",
+            "semver": "^5.6.0"
+          }
+        },
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "istanbul-reports": {
+      "version": "2.2.7",
+      "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.7.tgz",
+      "integrity": "sha512-uu1F/L1o5Y6LzPVSVZXNOoD/KXpJue9aeLRd0sM9uMXfZvzomB0WxVamWb5ue8kA2vVWEmW7EG+A5n3f1kqHKg==",
+      "dev": true,
+      "requires": {
+        "html-escaper": "^2.0.0"
+      }
+    },
+    "js-sha3": {
+      "version": "0.8.0",
+      "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
+      "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q=="
+    },
+    "js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+      "dev": true
+    },
+    "js-yaml": {
+      "version": "3.13.1",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
+      "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+      "requires": {
+        "argparse": "^1.0.7",
+        "esprima": "^4.0.0"
+      }
+    },
+    "jsesc": {
+      "version": "2.5.2",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
+      "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
+      "dev": true
+    },
+    "json-parse-better-errors": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
+      "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
+      "dev": true
+    },
+    "json-schema-traverse": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
+    },
+    "json-stable-stringify-without-jsonify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+      "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
+      "dev": true
+    },
+    "jsonfile": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+      "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
+      "requires": {
+        "graceful-fs": "^4.1.6"
+      }
+    },
+    "levn": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
+      "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
+      "dev": true,
+      "requires": {
+        "prelude-ls": "~1.1.2",
+        "type-check": "~0.3.2"
+      }
+    },
+    "lines-and-columns": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
+      "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=",
+      "dev": true
+    },
+    "load-json-file": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-6.2.0.tgz",
+      "integrity": "sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ==",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.1.15",
+        "parse-json": "^5.0.0",
+        "strip-bom": "^4.0.0",
+        "type-fest": "^0.6.0"
+      },
+      "dependencies": {
+        "parse-json": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz",
+          "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==",
+          "dev": true,
+          "requires": {
+            "@babel/code-frame": "^7.0.0",
+            "error-ex": "^1.3.1",
+            "json-parse-better-errors": "^1.0.1",
+            "lines-and-columns": "^1.1.6"
+          }
+        }
+      }
+    },
+    "locate-path": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+      "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+      "dev": true,
+      "requires": {
+        "p-locate": "^4.1.0"
+      }
+    },
+    "lodash": {
+      "version": "4.17.15",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
+      "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
+    },
+    "lodash._reinterpolate": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz",
+      "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0="
+    },
+    "lodash.camelcase": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+      "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=",
+      "dev": true
+    },
+    "lodash.flattendeep": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz",
+      "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=",
+      "dev": true
+    },
+    "lodash.get": {
+      "version": "4.4.2",
+      "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
+      "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
+      "dev": true
+    },
+    "lodash.kebabcase": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz",
+      "integrity": "sha1-hImxyw0p/4gZXM7KRI/21swpXDY=",
+      "dev": true
+    },
+    "lodash.snakecase": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
+      "integrity": "sha1-OdcUo1NXFHg3rv1ktdy7Fr7Nj40=",
+      "dev": true
+    },
+    "lodash.template": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz",
+      "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==",
+      "requires": {
+        "lodash._reinterpolate": "^3.0.0",
+        "lodash.templatesettings": "^4.0.0"
+      }
+    },
+    "lodash.templatesettings": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz",
+      "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==",
+      "requires": {
+        "lodash._reinterpolate": "^3.0.0"
+      }
+    },
+    "lodash.upperfirst": {
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz",
+      "integrity": "sha1-E2Xt9DFIBIHvDRxolXpe2Z1J984=",
+      "dev": true
+    },
+    "lodash.zip": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz",
+      "integrity": "sha1-7GZi5IlkCO1KtsVCo5kLcswIACA=",
+      "dev": true
+    },
+    "lru-cache": {
+      "version": "4.1.5",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
+      "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
+      "dev": true,
+      "requires": {
+        "pseudomap": "^1.0.2",
+        "yallist": "^2.1.2"
+      }
+    },
+    "lru-queue": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz",
+      "integrity": "sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=",
+      "requires": {
+        "es5-ext": "~0.10.2"
+      }
+    },
+    "make-dir": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.2.tgz",
+      "integrity": "sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w==",
+      "dev": true,
+      "requires": {
+        "semver": "^6.0.0"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "6.3.0",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+          "dev": true
+        }
+      }
+    },
+    "make-error": {
+      "version": "1.3.6",
+      "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+      "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+      "dev": true
+    },
+    "md5.js": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
+      "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
+      "requires": {
+        "hash-base": "^3.0.0",
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.1.2"
+      }
+    },
+    "memoizee": {
+      "version": "0.4.14",
+      "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.14.tgz",
+      "integrity": "sha512-/SWFvWegAIYAO4NQMpcX+gcra0yEZu4OntmUdrBaWrJncxOqAziGFlHxc7yjKVK2uu3lpPW27P27wkR82wA8mg==",
+      "requires": {
+        "d": "1",
+        "es5-ext": "^0.10.45",
+        "es6-weak-map": "^2.0.2",
+        "event-emitter": "^0.3.5",
+        "is-promise": "^2.1",
+        "lru-queue": "0.1",
+        "next-tick": "1",
+        "timers-ext": "^0.1.5"
+      }
+    },
+    "merge-source-map": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz",
+      "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==",
+      "dev": true,
+      "requires": {
+        "source-map": "^0.6.1"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "merge2": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz",
+      "integrity": "sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==",
+      "dev": true
+    },
+    "micromatch": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
+      "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==",
+      "dev": true,
+      "requires": {
+        "braces": "^3.0.1",
+        "picomatch": "^2.0.5"
+      }
+    },
+    "mimic-fn": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+      "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
+    },
+    "minimalistic-assert": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
+    },
+    "minimalistic-crypto-utils": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
+      "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
+    },
+    "minimatch": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+      "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+      "dev": true,
+      "requires": {
+        "brace-expansion": "^1.1.7"
+      }
+    },
+    "minimist": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
+      "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
+      "dev": true
+    },
+    "mkdirp": {
+      "version": "0.5.5",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+      "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
+      "dev": true,
+      "requires": {
+        "minimist": "^1.2.5"
+      }
+    },
+    "mkdirp-classic": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.2.tgz",
+      "integrity": "sha512-ejdnDQcR75gwknmMw/tx02AuRs8jCtqFoFqDZMjiNxsu85sRIJVXDKHuLYvUUPRBUtV2FpSZa9bL1BUa3BdR2g==",
+      "dev": true
+    },
+    "mocha": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz",
+      "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==",
+      "dev": true,
+      "requires": {
+        "browser-stdout": "1.3.1",
+        "commander": "2.15.1",
+        "debug": "3.1.0",
+        "diff": "3.5.0",
+        "escape-string-regexp": "1.0.5",
+        "glob": "7.1.2",
+        "growl": "1.10.5",
+        "he": "1.1.1",
+        "minimatch": "3.0.4",
+        "mkdirp": "0.5.1",
+        "supports-color": "5.4.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "glob": {
+          "version": "7.1.2",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
+          "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "^1.0.0",
+            "inflight": "^1.0.4",
+            "inherits": "2",
+            "minimatch": "^3.0.4",
+            "once": "^1.3.0",
+            "path-is-absolute": "^1.0.0"
+          }
+        },
+        "minimist": {
+          "version": "0.0.8",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+          "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
+          "dev": true
+        },
+        "mkdirp": {
+          "version": "0.5.1",
+          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+          "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+          "dev": true,
+          "requires": {
+            "minimist": "0.0.8"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+          "dev": true
+        },
+        "supports-color": {
+          "version": "5.4.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
+          "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^3.0.0"
+          }
+        }
+      }
+    },
+    "mock-stdin": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/mock-stdin/-/mock-stdin-0.3.1.tgz",
+      "integrity": "sha1-xlfZZC2QeGQ1xkyl6Zu9TQm9fdM=",
+      "dev": true
+    },
+    "moment": {
+      "version": "2.24.0",
+      "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
+      "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
+    },
+    "ms": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+    },
+    "mute-stream": {
+      "version": "0.0.8",
+      "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
+      "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="
+    },
+    "nan": {
+      "version": "2.14.0",
+      "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz",
+      "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg=="
+    },
+    "natural-compare": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+      "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
+      "dev": true
+    },
+    "natural-orderby": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/natural-orderby/-/natural-orderby-2.0.3.tgz",
+      "integrity": "sha512-p7KTHxU0CUrcOXe62Zfrb5Z13nLvPhSWR/so3kFulUQU0sgUll2Z0LwpsLN351eOOD+hRGu/F1g+6xDfPeD++Q=="
+    },
+    "nested-error-stacks": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz",
+      "integrity": "sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug==",
+      "dev": true
+    },
+    "next-tick": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
+      "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
+    },
+    "nice-try": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
+      "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
+    },
+    "node-fetch": {
+      "version": "1.7.3",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
+      "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==",
+      "requires": {
+        "encoding": "^0.1.11",
+        "is-stream": "^1.0.1"
+      }
+    },
+    "normalize-package-data": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
+      "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
+      "dev": true,
+      "requires": {
+        "hosted-git-info": "^2.1.4",
+        "resolve": "^1.10.0",
+        "semver": "2 || 3 || 4 || 5",
+        "validate-npm-package-license": "^3.0.1"
+      }
+    },
+    "npm-run-path": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
+      "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=",
+      "dev": true,
+      "requires": {
+        "path-key": "^2.0.0"
+      }
+    },
+    "nyc": {
+      "version": "14.1.1",
+      "resolved": "https://registry.npmjs.org/nyc/-/nyc-14.1.1.tgz",
+      "integrity": "sha512-OI0vm6ZGUnoGZv/tLdZ2esSVzDwUC88SNs+6JoSOMVxA+gKMB8Tk7jBwgemLx4O40lhhvZCVw1C+OYLOBOPXWw==",
+      "dev": true,
+      "requires": {
+        "archy": "^1.0.0",
+        "caching-transform": "^3.0.2",
+        "convert-source-map": "^1.6.0",
+        "cp-file": "^6.2.0",
+        "find-cache-dir": "^2.1.0",
+        "find-up": "^3.0.0",
+        "foreground-child": "^1.5.6",
+        "glob": "^7.1.3",
+        "istanbul-lib-coverage": "^2.0.5",
+        "istanbul-lib-hook": "^2.0.7",
+        "istanbul-lib-instrument": "^3.3.0",
+        "istanbul-lib-report": "^2.0.8",
+        "istanbul-lib-source-maps": "^3.0.6",
+        "istanbul-reports": "^2.2.4",
+        "js-yaml": "^3.13.1",
+        "make-dir": "^2.1.0",
+        "merge-source-map": "^1.1.0",
+        "resolve-from": "^4.0.0",
+        "rimraf": "^2.6.3",
+        "signal-exit": "^3.0.2",
+        "spawn-wrap": "^1.4.2",
+        "test-exclude": "^5.2.3",
+        "uuid": "^3.3.2",
+        "yargs": "^13.2.2",
+        "yargs-parser": "^13.0.0"
+      },
+      "dependencies": {
+        "find-up": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+          "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+          "dev": true,
+          "requires": {
+            "locate-path": "^3.0.0"
+          }
+        },
+        "locate-path": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+          "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+          "dev": true,
+          "requires": {
+            "p-locate": "^3.0.0",
+            "path-exists": "^3.0.0"
+          }
+        },
+        "make-dir": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
+          "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
+          "dev": true,
+          "requires": {
+            "pify": "^4.0.1",
+            "semver": "^5.6.0"
+          }
+        },
+        "p-locate": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+          "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+          "dev": true,
+          "requires": {
+            "p-limit": "^2.0.0"
+          }
+        },
+        "path-exists": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+          "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+          "dev": true
+        }
+      }
+    },
+    "once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+      "dev": true,
+      "requires": {
+        "wrappy": "1"
+      }
+    },
+    "onetime": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz",
+      "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==",
+      "requires": {
+        "mimic-fn": "^2.1.0"
+      }
+    },
+    "optionator": {
+      "version": "0.8.3",
+      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
+      "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
+      "dev": true,
+      "requires": {
+        "deep-is": "~0.1.3",
+        "fast-levenshtein": "~2.0.6",
+        "levn": "~0.3.0",
+        "prelude-ls": "~1.1.2",
+        "type-check": "~0.3.2",
+        "word-wrap": "~1.2.3"
+      }
+    },
+    "os-homedir": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+      "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
+      "dev": true
+    },
+    "os-tmpdir": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+      "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
+    },
+    "p-finally": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
+      "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
+      "dev": true
+    },
+    "p-limit": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+      "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+      "dev": true,
+      "requires": {
+        "p-try": "^2.0.0"
+      }
+    },
+    "p-locate": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+      "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+      "dev": true,
+      "requires": {
+        "p-limit": "^2.2.0"
+      }
+    },
+    "p-try": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+      "dev": true
+    },
+    "package-hash": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-3.0.0.tgz",
+      "integrity": "sha512-lOtmukMDVvtkL84rJHI7dpTYq+0rli8N2wlnqUcBuDWCfVhRUfOmnR9SsoHFMLpACvEV60dX7rd0rFaYDZI+FA==",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.1.15",
+        "hasha": "^3.0.0",
+        "lodash.flattendeep": "^4.4.0",
+        "release-zalgo": "^1.0.0"
+      }
+    },
+    "parent-module": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+      "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+      "dev": true,
+      "requires": {
+        "callsites": "^3.0.0"
+      }
+    },
+    "parse-json": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
+      "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
+      "dev": true,
+      "requires": {
+        "error-ex": "^1.3.1",
+        "json-parse-better-errors": "^1.0.1"
+      }
+    },
+    "password-prompt": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/password-prompt/-/password-prompt-1.1.2.tgz",
+      "integrity": "sha512-bpuBhROdrhuN3E7G/koAju0WjVw9/uQOG5Co5mokNj0MiOSBVZS1JTwM4zl55hu0WFmIEFvO9cU9sJQiBIYeIA==",
+      "requires": {
+        "ansi-escapes": "^3.1.0",
+        "cross-spawn": "^6.0.5"
+      }
+    },
+    "path-exists": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+      "dev": true
+    },
+    "path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+      "dev": true
+    },
+    "path-is-inside": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
+      "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=",
+      "dev": true
+    },
+    "path-key": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
+      "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A="
+    },
+    "path-parse": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
+      "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
+      "dev": true
+    },
+    "path-type": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+      "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+      "dev": true
+    },
+    "pathval": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz",
+      "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=",
+      "dev": true
+    },
+    "pbkdf2": {
+      "version": "3.0.17",
+      "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz",
+      "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==",
+      "requires": {
+        "create-hash": "^1.1.2",
+        "create-hmac": "^1.1.4",
+        "ripemd160": "^2.0.1",
+        "safe-buffer": "^5.0.1",
+        "sha.js": "^2.4.8"
+      }
+    },
+    "picomatch": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
+      "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==",
+      "dev": true
+    },
+    "pify": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
+      "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
+      "dev": true
+    },
+    "pkg-dir": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
+      "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+      "dev": true,
+      "requires": {
+        "find-up": "^4.0.0"
+      }
+    },
+    "prelude-ls": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
+      "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=",
+      "dev": true
+    },
+    "progress": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+      "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+      "dev": true
+    },
+    "proper-lockfile": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.1.tgz",
+      "integrity": "sha512-1w6rxXodisVpn7QYvLk706mzprPTAPCYAqxMvctmPN3ekuRk/kuGkGc82pangZiAt4R3lwSuUzheTTn0/Yb7Zg==",
+      "requires": {
+        "graceful-fs": "^4.1.11",
+        "retry": "^0.12.0",
+        "signal-exit": "^3.0.2"
+      }
+    },
+    "pseudomap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
+      "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=",
+      "dev": true
+    },
+    "pump": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
+      "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
+      "dev": true,
+      "requires": {
+        "end-of-stream": "^1.1.0",
+        "once": "^1.3.1"
+      }
+    },
+    "punycode": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+      "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
+    },
+    "qqjs": {
+      "version": "0.3.11",
+      "resolved": "https://registry.npmjs.org/qqjs/-/qqjs-0.3.11.tgz",
+      "integrity": "sha512-pB2X5AduTl78J+xRSxQiEmga1jQV0j43jOPs/MTgTLApGFEOn6NgdE2dEjp7nvDtjkIOZbvFIojAiYUx6ep3zg==",
+      "dev": true,
+      "requires": {
+        "chalk": "^2.4.1",
+        "debug": "^4.1.1",
+        "execa": "^0.10.0",
+        "fs-extra": "^6.0.1",
+        "get-stream": "^5.1.0",
+        "glob": "^7.1.2",
+        "globby": "^10.0.1",
+        "http-call": "^5.1.2",
+        "load-json-file": "^6.2.0",
+        "pkg-dir": "^4.2.0",
+        "tar-fs": "^2.0.0",
+        "tmp": "^0.1.0",
+        "write-json-file": "^4.1.1"
+      },
+      "dependencies": {
+        "fs-extra": {
+          "version": "6.0.1",
+          "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.1.tgz",
+          "integrity": "sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA==",
+          "dev": true,
+          "requires": {
+            "graceful-fs": "^4.1.2",
+            "jsonfile": "^4.0.0",
+            "universalify": "^0.1.0"
+          }
+        }
+      }
+    },
+    "ramda": {
+      "version": "0.26.1",
+      "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.26.1.tgz",
+      "integrity": "sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ==",
+      "dev": true
+    },
+    "randombytes": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+      "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+      "requires": {
+        "safe-buffer": "^5.1.0"
+      }
+    },
+    "read-pkg": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
+      "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=",
+      "dev": true,
+      "requires": {
+        "load-json-file": "^4.0.0",
+        "normalize-package-data": "^2.3.2",
+        "path-type": "^3.0.0"
+      },
+      "dependencies": {
+        "load-json-file": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz",
+          "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=",
+          "dev": true,
+          "requires": {
+            "graceful-fs": "^4.1.2",
+            "parse-json": "^4.0.0",
+            "pify": "^3.0.0",
+            "strip-bom": "^3.0.0"
+          }
+        },
+        "path-type": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
+          "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==",
+          "dev": true,
+          "requires": {
+            "pify": "^3.0.0"
+          }
+        },
+        "pify": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+          "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
+          "dev": true
+        },
+        "strip-bom": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+          "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
+          "dev": true
+        }
+      }
+    },
+    "read-pkg-up": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz",
+      "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==",
+      "dev": true,
+      "requires": {
+        "find-up": "^3.0.0",
+        "read-pkg": "^3.0.0"
+      },
+      "dependencies": {
+        "find-up": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+          "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+          "dev": true,
+          "requires": {
+            "locate-path": "^3.0.0"
+          }
+        },
+        "locate-path": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+          "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+          "dev": true,
+          "requires": {
+            "p-locate": "^3.0.0",
+            "path-exists": "^3.0.0"
+          }
+        },
+        "p-locate": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+          "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+          "dev": true,
+          "requires": {
+            "p-limit": "^2.0.0"
+          }
+        },
+        "path-exists": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+          "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+          "dev": true
+        }
+      }
+    },
+    "readable-stream": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+      "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.3",
+        "string_decoder": "^1.1.1",
+        "util-deprecate": "^1.0.1"
+      }
+    },
+    "redeyed": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz",
+      "integrity": "sha1-iYS1gV2ZyyIEacme7v/jiRPmzAs=",
+      "requires": {
+        "esprima": "~4.0.0"
+      }
+    },
+    "regenerator-runtime": {
+      "version": "0.13.5",
+      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz",
+      "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA=="
+    },
+    "regexpp": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz",
+      "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==",
+      "dev": true
+    },
+    "release-zalgo": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz",
+      "integrity": "sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA=",
+      "dev": true,
+      "requires": {
+        "es6-error": "^4.0.1"
+      }
+    },
+    "replace-ext": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz",
+      "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs="
+    },
+    "require-directory": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+      "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
+      "dev": true
+    },
+    "require-main-filename": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+      "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
+      "dev": true
+    },
+    "resolve": {
+      "version": "1.15.1",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz",
+      "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==",
+      "dev": true,
+      "requires": {
+        "path-parse": "^1.0.6"
+      }
+    },
+    "resolve-from": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+      "dev": true
+    },
+    "restore-cursor": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
+      "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
+      "requires": {
+        "onetime": "^5.1.0",
+        "signal-exit": "^3.0.2"
+      }
+    },
+    "ret": {
+      "version": "0.1.15",
+      "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
+      "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==",
+      "dev": true
+    },
+    "retry": {
+      "version": "0.12.0",
+      "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
+      "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs="
+    },
+    "reusify": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+      "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+      "dev": true
+    },
+    "rimraf": {
+      "version": "2.7.1",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+      "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+      "dev": true,
+      "requires": {
+        "glob": "^7.1.3"
+      }
+    },
+    "ripemd160": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
+      "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
+      "requires": {
+        "hash-base": "^3.0.0",
+        "inherits": "^2.0.1"
+      }
+    },
+    "run-async": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.0.tgz",
+      "integrity": "sha512-xJTbh/d7Lm7SBhc1tNvTpeCHaEzoyxPrqNlvSdMfBTYwaY++UJFyXUOxAtsRUXjlqOfj8luNaR9vjCh4KeV+pg==",
+      "requires": {
+        "is-promise": "^2.1.0"
+      }
+    },
+    "run-parallel": {
+      "version": "1.1.9",
+      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz",
+      "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==",
+      "dev": true
+    },
+    "rxjs": {
+      "version": "6.5.5",
+      "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz",
+      "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==",
+      "requires": {
+        "tslib": "^1.9.0"
+      }
+    },
+    "safe-buffer": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
+      "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg=="
+    },
+    "safe-regex": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
+      "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
+      "dev": true,
+      "requires": {
+        "ret": "~0.1.10"
+      }
+    },
+    "safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+    },
+    "secp256k1": {
+      "version": "3.8.0",
+      "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-3.8.0.tgz",
+      "integrity": "sha512-k5ke5avRZbtl9Tqx/SA7CbY3NF6Ro+Sj9cZxezFzuBlLDmyqPiL8hJJ+EmzD8Ig4LUDByHJ3/iPOVoRixs/hmw==",
+      "requires": {
+        "bindings": "^1.5.0",
+        "bip66": "^1.1.5",
+        "bn.js": "^4.11.8",
+        "create-hash": "^1.2.0",
+        "drbg.js": "^1.0.1",
+        "elliptic": "^6.5.2",
+        "nan": "^2.14.0",
+        "safe-buffer": "^5.1.2"
+      }
+    },
+    "semver": {
+      "version": "5.7.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+      "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
+    },
+    "set-blocking": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+      "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
+      "dev": true
+    },
+    "sha.js": {
+      "version": "2.4.11",
+      "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
+      "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
+      "requires": {
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "shebang-command": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
+      "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
+      "requires": {
+        "shebang-regex": "^1.0.0"
+      }
+    },
+    "shebang-regex": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
+      "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM="
+    },
+    "signal-exit": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
+      "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA=="
+    },
+    "slash": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+      "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+      "dev": true
+    },
+    "slice-ansi": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz",
+      "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==",
+      "dev": true,
+      "requires": {
+        "ansi-styles": "^3.2.0",
+        "astral-regex": "^1.0.0",
+        "is-fullwidth-code-point": "^2.0.0"
+      }
+    },
+    "slug": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/slug/-/slug-2.1.1.tgz",
+      "integrity": "sha512-yNGhDdS0DR0JyxnPC84qIx/Vd01RHVY4guJeBqBNdBoOLNWnzw5zkWJvxVSmsuUb92bikdnQFnw3PfGY8uZ82g==",
+      "requires": {
+        "unicode": ">= 0.3.1"
+      }
+    },
+    "sort-keys": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-4.0.0.tgz",
+      "integrity": "sha512-hlJLzrn/VN49uyNkZ8+9b+0q9DjmmYcYOnbMQtpkLrYpPwRApDPZfmqbUfJnAA3sb/nRib+nDot7Zi/1ER1fuA==",
+      "dev": true,
+      "requires": {
+        "is-plain-obj": "^2.0.0"
+      }
+    },
+    "source-map": {
+      "version": "0.5.7",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+      "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+      "dev": true
+    },
+    "source-map-support": {
+      "version": "0.5.16",
+      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz",
+      "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==",
+      "dev": true,
+      "requires": {
+        "buffer-from": "^1.0.0",
+        "source-map": "^0.6.0"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "spawn-wrap": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-1.4.3.tgz",
+      "integrity": "sha512-IgB8md0QW/+tWqcavuFgKYR/qIRvJkRLPJDFaoXtLLUaVcCDK0+HeFTkmQHj3eprcYhc+gOl0aEA1w7qZlYezw==",
+      "dev": true,
+      "requires": {
+        "foreground-child": "^1.5.6",
+        "mkdirp": "^0.5.0",
+        "os-homedir": "^1.0.1",
+        "rimraf": "^2.6.2",
+        "signal-exit": "^3.0.2",
+        "which": "^1.3.0"
+      }
+    },
+    "spdx-correct": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz",
+      "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==",
+      "dev": true,
+      "requires": {
+        "spdx-expression-parse": "^3.0.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "spdx-exceptions": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz",
+      "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==",
+      "dev": true
+    },
+    "spdx-expression-parse": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz",
+      "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==",
+      "dev": true,
+      "requires": {
+        "spdx-exceptions": "^2.1.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "spdx-license-ids": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz",
+      "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==",
+      "dev": true
+    },
+    "sprintf-js": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+      "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
+    },
+    "stdout-stderr": {
+      "version": "0.1.13",
+      "resolved": "https://registry.npmjs.org/stdout-stderr/-/stdout-stderr-0.1.13.tgz",
+      "integrity": "sha512-Xnt9/HHHYfjZ7NeQLvuQDyL1LnbsbddgMFKCuaQKwGCdJm8LnstZIXop+uOY36UR1UXXoHXfMbC1KlVdVd2JLA==",
+      "dev": true,
+      "requires": {
+        "debug": "^4.1.1",
+        "strip-ansi": "^6.0.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+          "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+          "dev": true
+        },
+        "strip-ansi": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+          "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^5.0.0"
+          }
+        }
+      }
+    },
+    "string-width": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+      "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+      "requires": {
+        "is-fullwidth-code-point": "^2.0.0",
+        "strip-ansi": "^4.0.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+          "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg="
+        },
+        "strip-ansi": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+          "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+          "requires": {
+            "ansi-regex": "^3.0.0"
+          }
+        }
+      }
+    },
+    "string_decoder": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+      "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+      "dev": true,
+      "requires": {
+        "safe-buffer": "~5.2.0"
+      }
+    },
+    "strip-ansi": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+      "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+      "requires": {
+        "ansi-regex": "^4.1.0"
+      }
+    },
+    "strip-bom": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz",
+      "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==",
+      "dev": true
+    },
+    "strip-eof": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
+      "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=",
+      "dev": true
+    },
+    "strip-json-comments": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+      "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
+      "dev": true
+    },
+    "supports-color": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+      "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+      "requires": {
+        "has-flag": "^3.0.0"
+      }
+    },
+    "supports-hyperlinks": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-1.0.1.tgz",
+      "integrity": "sha512-HHi5kVSefKaJkGYXbDuKbUGRVxqnWGn3J2e39CYcNJEfWciGq2zYtOhXLTlvrOZW1QU7VX67w7fMmWafHX9Pfw==",
+      "requires": {
+        "has-flag": "^2.0.0",
+        "supports-color": "^5.0.0"
+      },
+      "dependencies": {
+        "has-flag": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz",
+          "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE="
+        }
+      }
+    },
+    "table": {
+      "version": "5.4.6",
+      "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz",
+      "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==",
+      "dev": true,
+      "requires": {
+        "ajv": "^6.10.2",
+        "lodash": "^4.17.14",
+        "slice-ansi": "^2.1.0",
+        "string-width": "^3.0.0"
+      },
+      "dependencies": {
+        "string-width": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+          "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+          "dev": true,
+          "requires": {
+            "emoji-regex": "^7.0.1",
+            "is-fullwidth-code-point": "^2.0.0",
+            "strip-ansi": "^5.1.0"
+          }
+        }
+      }
+    },
+    "tar-fs": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz",
+      "integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==",
+      "dev": true,
+      "requires": {
+        "chownr": "^1.1.1",
+        "mkdirp-classic": "^0.5.2",
+        "pump": "^3.0.0",
+        "tar-stream": "^2.0.0"
+      }
+    },
+    "tar-stream": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.2.tgz",
+      "integrity": "sha512-UaF6FoJ32WqALZGOIAApXx+OdxhekNMChu6axLJR85zMMjXKWFGjbIRe+J6P4UnRGg9rAwWvbTT0oI7hD/Un7Q==",
+      "dev": true,
+      "requires": {
+        "bl": "^4.0.1",
+        "end-of-stream": "^1.4.1",
+        "fs-constants": "^1.0.0",
+        "inherits": "^2.0.3",
+        "readable-stream": "^3.1.1"
+      }
+    },
+    "test-exclude": {
+      "version": "5.2.3",
+      "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz",
+      "integrity": "sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==",
+      "dev": true,
+      "requires": {
+        "glob": "^7.1.3",
+        "minimatch": "^3.0.4",
+        "read-pkg-up": "^4.0.0",
+        "require-main-filename": "^2.0.0"
+      }
+    },
+    "text-table": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+      "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
+      "dev": true
+    },
+    "through": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+      "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
+    },
+    "timers-ext": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz",
+      "integrity": "sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==",
+      "requires": {
+        "es5-ext": "~0.10.46",
+        "next-tick": "1"
+      }
+    },
+    "tmp": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz",
+      "integrity": "sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==",
+      "dev": true,
+      "requires": {
+        "rimraf": "^2.6.3"
+      }
+    },
+    "to-fast-properties": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
+      "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=",
+      "dev": true
+    },
+    "to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "dev": true,
+      "requires": {
+        "is-number": "^7.0.0"
+      }
+    },
+    "treeify": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/treeify/-/treeify-1.1.0.tgz",
+      "integrity": "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A=="
+    },
+    "ts-node": {
+      "version": "8.8.2",
+      "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.8.2.tgz",
+      "integrity": "sha512-duVj6BpSpUpD/oM4MfhO98ozgkp3Gt9qIp3jGxwU2DFvl/3IRaEAvbLa8G60uS7C77457e/m5TMowjedeRxI1Q==",
+      "dev": true,
+      "requires": {
+        "arg": "^4.1.0",
+        "diff": "^4.0.1",
+        "make-error": "^1.1.1",
+        "source-map-support": "^0.5.6",
+        "yn": "3.1.1"
+      },
+      "dependencies": {
+        "diff": {
+          "version": "4.0.2",
+          "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+          "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+          "dev": true
+        }
+      }
+    },
+    "tslib": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz",
+      "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA=="
+    },
+    "tsutils": {
+      "version": "3.17.1",
+      "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz",
+      "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==",
+      "dev": true,
+      "requires": {
+        "tslib": "^1.8.1"
+      }
+    },
+    "tunnel-agent": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+      "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
+      "dev": true,
+      "requires": {
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "tweetnacl": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
+      "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="
+    },
+    "type": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz",
+      "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg=="
+    },
+    "type-check": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
+      "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
+      "dev": true,
+      "requires": {
+        "prelude-ls": "~1.1.2"
+      }
+    },
+    "type-detect": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+      "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+      "dev": true
+    },
+    "type-fest": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz",
+      "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==",
+      "dev": true
+    },
+    "typedarray-to-buffer": {
+      "version": "3.1.5",
+      "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
+      "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
+      "requires": {
+        "is-typedarray": "^1.0.0"
+      }
+    },
+    "typescript": {
+      "version": "3.8.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz",
+      "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==",
+      "dev": true
+    },
+    "unicode": {
+      "version": "12.1.0",
+      "resolved": "https://registry.npmjs.org/unicode/-/unicode-12.1.0.tgz",
+      "integrity": "sha512-Ty6+Ew21DiYTWLYtd05RF/X4c1ekOvOgANyHbBj0h3MaXpfaGr2Rdmc0hMFuGQLyPLb9cU4ArNxl0bTF5HSzXw=="
+    },
+    "unist-util-stringify-position": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz",
+      "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==",
+      "requires": {
+        "@types/unist": "^2.0.2"
+      }
+    },
+    "universalify": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+      "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="
+    },
+    "unorm": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz",
+      "integrity": "sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA=="
+    },
+    "uri-js": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
+      "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
+      "requires": {
+        "punycode": "^2.1.0"
+      }
+    },
+    "util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
+      "dev": true
+    },
+    "uuid": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
+      "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
+      "dev": true
+    },
+    "validate-npm-package-license": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+      "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+      "dev": true,
+      "requires": {
+        "spdx-correct": "^3.0.0",
+        "spdx-expression-parse": "^3.0.0"
+      }
+    },
+    "vfile": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.1.0.tgz",
+      "integrity": "sha512-BaTPalregj++64xbGK6uIlsurN3BCRNM/P2Pg8HezlGzKd1O9PrwIac6bd9Pdx2uTb0QHoioZ+rXKolbVXEgJg==",
+      "requires": {
+        "@types/unist": "^2.0.0",
+        "is-buffer": "^2.0.0",
+        "replace-ext": "1.0.0",
+        "unist-util-stringify-position": "^2.0.0",
+        "vfile-message": "^2.0.0"
+      }
+    },
+    "vfile-message": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz",
+      "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==",
+      "requires": {
+        "@types/unist": "^2.0.0",
+        "unist-util-stringify-position": "^2.0.0"
+      }
+    },
+    "websocket": {
+      "version": "1.0.31",
+      "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.31.tgz",
+      "integrity": "sha512-VAouplvGKPiKFDTeCCO65vYHsyay8DqoBSlzIO3fayrfOgU94lQN5a1uWVnFrMLceTJw/+fQXR5PGbUVRaHshQ==",
+      "requires": {
+        "debug": "^2.2.0",
+        "es5-ext": "^0.10.50",
+        "nan": "^2.14.0",
+        "typedarray-to-buffer": "^3.1.5",
+        "yaeti": "^0.0.6"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+        }
+      }
+    },
+    "whatwg-fetch": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz",
+      "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q=="
+    },
+    "which": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+      "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+      "requires": {
+        "isexe": "^2.0.0"
+      }
+    },
+    "which-module": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
+      "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
+      "dev": true
+    },
+    "widest-line": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz",
+      "integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==",
+      "requires": {
+        "string-width": "^2.1.1"
+      }
+    },
+    "word-wrap": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
+      "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
+      "dev": true
+    },
+    "wrap-ansi": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-4.0.0.tgz",
+      "integrity": "sha512-uMTsj9rDb0/7kk1PbcbCcwvHUxp60fGDB/NNXpVa0Q+ic/e7y5+BwTxKfQ33VYgDppSwi/FBzpetYzo8s6tfbg==",
+      "requires": {
+        "ansi-styles": "^3.2.0",
+        "string-width": "^2.1.1",
+        "strip-ansi": "^4.0.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+          "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg="
+        },
+        "strip-ansi": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+          "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+          "requires": {
+            "ansi-regex": "^3.0.0"
+          }
+        }
+      }
+    },
+    "wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+      "dev": true
+    },
+    "write": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz",
+      "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==",
+      "dev": true,
+      "requires": {
+        "mkdirp": "^0.5.1"
+      }
+    },
+    "write-file-atomic": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz",
+      "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==",
+      "dev": true,
+      "requires": {
+        "imurmurhash": "^0.1.4",
+        "is-typedarray": "^1.0.0",
+        "signal-exit": "^3.0.2",
+        "typedarray-to-buffer": "^3.1.5"
+      }
+    },
+    "write-json-file": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/write-json-file/-/write-json-file-4.3.0.tgz",
+      "integrity": "sha512-PxiShnxf0IlnQuMYOPPhPkhExoCQuTUNPOa/2JWCYTmBquU9njyyDuwRKN26IZBlp4yn1nt+Agh2HOOBl+55HQ==",
+      "dev": true,
+      "requires": {
+        "detect-indent": "^6.0.0",
+        "graceful-fs": "^4.1.15",
+        "is-plain-obj": "^2.0.0",
+        "make-dir": "^3.0.0",
+        "sort-keys": "^4.0.0",
+        "write-file-atomic": "^3.0.0"
+      }
+    },
+    "xxhashjs": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/xxhashjs/-/xxhashjs-0.2.2.tgz",
+      "integrity": "sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw==",
+      "requires": {
+        "cuint": "^0.2.2"
+      }
+    },
+    "y18n": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
+      "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
+      "dev": true
+    },
+    "yaeti": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz",
+      "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc="
+    },
+    "yallist": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
+      "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
+      "dev": true
+    },
+    "yargs": {
+      "version": "13.3.2",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
+      "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
+      "dev": true,
+      "requires": {
+        "cliui": "^5.0.0",
+        "find-up": "^3.0.0",
+        "get-caller-file": "^2.0.1",
+        "require-directory": "^2.1.1",
+        "require-main-filename": "^2.0.0",
+        "set-blocking": "^2.0.0",
+        "string-width": "^3.0.0",
+        "which-module": "^2.0.0",
+        "y18n": "^4.0.0",
+        "yargs-parser": "^13.1.2"
+      },
+      "dependencies": {
+        "find-up": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+          "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+          "dev": true,
+          "requires": {
+            "locate-path": "^3.0.0"
+          }
+        },
+        "locate-path": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+          "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+          "dev": true,
+          "requires": {
+            "p-locate": "^3.0.0",
+            "path-exists": "^3.0.0"
+          }
+        },
+        "p-locate": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+          "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+          "dev": true,
+          "requires": {
+            "p-limit": "^2.0.0"
+          }
+        },
+        "path-exists": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+          "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+          "dev": true
+        },
+        "string-width": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+          "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+          "dev": true,
+          "requires": {
+            "emoji-regex": "^7.0.1",
+            "is-fullwidth-code-point": "^2.0.0",
+            "strip-ansi": "^5.1.0"
+          }
+        }
+      }
+    },
+    "yargs-parser": {
+      "version": "13.1.2",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
+      "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
+      "dev": true,
+      "requires": {
+        "camelcase": "^5.0.0",
+        "decamelize": "^1.2.0"
+      }
+    },
+    "yn": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+      "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+      "dev": true
+    }
+  }
+}

+ 90 - 0
cli/package.json

@@ -0,0 +1,90 @@
+{
+  "name": "joystream-cli",
+  "description": "Command Line Interface for Joystream community and governance activities",
+  "version": "0.0.0",
+  "author": "Leszek Wiesner",
+  "bin": {
+    "joystream-cli": "./bin/run"
+  },
+  "bugs": "https://github.com/Joystream/substrate-runtime-joystream/issues",
+  "dependencies": {
+    "@joystream/types": "^0.6.0",
+    "@oclif/command": "^1.5.19",
+    "@oclif/config": "^1.14.0",
+    "@oclif/plugin-help": "^2.2.3",
+    "@polkadot/api": "^0.96.1",
+    "@types/inquirer": "^6.5.0",
+    "@types/proper-lockfile": "^4.1.1",
+    "@types/slug": "^0.9.1",
+    "cli-ux": "^5.4.5",
+    "inquirer": "^7.1.0",
+    "moment": "^2.24.0",
+    "proper-lockfile": "^4.1.1",
+    "slug": "^2.1.1",
+    "tslib": "^1.11.1"
+  },
+  "devDependencies": {
+    "@oclif/dev-cli": "^1.22.2",
+    "@oclif/test": "^1.2.5",
+    "@types/chai": "^4.2.11",
+    "@types/mocha": "^5.2.7",
+    "@types/node": "^10.17.18",
+    "chai": "^4.2.0",
+    "eslint": "^5.16.0",
+    "eslint-config-oclif": "^3.1.0",
+    "eslint-config-oclif-typescript": "^0.1.0",
+    "globby": "^10.0.2",
+    "mocha": "^5.2.0",
+    "nyc": "^14.1.1",
+    "ts-node": "^8.8.2",
+    "typescript": "^3.8.3",
+    "@polkadot/ts": "^0.1.56"
+  },
+  "engines": {
+    "node": ">=8.0.0"
+  },
+  "files": [
+    "/bin",
+    "/lib",
+    "/npm-shrinkwrap.json",
+    "/oclif.manifest.json"
+  ],
+  "homepage": "https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli",
+  "keywords": [
+    "oclif"
+  ],
+  "license": "MIT",
+  "main": "lib/index.js",
+  "oclif": {
+    "repositoryPrefix": "<%- repo %>/blob/master/cli/<%- commandPath %>",
+    "commands": "./lib/commands",
+    "bin": "joystream-cli",
+    "plugins": [
+      "@oclif/plugin-help"
+    ],
+    "topics": {
+      "council": {
+        "description": "Council-related information and activities like voting, becoming part of the council etc."
+      },
+      "account": {
+        "description": "Accounts management - create, import or switch currently used account"
+      },
+      "api": {
+        "description": "Inspect the substrate node api, perform lower-level api calls or change the current api provider uri"
+      }
+    }
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/Joystream/substrate-runtime-joystream",
+    "directory": "cli"
+  },
+  "scripts": {
+    "postpack": "rm -f oclif.manifest.json",
+    "posttest": "eslint . --ext .ts --config .eslintrc",
+    "prepack": "rm -rf lib && tsc -b && oclif-dev manifest && oclif-dev readme",
+    "test": "nyc --extension .ts mocha --forbid-only \"test/**/*.test.ts\"",
+    "version": "oclif-dev readme && git add README.md"
+  },
+  "types": "lib/index.d.ts"
+}

+ 114 - 0
cli/src/Api.ts

@@ -0,0 +1,114 @@
+import BN from 'bn.js';
+import { registerJoystreamTypes } from '@joystream/types';
+import { ApiPromise, WsProvider } from '@polkadot/api';
+import { QueryableStorageMultiArg } from '@polkadot/api/types';
+import { formatBalance } from '@polkadot/util';
+import { Hash } from '@polkadot/types/interfaces';
+import { KeyringPair } from '@polkadot/keyring/types';
+import { Codec } from '@polkadot/types/types';
+import { AccountSummary, CouncilInfoObj, CouncilInfoTuple, createCouncilInfoObj } from './Types';
+import { DerivedFees, DerivedBalances } from '@polkadot/api-derive/types';
+import { CLIError } from '@oclif/errors';
+import ExitCodes from './ExitCodes';
+
+export const DEFAULT_API_URI = 'wss://rome-rpc-endpoint.joystream.org:9944/';
+export const TOKEN_SYMBOL = 'JOY';
+
+// Api wrapper for handling most common api calls and allowing easy API implementation switch in the future
+
+export default class Api {
+    private _api: ApiPromise;
+
+    private constructor(originalApi:ApiPromise) {
+        this._api = originalApi;
+    }
+
+    public getOriginalApi(): ApiPromise {
+        return this._api;
+    }
+
+    private static async initApi(apiUri: string = DEFAULT_API_URI): Promise<ApiPromise> {
+        formatBalance.setDefaults({ unit: TOKEN_SYMBOL });
+        const wsProvider:WsProvider = new WsProvider(apiUri);
+        registerJoystreamTypes();
+
+        return await ApiPromise.create({ provider: wsProvider });
+    }
+
+    static async create(apiUri: string = DEFAULT_API_URI): Promise<Api> {
+        const originalApi: ApiPromise = await Api.initApi(apiUri);
+        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 }
+        );
+        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[]> {
+        let accountsBalances: DerivedBalances[] = await this._api.derive.balances.votingBalances(accountAddresses);
+
+        return accountsBalances;
+    }
+
+    // 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];
+        // TODO: Some more information can be fetched here in the future
+
+        return { balances };
+    }
+
+    async getCouncilInfo(): Promise<CouncilInfoObj> {
+        const queries: { [P in keyof CouncilInfoObj]: QueryableStorageMultiArg<"promise"> } = {
+            activeCouncil:    this._api.query.council.activeCouncil,
+            termEndsAt:       this._api.query.council.termEndsAt,
+            autoStart:        this._api.query.councilElection.autoStart,
+            newTermDuration:  this._api.query.councilElection.newTermDuration,
+            candidacyLimit:   this._api.query.councilElection.candidacyLimit,
+            councilSize:      this._api.query.councilElection.councilSize,
+            minCouncilStake:  this._api.query.councilElection.minCouncilStake,
+            minVotingStake:   this._api.query.councilElection.minVotingStake,
+            announcingPeriod: this._api.query.councilElection.announcingPeriod,
+            votingPeriod:     this._api.query.councilElection.votingPeriod,
+            revealingPeriod:  this._api.query.councilElection.revealingPeriod,
+            round:            this._api.query.councilElection.round,
+            stage:            this._api.query.councilElection.stage
+        }
+        const results: CouncilInfoTuple = <CouncilInfoTuple> await this.queryMultiOnce(Object.values(queries));
+
+        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 transfer(account: KeyringPair, recipientAddr: string, amount: BN): Promise<Hash> {
+        const txHash = await this._api.tx.balances
+            .transfer(recipientAddr, amount)
+            .signAndSend(account);
+        return txHash;
+    }
+}

+ 14 - 0
cli/src/ExitCodes.ts

@@ -0,0 +1,14 @@
+enum ExitCodes {
+    OK = 0,
+
+    InvalidInput = 400,
+    FileNotFound = 401,
+    InvalidFile = 402,
+    NoAccountFound = 403,
+    NoAccountSelected = 404,
+
+    UnexpectedException = 500,
+    FsOperationFailed = 501,
+    ApiError = 502,
+}
+export = ExitCodes;

+ 63 - 0
cli/src/Types.ts

@@ -0,0 +1,63 @@
+import BN from 'bn.js';
+import { ElectionStage, Seat } from '@joystream/types';
+import { Option } from '@polkadot/types';
+import { BlockNumber, Balance } from '@polkadot/types/interfaces';
+import { DerivedBalances } from '@polkadot/api-derive/types';
+import { KeyringPair } from '@polkadot/keyring/types';
+
+// KeyringPair type extended with mandatory "meta.name"
+// It's used for accounts/keys management within CLI.
+// If not provided in the account json file, the meta.name value is set to "Unnamed Account"
+export type NamedKeyringPair = KeyringPair & {
+    meta: {
+        name: string
+    }
+}
+
+// Summary of the account information fetched from the api for "account:current" purposes (currently just balances)
+export type AccountSummary = {
+    balances: DerivedBalances
+}
+
+// Object/Tuple containing council/councilElection information (council:info).
+// The tuple is useful, because that's how api.queryMulti returns the results.
+export type CouncilInfoTuple = Parameters<typeof createCouncilInfoObj>;
+export type CouncilInfoObj = ReturnType<typeof createCouncilInfoObj>;
+// This function allows us to easily transform the tuple into the object
+// and simplifies the creation of consitent Object and Tuple types (seen above).
+export function createCouncilInfoObj(
+    activeCouncil: Seat[],
+    termEndsAt: BlockNumber,
+    autoStart: Boolean,
+    newTermDuration: BN,
+    candidacyLimit: BN,
+    councilSize: BN,
+    minCouncilStake: Balance,
+    minVotingStake: Balance,
+    announcingPeriod: BlockNumber,
+    votingPeriod: BlockNumber,
+    revealingPeriod: BlockNumber,
+    round: BN,
+    stage: Option<ElectionStage>
+) {
+    return {
+        activeCouncil,
+        termEndsAt,
+        autoStart,
+        newTermDuration,
+        candidacyLimit,
+        councilSize,
+        minCouncilStake,
+        minVotingStake,
+        announcingPeriod,
+        votingPeriod,
+        revealingPeriod,
+        round,
+        stage
+    };
+}
+
+// Object with "name" and "value" properties, used for rendering simple CLI tables like:
+// Total balance:   100 JOY
+// Free calance:     50 JOY
+export type NameValueObj = { name: string, value: string };

+ 217 - 0
cli/src/base/AccountsCommandBase.ts

@@ -0,0 +1,217 @@
+import fs from 'fs';
+import path from 'path';
+import slug from 'slug';
+import inquirer from 'inquirer';
+import ExitCodes from '../ExitCodes';
+import { CLIError } from '@oclif/errors';
+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 { toFixedLength } from '../helpers/display';
+
+const ACCOUNTS_DIRNAME = '/accounts';
+
+/**
+ * Abstract base class for account-related commands.
+ *
+ * All the accounts available in the CLI are stored in the form of json backup files inside:
+ * { this.config.dataDir }/{ ACCOUNTS_DIRNAME } (ie. ~/.local/share/joystream-cli/accounts on Ubuntu)
+ * Where: this.config.dataDir is provided by oclif and ACCOUNTS_DIRNAME is a const (see above).
+ */
+export default abstract class AccountsCommandBase extends ApiCommandBase {
+    getAccountsDirPath(): string {
+        return path.join(this.config.dataDir, ACCOUNTS_DIRNAME);
+    }
+
+    getAccountFilePath(account: NamedKeyringPair): string {
+        return path.join(this.getAccountsDirPath(), this.generateAccountFilename(account));
+    }
+
+    generateAccountFilename(account: NamedKeyringPair): string {
+        return `${ slug(account.meta.name, '_') }__${ account.address }.json`;
+    }
+
+    private initAccountsFs(): void {
+        if (!fs.existsSync(this.getAccountsDirPath())) {
+            fs.mkdirSync(this.getAccountsDirPath());
+        }
+    }
+
+    saveAccount(account: NamedKeyringPair, password: string): void {
+        try {
+            fs.writeFileSync(this.getAccountFilePath(account), JSON.stringify(account.toJson(password)));
+        } catch(e) {
+            throw this.createDataWriteError();
+        }
+    }
+
+    fetchAccountFromJsonFile(jsonBackupFilePath: string): NamedKeyringPair {
+        if (!fs.existsSync(jsonBackupFilePath)) {
+            throw new CLIError('Input file does not exist!', { exit: ExitCodes.FileNotFound });
+        }
+        if (path.extname(jsonBackupFilePath) !== '.json') {
+            throw new CLIError('Invalid input file: File extension should be .json', { exit: ExitCodes.InvalidFile });
+        }
+        let accountJsonObj: any;
+        try {
+            accountJsonObj = require(jsonBackupFilePath);
+        } catch (e) {
+            throw new CLIError('Provided backup file is not valid or cannot be accessed', { exit: ExitCodes.InvalidFile });
+        }
+        if (typeof accountJsonObj !== 'object' || accountJsonObj === null) {
+            throw new CLIError('Provided backup file is not valid', { exit: ExitCodes.InvalidFile });
+        }
+
+        // Force some default account name if none is provided in the original backup
+        if (!accountJsonObj.meta) accountJsonObj.meta = {};
+        if (!accountJsonObj.meta.name) accountJsonObj.meta.name = 'Unnamed Account';
+
+        let keyring = new Keyring();
+        let account:NamedKeyringPair;
+        try {
+            // Try adding and retrieving the keys in order to validate that the backup file is correct
+            keyring.addFromJson(accountJsonObj);
+            account = <NamedKeyringPair> keyring.getPair(accountJsonObj.address); // We can be sure it's named, because we forced it before
+        } catch (e) {
+            throw new CLIError('Provided backup file is not valid', { exit: ExitCodes.InvalidFile });
+        }
+
+        return account;
+    }
+
+    private fetchAccountOrNullFromFile(jsonFilePath: string): NamedKeyringPair | null {
+        try {
+            return this.fetchAccountFromJsonFile(jsonFilePath);
+        } catch (e) {
+            // Here in case of a typical CLIError we just return null (otherwise we throw)
+            if (!(e instanceof CLIError)) throw e;
+            return null;
+        }
+    }
+
+    fetchAccounts(): NamedKeyringPair[] {
+        let files: string[] = [];
+        const accountDir = this.getAccountsDirPath();
+        try {
+            files = fs.readdirSync(accountDir);
+        }
+        catch(e) {
+        }
+
+        // We have to assert the type, because TS is not aware that we're filtering out the nulls at the end
+        return <NamedKeyringPair[]> files
+            .map(fileName => {
+                const filePath = path.join(accountDir, fileName);
+                return this.fetchAccountOrNullFromFile(filePath);
+            })
+            .filter(accObj => accObj !== null);
+    }
+
+    getSelectedAccountFilename(): string {
+        return this.getPreservedState().selectedAccountFilename;
+    }
+
+    getSelectedAccount(): NamedKeyringPair | null {
+        const selectedAccountFilename = this.getSelectedAccountFilename();
+
+        if (!selectedAccountFilename) {
+            return null;
+        }
+
+        const account = this.fetchAccountOrNullFromFile(
+            path.join(this.getAccountsDirPath(), selectedAccountFilename)
+        );
+
+        return account;
+    }
+
+    // Use when account usage is required in given command
+    async getRequiredSelectedAccount(promptIfMissing: boolean = true): Promise<NamedKeyringPair> {
+        let selectedAccount: NamedKeyringPair | null = this.getSelectedAccount();
+        if (!selectedAccount) {
+            this.warn('No default account selected! Use account:choose to set the default account!');
+            if (!promptIfMissing) this.exit(ExitCodes.NoAccountSelected);
+            const accounts: NamedKeyringPair[] = this.fetchAccounts();
+            if (!accounts.length) {
+                this.error('There are no accounts available!', { exit: ExitCodes.NoAccountFound });
+            }
+
+            selectedAccount = await this.promptForAccount(accounts);
+        }
+
+        return selectedAccount;
+    }
+
+    async setSelectedAccount(account: NamedKeyringPair): Promise<void> {
+        await this.setPreservedState({ selectedAccountFilename: this.generateAccountFilename(account) });
+    }
+
+    async promptForPassword(message:string = 'Your account\'s password') {
+        const { password } = await inquirer.prompt([
+            { name: 'password', type: 'password', message }
+        ]);
+
+        return password;
+    }
+
+    async requireConfirmation(message: string = 'Are you sure you want to execute this action?'): Promise<void> {
+        const { confirmed } = await inquirer.prompt([
+            { type: 'confirm', name: 'confirmed', message, default: false }
+        ]);
+        if (!confirmed) this.exit(ExitCodes.OK);
+    }
+
+    async promptForAccount(
+        accounts: NamedKeyringPair[],
+        defaultAccount: NamedKeyringPair | null = null,
+        message: string = 'Select an account',
+        showBalances: boolean = true
+    ): Promise<NamedKeyringPair> {
+        let balances: DerivedBalances[];
+        if (showBalances) {
+            balances = await this.getApi().getAccountsBalancesInfo(accounts.map(acc => acc.address));
+        }
+        const longestAccNameLength: number = accounts.reduce((prev, curr) => Math.max(curr.meta.name.length, prev), 0);
+        const accNameColLength: number = Math.min(longestAccNameLength + 1, 20);
+        const { chosenAccountFilename } = await inquirer.prompt([{
+            name: 'chosenAccountFilename',
+            message,
+            type: 'list',
+            choices: accounts.map((account: NamedKeyringPair, i) => ({
+                name: (
+                    `${ toFixedLength(account.meta.name, accNameColLength) } | `+
+                    `${ account.address } | ` +
+                    ((showBalances || '') && (
+                        `${ formatBalance(balances[i].availableBalance) } / `+
+                        `${ formatBalance(balances[i].votingBalance) }`
+                    ))
+                ),
+                value: this.generateAccountFilename(account),
+                short: `${ account.meta.name } (${ account.address })`
+            })),
+            default: defaultAccount && this.generateAccountFilename(defaultAccount)
+        }]);
+
+        return <NamedKeyringPair> accounts.find(acc => this.generateAccountFilename(acc) === chosenAccountFilename);
+    }
+
+    async requestAccountDecoding(account: NamedKeyringPair): Promise<void> {
+        const password: string = await this.promptForPassword();
+        try {
+            account.decodePkcs8(password);
+        } catch (e) {
+            this.error('Invalid password!', { exit: ExitCodes.InvalidInput });
+        }
+    }
+
+    async init() {
+        await super.init();
+        try {
+            this.initAccountsFs();
+        } catch (e) {
+            throw this.createDataDirInitError();
+        }
+    }
+}

+ 28 - 0
cli/src/base/ApiCommandBase.ts

@@ -0,0 +1,28 @@
+import ExitCodes from '../ExitCodes';
+import { CLIError } from '@oclif/errors';
+import StateAwareCommandBase from './StateAwareCommandBase';
+import Api from '../Api';
+import { ApiPromise } from '@polkadot/api'
+
+/**
+ * Abstract base class for commands that require access to the API.
+ */
+export default abstract class ApiCommandBase extends StateAwareCommandBase {
+    private api: Api | null = null;
+
+    getApi(): Api {
+        if (!this.api) throw new CLIError('Tried to get API before initialization.', { exit: ExitCodes.ApiError });
+        return this.api;
+    }
+
+    // Get original api for lower-level api calls
+    getOriginalApi(): ApiPromise {
+        return this.getApi().getOriginalApi();
+    }
+
+    async init() {
+        await super.init();
+        const apiUri: string = this.getPreservedState().apiUri;
+        this.api = await Api.create(apiUri);
+    }
+}

+ 15 - 0
cli/src/base/DefaultCommandBase.ts

@@ -0,0 +1,15 @@
+import ExitCodes from '../ExitCodes';
+import Command from '@oclif/command';
+
+/**
+ * Abstract base class for pretty much all commands
+ * (prevents console.log from hanging the process and unifies the default exit code)
+ */
+export default abstract class DefaultCommandBase extends Command {
+    async finally(err: any) {
+        // called after run and catch regardless of whether or not the command errored
+        // We'll force exit here, in case there is no error, to prevent console.log from hanging the process
+        if (!err) this.exit(ExitCodes.OK);
+        super.finally(err);
+    }
+}

+ 115 - 0
cli/src/base/StateAwareCommandBase.ts

@@ -0,0 +1,115 @@
+import fs from 'fs';
+import path from 'path';
+import ExitCodes from '../ExitCodes';
+import { CLIError } from '@oclif/errors';
+import { DEFAULT_API_URI } from '../Api';
+import lockFile from 'proper-lockfile';
+import DefaultCommandBase from './DefaultCommandBase';
+
+// Type for the state object (which is preserved as json in the state file)
+type StateObject = {
+    selectedAccountFilename: string,
+    apiUri: string
+};
+
+// State object default values
+const DEFAULT_STATE: StateObject = {
+    selectedAccountFilename: '',
+    apiUri: DEFAULT_API_URI
+}
+
+// State file path (relative to this.config.dataDir)
+const STATE_FILE = '/state.json';
+
+// Possible data directory access errors
+enum DataDirErrorType {
+    Init = 0,
+    Read = 1,
+    Write = 2,
+}
+
+/**
+ * Abstract base class for commands that need to work with the preserved state.
+ *
+ * The preserved state is kept in a json file inside the data directory (this.config.dataDir, supplied by oclif).
+ * The state object contains all the information that needs to be preserved across sessions, ie. the default account
+ * choosen by the user after executing account:choose command etc. (see "StateObject" type above).
+ */
+export default abstract class StateAwareCommandBase extends DefaultCommandBase {
+    getStateFilePath(): string {
+        return path.join(this.config.dataDir, STATE_FILE);
+    }
+
+    private createDataDirFsError(errorType: DataDirErrorType, specificPath: string = '') {
+        const actionStrs: { [x in DataDirErrorType]: string } = {
+            [DataDirErrorType.Init]: 'initialize',
+            [DataDirErrorType.Read]: 'read from',
+            [DataDirErrorType.Write]: 'write into'
+        };
+
+        const errorMsg =
+            `Unexpected error while trying to ${ actionStrs[errorType] } the data directory.`+
+            `(${ path.join(this.config.dataDir, specificPath) })! Permissions issue?`;
+
+        return new CLIError(errorMsg, { exit: ExitCodes.FsOperationFailed });
+    }
+
+    createDataReadError(specificPath: string = ''): CLIError {
+        return this.createDataDirFsError(DataDirErrorType.Read, specificPath);
+    }
+
+    createDataWriteError(specificPath: string = ''): CLIError {
+        return this.createDataDirFsError(DataDirErrorType.Write, specificPath);
+    }
+
+    createDataDirInitError(specificPath: string = ''): CLIError {
+        return this.createDataDirFsError(DataDirErrorType.Init, specificPath);
+    }
+
+    private initStateFs(): void {
+        if (!fs.existsSync(this.config.dataDir)) {
+            fs.mkdirSync(this.config.dataDir);
+        }
+        if (!fs.existsSync(this.getStateFilePath())) {
+            fs.writeFileSync(this.getStateFilePath(), JSON.stringify(DEFAULT_STATE));
+        }
+    }
+
+    getPreservedState(): StateObject {
+        let preservedState: StateObject;
+        try {
+            preservedState = <StateObject> require(this.getStateFilePath());
+        } catch(e) {
+            throw this.createDataReadError();
+        }
+        // The state preserved in a file may be missing some required values ie.
+        // if the user previously used the older version of the software.
+        // That's why we combine it with default state before returing.
+        return { ...DEFAULT_STATE, ...preservedState };
+    }
+
+    // Modifies preserved state. Uses file lock in order to avoid updating an older state.
+    // (which could potentialy change between read and write operation)
+    async setPreservedState(modifiedState: Partial<StateObject>): Promise<void> {
+        const stateFilePath = this.getStateFilePath();
+        const unlock = await lockFile.lock(stateFilePath);
+        let oldState: StateObject = this.getPreservedState();
+        let newState: StateObject = { ...oldState, ...modifiedState };
+        try {
+            fs.writeFileSync(stateFilePath, JSON.stringify(newState));
+        } catch(e) {
+            await unlock();
+            throw this.createDataWriteError();
+        }
+        await unlock();
+    }
+
+    async init() {
+        await super.init();
+        try {
+            await this.initStateFs();
+        } catch (e) {
+            throw this.createDataDirInitError();
+        }
+    }
+}

+ 25 - 0
cli/src/commands/account/choose.ts

@@ -0,0 +1,25 @@
+import AccountsCommandBase from '../../base/AccountsCommandBase';
+import chalk from 'chalk';
+import ExitCodes from '../../ExitCodes';
+import { NamedKeyringPair } from '../../Types'
+
+export default class AccountChoose extends AccountsCommandBase {
+    static description = 'Choose default account to use in the CLI';
+
+    async run() {
+        const accounts: NamedKeyringPair[] = this.fetchAccounts();
+        const selectedAccount: NamedKeyringPair | null = this.getSelectedAccount();
+
+        this.log(chalk.white(`Found ${ accounts.length } existing accounts...\n`));
+
+        if (accounts.length === 0) {
+            this.warn('No account to choose from. Add accont using account:import or account:create.');
+            this.exit(ExitCodes.NoAccountFound);
+        }
+
+        const choosenAccount: NamedKeyringPair = await this.promptForAccount(accounts, selectedAccount);
+
+        await this.setSelectedAccount(choosenAccount);
+        this.log(chalk.greenBright("\nAccount switched!"));
+    }
+  }

+ 47 - 0
cli/src/commands/account/create.ts

@@ -0,0 +1,47 @@
+import chalk from 'chalk';
+import ExitCodes from '../../ExitCodes';
+import AccountsCommandBase from '../../base/AccountsCommandBase';
+import { Keyring } from '@polkadot/api';
+import { mnemonicGenerate } from '@polkadot/util-crypto'
+import { NamedKeyringPair } from '../../Types';
+
+type AccountCreateArgs = {
+    name: string
+};
+
+export default class AccountCreate extends AccountsCommandBase {
+    static description = 'Create new account';
+
+    static args = [
+        {
+            name: 'name',
+            required: true,
+            description: 'Account name'
+        },
+    ];
+
+    validatePass(password: string, password2: string): void {
+        if (password !== password2) this.error('Passwords are not the same!', { exit: ExitCodes.InvalidInput });
+        if (!password) this.error('You didn\'t provide a password', { exit: ExitCodes.InvalidInput });
+    }
+
+    async run() {
+        const args: AccountCreateArgs = <AccountCreateArgs> this.parse(AccountCreate).args;
+        const keyring: Keyring = new Keyring();
+        const mnemonic: string = mnemonicGenerate();
+
+        keyring.addFromMnemonic(mnemonic, { name: args.name, whenCreated: Date.now() });
+        const keys: NamedKeyringPair = <NamedKeyringPair> keyring.pairs[0]; // We assigned the name above
+
+        const password = await this.promptForPassword('Set your account\'s password');
+        const password2 = await this.promptForPassword('Confirm your password');
+
+        this.validatePass(password, password2);
+
+        this.saveAccount(keys, password);
+
+        this.log(chalk.greenBright(`\nAccount succesfully created!`));
+        this.log(chalk.white(`${chalk.bold('Name:    ') }${ args.name }`));
+        this.log(chalk.white(`${chalk.bold('Address: ') }${ keys.address }`));
+    }
+  }

+ 41 - 0
cli/src/commands/account/current.ts

@@ -0,0 +1,41 @@
+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';
+
+export default class AccountCurrent extends AccountsCommandBase {
+    static description = 'Display information about currently choosen default account';
+    static aliases = ['account:info', 'account:default'];
+
+    async run() {
+        const currentAccount: NamedKeyringPair = await this.getRequiredSelectedAccount(false);
+        const summary: AccountSummary = await this.getApi().getAccountSummary(currentAccount.address);
+
+        displayHeader('Account information');
+        const creationDate: string = currentAccount.meta.whenCreated ?
+            moment(currentAccount.meta.whenCreated).format('YYYY-MM-DD HH:mm:ss')
+            : '?';
+        const accountRows: NameValueObj[] = [
+            { name: 'Account name:', value: currentAccount.meta.name },
+            { name: 'Address:', value: currentAccount.address },
+            { name: 'Created:', value: creationDate }
+        ];
+        displayNameValueTable(accountRows);
+
+        displayHeader('Balances');
+        const balances: DerivedBalances = summary.balances;
+        let balancesRows: NameValueObj[] = [
+            { name: 'Total balance:', value: formatBalance(balances.votingBalance) },
+            { name: 'Transferable balance:', value: formatBalance(balances.availableBalance) }
+        ];
+        if (balances.lockedBalance.gtn(0)) {
+            balancesRows.push({ name: 'Locked balance:', value: formatBalance(balances.lockedBalance) });
+        }
+        if (balances.reservedBalance.gtn(0)) {
+            balancesRows.push({ name: 'Reserved balance:', value: formatBalance(balances.reservedBalance) });
+        }
+        displayNameValueTable(balancesRows);
+    }
+  }

+ 73 - 0
cli/src/commands/account/export.ts

@@ -0,0 +1,73 @@
+import fs from 'fs';
+import chalk from 'chalk';
+import path from 'path';
+import ExitCodes from '../../ExitCodes';
+import AccountsCommandBase from '../../base/AccountsCommandBase';
+import { flags } from '@oclif/command';
+import { NamedKeyringPair } from '../../Types';
+
+type AccountExportFlags = { all: boolean };
+type AccountExportArgs = { path: string };
+
+export default class AccountExport extends AccountsCommandBase {
+    static description = 'Export account(s) to given location';
+    static MULTI_EXPORT_FOLDER_NAME = 'exported_accounts';
+
+    static args = [
+        {
+            name: 'path',
+            required: true,
+            description: 'Path where the exported files should be placed'
+        }
+    ];
+
+    static flags = {
+        all: flags.boolean({
+            char: 'a',
+            description: `If provided, exports all existing accounts into "${ AccountExport.MULTI_EXPORT_FOLDER_NAME }" folder inside given path`,
+        }),
+    };
+
+    exportAccount(account: NamedKeyringPair, destPath: string): string {
+        const sourceFilePath: string = this.getAccountFilePath(account);
+        const destFilePath: string = path.join(destPath, this.generateAccountFilename(account));
+        try {
+            fs.copyFileSync(sourceFilePath, destFilePath);
+        }
+        catch (e) {
+            this.error(
+                `Error while trying to copy into the export file: (${ destFilePath }). Permissions issue?`,
+                { exit: ExitCodes.FsOperationFailed }
+            );
+        }
+
+        return destFilePath;
+    }
+
+    async run() {
+        const args: AccountExportArgs = <AccountExportArgs> this.parse(AccountExport).args;
+        const flags: AccountExportFlags = <AccountExportFlags> this.parse(AccountExport).flags;
+        const accounts: NamedKeyringPair[] = this.fetchAccounts();
+
+        if (!accounts.length) {
+            this.error('No accounts found!', { exit: ExitCodes.NoAccountFound });
+        }
+
+        if (flags.all) {
+            const destPath: string = path.join(args.path, AccountExport.MULTI_EXPORT_FOLDER_NAME);
+            try {
+                if (!fs.existsSync(destPath)) fs.mkdirSync(destPath);
+            } catch(e) {
+                this.error(`Failed to create the export folder (${ destPath })`, { exit: ExitCodes.FsOperationFailed });
+            }
+            for (let account of accounts) this.exportAccount(account, destPath);
+            this.log(chalk.greenBright(`All accounts succesfully exported succesfully to: ${ chalk.white(destPath) }!`));
+        }
+        else {
+            const destPath: string = args.path;
+            const choosenAccount: NamedKeyringPair = await this.promptForAccount(accounts, null, 'Select an account to export');
+            const exportedFilePath: string = this.exportAccount(choosenAccount, destPath);
+            this.log(chalk.greenBright(`Account succesfully exported to: ${ chalk.white(exportedFilePath) }`));
+        }
+    }
+  }

+ 29 - 0
cli/src/commands/account/forget.ts

@@ -0,0 +1,29 @@
+import fs from 'fs';
+import chalk from 'chalk';
+import ExitCodes from '../../ExitCodes';
+import AccountsCommandBase from '../../base/AccountsCommandBase';
+import { NamedKeyringPair } from '../../Types';
+
+export default class AccountForget extends AccountsCommandBase {
+    static description = 'Forget (remove) account from the list of available accounts';
+
+    async run() {
+        const accounts: NamedKeyringPair[] = this.fetchAccounts();
+
+        if (!accounts.length) {
+            this.error('No accounts found!', { exit: ExitCodes.NoAccountFound });
+        }
+
+        const choosenAccount: NamedKeyringPair = await this.promptForAccount(accounts, null, 'Select an account to forget');
+        await this.requireConfirmation('Are you sure you want this account to be forgotten?');
+
+        const accountFilePath: string = this.getAccountFilePath(choosenAccount);
+        try {
+            fs.unlinkSync(accountFilePath);
+        } catch (e) {
+            this.error(`Could not remove account file (${ accountFilePath }). Permissions issue?`, { exit: ExitCodes.FsOperationFailed });
+        }
+
+        this.log(chalk.greenBright(`\nAccount has been forgotten!`))
+    }
+  }

+ 46 - 0
cli/src/commands/account/import.ts

@@ -0,0 +1,46 @@
+import fs from 'fs';
+import chalk from 'chalk';
+import path from 'path';
+import ExitCodes from '../../ExitCodes';
+import AccountsCommandBase from '../../base/AccountsCommandBase';
+import { NamedKeyringPair } from '../../Types';
+
+type AccountImportArgs = {
+    backupFilePath: string
+};
+
+export default class AccountImport extends AccountsCommandBase {
+    static description = 'Import account using JSON backup file';
+
+    static args = [
+        {
+            name: 'backupFilePath',
+            required: true,
+            description: 'Path to account backup JSON file'
+        },
+    ];
+
+    async run() {
+        const args: AccountImportArgs = <AccountImportArgs> this.parse(AccountImport).args;
+        const backupAcc: NamedKeyringPair = this.fetchAccountFromJsonFile(args.backupFilePath);
+        const accountName: string = backupAcc.meta.name;
+        const accountAddress: string = backupAcc.address;
+
+        const sourcePath: string = args.backupFilePath;
+        const destPath: string = path.join(this.getAccountsDirPath(), this.generateAccountFilename(backupAcc));
+
+        try {
+            fs.copyFileSync(sourcePath, destPath);
+        }
+        catch (e) {
+            this.error(
+                'Unexpected error while trying to copy input file! Permissions issue?',
+                { exit: ExitCodes.FsOperationFailed }
+            );
+        }
+
+        this.log(chalk.bold.greenBright(`ACCOUNT IMPORTED SUCCESFULLY!`));
+        this.log(chalk.bold.white(`NAME:    `), accountName);
+        this.log(chalk.bold.white(`ADDRESS: `), accountAddress);
+    }
+  }

+ 68 - 0
cli/src/commands/account/transferTokens.ts

@@ -0,0 +1,68 @@
+import BN from 'bn.js';
+import AccountsCommandBase from '../../base/AccountsCommandBase';
+import chalk from 'chalk';
+import ExitCodes from '../../ExitCodes';
+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,
+    amount: string
+};
+
+export default class AccountTransferTokens extends AccountsCommandBase {
+    static description = 'Transfer tokens from currently choosen account';
+
+    static args = [
+        {
+            name: 'recipient',
+            required: true,
+            description: 'Address of the transfer recipient'
+        },
+        {
+            name: 'amount',
+            required: true,
+            description: 'Amount of tokens to transfer'
+        },
+    ];
+
+    async run() {
+        const args: AccountTransferArgs = <AccountTransferArgs> this.parse(AccountTransferTokens).args;
+        const selectedAccount: NamedKeyringPair = await this.getRequiredSelectedAccount();
+        const amountBN: BN = new BN(args.amount);
+
+        // Initial validation
+        validateAddress(args.recipient, 'Invalid recipient address');
+        const accBalances: DerivedBalances = (await this.getApi().getAccountsBalancesInfo([ selectedAccount.address ]))[0];
+        checkBalance(accBalances, amountBN);
+
+        await this.requestAccountDecoding(selectedAccount);
+
+        this.log(chalk.white('Estimating fee...'));
+        let estimatedFee: BN;
+        try {
+            estimatedFee = await this.getApi().estimateFee(selectedAccount, args.recipient, amountBN);
+        }
+        catch (e) {
+            this.error('Could not estimate the fee.', { exit: ExitCodes.UnexpectedException });
+        }
+        const totalAmount: BN = amountBN.add(estimatedFee);
+        this.log(chalk.white('Estimated fee:', formatBalance(estimatedFee)));
+        this.log(chalk.white('Total transfer amount:', formatBalance(totalAmount)));
+
+        checkBalance(accBalances, totalAmount);
+
+        await this.requireConfirmation('Do you confirm the transfer?');
+
+        try {
+            const txHash: Hash = await this.getApi().transfer(selectedAccount, args.recipient, amountBN);
+            this.log(chalk.greenBright('Transaction succesfully sent!'));
+            this.log(chalk.white('Hash:', txHash.toString()));
+        } catch (e) {
+            this.error('Could not send the transaction.', { exit: ExitCodes.UnexpectedException });
+        }
+    }
+  }

+ 12 - 0
cli/src/commands/api/getUri.ts

@@ -0,0 +1,12 @@
+import StateAwareCommandBase from '../../base/StateAwareCommandBase';
+import chalk from 'chalk';
+
+
+export default class ApiGetUri extends StateAwareCommandBase {
+    static description = 'Get current api WS provider uri';
+
+    async run() {
+        const currentUri:string = this.getPreservedState().apiUri;
+        this.log(chalk.green(currentUri));
+    }
+  }

+ 277 - 0
cli/src/commands/api/inspect.ts

@@ -0,0 +1,277 @@
+import { flags } from '@oclif/command';
+import { CLIError } from '@oclif/errors';
+import { displayNameValueTable } from '../../helpers/display';
+import { ApiPromise } from '@polkadot/api';
+import { getTypeDef } from '@polkadot/types';
+import { Codec, TypeDef, TypeDefInfo } from '@polkadot/types/types';
+import { ConstantCodec } from '@polkadot/api-metadata/consts/types';
+import ExitCodes from '../../ExitCodes';
+import chalk from 'chalk';
+import { NameValueObj } from '../../Types';
+import inquirer from 'inquirer';
+import ApiCommandBase from '../../base/ApiCommandBase';
+
+// Command flags type
+type ApiInspectFlags = {
+    type: string,
+    module: string,
+    method: string,
+    exec: boolean,
+    callArgs: string
+};
+
+// Currently "inspectable" api types
+const TYPES_AVAILABLE = [
+    'query',
+    'consts',
+] as const;
+
+// String literals type based on TYPES_AVAILABLE const.
+// It works as if we specified: type ApiType = 'query' | 'consts'...;
+type ApiType = typeof TYPES_AVAILABLE[number];
+
+// Format of the api input args (as they are specified in the CLI)
+type ApiMethodInputSimpleArg = string;
+// This recurring type allows the correct handling of nested types like:
+// ((Type1, Type2), Option<Type3>) etc.
+type ApiMethodInputArg = ApiMethodInputSimpleArg | ApiMethodInputArg[];
+
+export default class ApiInspect extends ApiCommandBase {
+    static description =
+        'Lists available node API modules/methods and/or their description(s), '+
+        'or calls one of the API methods (depending on provided arguments and flags)';
+
+    static examples = [
+        '$ api:inspect',
+        '$ api:inspect -t=query',
+        '$ api:inspect -t=query -M=members',
+        '$ api:inspect -t=query -M=members -m=memberProfile',
+        '$ api:inspect -t=query -M=members -m=memberProfile -e',
+        '$ api:inspect -t=query -M=members -m=memberProfile -e -a=1',
+    ];
+
+    static flags = {
+        type: flags.string({
+            char: 't',
+            description:
+                'Specifies the type/category of the inspected request (ie. "query", "consts" etc.).\n'+
+                'If no "--module" flag is provided then all available modules in that type will be listed.\n'+
+                'If this flag is not provided then all available types will be listed.',
+        }),
+        module: flags.string({
+            char: 'M',
+            description:
+                'Specifies the api module, ie. "system", "staking" etc.\n'+
+                'If no "--method" flag is provided then all methods in that module will be listed along with the descriptions.',
+            dependsOn: ['type'],
+        }),
+        method: flags.string({
+            char: 'm',
+            description: 'Specifies the api method to call/describe.',
+            dependsOn: ['module'],
+        }),
+        exec: flags.boolean({
+            char: 'e',
+            description: 'Provide this flag if you want to execute the actual call, instead of displaying the method description (which is default)',
+            dependsOn: ['method'],
+        }),
+        callArgs: flags.string({
+            char: 'a',
+            description:
+                'Specifies the arguments to use when calling a method. Multiple arguments can be separated with a comma, ie. "-a=arg1,arg2".\n'+
+                'You can omit this flag even if the method requires some aguments.\n'+
+                'In that case you will be promted to provide value for each required argument.\n' +
+                '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)',
+            dependsOn: ['exec'],
+        })
+    };
+
+    getMethodMeta(apiType: ApiType, apiModule: string, apiMethod: string) {
+        if (apiType === 'query') {
+            return this.getOriginalApi().query[apiModule][apiMethod].creator.meta;
+        }
+        else {
+            // Currently the only other optoin is api.consts
+            const method:ConstantCodec = <ConstantCodec> this.getOriginalApi().consts[apiModule][apiMethod];
+            return method.meta;
+        }
+    }
+
+    getMethodDescription(apiType: ApiType, apiModule: string, apiMethod: string): string {
+        let description:string = this.getMethodMeta(apiType, apiModule, apiMethod).documentation.join(' ');
+        return description || 'No description available.';
+    }
+
+    getQueryMethodParamsTypes(apiModule: string, apiMethod: string): string[] {
+        const method = this.getOriginalApi().query[apiModule][apiMethod];
+        const { type } = method.creator.meta;
+        if (type.isDoubleMap) {
+            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 [];
+    }
+
+    getMethodReturnType(apiType: ApiType, apiModule: string, apiMethod: string): string {
+        if (apiType === 'query') {
+            const method = this.getOriginalApi().query[apiModule][apiMethod];
+            const { meta: { type, modifier } } = method.creator;
+            if (type.isDoubleMap) {
+                return type.asDoubleMap.value.toString();
+            }
+            if (modifier.isOptional) {
+                return `Option<${type.toString()}>`;
+            }
+        }
+        // Fallback for "query" and default for "consts"
+        return this.getMethodMeta(apiType, apiModule, apiMethod).type.toString();
+    }
+
+    // Validate the flags - throws an error if flags.type, flags.module or flags.method is invalid / does not exist in the api.
+    // Returns type, module and method which validity we can be sure about (notice they may still be "undefined" if weren't provided).
+    validateFlags(api: ApiPromise, flags: ApiInspectFlags): { apiType: ApiType | undefined, apiModule: string | undefined, apiMethod: string | undefined } {
+        let apiType: ApiType | undefined = undefined;
+        const { module: apiModule, method: apiMethod } = flags;
+
+        if (flags.type !== undefined) {
+            const availableTypes: readonly string[] = TYPES_AVAILABLE;
+            if (!availableTypes.includes(flags.type)) {
+                throw new CLIError('Such type is not available', { exit: ExitCodes.InvalidInput });
+            }
+            apiType = <ApiType> flags.type;
+            if (apiModule !== undefined) {
+                if (!api[apiType][apiModule]) {
+                    throw new CLIError('Such module was not found', { exit: ExitCodes.InvalidInput });
+                }
+                if (apiMethod !== undefined && !api[apiType][apiModule][apiMethod]) {
+                    throw new CLIError('Such method was not found', { exit: ExitCodes.InvalidInput });
+                }
+            }
+        }
+
+        return { apiType, apiModule, apiMethod };
+    }
+
+    // Prompt for simple value (string)
+    async promptForSimple(typeName: string): Promise<string> {
+        const userInput = await inquirer.prompt([{
+            name: 'providedValue',
+            message: `Provide value for ${ typeName }`,
+            type: 'input'
+        } ])
+        return <string> userInput.providedValue;
+    }
+
+    // Prompt for optional value (returns undefined if user refused to provide)
+    async promptForOption(typeDef: TypeDef): Promise<ApiMethodInputArg | undefined> {
+        const userInput = await inquirer.prompt([{
+            name: 'confirmed',
+            message: `Do you want to provide the optional ${ typeDef.type } parameter?`,
+            type: 'confirm'
+        } ]);
+
+        if (userInput.confirmed) {
+            const subtype = <TypeDef> typeDef.sub; // We assume that Opion always has a single subtype
+            let value = await this.promptForParam(subtype.type);
+            return value;
+        }
+    }
+
+    // Prompt for tuple - returns array of values
+    async promptForTuple(typeDef: TypeDef): Promise<(ApiMethodInputArg)[]> {
+        let result: ApiMethodInputArg[] = [];
+
+        if (!typeDef.sub) return [ await this.promptForSimple(typeDef.type) ];
+
+        const subtypes: TypeDef[] = Array.isArray(typeDef.sub) ? typeDef.sub : [ typeDef.sub ];
+
+        for (let subtype of subtypes) {
+            let inputParam = await this.promptForParam(subtype.type);
+            if (inputParam !== undefined) result.push(inputParam);
+        }
+
+        return result;
+    }
+
+    // Prompt for param based on "paramType" string (ie. Option<MemeberId>)
+    async promptForParam(paramType: string): Promise<ApiMethodInputArg | undefined> {
+        const typeDef: TypeDef = getTypeDef(paramType);
+        if (typeDef.info === TypeDefInfo.Option) return await this.promptForOption(typeDef);
+        else if (typeDef.info === TypeDefInfo.Tuple) return await this.promptForTuple(typeDef);
+        else return await this.promptForSimple(typeDef.type);
+    }
+
+    // Request values for params using array of param types (strings)
+    async requestParamsValues(paramTypes: string[]): Promise<ApiMethodInputArg[]> {
+        let result: ApiMethodInputArg[] = [];
+        for (let [key, paramType] of Object.entries(paramTypes)) {
+            this.log(chalk.bold.white(`Parameter no. ${ parseInt(key)+1 } (${ paramType }):`));
+            let paramValue = await this.promptForParam(paramType);
+            if (paramValue !== undefined) result.push(paramValue);
+        }
+
+        return result;
+    }
+
+    async run() {
+        const api: ApiPromise = this.getOriginalApi();
+        const flags: ApiInspectFlags = <ApiInspectFlags> this.parse(ApiInspect).flags;
+        const availableTypes: readonly string[] = TYPES_AVAILABLE;
+        const { apiType, apiModule, apiMethod } = this.validateFlags(api, flags);
+
+        // Executing a call
+        if (apiType && apiModule && apiMethod && flags.exec) {
+            let result: Codec;
+
+            if (apiType === 'query') {
+                // Api query - call with (or without) arguments
+                let args: ApiMethodInputArg[] = flags.callArgs ? flags.callArgs.split(',') : [];
+                const paramsTypes: string[] = this.getQueryMethodParamsTypes(apiModule, apiMethod);
+                if (args.length < paramsTypes.length) {
+                    this.warn('Some parameters are missing! Please, provide the missing parameters:');
+                    let missingParamsValues = await this.requestParamsValues(paramsTypes.slice(args.length));
+                    args = args.concat(missingParamsValues);
+                }
+                result = await api.query[apiModule][apiMethod](...args);
+            }
+            else {
+                // Api consts - just assign the value
+                result = api.consts[apiModule][apiMethod];
+            }
+
+            this.log(chalk.green(result.toString()));
+        }
+        // Describing a method
+        else if (apiType && apiModule && apiMethod) {
+            this.log(chalk.bold.white(`${ apiType }.${ apiModule }.${ apiMethod }`));
+            const description: string = this.getMethodDescription(apiType, apiModule, apiMethod);
+            this.log(`\n${ description }\n`);
+            let typesRows: NameValueObj[] = [];
+            if (apiType === 'query') {
+                typesRows.push({ name: 'Params:', value: this.getQueryMethodParamsTypes(apiModule, apiMethod).join(', ') || '-' });
+            }
+            typesRows.push({ name: 'Returns:', value: this.getMethodReturnType(apiType, apiModule, apiMethod) });
+            displayNameValueTable(typesRows);
+        }
+        // Displaying all available methods
+        else if (apiType && apiModule) {
+            const module = api[apiType][apiModule];
+            const rows: NameValueObj[] = Object.keys(module).map((key: string) => {
+                return { name: key, value: this.getMethodDescription(apiType, apiModule, key) };
+            });
+            displayNameValueTable(rows);
+        }
+        // Displaying all available modules
+        else if (apiType) {
+            this.log(chalk.bold.white('Available modules:'));
+            this.log(Object.keys(api[apiType]).map(key => chalk.white(key)).join('\n'));
+        }
+        // Displaying all available types
+        else {
+            this.log(chalk.bold.white('Available types:'));
+            this.log(availableTypes.map(type => chalk.white(type)).join('\n'));
+        }
+    }
+}

+ 28 - 0
cli/src/commands/api/setUri.ts

@@ -0,0 +1,28 @@
+import StateAwareCommandBase from '../../base/StateAwareCommandBase';
+import chalk from 'chalk';
+import { WsProvider } from '@polkadot/api';
+import ExitCodes from '../../ExitCodes';
+
+type ApiSetUriArgs = { uri: string };
+
+export default class ApiSetUri extends StateAwareCommandBase {
+    static description = 'Set api WS provider uri';
+    static args = [
+        {
+            name: 'uri',
+            required: true,
+            description: 'Uri of the node api WS provider'
+        }
+    ];
+
+    async run() {
+        const args: ApiSetUriArgs = <ApiSetUriArgs> this.parse(ApiSetUri).args;
+        try {
+            new WsProvider(args.uri);
+        } catch(e) {
+            this.error('The WS provider uri seems to be incorrect', { exit: ExitCodes.InvalidInput });
+        }
+        await this.setPreservedState({ apiUri: args.uri });
+        this.log(chalk.greenBright('Api uri successfuly changed! New uri: ') + chalk.white(args.uri))
+    }
+  }

+ 57 - 0
cli/src/commands/council/info.ts

@@ -0,0 +1,57 @@
+import { ElectionStage } from '@joystream/types';
+import { formatNumber, formatBalance } from '@polkadot/util';
+import { BlockNumber } from '@polkadot/types/interfaces';
+import { CouncilInfoObj, NameValueObj } from '../../Types';
+import { displayHeader, displayNameValueTable } from '../../helpers/display';
+import ApiCommandBase from '../../base/ApiCommandBase';
+
+export default class CouncilInfo extends ApiCommandBase {
+    static description = 'Get current council and council elections information';
+
+    displayInfo(infoObj: CouncilInfoObj) {
+        const { activeCouncil = [], round, stage } = infoObj;
+
+        displayHeader('Council');
+        const councilRows: NameValueObj[] = [
+            { name: 'Elected:', value: activeCouncil.length ? 'YES' : 'NO' },
+            { name: 'Members:', value: activeCouncil.length.toString() },
+            { name: 'Term ends at block:', value: `#${formatNumber(infoObj.termEndsAt) }` },
+        ];
+        displayNameValueTable(councilRows);
+
+
+        displayHeader('Election');
+        let electionTableRows: NameValueObj[] = [
+            { name: 'Running:', value: stage && stage.isSome ? 'YES' : 'NO' },
+            { name: 'Election round:', value: formatNumber(round) }
+        ];
+        if (stage && stage.isSome) {
+            const stageValue = <ElectionStage> stage.value;
+            const stageName: string = stageValue.type;
+            const stageEndsAt = <BlockNumber> stageValue.value;
+            electionTableRows.push({ name: 'Stage:', value: stageName });
+            electionTableRows.push({ name: 'Stage ends at block:', value: `#${stageEndsAt}` });
+        }
+        displayNameValueTable(electionTableRows);
+
+        displayHeader('Configuration');
+        const isAutoStart = (infoObj.autoStart || false).valueOf();
+        const configTableRows: NameValueObj[] = [
+            { name: 'Auto-start elections:', value: isAutoStart ? 'YES' : 'NO' },
+            { name: 'New term duration:', value: formatNumber(infoObj.newTermDuration) },
+            { name: 'Candidacy limit:', value: formatNumber(infoObj.candidacyLimit) },
+            { name: 'Council size:', value: formatNumber(infoObj.councilSize) },
+            { name: 'Min. council stake:', value: formatBalance(infoObj.minCouncilStake) },
+            { name: 'Min. voting stake:', value: formatBalance(infoObj.minVotingStake) },
+            { name: 'Announcing period:', value: `${ formatNumber(infoObj.announcingPeriod) } blocks` },
+            { name: 'Voting period:', value: `${ formatNumber(infoObj.votingPeriod) } blocks` },
+            { name: 'Revealing period:', value: `${ formatNumber(infoObj.revealingPeriod) } blocks` }
+        ];
+        displayNameValueTable(configTableRows);
+    }
+
+    async run() {
+        const infoObj = await this.getApi().getCouncilInfo();
+        this.displayInfo(infoObj);
+    }
+  }

+ 33 - 0
cli/src/helpers/display.ts

@@ -0,0 +1,33 @@
+import { cli } from 'cli-ux';
+import chalk from 'chalk';
+import { NameValueObj } from '../Types';
+
+export function displayHeader(caption: string, placeholderSign: string = '_', size: number = 50) {
+    let singsPerSide: number = Math.floor((size - (caption.length + 2)) / 2);
+    let finalStr: string = '';
+    for (let i = 0; i < singsPerSide; ++i) finalStr += placeholderSign;
+    finalStr += ` ${ caption} `;
+    while (finalStr.length < size) finalStr += placeholderSign;
+
+    process.stdout.write("\n" + chalk.bold.blueBright(finalStr) + "\n\n");
+}
+
+export function displayNameValueTable(rows: NameValueObj[]) {
+    cli.table(
+        rows,
+        {
+            name: { minWidth: 30, get: row => chalk.bold.white(row.name) },
+            value: { get: row => chalk.white(row.value) }
+        },
+        { 'no-header': true }
+    );
+}
+
+export function toFixedLength(text: string, length: number, spacesOnLeft = false): string {
+    if (text.length > length && length > 3) {
+        return text.slice(0, length-3) + '...';
+    }
+    while(text.length < length) { spacesOnLeft ? text = ' '+text : text += ' ' };
+
+    return text;
+}

+ 19 - 0
cli/src/helpers/validation.ts

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

+ 1 - 0
cli/src/index.ts

@@ -0,0 +1 @@
+export {run} from '@oclif/command'

+ 11 - 0
cli/test/commands/council/info.test.ts

@@ -0,0 +1,11 @@
+import {expect, test} from '@oclif/test'
+
+describe('info', () => {
+  test
+  .stdout()
+  .command(['council:info'])
+  .exit(0)
+  .it('displays "Council" string', ctx => {
+    expect(ctx.stdout).to.contain('Council')
+  })
+})

+ 5 - 0
cli/test/mocha.opts

@@ -0,0 +1,5 @@
+--require ts-node/register
+--watch-extensions ts
+--recursive
+--reporter spec
+--timeout 5000

+ 9 - 0
cli/test/tsconfig.json

@@ -0,0 +1,9 @@
+{
+  "extends": "../tsconfig",
+  "compilerOptions": {
+    "noEmit": true
+  },
+  "references": [
+    {"path": ".."}
+  ]
+}

+ 15 - 0
cli/tsconfig.json

@@ -0,0 +1,15 @@
+{
+  "compilerOptions": {
+    "declaration": true,
+    "importHelpers": true,
+    "module": "commonjs",
+    "outDir": "lib",
+    "rootDir": "src",
+    "strict": true,
+    "target": "es2017",
+    "esModuleInterop": true
+  },
+  "include": [
+    "src/**/*"
+  ]
+}

+ 7 - 0
devops/dockerfiles/node-and-runtime/Dockerfile

@@ -14,6 +14,13 @@ COPY --from=builder /joystream/target/release/wbuild/joystream-node-runtime/joys
 # 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.:

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

@@ -1,4 +1,4 @@
-FROM liuchong/rustup:1.42.0 AS builder
+FROM liuchong/rustup:1.43.0 AS builder
 LABEL description="Rust and WASM build environment for joystream and substrate"
 
 WORKDIR /setup

+ 5 - 0
devops/git-hooks/pre-commit

@@ -0,0 +1,5 @@
+#!/bin/sh
+set -e
+
+echo 'cargo fmt --all -- --check'
+cargo fmt --all -- --check

+ 10 - 0
devops/git-hooks/pre-push

@@ -0,0 +1,10 @@
+#!/bin/sh
+set -e
+
+export BUILD_DUMMY_WASM_BINARY=1
+
+echo '+cargo test --all'
+cargo test --all
+
+echo '+cargo clippy --all -- -D warnings'
+cargo clippy --all -- -D warnings

+ 1 - 1
node/Cargo.toml

@@ -3,7 +3,7 @@ authors = ['Joystream']
 build = 'build.rs'
 edition = '2018'
 name = 'joystream-node'
-version = '2.1.3'
+version = '2.2.0'
 default-run = "joystream-node"
 
 [[bin]]

+ 55 - 27
node/src/chain_spec.rs

@@ -14,13 +14,18 @@
 // You should have received a copy of the GNU General Public License
 // along with Joystream node.  If not, see <http://www.gnu.org/licenses/>.
 
+// Clippy linter warning.
+#![allow(clippy::identity_op)] // disable it because we use such syntax for a code readability
+                               // Example:  voting_period: 1 * DAY
+
 use node_runtime::{
     versioned_store::InputValidationLengthConstraint as VsInputValidation, ActorsConfig,
     AuthorityDiscoveryConfig, BabeConfig, Balance, BalancesConfig, ContentWorkingGroupConfig,
     CouncilConfig, CouncilElectionConfig, DataObjectStorageRegistryConfig,
-    DataObjectTypeRegistryConfig, GrandpaConfig, ImOnlineConfig, IndicesConfig, MembersConfig,
-    Perbill, ProposalsConfig, SessionConfig, SessionKeys, Signature, StakerStatus, StakingConfig,
-    SudoConfig, SystemConfig, VersionedStoreConfig, DAYS, WASM_BINARY,
+    DataObjectTypeRegistryConfig, ElectionParameters, GrandpaConfig, ImOnlineConfig, IndicesConfig,
+    MembersConfig, MigrationConfig, Perbill, ProposalsCodexConfig, SessionConfig, SessionKeys,
+    Signature, StakerStatus, StakingConfig, SudoConfig, SystemConfig, VersionedStoreConfig, DAYS,
+    WASM_BINARY,
 };
 pub use node_runtime::{AccountId, GenesisConfig};
 use primitives::{sr25519, Pair, Public};
@@ -30,7 +35,6 @@ use babe_primitives::AuthorityId as BabeId;
 use grandpa_primitives::AuthorityId as GrandpaId;
 use im_online::sr25519::AuthorityId as ImOnlineId;
 use serde_json as json;
-use substrate_service;
 
 type AccountPublic = <Signature as Verify>::Signer;
 
@@ -154,7 +158,7 @@ impl Alternative {
 }
 
 fn new_vs_validation(min: u16, max_min_diff: u16) -> VsInputValidation {
-    return VsInputValidation { min, max_min_diff };
+    VsInputValidation { min, max_min_diff }
 }
 
 pub fn chain_spec_properties() -> json::map::Map<String, json::Value> {
@@ -180,6 +184,9 @@ pub fn testnet_genesis(
     const STASH: Balance = 20 * DOLLARS;
     const ENDOWMENT: Balance = 100_000 * DOLLARS;
 
+    // default codex proposals config parameters
+    let cpcp = node_runtime::ProposalsConfigParameters::default();
+
     GenesisConfig {
         system: Some(SystemConfig {
             code: WASM_BINARY.to_vec(),
@@ -218,9 +225,7 @@ pub fn testnet_genesis(
             slash_reward_fraction: Perbill::from_percent(10),
             ..Default::default()
         }),
-        sudo: Some(SudoConfig {
-            key: root_key.clone(),
-        }),
+        sudo: Some(SudoConfig { key: root_key }),
         babe: Some(BabeConfig {
             authorities: vec![],
         }),
@@ -235,24 +240,16 @@ pub fn testnet_genesis(
         }),
         election: Some(CouncilElectionConfig {
             auto_start: true,
-            announcing_period: 3 * DAYS,
-            voting_period: 1 * DAYS,
-            revealing_period: 1 * DAYS,
-            council_size: 12,
-            candidacy_limit: 25,
-            min_council_stake: 10 * DOLLARS,
-            new_term_duration: 14 * DAYS,
-            min_voting_stake: 1 * DOLLARS,
-        }),
-        proposals: Some(ProposalsConfig {
-            approval_quorum: 66,
-            min_stake: 2 * DOLLARS,
-            cancellation_fee: 10 * CENTS,
-            rejection_fee: 1 * DOLLARS,
-            voting_period: 2 * DAYS,
-            name_max_len: 512,
-            description_max_len: 10_000,
-            wasm_code_max_len: 2_000_000,
+            election_parameters: ElectionParameters {
+                announcing_period: 3 * DAYS,
+                voting_period: 1 * DAYS,
+                revealing_period: 1 * DAYS,
+                council_size: 12,
+                candidacy_limit: 25,
+                min_council_stake: 10 * DOLLARS,
+                new_term_duration: 14 * DAYS,
+                min_voting_stake: 1 * DOLLARS,
+            },
         }),
         members: Some(MembersConfig {
             default_paid_membership_fee: 100u128,
@@ -282,7 +279,7 @@ pub fn testnet_genesis(
             class_description_constraint: new_vs_validation(1, 999),
         }),
         content_wg: Some(ContentWorkingGroupConfig {
-            mint_capacity: 100000,
+            mint_capacity: 100_000,
             curator_opening_by_id: vec![],
             next_curator_opening_id: 0,
             curator_application_by_id: vec![],
@@ -305,5 +302,36 @@ pub fn testnet_genesis(
             channel_banner_constraint: crate::forum_config::new_validation(5, 1024),
             channel_title_constraint: crate::forum_config::new_validation(5, 1024),
         }),
+        migration: Some(MigrationConfig {}),
+        proposals_codex: Some(ProposalsCodexConfig {
+            set_validator_count_proposal_voting_period: cpcp
+                .set_validator_count_proposal_voting_period,
+            set_validator_count_proposal_grace_period: cpcp
+                .set_validator_count_proposal_grace_period,
+            runtime_upgrade_proposal_voting_period: cpcp.runtime_upgrade_proposal_voting_period,
+            runtime_upgrade_proposal_grace_period: cpcp.runtime_upgrade_proposal_grace_period,
+            text_proposal_voting_period: cpcp.text_proposal_voting_period,
+            text_proposal_grace_period: cpcp.text_proposal_grace_period,
+            set_election_parameters_proposal_voting_period: cpcp
+                .set_election_parameters_proposal_voting_period,
+            set_election_parameters_proposal_grace_period: cpcp
+                .set_election_parameters_proposal_grace_period,
+            set_content_working_group_mint_capacity_proposal_voting_period: cpcp
+                .set_content_working_group_mint_capacity_proposal_voting_period,
+            set_content_working_group_mint_capacity_proposal_grace_period: cpcp
+                .set_content_working_group_mint_capacity_proposal_grace_period,
+            set_lead_proposal_voting_period: cpcp.set_lead_proposal_voting_period,
+            set_lead_proposal_grace_period: cpcp.set_lead_proposal_voting_period,
+            spending_proposal_voting_period: cpcp.spending_proposal_voting_period,
+            spending_proposal_grace_period: cpcp.spending_proposal_grace_period,
+            evict_storage_provider_proposal_voting_period: cpcp
+                .evict_storage_provider_proposal_voting_period,
+            evict_storage_provider_proposal_grace_period: cpcp
+                .evict_storage_provider_proposal_grace_period,
+            set_storage_role_parameters_proposal_voting_period: cpcp
+                .set_storage_role_parameters_proposal_voting_period,
+            set_storage_role_parameters_proposal_grace_period: cpcp
+                .set_storage_role_parameters_proposal_grace_period,
+        }),
     }
 }

+ 1 - 1
node/src/cli.rs

@@ -90,7 +90,7 @@ where
         let exit = e
             .into_exit()
             .map_err(|_| error::Error::Other("Exit future failed.".into()));
-        let service = service.map_err(|err| error::Error::Service(err));
+        let service = service.map_err(error::Error::Service);
         let select = service.select(exit).map(|_| ()).map_err(|(err, _)| err);
         runtime.block_on(select)
     };

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

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

+ 1 - 1
node/src/forum_config/mod.rs

@@ -6,5 +6,5 @@ pub mod from_serialized;
 use node_runtime::forum::InputValidationLengthConstraint;
 
 pub fn new_validation(min: u16, max_min_diff: u16) -> InputValidationLengthConstraint {
-    return InputValidationLengthConstraint { min, max_min_diff };
+    InputValidationLengthConstraint { min, max_min_diff }
 }

+ 9 - 3
node/src/service.rs

@@ -16,6 +16,12 @@
 
 #![warn(unused_extern_crates)]
 
+// Clippy linter warning.
+#![allow(clippy::type_complexity)] // disable it because this is foreign code and can be changed any time
+
+// Clippy linter warning.
+#![allow(clippy::redundant_closure_call)] // disable it because of the substrate lib design
+
 //! Service and ServiceFactory implementation. Specialized wrapper over substrate service.
 
 use client_db::Backend;
@@ -43,9 +49,9 @@ construct_simple_protocol! {
 // Declare an instance of the native executor named `Executor`. Include the wasm binary as the
 // equivalent wasm code.
 native_executor_instance!(
-	pub Executor,
-	node_runtime::api::dispatch,
-	node_runtime::native_version
+    pub Executor,
+    node_runtime::api::dispatch,
+    node_runtime::native_version
 );
 
 /// Starts a `ServiceBuilder` for a full service.

+ 21 - 0
package.json

@@ -0,0 +1,21 @@
+{
+	"private": true,
+	"name": "joystream",
+	"license": "GPL-3.0-only",
+	"scripts": {
+		"test": "yarn && yarn workspaces run test",
+		"test-migration": "yarn && yarn workspaces run test-migration"
+	},
+	"workspaces": [
+		"tests/network-tests"
+	],
+	"devDependencies": {
+		"husky": "^4.2.5"
+	},
+	"husky": {
+	  "hooks": {
+		"pre-commit": "devops/git-hooks/pre-commit",
+		"pre-push": "devops/git-hooks/pre-push"
+	  }
+	}
+}

+ 0 - 1
runtime-modules/common/src/currency.rs

@@ -1,6 +1,5 @@
 use sr_primitives::traits::Convert;
 use srml_support::traits::{Currency, LockableCurrency, ReservableCurrency};
-use system;
 
 pub trait GovernanceCurrency: system::Trait + Sized {
     type Currency: Currency<Self::AccountId>

+ 1 - 0
runtime-modules/common/src/lib.rs

@@ -2,3 +2,4 @@
 #![cfg_attr(not(feature = "std"), no_std)]
 
 pub mod currency;
+pub mod origin_validator;

+ 5 - 0
runtime-modules/common/src/origin_validator.rs

@@ -0,0 +1,5 @@
+/// Abstract validator for the origin(account_id) and actor_id (eg.: thread author id).
+pub trait ActorOriginValidator<Origin, ActorId, AccountId> {
+    /// Check for valid combination of origin and actor_id
+    fn ensure_actor_origin(origin: Origin, actor_id: ActorId) -> Result<AccountId, &'static str>;
+}

+ 3 - 3
runtime-modules/content-working-group/src/genesis.rs

@@ -43,11 +43,11 @@ pub struct GenesisConfigBuilder<T: Trait> {
 }
 
 impl<T: Trait> GenesisConfigBuilder<T> {
-    /*
-    pub fn set_mint(mut self, mint: <T as minting::Trait>::MintId) -> Self {
-        self.mint = mint;
+    pub fn with_mint_capacity(mut self, capacity: minting::BalanceOf<T>) -> Self {
+        self.mint_capacity = capacity;
         self
     }
+    /*
     pub fn set_channel_handle_constraint(mut self, constraint: InputValidationLengthConstraint) -> Self {
         self.channel_description_constraint = constraint;
         self

+ 199 - 151
runtime-modules/content-working-group/src/lib.rs

@@ -1,3 +1,10 @@
+// Clippy linter warning. TODO: remove after the Constaninople release
+#![allow(clippy::type_complexity)]
+// disable it because of possible frontend API break
+
+// Clippy linter warning. TODO: refactor "this function has too many argument"
+#![allow(clippy::too_many_arguments)] // disable it because of possible API break
+
 // Ensure we're `no_std` when compiling for Wasm.
 #![cfg_attr(not(feature = "std"), no_std)]
 
@@ -15,6 +22,7 @@ use serde::{Deserialize, Serialize};
 use codec::{Decode, Encode}; // Codec
                              //use rstd::collections::btree_map::BTreeMap;
 use membership::{members, role_types};
+use rstd::borrow::ToOwned;
 use rstd::collections::btree_map::BTreeMap;
 use rstd::collections::btree_set::BTreeSet;
 use rstd::convert::From;
@@ -310,12 +318,12 @@ impl<BlockNumber: Clone> CuratorExitSummary<BlockNumber> {
     pub fn new(
         origin: &CuratorExitInitiationOrigin,
         initiated_at_block_number: &BlockNumber,
-        rationale_text: &Vec<u8>,
+        rationale_text: &[u8],
     ) -> Self {
         CuratorExitSummary {
             origin: (*origin).clone(),
             initiated_at_block_number: (*initiated_at_block_number).clone(),
-            rationale_text: (*rationale_text).clone(),
+            rationale_text: rationale_text.to_owned(),
         }
     }
 }
@@ -1080,7 +1088,9 @@ decl_event! {
         CuratorApplicationId = CuratorApplicationId<T>,
         CuratorId = CuratorId<T>,
         CuratorApplicationIdToCuratorIdMap = CuratorApplicationIdToCuratorIdMap<T>,
+        MintBalanceOf = minting::BalanceOf<T>,
         <T as system::Trait>::AccountId,
+        <T as minting::Trait>::MintId,
     {
         ChannelCreated(ChannelId),
         ChannelOwnershipTransferred(ChannelId),
@@ -1100,6 +1110,8 @@ decl_event! {
         CuratorRewardAccountUpdated(CuratorId, AccountId),
         ChannelUpdatedByCurationActor(ChannelId),
         ChannelCreationEnabledUpdated(bool),
+        MintCapacityIncreased(MintId, MintBalanceOf, MintBalanceOf),
+        MintCapacityDecreased(MintId, MintBalanceOf, MintBalanceOf),
     }
 }
 
@@ -1186,12 +1198,12 @@ decl_module! {
             ChannelById::<T>::insert(next_channel_id, new_channel);
 
             // Add id to ChannelIdByHandle under handle
-            ChannelIdByHandle::<T>::insert(handle.clone(), next_channel_id);
+            ChannelIdByHandle::<T>::insert(handle, next_channel_id);
 
             // Increment NextChannelId
             NextChannelId::<T>::mutate(|id| *id += <ChannelId<T> as One>::one());
 
-            /// CREDENTIAL STUFF ///
+            // CREDENTIAL STUFF //
 
             // Dial out to membership module and inform about new role as channe owner.
             let registered_role = <members::Module<T>>::register_role_on_member(owner, &member_in_role).is_ok();
@@ -1227,7 +1239,7 @@ decl_module! {
             // Construct new channel with altered properties
             let new_channel = Channel {
                 owner: new_owner,
-                role_account: new_role_account.clone(),
+                role_account: new_role_account,
                 ..channel
             };
 
@@ -1304,14 +1316,14 @@ decl_module! {
 
             Self::update_channel(
                 &channel_id,
-                &None, // verified
+                None, // verified
                 &new_handle,
                 &new_title,
                 &new_description,
                 &new_avatar,
                 &new_banner,
-                &new_publication_status,
-                &None // curation_status
+                new_publication_status,
+                None // curation_status
             );
         }
 
@@ -1333,14 +1345,14 @@ decl_module! {
 
             Self::update_channel(
                 &channel_id,
-                &new_verified,
+                new_verified,
                 &None, // handle
                 &None, // title
                 &None, // description,
                 &None, // avatar
                 &None, // banner
-                &None, // publication_status
-                &new_curation_status
+                None, // publication_status
+                new_curation_status
             );
         }
 
@@ -1460,7 +1472,7 @@ decl_module! {
             let successful_iter = successful_curator_application_ids
                                     .iter()
                                     // recover curator application from id
-                                    .map(|curator_application_id| { Self::ensure_curator_application_exists(curator_application_id) })
+                                    .map(|curator_application_id| { Self::ensure_curator_application_exists(curator_application_id)})
                                     // remove Err cases, i.e. non-existing applications
                                     .filter_map(|result| result.ok());
 
@@ -1470,8 +1482,7 @@ decl_module! {
             // Ensure all curator applications exist
             let number_of_successful_applications = successful_iter
                                                     .clone()
-                                                    .collect::<Vec<_>>()
-                                                    .len();
+                                                    .count();
 
             ensure!(
                 number_of_successful_applications == num_provided_successful_curator_application_ids,
@@ -1489,7 +1500,7 @@ decl_module! {
                                                                         .clone()
                                                                         .map(|(successful_curator_application, _, _)| successful_curator_application.member_id)
                                                                         .filter_map(|successful_member_id| Self::ensure_can_register_curator_role_on_member(&successful_member_id).ok() )
-                                                                        .collect::<Vec<_>>().len();
+                                                                        .count();
 
             ensure!(
                 num_successful_applications_that_can_register_as_curator == num_provided_successful_curator_application_ids,
@@ -1883,7 +1894,7 @@ decl_module! {
             origin,
             curator_id: CuratorId<T>,
             rationale_text: Vec<u8>
-            ) {
+        ) {
 
             // Ensure lead is set and is origin signer
             Self::ensure_origin_is_set_lead(origin)?;
@@ -1906,103 +1917,23 @@ decl_module! {
             );
         }
 
-        /*
-         * Root origin routines for managing lead.
-         */
-
-
-        /// Introduce a lead when one is not currently set.
-        pub fn set_lead(origin, member: T::MemberId, role_account: T::AccountId) {
-
+        /// Replace the current lead. First unsets the active lead if there is one.
+        /// If a value is provided for new_lead it will then set that new lead.
+        /// It is responsibility of the caller to ensure the new lead can be set
+        /// to avoid the lead role being vacant at the end of the call.
+        pub fn replace_lead(origin, new_lead: Option<(T::MemberId, T::AccountId)>) {
             // Ensure root is origin
             ensure_root(origin)?;
 
-            // Ensure there is no current lead
-            ensure!(
-                <CurrentLeadId<T>>::get().is_none(),
-                MSG_CURRENT_LEAD_ALREADY_SET
-            );
-
-            // Ensure that member can actually become lead
-            let new_lead_id = <NextLeadId<T>>::get();
-
-            let new_lead_role =
-                role_types::ActorInRole::new(role_types::Role::CuratorLead, new_lead_id);
-
-            let _profile = <members::Module<T>>::can_register_role_on_member(
-                &member,
-                &role_types::ActorInRole::new(role_types::Role::CuratorLead, new_lead_id),
-            )?;
-
-            //
-            // == MUTATION SAFE ==
-            //
-
-            // Construct lead
-            let new_lead = Lead {
-                role_account: role_account.clone(),
-                reward_relationship: None,
-                inducted: <system::Module<T>>::block_number(),
-                stage: LeadRoleState::Active,
-            };
-
-            // Store lead
-            <LeadById<T>>::insert(new_lead_id, new_lead);
-
-            // Update current lead
-            <CurrentLeadId<T>>::put(new_lead_id); // Some(new_lead_id)
-
-            // Update next lead counter
-            <NextLeadId<T>>::mutate(|id| *id += <LeadId<T> as One>::one());
-
-            // Register in role
-            let registered_role =
-                <members::Module<T>>::register_role_on_member(member, &new_lead_role).is_ok();
-
-            assert!(registered_role);
-
-            // Trigger event
-            Self::deposit_event(RawEvent::LeadSet(new_lead_id));
-        }
-
-        /// Evict the currently unset lead
-        pub fn unset_lead(origin) {
-
-            // Ensure root is origin
-            ensure_root(origin)?;
-
-            // Ensure there is a lead set
-            let (lead_id,lead) = Self::ensure_lead_is_set()?;
-
-            //
-            // == MUTATION SAFE ==
-            //
-
-            // Unregister from role in membership model
-            let current_lead_role = role_types::ActorInRole{
-                role: role_types::Role::CuratorLead,
-                actor_id: lead_id
-            };
-
-            let unregistered_role = <members::Module<T>>::unregister_role(current_lead_role).is_ok();
-
-            assert!(unregistered_role);
-
-            // Update lead stage as exited
-            let current_block = <system::Module<T>>::block_number();
-
-            let new_lead = Lead{
-                stage: LeadRoleState::Exited(ExitedLeadRole { initiated_at_block_number: current_block}),
-                ..lead
-            };
-
-            <LeadById<T>>::insert(lead_id, new_lead);
-
-            // Update current lead
-            <CurrentLeadId<T>>::take(); // None
+            // Unset current lead first
+            if Self::ensure_lead_is_set().is_ok() {
+                Self::unset_lead()?;
+            }
 
-            // Trigger event
-            Self::deposit_event(RawEvent::LeadUnset(lead_id));
+            // Try to set new lead
+            if let Some((member_id, role_account)) = new_lead {
+                Self::set_lead(member_id, role_account)?;
+            }
         }
 
         /// Add an opening for a curator role.
@@ -2022,7 +1953,11 @@ decl_module! {
             Self::deposit_event(RawEvent::ChannelCreationEnabledUpdated(enabled));
         }
 
-        /// Add to capacity of current acive mint
+        /// Add to capacity of current acive mint.
+        /// This may be deprecated in the future, since set_mint_capacity is sufficient to
+        /// both increase and decrease capacity. Although when considering that it may be executed
+        /// by a proposal, given the temporal delay in approving a proposal, it might be more suitable
+        /// than set_mint_capacity?
         pub fn increase_mint_capacity(
             origin,
             additional_capacity: minting::BalanceOf<T>
@@ -2032,7 +1967,42 @@ decl_module! {
             let mint_id = Self::mint();
             let mint = <minting::Module<T>>::mints(mint_id); // must exist
             let new_capacity = mint.capacity() + additional_capacity;
-            let _ = <minting::Module<T>>::set_mint_capacity(mint_id, new_capacity);
+            <minting::Module<T>>::set_mint_capacity(mint_id, new_capacity)?;
+
+            Self::deposit_event(RawEvent::MintCapacityIncreased(
+                mint_id, additional_capacity, new_capacity
+            ));
+        }
+
+        /// Sets the capacity of the current active mint
+        pub fn set_mint_capacity(
+            origin,
+            new_capacity: minting::BalanceOf<T>
+        ) {
+            ensure_root(origin)?;
+
+            let mint_id = Self::mint();
+
+            // Mint must exist - it is set at genesis
+            let mint = <minting::Module<T>>::mints(mint_id);
+
+            let current_capacity = mint.capacity();
+
+            if new_capacity != current_capacity {
+                // Cannot fail if mint exists
+                <minting::Module<T>>::set_mint_capacity(mint_id, new_capacity)?;
+
+                if new_capacity > current_capacity {
+                    Self::deposit_event(RawEvent::MintCapacityIncreased(
+                        mint_id, new_capacity - current_capacity, new_capacity
+                    ));
+                } else {
+                    Self::deposit_event(RawEvent::MintCapacityDecreased(
+                        mint_id, current_capacity - new_capacity, new_capacity
+                    ));
+                }
+            }
+
         }
     }
 }
@@ -2079,6 +2049,87 @@ impl<T: Trait> versioned_store_permissions::CredentialChecker<T> for Module<T> {
 }
 
 impl<T: Trait> Module<T> {
+    /// Introduce a lead when one is not currently set.
+    fn set_lead(member: T::MemberId, role_account: T::AccountId) -> dispatch::Result {
+        // Ensure there is no current lead
+        ensure!(
+            <CurrentLeadId<T>>::get().is_none(),
+            MSG_CURRENT_LEAD_ALREADY_SET
+        );
+
+        let new_lead_id = <NextLeadId<T>>::get();
+
+        let new_lead_role =
+            role_types::ActorInRole::new(role_types::Role::CuratorLead, new_lead_id);
+
+        //
+        // == MUTATION SAFE ==
+        //
+
+        // Register in role - will fail if member cannot become lead
+        members::Module::<T>::register_role_on_member(member, &new_lead_role)?;
+
+        // Construct lead
+        let new_lead = Lead {
+            role_account,
+            reward_relationship: None,
+            inducted: <system::Module<T>>::block_number(),
+            stage: LeadRoleState::Active,
+        };
+
+        // Store lead
+        <LeadById<T>>::insert(new_lead_id, new_lead);
+
+        // Update current lead
+        <CurrentLeadId<T>>::put(new_lead_id); // Some(new_lead_id)
+
+        // Update next lead counter
+        <NextLeadId<T>>::mutate(|id| *id += <LeadId<T> as One>::one());
+
+        // Trigger event
+        Self::deposit_event(RawEvent::LeadSet(new_lead_id));
+
+        Ok(())
+    }
+
+    /// Evict the currently set lead
+    fn unset_lead() -> dispatch::Result {
+        // Ensure there is a lead set
+        let (lead_id, lead) = Self::ensure_lead_is_set()?;
+
+        //
+        // == MUTATION SAFE ==
+        //
+
+        // Unregister from role in membership model
+        let current_lead_role = role_types::ActorInRole {
+            role: role_types::Role::CuratorLead,
+            actor_id: lead_id,
+        };
+
+        <members::Module<T>>::unregister_role(current_lead_role)?;
+
+        // Update lead stage as exited
+        let current_block = <system::Module<T>>::block_number();
+
+        let new_lead = Lead {
+            stage: LeadRoleState::Exited(ExitedLeadRole {
+                initiated_at_block_number: current_block,
+            }),
+            ..lead
+        };
+
+        <LeadById<T>>::insert(lead_id, new_lead);
+
+        // Update current lead
+        <CurrentLeadId<T>>::take(); // None
+
+        // Trigger event
+        Self::deposit_event(RawEvent::LeadUnset(lead_id));
+
+        Ok(())
+    }
+
     fn ensure_member_has_no_active_application_on_opening(
         curator_applications: CuratorApplicationIdSet<T>,
         member_id: T::MemberId,
@@ -2136,7 +2187,7 @@ impl<T: Trait> Module<T> {
         ),
         &'static str,
     > {
-        let next_channel_id = opt_channel_id.unwrap_or(NextChannelId::<T>::get());
+        let next_channel_id = opt_channel_id.unwrap_or_else(NextChannelId::<T>::get);
 
         Self::ensure_can_register_role_on_member(
             member_id,
@@ -2148,7 +2199,7 @@ impl<T: Trait> Module<T> {
 
     // TODO: convert InputConstraint ensurer routines into macroes
 
-    fn ensure_channel_handle_is_valid(handle: &Vec<u8>) -> dispatch::Result {
+    fn ensure_channel_handle_is_valid(handle: &[u8]) -> dispatch::Result {
         ChannelHandleConstraint::get().ensure_valid(
             handle.len(),
             MSG_CHANNEL_HANDLE_TOO_SHORT,
@@ -2212,7 +2263,7 @@ impl<T: Trait> Module<T> {
         }
     }
 
-    fn ensure_curator_application_text_is_valid(text: &Vec<u8>) -> dispatch::Result {
+    fn ensure_curator_application_text_is_valid(text: &[u8]) -> dispatch::Result {
         CuratorApplicationHumanReadableText::get().ensure_valid(
             text.len(),
             MSG_CURATOR_APPLICATION_TEXT_TOO_SHORT,
@@ -2220,7 +2271,7 @@ impl<T: Trait> Module<T> {
         )
     }
 
-    fn ensure_curator_exit_rationale_text_is_valid(text: &Vec<u8>) -> dispatch::Result {
+    fn ensure_curator_exit_rationale_text_is_valid(text: &[u8]) -> dispatch::Result {
         CuratorExitRationaleText::get().ensure_valid(
             text.len(),
             MSG_CURATOR_EXIT_RATIONALE_TEXT_TOO_SHORT,
@@ -2228,7 +2279,7 @@ impl<T: Trait> Module<T> {
         )
     }
 
-    fn ensure_opening_human_readable_text_is_valid(text: &Vec<u8>) -> dispatch::Result {
+    fn ensure_opening_human_readable_text_is_valid(text: &[u8]) -> dispatch::Result {
         OpeningHumanReadableText::get().ensure_valid(
             text.len(),
             MSG_CHANNEL_DESCRIPTION_TOO_SHORT,
@@ -2496,7 +2547,7 @@ impl<T: Trait> Module<T> {
 
         Ok((
             curator_application,
-            curator_application_id.clone(),
+            *curator_application_id,
             curator_opening,
         ))
     }
@@ -2595,7 +2646,7 @@ impl<T: Trait> Module<T> {
             PrincipalId<T>,
         >,
         exit_initiation_origin: &CuratorExitInitiationOrigin,
-        rationale_text: &Vec<u8>,
+        rationale_text: &[u8],
     ) {
         // Stop any possible recurring rewards
         let _did_deactivate_recurring_reward = if let Some(ref reward_relationship_id) =
@@ -2603,14 +2654,14 @@ impl<T: Trait> Module<T> {
         {
             // Attempt to deactivate
             recurringrewards::Module::<T>::try_to_deactivate_relationship(*reward_relationship_id)
-                .expect("Relatioship must exist")
+                .expect("Relationship must exist")
         } else {
             // Did not deactivate, there was no reward relationship!
             false
         };
 
-        // When the curator is staked, unstaking must first be initated,
-        // otherwise they can be terminted right away.
+        // When the curator is staked, unstaking must first be initiated,
+        // otherwise they can be terminated right away.
 
         // Create exit summary for this termination
         let current_block = <system::Module<T>>::block_number();
@@ -2619,34 +2670,31 @@ impl<T: Trait> Module<T> {
             CuratorExitSummary::new(exit_initiation_origin, &current_block, rationale_text);
 
         // Determine new curator stage and event to emit
-        let (new_curator_stage, unstake_directions, event) =
-            if let Some(ref stake_profile) = curator.role_stake_profile {
-                // Determine unstaknig period based on who initiated deactivation
-                let unstaking_period = match curator_exit_summary.origin {
-                    CuratorExitInitiationOrigin::Lead => stake_profile.termination_unstaking_period,
-                    CuratorExitInitiationOrigin::Curator => stake_profile.exit_unstaking_period,
-                };
-
-                (
-                    CuratorRoleStage::Unstaking(curator_exit_summary),
-                    Some((stake_profile.stake_id.clone(), unstaking_period)),
-                    RawEvent::CuratorUnstaking(curator_id.clone()),
-                )
-            } else {
-                (
-                    CuratorRoleStage::Exited(curator_exit_summary.clone()),
-                    None,
-                    match curator_exit_summary.origin {
-                        CuratorExitInitiationOrigin::Lead => {
-                            RawEvent::TerminatedCurator(curator_id.clone())
-                        }
-                        CuratorExitInitiationOrigin::Curator => {
-                            RawEvent::CuratorExited(curator_id.clone())
-                        }
-                    },
-                )
+        let (new_curator_stage, unstake_directions, event) = if let Some(ref stake_profile) =
+            curator.role_stake_profile
+        {
+            // Determine unstaknig period based on who initiated deactivation
+            let unstaking_period = match curator_exit_summary.origin {
+                CuratorExitInitiationOrigin::Lead => stake_profile.termination_unstaking_period,
+                CuratorExitInitiationOrigin::Curator => stake_profile.exit_unstaking_period,
             };
 
+            (
+                CuratorRoleStage::Unstaking(curator_exit_summary),
+                Some((stake_profile.stake_id, unstaking_period)),
+                RawEvent::CuratorUnstaking(*curator_id),
+            )
+        } else {
+            (
+                CuratorRoleStage::Exited(curator_exit_summary.clone()),
+                None,
+                match curator_exit_summary.origin {
+                    CuratorExitInitiationOrigin::Lead => RawEvent::TerminatedCurator(*curator_id),
+                    CuratorExitInitiationOrigin::Curator => RawEvent::CuratorExited(*curator_id),
+                },
+            )
+        };
+
         // Update curator
         let new_curator = Curator {
             stage: new_curator_stage,
@@ -2658,7 +2706,7 @@ impl<T: Trait> Module<T> {
         // Unstake if directions provided
         if let Some(directions) = unstake_directions {
             // Keep track of curator unstaking
-            let unstaker = WorkingGroupUnstaker::Curator(curator_id.clone());
+            let unstaker = WorkingGroupUnstaker::Curator(*curator_id);
             UnstakerByStakeId::<T>::insert(directions.0, unstaker);
 
             // Unstake
@@ -2687,14 +2735,14 @@ impl<T: Trait> Module<T> {
 
     fn update_channel(
         channel_id: &ChannelId<T>,
-        new_verified: &Option<bool>,
+        new_verified: Option<bool>,
         new_handle: &Option<Vec<u8>>,
         new_title: &Option<OptionalText>,
         new_description: &Option<OptionalText>,
         new_avatar: &Option<OptionalText>,
         new_banner: &Option<OptionalText>,
-        new_publication_status: &Option<ChannelPublicationStatus>,
-        new_curation_status: &Option<ChannelCurationStatus>,
+        new_publication_status: Option<ChannelPublicationStatus>,
+        new_curation_status: Option<ChannelCurationStatus>,
     ) {
         // Update channel id to handle mapping, if there is a new handle.
         if let Some(ref handle) = new_handle {

+ 7 - 2
runtime-modules/content-working-group/src/mock.rs

@@ -68,7 +68,9 @@ pub type RawLibTestEvent = RawEvent<
     CuratorApplicationId<Test>,
     CuratorId<Test>,
     CuratorApplicationIdToCuratorIdMap<Test>,
+    minting::BalanceOf<Test>,
     <Test as system::Trait>::AccountId,
+    <Test as minting::Trait>::MintId,
 >;
 
 pub fn get_last_event_or_panic() -> RawLibTestEvent {
@@ -220,11 +222,13 @@ impl<T: Trait> TestExternalitiesBuilder<T> {
         self.membership_config = Some(membership_config);
         self
     }
-    pub fn set_content_wg_config(mut self, conteng_wg_config: GenesisConfig<T>) -> Self {
+    */
+
+    pub fn with_content_wg_config(mut self, conteng_wg_config: GenesisConfig<T>) -> Self {
         self.content_wg_config = Some(conteng_wg_config);
         self
     }
-    */
+
     pub fn build(self) -> runtime_io::TestExternalities {
         // Add system
         let mut t = self
@@ -260,3 +264,4 @@ impl<T: Trait> TestExternalitiesBuilder<T> {
 pub type System = system::Module<Test>;
 pub type Balances = balances::Module<Test>;
 pub type ContentWorkingGroup = Module<Test>;
+pub type Minting = minting::Module<Test>;

+ 92 - 6
runtime-modules/content-working-group/src/tests.rs

@@ -1,6 +1,6 @@
 #![cfg(test)]
 
-//use super::genesis;
+use super::genesis;
 use super::mock::{self, *};
 //use crate::membership;
 use hiring;
@@ -1160,7 +1160,10 @@ struct SetLeadFixture {
 
 impl SetLeadFixture {
     fn call(&self) -> Result<(), &'static str> {
-        ContentWorkingGroup::set_lead(self.origin.clone(), self.member_id, self.new_role_account)
+        ContentWorkingGroup::replace_lead(
+            self.origin.clone(),
+            Some((self.member_id, self.new_role_account)),
+        )
     }
 
     pub fn call_and_assert_success(&self) {
@@ -1221,7 +1224,7 @@ struct UnsetLeadFixture {
 
 impl UnsetLeadFixture {
     fn call(&self) -> Result<(), &'static str> {
-        ContentWorkingGroup::unset_lead(self.origin.clone())
+        ContentWorkingGroup::replace_lead(self.origin.clone(), None)
     }
 
     pub fn call_and_assert_success(&self) {
@@ -2121,10 +2124,9 @@ pub fn set_lead(
 
     // Set lead
     assert_eq!(
-        ContentWorkingGroup::set_lead(
+        ContentWorkingGroup::replace_lead(
             mock::Origin::system(system::RawOrigin::Root),
-            member_id,
-            new_role_account
+            Some((member_id, new_role_account))
         )
         .unwrap(),
         ()
@@ -2184,3 +2186,87 @@ pub fn generate_too_short_length_buffer(constraint: &InputValidationLengthConstr
 pub fn generate_too_long_length_buffer(constraint: &InputValidationLengthConstraint) -> Vec<u8> {
     generate_text((constraint.max() + 1) as usize)
 }
+
+#[test]
+fn increasing_mint_capacity() {
+    const MINT_CAPACITY: u64 = 50000;
+
+    TestExternalitiesBuilder::<Test>::default()
+        .with_content_wg_config(
+            genesis::GenesisConfigBuilder::<Test>::default()
+                .with_mint_capacity(MINT_CAPACITY)
+                .build(),
+        )
+        .build()
+        .execute_with(|| {
+            let mint_id = ContentWorkingGroup::mint();
+            let mint = Minting::mints(mint_id);
+            assert_eq!(mint.capacity(), MINT_CAPACITY);
+
+            let increase = 25000;
+            // Increasing mint capacity
+            let expected_new_capacity = MINT_CAPACITY + increase;
+            assert_ok!(ContentWorkingGroup::increase_mint_capacity(
+                Origin::ROOT,
+                increase
+            ));
+            // Excpected event after increasing
+            assert_eq!(
+                get_last_event_or_panic(),
+                crate::RawEvent::MintCapacityIncreased(mint_id, increase, expected_new_capacity)
+            );
+            // Excpected value of capacity after increasing
+            let mint = Minting::mints(mint_id);
+            assert_eq!(mint.capacity(), expected_new_capacity);
+        });
+}
+
+#[test]
+fn setting_mint_capacity() {
+    const MINT_CAPACITY: u64 = 50000;
+
+    TestExternalitiesBuilder::<Test>::default()
+        .with_content_wg_config(
+            genesis::GenesisConfigBuilder::<Test>::default()
+                .with_mint_capacity(MINT_CAPACITY)
+                .build(),
+        )
+        .build()
+        .execute_with(|| {
+            let mint_id = ContentWorkingGroup::mint();
+            let mint = Minting::mints(mint_id);
+            assert_eq!(mint.capacity(), MINT_CAPACITY);
+
+            // Decreasing mint capacity
+            let new_lower_capacity = 10000;
+            let decrease = MINT_CAPACITY - new_lower_capacity;
+            assert_ok!(ContentWorkingGroup::set_mint_capacity(
+                Origin::ROOT,
+                new_lower_capacity
+            ));
+            // Correct event after decreasing
+            assert_eq!(
+                get_last_event_or_panic(),
+                crate::RawEvent::MintCapacityDecreased(mint_id, decrease, new_lower_capacity)
+            );
+            // Correct value of capacity after decreasing
+            let mint = Minting::mints(mint_id);
+            assert_eq!(mint.capacity(), new_lower_capacity);
+
+            // Increasing mint capacity
+            let new_higher_capacity = 25000;
+            let increase = new_higher_capacity - mint.capacity();
+            assert_ok!(ContentWorkingGroup::set_mint_capacity(
+                Origin::ROOT,
+                new_higher_capacity
+            ));
+            // Excpected event after increasing
+            assert_eq!(
+                get_last_event_or_panic(),
+                crate::RawEvent::MintCapacityIncreased(mint_id, increase, new_higher_capacity)
+            );
+            // Excpected value of capacity after increasing
+            let mint = Minting::mints(mint_id);
+            assert_eq!(mint.capacity(), new_higher_capacity);
+        });
+}

+ 91 - 268
runtime-modules/forum/src/lib.rs

@@ -1,209 +1,7 @@
-// Copyright 2017-2019 Parity Technologies (UK) Ltd.
-
-// This is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-
-// You should have received a copy of the GNU General Public License
-// along with Substrate.  If not, see <http://www.gnu.org/licenses/>.
-
-// Copyright 2019 Joystream Contributors
-
-//! # Runtime Example Module
-//!
-//! <!-- Original author of paragraph: @gavofyork -->
-//! The Example: A simple example of a runtime module demonstrating
-//! concepts, APIs and structures common to most runtime modules.
-//!
-//! Run `cargo doc --package runtime-example-module --open` to view this module's documentation.
-//!
-//! ### Documentation Template:<br>
-//! Add heading with custom module name
-//!
-//! # <INSERT_CUSTOM_MODULE_NAME> Module
-//!
-//! Add simple description
-//!
-//! Include the following links that shows what trait needs to be implemented to use the module
-//! and the supported dispatchables that are documented in the Call enum.
-//!
-//! - [`<INSERT_CUSTOM_MODULE_NAME>::Trait`](./trait.Trait.html)
-//! - [`Call`](./enum.Call.html)
-//! - [`Module`](./struct.Module.html)
-//!
-//! ## Overview
-//!
-//! <!-- Original author of paragraph: Various. See https://github.com/paritytech/substrate-developer-hub/issues/44 -->
-//! Short description of module purpose.
-//! Links to Traits that should be implemented.
-//! What this module is for.
-//! What functionality the module provides.
-//! When to use the module (use case examples).
-//! How it is used.
-//! Inputs it uses and the source of each input.
-//! Outputs it produces.
-//!
-//! <!-- Original author of paragraph: @Kianenigma in PR https://github.com/paritytech/substrate/pull/1951 -->
-//! <!-- and comment https://github.com/paritytech/substrate-developer-hub/issues/44#issuecomment-471982710 -->
-//!
-//! ## Terminology
-//!
-//! Add terminology used in the custom module. Include concepts, storage items, or actions that you think
-//! deserve to be noted to give context to the rest of the documentation or module usage. The author needs to
-//! use some judgment about what is included. We don't want a list of every storage item nor types - the user
-//! can go to the code for that. For example, "transfer fee" is obvious and should not be included, but
-//! "free balance" and "reserved balance" should be noted to give context to the module.
-//! Please do not link to outside resources. The reference docs should be the ultimate source of truth.
-//!
-//! <!-- Original author of heading: @Kianenigma in PR https://github.com/paritytech/substrate/pull/1951 -->
-//!
-//! ## Goals
-//!
-//! Add goals that the custom module is designed to achieve.
-//!
-//! <!-- Original author of heading: @Kianenigma in PR https://github.com/paritytech/substrate/pull/1951 -->
-//!
-//! ### Scenarios
-//!
-//! <!-- Original author of paragraph: @Kianenigma. Based on PR https://github.com/paritytech/substrate/pull/1951 -->
-//!
-//! #### <INSERT_SCENARIO_NAME>
-//!
-//! Describe requirements prior to interacting with the custom module.
-//! Describe the process of interacting with the custom module for this scenario and public API functions used.
-//!
-//! ## Interface
-//!
-//! ### Supported Origins
-//!
-//! What origins are used and supported in this module (root, signed, inherent)
-//! i.e. root when `ensure_root` used
-//! i.e. inherent when `ensure_inherent` used
-//! i.e. signed when `ensure_signed` used
-//!
-//! `inherent` <INSERT_DESCRIPTION>
-//!
-//! <!-- Original author of paragraph: @Kianenigma in comment -->
-//! <!-- https://github.com/paritytech/substrate-developer-hub/issues/44#issuecomment-471982710 -->
-//!
-//! ### Types
-//!
-//! Type aliases. Include any associated types and where the user would typically define them.
-//!
-//! `ExampleType` <INSERT_DESCRIPTION>
-//!
-//! <!-- Original author of paragraph: ??? -->
-//!
-//!
-//! ### Dispatchable Functions
-//!
-//! <!-- Original author of paragraph: @AmarRSingh & @joepetrowski -->
-//!
-//! // A brief description of dispatchable functions and a link to the rustdoc with their actual documentation.
-//!
-//! <b>MUST</b> have link to Call enum
-//! <b>MUST</b> have origin information included in function doc
-//! <b>CAN</b> have more info up to the user
-//!
-//! ### Public Functions
-//!
-//! <!-- Original author of paragraph: @joepetrowski -->
-//!
-//! A link to the rustdoc and any notes about usage in the module, not for specific functions.
-//! For example, in the balances module: "Note that when using the publicly exposed functions,
-//! you (the runtime developer) are responsible for implementing any necessary checks
-//! (e.g. that the sender is the signer) before calling a function that will affect storage."
-//!
-//! <!-- Original author of paragraph: @AmarRSingh -->
-//!
-//! It is up to the writer of the respective module (with respect to how much information to provide).
-//!
-//! #### Public Inspection functions - Immutable (getters)
-//!
-//! Insert a subheading for each getter function signature
-//!
-//! ##### `example_getter_name()`
-//!
-//! What it returns
-//! Why, when, and how often to call it
-//! When it could panic or error
-//! When safety issues to consider
-//!
-//! #### Public Mutable functions (changing state)
-//!
-//! Insert a subheading for each setter function signature
-//!
-//! ##### `example_setter_name(origin, parameter_name: T::ExampleType)`
-//!
-//! What state it changes
-//! Why, when, and how often to call it
-//! When it could panic or error
-//! When safety issues to consider
-//! What parameter values are valid and why
-//!
-//! ### Storage Items
-//!
-//! Explain any storage items included in this module
-//!
-//! ### Digest Items
-//!
-//! Explain any digest items included in this module
-//!
-//! ### Inherent Data
-//!
-//! Explain what inherent data (if any) is defined in the module and any other related types
-//!
-//! ### Events:
-//!
-//! Insert events for this module if any
-//!
-//! ### Errors:
-//!
-//! Explain what generates errors
-//!
-//! ## Usage
-//!
-//! Insert 2-3 examples of usage and code snippets that show how to use <INSERT_CUSTOM_MODULE_NAME> module in a custom module.
-//!
-//! ### Prerequisites
-//!
-//! Show how to include necessary imports for <INSERT_CUSTOM_MODULE_NAME> and derive
-//! your module configuration trait with the `INSERT_CUSTOM_MODULE_NAME` trait.
-//!
-//! ```rust
-//! // use <INSERT_CUSTOM_MODULE_NAME>;
-//!
-//! // pub trait Trait: <INSERT_CUSTOM_MODULE_NAME>::Trait { }
-//! ```
-//!
-//! ### Simple Code Snippet
-//!
-//! Show a simple example (e.g. how to query a public getter function of <INSERT_CUSTOM_MODULE_NAME>)
-//!
-//! ## Genesis Config
-//!
-//! <!-- Original author of paragraph: @joepetrowski -->
-//!
-//! ## Dependencies
-//!
-//! Dependencies on other SRML modules and the genesis config should be mentioned,
-//! but not the Rust Standard Library.
-//! Genesis configuration modifications that may be made to incorporate this module
-//! Interaction with other modules
-//!
-//! <!-- Original author of heading: @AmarRSingh -->
-//!
-//! ## Related Modules
-//!
-//! Interaction with other modules in the form of a bullet point list
-//!
-//! ## References
-//!
-//! <!-- Original author of paragraph: @joepetrowski -->
-//!
-//! Links to reference material, if applicable. For example, Phragmen, W3F research, etc.
-//! that the implementation is based on.
+// Clippy linter warning
+#![allow(clippy::type_complexity)]
+// disable it because of possible frontend API break
+// TODO: remove post-Constaninople
 
 // Ensure we're `no_std` when compiling for Wasm.
 #![cfg_attr(not(feature = "std"), no_std)]
@@ -211,10 +9,12 @@
 #[cfg(feature = "std")]
 use serde_derive::{Deserialize, Serialize};
 
+use rstd::borrow::ToOwned;
 use rstd::prelude::*;
 
-use codec::{Decode, Encode};
-use srml_support::{decl_event, decl_module, decl_storage, dispatch, ensure};
+use codec::{Codec, Decode, Encode};
+use runtime_primitives::traits::{MaybeSerialize, Member, One, SimpleArithmetic};
+use srml_support::{decl_event, decl_module, decl_storage, dispatch, ensure, Parameter};
 
 mod mock;
 mod tests;
@@ -307,7 +107,6 @@ const ERROR_CATEGORY_CANNOT_BE_UNARCHIVED_WHEN_DELETED: &str =
 //#[cfg(any(feature = "std", test))]
 //use sr_primitives::{StorageOverlay, ChildrenStorageOverlay};
 
-use system;
 use system::{ensure_root, ensure_signed};
 
 /// Represents a user in this forum.
@@ -358,13 +157,10 @@ pub struct PostTextChange<BlockNumber, Moment> {
     text: Vec<u8>,
 }
 
-/// Represents a post identifier
-pub type PostId = u64;
-
 /// Represents a thread post
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
 #[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
-pub struct Post<BlockNumber, Moment, AccountId> {
+pub struct Post<BlockNumber, Moment, AccountId, ThreadId, PostId> {
     /// Post identifier
     id: PostId,
 
@@ -394,13 +190,10 @@ pub struct Post<BlockNumber, Moment, AccountId> {
     author_id: AccountId,
 }
 
-/// Represents a thread identifier
-pub type ThreadId = u64;
-
 /// Represents a thread
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
 #[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
-pub struct Thread<BlockNumber, Moment, AccountId> {
+pub struct Thread<BlockNumber, Moment, AccountId, ThreadId> {
     /// Thread identifier
     id: ThreadId,
 
@@ -440,7 +233,7 @@ pub struct Thread<BlockNumber, Moment, AccountId> {
     author_id: AccountId,
 }
 
-impl<BlockNumber, Moment, AccountId> Thread<BlockNumber, Moment, AccountId> {
+impl<BlockNumber, Moment, AccountId, ThreadId> Thread<BlockNumber, Moment, AccountId, ThreadId> {
     fn num_posts_ever_created(&self) -> u32 {
         self.num_unmoderated_posts + self.num_moderated_posts
     }
@@ -523,6 +316,26 @@ pub trait Trait: system::Trait + timestamp::Trait + Sized {
     type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
 
     type MembershipRegistry: ForumUserRegistry<Self::AccountId>;
+
+    /// Thread Id type
+    type ThreadId: Parameter
+        + Member
+        + SimpleArithmetic
+        + Codec
+        + Default
+        + Copy
+        + MaybeSerialize
+        + PartialEq;
+
+    /// Post Id type
+    type PostId: Parameter
+        + Member
+        + SimpleArithmetic
+        + Codec
+        + Default
+        + Copy
+        + MaybeSerialize
+        + PartialEq;
 }
 
 decl_storage! {
@@ -535,16 +348,16 @@ decl_storage! {
         pub NextCategoryId get(next_category_id) config(): CategoryId;
 
         /// Map thread identifier to corresponding thread.
-        pub ThreadById get(thread_by_id) config(): map ThreadId => Thread<T::BlockNumber, T::Moment, T::AccountId>;
+        pub ThreadById get(thread_by_id) config(): map T::ThreadId => Thread<T::BlockNumber, T::Moment, T::AccountId, T::ThreadId>;
 
         /// Thread identifier value to be used for next Thread in threadById.
-        pub NextThreadId get(next_thread_id) config(): ThreadId;
+        pub NextThreadId get(next_thread_id) config(): T::ThreadId;
 
         /// Map post identifier to corresponding post.
-        pub PostById get(post_by_id) config(): map PostId => Post<T::BlockNumber, T::Moment, T::AccountId>;
+        pub PostById get(post_by_id) config(): map T::PostId => Post<T::BlockNumber, T::Moment, T::AccountId, T::ThreadId, T::PostId>;
 
         /// Post identifier value to be used for for next post created.
-        pub NextPostId get(next_post_id) config(): PostId;
+        pub NextPostId get(next_post_id) config(): T::PostId;
 
         /// Account of forum sudo.
         pub ForumSudo get(forum_sudo) config(): Option<T::AccountId>;
@@ -588,6 +401,8 @@ decl_event!(
     pub enum Event<T>
     where
         <T as system::Trait>::AccountId,
+        <T as Trait>::ThreadId,
+        <T as Trait>::PostId,
     {
         /// A category was introduced
         CategoryCreated(CategoryId),
@@ -633,7 +448,7 @@ decl_module! {
              */
 
             // Hold on to old value
-            let old_forum_sudo = <ForumSudo<T>>::get().clone();
+            let old_forum_sudo = <ForumSudo<T>>::get();
 
             // Update forum sudo
             match new_forum_sudo.clone() {
@@ -703,8 +518,8 @@ decl_module! {
             // Create new category
             let new_category = Category {
                 id : next_category_id,
-                title : title.clone(),
-                description: description.clone(),
+                title,
+                description,
                 created_at : Self::current_block_and_time(),
                 deleted: false,
                 archived: false,
@@ -753,7 +568,7 @@ decl_module! {
                 // We must skip checking category itself.
                 // NB: This is kind of hacky way to avoid last element,
                 // something clearn can be done later.
-                let mut path_to_check = category_tree_path.clone();
+                let mut path_to_check = category_tree_path;
                 path_to_check.remove(0);
 
                 Self::ensure_can_mutate_in_path_leaf(&path_to_check)?;
@@ -834,7 +649,7 @@ decl_module! {
         }
 
         /// Moderate thread
-        fn moderate_thread(origin, thread_id: ThreadId, rationale: Vec<u8>) -> dispatch::Result {
+        fn moderate_thread(origin, thread_id: T::ThreadId, rationale: Vec<u8>) -> dispatch::Result {
 
             // Check that its a valid signature
             let who = ensure_signed(origin)?;
@@ -843,7 +658,7 @@ decl_module! {
             Self::ensure_is_forum_sudo(&who)?;
 
             // Get thread
-            let mut thread = Self::ensure_thread_exists(&thread_id)?;
+            let mut thread = Self::ensure_thread_exists(thread_id)?;
 
             // Thread is not already moderated
             ensure!(thread.moderation.is_none(), ERROR_THREAD_ALREADY_MODERATED);
@@ -867,7 +682,7 @@ decl_module! {
             thread.moderation = Some(ModerationAction {
                 moderated_at: Self::current_block_and_time(),
                 moderator_id: who,
-                rationale: rationale.clone()
+                rationale
             });
 
             <ThreadById<T>>::insert(thread_id, thread.clone());
@@ -885,7 +700,7 @@ decl_module! {
         }
 
         /// Edit post text
-        fn add_post(origin, thread_id: ThreadId, text: Vec<u8>) -> dispatch::Result {
+        fn add_post(origin, thread_id: T::ThreadId, text: Vec<u8>) -> dispatch::Result {
 
             /*
              * Update SPEC with new errors,
@@ -901,7 +716,7 @@ decl_module! {
             Self::ensure_post_text_is_valid(&text)?;
 
             // Make sure thread exists and is mutable
-            let thread = Self::ensure_thread_is_mutable(&thread_id)?;
+            let thread = Self::ensure_thread_is_mutable(thread_id)?;
 
             // Get path from parent to root of category tree.
             let category_tree_path = Self::ensure_valid_category_and_build_category_tree_path(thread.category_id)?;
@@ -922,7 +737,7 @@ decl_module! {
         }
 
         /// Edit post text
-        fn edit_post_text(origin, post_id: PostId, new_text: Vec<u8>) -> dispatch::Result {
+        fn edit_post_text(origin, post_id: T::PostId, new_text: Vec<u8>) -> dispatch::Result {
 
             /* Edit spec.
               - forum member guard missing
@@ -939,7 +754,7 @@ decl_module! {
             Self::ensure_post_text_is_valid(&new_text)?;
 
             // Make sure there exists a mutable post with post id `post_id`
-            let post = Self::ensure_post_is_mutable(&post_id)?;
+            let post = Self::ensure_post_is_mutable(post_id)?;
 
             // Signer does not match creator of post with identifier postId
             ensure!(post.author_id == who, ERROR_ACCOUNT_DOES_NOT_MATCH_POST_AUTHOR);
@@ -969,7 +784,7 @@ decl_module! {
         }
 
         /// Moderate post
-        fn moderate_post(origin, post_id: PostId, rationale: Vec<u8>) -> dispatch::Result {
+        fn moderate_post(origin, post_id: T::PostId, rationale: Vec<u8>) -> dispatch::Result {
 
             // Check that its a valid signature
             let who = ensure_signed(origin)?;
@@ -978,7 +793,7 @@ decl_module! {
             Self::ensure_is_forum_sudo(&who)?;
 
             // Make sure post exists and is mutable
-            let post = Self::ensure_post_is_mutable(&post_id)?;
+            let post = Self::ensure_post_is_mutable(post_id)?;
 
             Self::ensure_post_moderation_rationale_is_valid(&rationale)?;
 
@@ -990,7 +805,7 @@ decl_module! {
             let moderation_action = ModerationAction{
                 moderated_at: Self::current_block_and_time(),
                 moderator_id: who,
-                rationale: rationale.clone()
+                rationale
             };
 
             <PostById<T>>::mutate(post_id, |p| {
@@ -1013,7 +828,7 @@ decl_module! {
 }
 
 impl<T: Trait> Module<T> {
-    fn ensure_category_title_is_valid(title: &Vec<u8>) -> dispatch::Result {
+    fn ensure_category_title_is_valid(title: &[u8]) -> dispatch::Result {
         CategoryTitleConstraint::get().ensure_valid(
             title.len(),
             ERROR_CATEGORY_TITLE_TOO_SHORT,
@@ -1021,7 +836,7 @@ impl<T: Trait> Module<T> {
         )
     }
 
-    fn ensure_category_description_is_valid(description: &Vec<u8>) -> dispatch::Result {
+    fn ensure_category_description_is_valid(description: &[u8]) -> dispatch::Result {
         CategoryDescriptionConstraint::get().ensure_valid(
             description.len(),
             ERROR_CATEGORY_DESCRIPTION_TOO_SHORT,
@@ -1029,7 +844,7 @@ impl<T: Trait> Module<T> {
         )
     }
 
-    fn ensure_thread_moderation_rationale_is_valid(rationale: &Vec<u8>) -> dispatch::Result {
+    fn ensure_thread_moderation_rationale_is_valid(rationale: &[u8]) -> dispatch::Result {
         ThreadModerationRationaleConstraint::get().ensure_valid(
             rationale.len(),
             ERROR_THREAD_MODERATION_RATIONALE_TOO_SHORT,
@@ -1037,7 +852,7 @@ impl<T: Trait> Module<T> {
         )
     }
 
-    fn ensure_thread_title_is_valid(title: &Vec<u8>) -> dispatch::Result {
+    fn ensure_thread_title_is_valid(title: &[u8]) -> dispatch::Result {
         ThreadTitleConstraint::get().ensure_valid(
             title.len(),
             ERROR_THREAD_TITLE_TOO_SHORT,
@@ -1045,7 +860,7 @@ impl<T: Trait> Module<T> {
         )
     }
 
-    fn ensure_post_text_is_valid(text: &Vec<u8>) -> dispatch::Result {
+    fn ensure_post_text_is_valid(text: &[u8]) -> dispatch::Result {
         PostTextConstraint::get().ensure_valid(
             text.len(),
             ERROR_POST_TEXT_TOO_SHORT,
@@ -1053,7 +868,7 @@ impl<T: Trait> Module<T> {
         )
     }
 
-    fn ensure_post_moderation_rationale_is_valid(rationale: &Vec<u8>) -> dispatch::Result {
+    fn ensure_post_moderation_rationale_is_valid(rationale: &[u8]) -> dispatch::Result {
         PostModerationRationaleConstraint::get().ensure_valid(
             rationale.len(),
             ERROR_POST_MODERATION_RATIONALE_TOO_SHORT,
@@ -1069,8 +884,9 @@ impl<T: Trait> Module<T> {
     }
 
     fn ensure_post_is_mutable(
-        post_id: &PostId,
-    ) -> Result<Post<T::BlockNumber, T::Moment, T::AccountId>, &'static str> {
+        post_id: T::PostId,
+    ) -> Result<Post<T::BlockNumber, T::Moment, T::AccountId, T::ThreadId, T::PostId>, &'static str>
+    {
         // Make sure post exists
         let post = Self::ensure_post_exists(post_id)?;
 
@@ -1078,14 +894,15 @@ impl<T: Trait> Module<T> {
         ensure!(post.moderation.is_none(), ERROR_POST_MODERATED);
 
         // and make sure thread is mutable
-        Self::ensure_thread_is_mutable(&post.thread_id)?;
+        Self::ensure_thread_is_mutable(post.thread_id)?;
 
         Ok(post)
     }
 
     fn ensure_post_exists(
-        post_id: &PostId,
-    ) -> Result<Post<T::BlockNumber, T::Moment, T::AccountId>, &'static str> {
+        post_id: T::PostId,
+    ) -> Result<Post<T::BlockNumber, T::Moment, T::AccountId, T::ThreadId, T::PostId>, &'static str>
+    {
         if <PostById<T>>::exists(post_id) {
             Ok(<PostById<T>>::get(post_id))
         } else {
@@ -1094,10 +911,10 @@ impl<T: Trait> Module<T> {
     }
 
     fn ensure_thread_is_mutable(
-        thread_id: &ThreadId,
-    ) -> Result<Thread<T::BlockNumber, T::Moment, T::AccountId>, &'static str> {
+        thread_id: T::ThreadId,
+    ) -> Result<Thread<T::BlockNumber, T::Moment, T::AccountId, T::ThreadId>, &'static str> {
         // Make sure thread exists
-        let thread = Self::ensure_thread_exists(&thread_id)?;
+        let thread = Self::ensure_thread_exists(thread_id)?;
 
         // and is unmoderated
         ensure!(thread.moderation.is_none(), ERROR_THREAD_MODERATED);
@@ -1109,8 +926,8 @@ impl<T: Trait> Module<T> {
     }
 
     fn ensure_thread_exists(
-        thread_id: &ThreadId,
-    ) -> Result<Thread<T::BlockNumber, T::Moment, T::AccountId>, &'static str> {
+        thread_id: T::ThreadId,
+    ) -> Result<Thread<T::BlockNumber, T::Moment, T::AccountId, T::ThreadId>, &'static str> {
         if <ThreadById<T>>::exists(thread_id) {
             Ok(<ThreadById<T>>::get(thread_id))
         } else {
@@ -1153,6 +970,9 @@ impl<T: Trait> Module<T> {
         Self::ensure_can_mutate_in_path_leaf(&category_tree_path)
     }
 
+    // Clippy linter warning
+    #[allow(clippy::ptr_arg)] // disable it because of possible frontend API break
+                              // TODO: remove post-Constaninople
     fn ensure_can_mutate_in_path_leaf(
         category_tree_path: &CategoryTreePath<T::BlockNumber, T::Moment, T::AccountId>,
     ) -> dispatch::Result {
@@ -1167,6 +987,9 @@ impl<T: Trait> Module<T> {
         Ok(())
     }
 
+    // TODO: remove post-Constaninople
+    // Clippy linter warning
+    #[allow(clippy::ptr_arg)] // disable it because of possible frontend API break
     fn ensure_can_add_subcategory_path_leaf(
         category_tree_path: &CategoryTreePath<T::BlockNumber, T::Moment, T::AccountId>,
     ) -> dispatch::Result {
@@ -1194,7 +1017,7 @@ impl<T: Trait> Module<T> {
         // Get path from parent to root of category tree.
         let category_tree_path = Self::build_category_tree_path(category_id);
 
-        assert!(category_tree_path.len() > 0);
+        assert!(!category_tree_path.is_empty());
 
         Ok(category_tree_path)
     }
@@ -1239,19 +1062,19 @@ impl<T: Trait> Module<T> {
 
     fn add_new_thread(
         category_id: CategoryId,
-        title: &Vec<u8>,
+        title: &[u8],
         author_id: &T::AccountId,
-    ) -> Thread<T::BlockNumber, T::Moment, T::AccountId> {
+    ) -> Thread<T::BlockNumber, T::Moment, T::AccountId, T::ThreadId> {
         // Get category
         let category = <CategoryById<T>>::get(category_id);
 
         // Create and add new thread
-        let new_thread_id = NextThreadId::get();
+        let new_thread_id = NextThreadId::<T>::get();
 
         let new_thread = Thread {
             id: new_thread_id,
-            title: title.clone(),
-            category_id: category_id,
+            title: title.to_owned(),
+            category_id,
             nr_in_category: category.num_threads_created() + 1,
             moderation: None,
             num_unmoderated_posts: 0,
@@ -1264,8 +1087,8 @@ impl<T: Trait> Module<T> {
         <ThreadById<T>>::insert(new_thread_id, new_thread.clone());
 
         // Update next thread id
-        NextThreadId::mutate(|n| {
-            *n += 1;
+        NextThreadId::<T>::mutate(|n| {
+            *n += One::one();
         });
 
         // Update unmoderated thread count in corresponding category
@@ -1279,21 +1102,21 @@ impl<T: Trait> Module<T> {
     /// Creates and ads a new post ot the given thread, and makes all required state updates
     /// `thread_id` must be valid
     fn add_new_post(
-        thread_id: ThreadId,
-        text: &Vec<u8>,
+        thread_id: T::ThreadId,
+        text: &[u8],
         author_id: &T::AccountId,
-    ) -> Post<T::BlockNumber, T::Moment, T::AccountId> {
+    ) -> Post<T::BlockNumber, T::Moment, T::AccountId, T::ThreadId, T::PostId> {
         // Get thread
         let thread = <ThreadById<T>>::get(thread_id);
 
         // Make and add initial post
-        let new_post_id = NextPostId::get();
+        let new_post_id = NextPostId::<T>::get();
 
         let new_post = Post {
             id: new_post_id,
-            thread_id: thread_id,
+            thread_id,
             nr_in_thread: thread.num_posts_ever_created() + 1,
-            current_text: text.clone(),
+            current_text: text.to_owned(),
             moderation: None,
             text_change_history: vec![],
             created_at: Self::current_block_and_time(),
@@ -1304,8 +1127,8 @@ impl<T: Trait> Module<T> {
         <PostById<T>>::insert(new_post_id, new_post.clone());
 
         // Update next post id
-        NextPostId::mutate(|n| {
-            *n += 1;
+        NextPostId::<T>::mutate(|n| {
+            *n += One::one();
         });
 
         // Update unmoderated post count of thread

+ 21 - 14
runtime-modules/forum/src/mock.rs

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

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

@@ -17,6 +17,7 @@ std = [
 	'rstd/std',
 	'common/std',
 	'membership/std',
+	'minting/std',
 ]
 
 [dependencies.sr-primitives]
@@ -86,4 +87,14 @@ rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
 default_features = false
 git = 'https://github.com/paritytech/substrate.git'
 package = 'srml-balances'
-rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.minting]
+default_features = false
+package = 'substrate-token-mint-module'
+path = '../token-minting'
+
+[dependencies.recurringrewards]
+default_features = false
+package = 'substrate-recurring-reward-module'
+path = '../recurring-reward'

+ 209 - 11
runtime-modules/governance/src/council.rs

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

+ 173 - 89
runtime-modules/governance/src/election.rs

@@ -1,3 +1,34 @@
+//! Council Elections Manager
+//!
+//! # Election Parameters:
+//! We don't currently handle zero periods, zero council term, zero council size and candidacy
+//! limit in any special way. The behaviour in such cases:
+//!
+//! - Setting any period to 0 will mean the election getting stuck in that stage, until force changing
+//! the state.
+//!
+//! - Council Size of 0 - no limit to size of council, all applicants that move beyond
+//! announcing stage would become council members, so effectively the candidacy limit will
+//! be the size of the council, voting and revealing have no impact on final results.
+//!
+//! - If candidacy limit is zero and council size > 0, council_size number of applicants will reach the voting stage.
+//! and become council members, voting will have no impact on final results.
+//!
+//! - If both candidacy limit and council size are zero then all applicant become council members
+//! since no filtering occurs at end of announcing stage.
+//!
+//! We only guard against these edge cases in the [`set_election_parameters`] call.
+//!
+//! [`set_election_parameters`]: struct.Module.html#method.set_election_parameters
+
+// Clippy linter warning
+#![allow(clippy::type_complexity)]
+// disable it because of possible frontend API break
+// TODO: remove post-Constaninople
+
+// Clippy linter warning
+#![allow(clippy::redundant_closure_call)] // disable it because of the substrate lib design
+
 use rstd::prelude::*;
 use srml_support::traits::{Currency, ReservableCurrency};
 use srml_support::{decl_event, decl_module, decl_storage, dispatch::Result, ensure};
@@ -14,6 +45,7 @@ use super::sealed_vote::SealedVote;
 use super::stake::Stake;
 
 use super::council;
+use crate::election_params::ElectionParameters;
 pub use common::currency::{BalanceOf, GovernanceCurrency};
 
 pub trait Trait:
@@ -24,6 +56,8 @@ pub trait Trait:
     type CouncilElected: CouncilElected<Seats<Self::AccountId, BalanceOf<Self>>, Self::BlockNumber>;
 }
 
+pub static MSG_CANNOT_CHANGE_PARAMS_DURING_ELECTION: &str = "CannotChangeParamsDuringElection";
+
 #[derive(Clone, Copy, Encode, Decode)]
 pub enum ElectionStage<BlockNumber> {
     Announcing(BlockNumber),
@@ -73,6 +107,19 @@ impl<Elected, Term, X: CouncilElected<Elected, Term>> CouncilElected<Elected, Te
         X::council_elected(new_council, term);
     }
 }
+// Chain of handlers.
+impl<
+        Elected: Clone,
+        Term: Clone,
+        X: CouncilElected<Elected, Term>,
+        Y: CouncilElected<Elected, Term>,
+    > CouncilElected<Elected, Term> for (X, Y)
+{
+    fn council_elected(new_council: Elected, term: Term) {
+        X::council_elected(new_council.clone(), term.clone());
+        Y::council_elected(new_council, term);
+    }
+}
 
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
 #[derive(Clone, Copy, Encode, Decode, Default)]
@@ -106,16 +153,25 @@ decl_storage! {
         // TODO value type of this map looks scary, is there any way to simplify the notation?
         Votes get(votes): map T::Hash => SealedVote<T::AccountId, ElectionStake<T>, T::Hash, T::AccountId>;
 
-        // Current Election Parameters - default "zero" values are not meaningful. Running an election without
-        // settings reasonable values is a bad idea. Parameters can be set in the TriggerElection hook.
-        AnnouncingPeriod get(announcing_period) config(): T::BlockNumber = T::BlockNumber::from(100);
-        VotingPeriod get(voting_period) config(): T::BlockNumber = T::BlockNumber::from(100);
-        RevealingPeriod get(revealing_period) config(): T::BlockNumber = T::BlockNumber::from(100);
-        CouncilSize get(council_size) config(): u32 = 10;
-        CandidacyLimit get (candidacy_limit) config(): u32 = 20;
-        MinCouncilStake get(min_council_stake) config(): BalanceOf<T> = BalanceOf::<T>::from(100);
-        NewTermDuration get(new_term_duration) config(): T::BlockNumber = T::BlockNumber::from(1000);
-        MinVotingStake get(min_voting_stake) config(): BalanceOf<T> = BalanceOf::<T>::from(10);
+        // Current Election Parameters.
+        // Should we replace all the individual values with a single ElectionParameters type?
+        // Having them individually makes it more flexible to add and remove new parameters in future
+        // without dealing with migration issues.
+        AnnouncingPeriod get(announcing_period): T::BlockNumber;
+        VotingPeriod get(voting_period): T::BlockNumber;
+        RevealingPeriod get(revealing_period): T::BlockNumber;
+        CouncilSize get(council_size): u32;
+        CandidacyLimit get (candidacy_limit): u32;
+        MinCouncilStake get(min_council_stake): BalanceOf<T>;
+        NewTermDuration get(new_term_duration): T::BlockNumber;
+        MinVotingStake get(min_voting_stake): BalanceOf<T>;
+    }
+    add_extra_genesis {
+        config(election_parameters): ElectionParameters<BalanceOf<T>, T::BlockNumber>;
+        build(|config: &GenesisConfig<T>| {
+            config.election_parameters.ensure_valid().expect("Invalid Election Parameters");
+            Module::<T>::set_verified_election_parameters(config.election_parameters);
+        });
     }
 }
 
@@ -156,7 +212,7 @@ impl<T: Trait> Module<T> {
     }
 
     fn can_participate(sender: &T::AccountId) -> bool {
-        !T::Currency::free_balance(sender).is_zero()
+        !<T as GovernanceCurrency>::Currency::free_balance(sender).is_zero()
             && <membership::members::Module<T>>::is_member_account(sender)
     }
 
@@ -187,15 +243,15 @@ impl<T: Trait> Module<T> {
     fn start_election(current_council: Seats<T::AccountId, BalanceOf<T>>) -> Result {
         ensure!(!Self::is_election_running(), "election already in progress");
         ensure!(
-            Self::existing_stake_holders().len() == 0,
+            Self::existing_stake_holders().is_empty(),
             "stake holders must be empty"
         );
-        ensure!(Self::applicants().len() == 0, "applicants must be empty");
-        ensure!(Self::commitments().len() == 0, "commitments must be empty");
+        ensure!(Self::applicants().is_empty(), "applicants must be empty");
+        ensure!(Self::commitments().is_empty(), "commitments must be empty");
 
         // Take snapshot of seat and backing stakes of an existing council
         // Its important to note that the election system takes ownership of these stakes, and is responsible
-        // to return any unused stake to original owners and the end of the election.
+        // to return any unused stake to original owners at the end of the election.
         Self::initialize_transferable_stakes(current_council);
 
         Self::deposit_event(RawEvent::ElectionStarted());
@@ -249,6 +305,7 @@ impl<T: Trait> Module<T> {
         if len >= applicants.len() {
             &[]
         } else {
+            #[allow(clippy::redundant_closure)] // disable incorrect Clippy linter warning
             applicants.sort_by_key(|applicant| Self::applicant_stakes(applicant));
             &applicants[0..applicants.len() - len]
         }
@@ -303,17 +360,21 @@ impl<T: Trait> Module<T> {
             }
         }
 
-        if new_council.len() == Self::council_size_usize() {
-            // all applicants in the tally will form the new council
-        } else if new_council.len() > Self::council_size_usize() {
-            // we have more than enough applicants to form the new council.
-            // select top staked
-            Self::filter_top_staked(&mut new_council, Self::council_size_usize());
-        } else {
-            // Not enough applicants with votes to form a council.
-            // This may happen if we didn't add applicants with zero votes to the tally,
-            // or in future if we allow applicants to withdraw candidacy during voting or revealing stages.
-            // or council size was increased during voting, revealing stages.
+        match new_council.len() {
+            ncl if ncl == Self::council_size_usize() => {
+                // all applicants in the tally will form the new council
+            }
+            ncl if ncl > Self::council_size_usize() => {
+                // we have more than enough applicants to form the new council.
+                // select top staked
+                Self::filter_top_staked(&mut new_council, Self::council_size_usize());
+            }
+            _ => {
+                // Not enough applicants with votes to form a council.
+                // This may happen if we didn't add applicants with zero votes to the tally,
+                // or in future if we allow applicants to withdraw candidacy during voting or revealing stages.
+                // or council size was increased during voting, revealing stages.
+            }
         }
 
         // unless we want to add more filtering criteria to what is considered a successful election
@@ -332,7 +393,7 @@ impl<T: Trait> Module<T> {
     }
 
     fn teardown_election(
-        votes: &Vec<SealedVote<T::AccountId, Stake<BalanceOf<T>>, T::Hash, T::AccountId>>,
+        votes: &[SealedVote<T::AccountId, Stake<BalanceOf<T>>, T::Hash, T::AccountId>],
         new_council: &BTreeMap<T::AccountId, Seat<T::AccountId, BalanceOf<T>>>,
         unlock_ts: bool,
     ) {
@@ -356,7 +417,10 @@ impl<T: Trait> Module<T> {
         for stakeholder in Self::existing_stake_holders().iter() {
             let stake = Self::transferable_stakes(stakeholder);
             if !stake.seat.is_zero() || !stake.backing.is_zero() {
-                T::Currency::unreserve(stakeholder, stake.seat + stake.backing);
+                <T as GovernanceCurrency>::Currency::unreserve(
+                    stakeholder,
+                    stake.seat + stake.backing,
+                );
             }
         }
     }
@@ -381,7 +445,7 @@ impl<T: Trait> Module<T> {
 
         // return new stake to account's free balance
         if !stake.new.is_zero() {
-            T::Currency::unreserve(applicant, stake.new);
+            <T as GovernanceCurrency>::Currency::unreserve(applicant, stake.new);
         }
 
         // return unused transferable stake
@@ -418,7 +482,7 @@ impl<T: Trait> Module<T> {
     }
 
     fn refund_voting_stakes(
-        sealed_votes: &Vec<SealedVote<T::AccountId, Stake<BalanceOf<T>>, T::Hash, T::AccountId>>,
+        sealed_votes: &[SealedVote<T::AccountId, Stake<BalanceOf<T>>, T::Hash, T::AccountId>],
         new_council: &BTreeMap<T::AccountId, Seat<T::AccountId, BalanceOf<T>>>,
     ) {
         for sealed_vote in sealed_votes.iter() {
@@ -435,7 +499,7 @@ impl<T: Trait> Module<T> {
                 // return new stake to account's free balance
                 let SealedVote { voter, stake, .. } = sealed_vote;
                 if !stake.new.is_zero() {
-                    T::Currency::unreserve(voter, stake.new);
+                    <T as GovernanceCurrency>::Currency::unreserve(voter, stake.new);
                 }
 
                 // return unused transferable stake
@@ -456,7 +520,7 @@ impl<T: Trait> Module<T> {
     }
 
     fn tally_votes(
-        sealed_votes: &Vec<SealedVote<T::AccountId, Stake<BalanceOf<T>>, T::Hash, T::AccountId>>,
+        sealed_votes: &[SealedVote<T::AccountId, Stake<BalanceOf<T>>, T::Hash, T::AccountId>],
     ) -> BTreeMap<T::AccountId, Seat<T::AccountId, BalanceOf<T>>> {
         let mut tally: BTreeMap<T::AccountId, Seat<T::AccountId, BalanceOf<T>>> = BTreeMap::new();
 
@@ -612,7 +676,7 @@ impl<T: Trait> Module<T> {
             *transferable
         };
 
-        *transferable = *transferable - transferred;
+        *transferable -= transferred;
 
         Stake {
             new: new_stake - transferred,
@@ -626,12 +690,12 @@ impl<T: Trait> Module<T> {
         let new_stake = Self::new_stake_reusing_transferable(&mut transferable_stake.seat, stake);
 
         ensure!(
-            T::Currency::can_reserve(&applicant, new_stake.new),
+            <T as GovernanceCurrency>::Currency::can_reserve(&applicant, new_stake.new),
             "not enough free balance to reserve"
         );
 
         ensure!(
-            T::Currency::reserve(&applicant, new_stake.new).is_ok(),
+            <T as GovernanceCurrency>::Currency::reserve(&applicant, new_stake.new).is_ok(),
             "failed to reserve applicant stake!"
         );
 
@@ -648,7 +712,7 @@ impl<T: Trait> Module<T> {
             <Applicants<T>>::mutate(|applicants| applicants.insert(0, applicant.clone()));
         }
 
-        <ApplicantStakes<T>>::insert(applicant.clone(), total_stake);
+        <ApplicantStakes<T>>::insert(applicant, total_stake);
 
         Ok(())
     }
@@ -662,12 +726,12 @@ impl<T: Trait> Module<T> {
             Self::new_stake_reusing_transferable(&mut transferable_stake.backing, stake);
 
         ensure!(
-            T::Currency::can_reserve(&voter, vote_stake.new),
+            <T as GovernanceCurrency>::Currency::can_reserve(&voter, vote_stake.new),
             "not enough free balance to reserve"
         );
 
         ensure!(
-            T::Currency::reserve(&voter, vote_stake.new).is_ok(),
+            <T as GovernanceCurrency>::Currency::reserve(&voter, vote_stake.new).is_ok(),
             "failed to reserve voting stake!"
         );
 
@@ -703,7 +767,7 @@ impl<T: Trait> Module<T> {
             "vote for non-applicant not allowed"
         );
 
-        let mut salt = salt.clone();
+        let mut salt = salt;
 
         // Tries to unseal, if salt is invalid will return error
         sealed_vote.unseal(vote_for, &mut salt, <T as system::Trait>::Hashing::hash)?;
@@ -713,6 +777,17 @@ impl<T: Trait> Module<T> {
 
         Ok(())
     }
+
+    fn set_verified_election_parameters(params: ElectionParameters<BalanceOf<T>, T::BlockNumber>) {
+        <AnnouncingPeriod<T>>::put(params.announcing_period);
+        <VotingPeriod<T>>::put(params.voting_period);
+        <RevealingPeriod<T>>::put(params.revealing_period);
+        <MinCouncilStake<T>>::put(params.min_council_stake);
+        <NewTermDuration<T>>::put(params.new_term_duration);
+        CouncilSize::put(params.council_size);
+        CandidacyLimit::put(params.candidacy_limit);
+        <MinVotingStake<T>>::put(params.min_voting_stake);
+    }
 }
 
 decl_module! {
@@ -803,52 +878,16 @@ decl_module! {
             <Stage<T>>::put(ElectionStage::Voting(ends_at));
         }
 
-        fn set_param_announcing_period(origin, period: T::BlockNumber) {
-            ensure_root(origin)?;
-            ensure!(!Self::is_election_running(), "cannot change params during election");
-            ensure!(!period.is_zero(), "period cannot be zero");
-            <AnnouncingPeriod<T>>::put(period);
-        }
-        fn set_param_voting_period(origin,  period: T::BlockNumber) {
-            ensure_root(origin)?;
-            ensure!(!Self::is_election_running(), "cannot change params during election");
-            ensure!(!period.is_zero(), "period cannot be zero");
-            <VotingPeriod<T>>::put(period);
-        }
-        fn set_param_revealing_period(origin, period: T::BlockNumber) {
-            ensure_root(origin)?;
-            ensure!(!Self::is_election_running(), "cannot change params during election");
-            ensure!(!period.is_zero(), "period cannot be zero");
-            <RevealingPeriod<T>>::put(period);
-        }
-        fn set_param_min_council_stake(origin, amount: BalanceOf<T>) {
-            ensure_root(origin)?;
-            ensure!(!Self::is_election_running(), "cannot change params during election");
-            <MinCouncilStake<T>>::put(amount);
-        }
-        fn set_param_new_term_duration(origin, duration: T::BlockNumber) {
-            ensure_root(origin)?;
-            ensure!(!Self::is_election_running(), "cannot change params during election");
-            ensure!(!duration.is_zero(), "new term duration cannot be zero");
-            <NewTermDuration<T>>::put(duration);
-        }
-        fn set_param_council_size(origin, council_size: u32) {
+        /// Sets new election parameters. Some combination of parameters that are not desirable, so
+        /// the parameters are checked for validity.
+        /// The call will fail if an election is in progress. If a council is not being elected for some
+        /// reaon after multiple rounds, force_stop_election() can be called to stop elections and followed by
+        /// set_election_parameters().
+        pub fn set_election_parameters(origin, params: ElectionParameters<BalanceOf<T>, T::BlockNumber>) {
             ensure_root(origin)?;
-            ensure!(!Self::is_election_running(), "cannot change params during election");
-            ensure!(council_size > 0, "council size cannot be zero");
-            ensure!(council_size <= Self::candidacy_limit(), "council size cannot greater than candidacy limit");
-            CouncilSize::put(council_size);
-        }
-        fn set_param_candidacy_limit(origin, limit: u32) {
-            ensure_root(origin)?;
-            ensure!(!Self::is_election_running(), "cannot change params during election");
-            ensure!(limit >= Self::council_size(), "candidacy limit cannot be less than council size");
-            CandidacyLimit::put(limit);
-        }
-        fn set_param_min_voting_stake(origin, amount: BalanceOf<T>) {
-            ensure_root(origin)?;
-            ensure!(!Self::is_election_running(), "cannot change params during election");
-            <MinVotingStake<T>>::put(amount);
+            ensure!(!Self::is_election_running(), MSG_CANNOT_CHANGE_PARAMS_DURING_ELECTION);
+            params.ensure_valid()?;
+            Self::set_verified_election_parameters(params);
         }
 
         fn force_stop_election(origin) {
@@ -886,11 +925,7 @@ decl_module! {
 impl<T: Trait> council::CouncilTermEnded for Module<T> {
     fn council_term_ended() {
         if Self::auto_start() {
-            if Self::start_election(<council::Module<T>>::active_council()).is_ok() {
-                // emit ElectionStarted
-            } else {
-                // emit ElectionFailedStart
-            }
+            let _ = Self::start_election(<council::Module<T>>::active_council());
         }
     }
 }
@@ -2042,4 +2077,53 @@ mod tests {
             assert_ok!(Election::start_election(vec![]));
         });
     }
+
+    #[test]
+    fn setting_election_parameters() {
+        initial_test_ext().execute_with(|| {
+            let default_parameters: ElectionParameters<u64, u64> = ElectionParameters::default();
+            // default all zeros is invalid
+            assert!(default_parameters.ensure_valid().is_err());
+
+            let new_parameters = ElectionParameters {
+                announcing_period: 1,
+                voting_period: 2,
+                revealing_period: 3,
+                council_size: 4,
+                candidacy_limit: 5,
+                min_voting_stake: 6,
+                min_council_stake: 7,
+                new_term_duration: 8,
+            };
+
+            assert_ok!(Election::set_election_parameters(
+                Origin::ROOT,
+                new_parameters
+            ));
+
+            assert_eq!(
+                <AnnouncingPeriod<Test>>::get(),
+                new_parameters.announcing_period
+            );
+            assert_eq!(<VotingPeriod<Test>>::get(), new_parameters.voting_period);
+            assert_eq!(
+                <RevealingPeriod<Test>>::get(),
+                new_parameters.revealing_period
+            );
+            assert_eq!(
+                <MinCouncilStake<Test>>::get(),
+                new_parameters.min_council_stake
+            );
+            assert_eq!(
+                <NewTermDuration<Test>>::get(),
+                new_parameters.new_term_duration
+            );
+            assert_eq!(CouncilSize::get(), new_parameters.council_size);
+            assert_eq!(CandidacyLimit::get(), new_parameters.candidacy_limit);
+            assert_eq!(
+                <MinVotingStake<Test>>::get(),
+                new_parameters.min_voting_stake
+            );
+        });
+    }
 }

+ 48 - 0
runtime-modules/governance/src/election_params.rs

@@ -0,0 +1,48 @@
+use codec::{Decode, Encode};
+#[cfg(feature = "std")]
+use serde::{Deserialize, Serialize};
+use sr_primitives::traits::Zero;
+use srml_support::{dispatch::Result, ensure};
+
+pub static MSG_PERIOD_CANNOT_BE_ZERO: &str = "PeriodCannotBeZero";
+pub static MSG_COUNCIL_SIZE_CANNOT_BE_ZERO: &str = "CouncilSizeCannotBeZero";
+pub static MSG_CANDIDACY_LIMIT_WAS_LOWER_THAN_COUNCIL_SIZE: &str =
+    "CandidacyWasLessThanCouncilSize";
+
+/// Combined Election parameters, as argument for set_election_parameters
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Clone, Copy, Encode, Decode, Default, PartialEq, Debug)]
+pub struct ElectionParameters<Balance, BlockNumber> {
+    pub announcing_period: BlockNumber,
+    pub voting_period: BlockNumber,
+    pub revealing_period: BlockNumber,
+    pub council_size: u32,
+    pub candidacy_limit: u32,
+    pub new_term_duration: BlockNumber,
+    pub min_council_stake: Balance,
+    pub min_voting_stake: Balance,
+}
+
+impl<Balance, BlockNumber: PartialOrd + Zero> ElectionParameters<Balance, BlockNumber> {
+    pub fn ensure_valid(&self) -> Result {
+        self.ensure_periods_are_valid()?;
+        self.ensure_council_size_and_candidacy_limit_are_valid()?;
+        Ok(())
+    }
+
+    fn ensure_periods_are_valid(&self) -> Result {
+        ensure!(!self.announcing_period.is_zero(), MSG_PERIOD_CANNOT_BE_ZERO);
+        ensure!(!self.voting_period.is_zero(), MSG_PERIOD_CANNOT_BE_ZERO);
+        ensure!(!self.revealing_period.is_zero(), MSG_PERIOD_CANNOT_BE_ZERO);
+        Ok(())
+    }
+
+    fn ensure_council_size_and_candidacy_limit_are_valid(&self) -> Result {
+        ensure!(self.council_size > 0, MSG_COUNCIL_SIZE_CANNOT_BE_ZERO);
+        ensure!(
+            self.council_size <= self.candidacy_limit,
+            MSG_CANDIDACY_LIMIT_WAS_LOWER_THAN_COUNCIL_SIZE
+        );
+        Ok(())
+    }
+}

+ 1 - 1
runtime-modules/governance/src/lib.rs

@@ -3,7 +3,7 @@
 
 pub mod council;
 pub mod election;
-pub mod proposals;
+pub mod election_params;
 
 mod sealed_vote;
 mod stake;

+ 10 - 2
runtime-modules/governance/src/mock.rs

@@ -1,6 +1,6 @@
 #![cfg(test)]
 
-pub use super::{council, election, proposals};
+pub use super::{council, election};
 pub use common::currency::GovernanceCurrency;
 pub use system;
 
@@ -70,7 +70,15 @@ impl membership::members::Trait for Test {
     type ActorId = u32;
     type InitialMembersBalance = InitialMembersBalance;
 }
-
+impl minting::Trait for Test {
+    type Currency = Balances;
+    type MintId = u64;
+}
+impl recurringrewards::Trait for Test {
+    type PayoutStatusHandler = ();
+    type RecipientId = u64;
+    type RewardRelationshipId = u64;
+}
 parameter_types! {
     pub const ExistentialDeposit: u32 = 0;
     pub const TransferFee: u32 = 0;

+ 0 - 1572
runtime-modules/governance/src/proposals.rs

@@ -1,1572 +0,0 @@
-use codec::{Decode, Encode};
-use rstd::prelude::*;
-use sr_primitives::{
-    print,
-    traits::{Hash, SaturatedConversion, Zero},
-};
-use srml_support::traits::{Currency, Get, ReservableCurrency};
-use srml_support::{decl_event, decl_module, decl_storage, dispatch, ensure};
-use system::{self, ensure_root, ensure_signed};
-
-#[cfg(feature = "std")]
-use serde::{Deserialize, Serialize};
-
-#[cfg(test)]
-use primitives::storage::well_known_keys;
-
-use super::council;
-pub use common::currency::{BalanceOf, GovernanceCurrency};
-
-const DEFAULT_APPROVAL_QUORUM: u32 = 60;
-const DEFAULT_MIN_STAKE: u32 = 100;
-const DEFAULT_CANCELLATION_FEE: u32 = 5;
-const DEFAULT_REJECTION_FEE: u32 = 10;
-
-const DEFAULT_VOTING_PERIOD_IN_DAYS: u32 = 10;
-const DEFAULT_VOTING_PERIOD_IN_SECS: u32 = DEFAULT_VOTING_PERIOD_IN_DAYS * 24 * 60 * 60;
-
-const DEFAULT_NAME_MAX_LEN: u32 = 100;
-const DEFAULT_DESCRIPTION_MAX_LEN: u32 = 10_000;
-const DEFAULT_WASM_CODE_MAX_LEN: u32 = 2_000_000;
-
-const MSG_STAKE_IS_TOO_LOW: &str = "Stake is too low";
-const MSG_STAKE_IS_GREATER_THAN_BALANCE: &str = "Balance is too low to be staked";
-const MSG_ONLY_MEMBERS_CAN_PROPOSE: &str = "Only members can make a proposal";
-const MSG_ONLY_COUNCILORS_CAN_VOTE: &str = "Only councilors can vote on proposals";
-const MSG_PROPOSAL_NOT_FOUND: &str = "This proposal does not exist";
-const MSG_PROPOSAL_EXPIRED: &str = "Voting period is expired for this proposal";
-const MSG_PROPOSAL_FINALIZED: &str = "Proposal is finalized already";
-const MSG_YOU_ALREADY_VOTED: &str = "You have already voted on this proposal";
-const MSG_YOU_DONT_OWN_THIS_PROPOSAL: &str = "You do not own this proposal";
-const MSG_PROPOSAL_STATUS_ALREADY_UPDATED: &str = "Proposal status has been updated already";
-const MSG_EMPTY_NAME_PROVIDED: &str = "Proposal cannot have an empty name";
-const MSG_EMPTY_DESCRIPTION_PROVIDED: &str = "Proposal cannot have an empty description";
-const MSG_EMPTY_WASM_CODE_PROVIDED: &str = "Proposal cannot have an empty WASM code";
-const MSG_TOO_LONG_NAME: &str = "Name is too long";
-const MSG_TOO_LONG_DESCRIPTION: &str = "Description is too long";
-const MSG_TOO_LONG_WASM_CODE: &str = "WASM code is too big";
-
-#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
-#[derive(Encode, Decode, Clone, PartialEq, Eq)]
-pub enum ProposalStatus {
-    /// A new proposal that is available for voting.
-    Active,
-    /// If cancelled by a proposer.
-    Cancelled,
-    /// Not enough votes and voting period expired.
-    Expired,
-    /// To clear the quorum requirement, the percentage of council members with revealed votes
-    /// must be no less than the quorum value for the given proposal type.
-    Approved,
-    Rejected,
-    /// If all revealed votes are slashes, then the proposal is rejected,
-    /// and the proposal stake is slashed.
-    Slashed,
-}
-
-impl Default for ProposalStatus {
-    fn default() -> Self {
-        ProposalStatus::Active
-    }
-}
-
-use self::ProposalStatus::*;
-
-#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
-#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)]
-pub enum VoteKind {
-    /// Signals presence, but unwillingness to cast judgment on substance of vote.
-    Abstain,
-    /// Pass, an alternative or a ranking, for binary, multiple choice
-    /// and ranked choice propositions, respectively.
-    Approve,
-    /// Against proposal.
-    Reject,
-    /// Against the proposal, and slash proposal stake.
-    Slash,
-}
-
-impl Default for VoteKind {
-    fn default() -> Self {
-        VoteKind::Abstain
-    }
-}
-
-use self::VoteKind::*;
-
-#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
-#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
-/// Proposal for node runtime update.
-pub struct RuntimeUpgradeProposal<AccountId, Balance, BlockNumber, Hash> {
-    id: u32,
-    proposer: AccountId,
-    stake: Balance,
-    name: Vec<u8>,
-    description: Vec<u8>,
-    wasm_hash: Hash,
-    proposed_at: BlockNumber,
-    status: ProposalStatus,
-}
-
-#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
-#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
-pub struct TallyResult<BlockNumber> {
-    proposal_id: u32,
-    abstentions: u32,
-    approvals: u32,
-    rejections: u32,
-    slashes: u32,
-    status: ProposalStatus,
-    finalized_at: BlockNumber,
-}
-
-pub trait Trait:
-    timestamp::Trait + council::Trait + GovernanceCurrency + membership::members::Trait
-{
-    /// The overarching event type.
-    type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
-}
-
-decl_event!(
-    pub enum Event<T>
-    where
-        <T as system::Trait>::Hash,
-        <T as system::Trait>::BlockNumber,
-        <T as system::Trait>::AccountId
-    {
-        // New events
-
-        /// Params:
-        /// * Account id of a member who proposed.
-        /// * Id of a newly created proposal after it was saved in storage.
-        ProposalCreated(AccountId, u32),
-        ProposalCanceled(AccountId, u32),
-        ProposalStatusUpdated(u32, ProposalStatus),
-
-        /// Params:
-        /// * Voter - an account id of a councilor.
-        /// * Id of a proposal.
-        /// * Kind of vote.
-        Voted(AccountId, u32, VoteKind),
-
-        TallyFinalized(TallyResult<BlockNumber>),
-
-        /// * Hash - hash of wasm code of runtime update.
-        RuntimeUpdated(u32, Hash),
-
-        /// Root cancelled proposal
-        ProposalVetoed(u32),
-    }
-);
-
-decl_storage! {
-    trait Store for Module<T: Trait> as Proposals {
-
-        // Parameters (defaut values could be exported to config):
-
-        // TODO rename 'approval_quorum' -> 'quorum_percent' ?!
-        /// A percent (up to 100) of the council participants
-        /// that must vote affirmatively in order to pass.
-        ApprovalQuorum get(approval_quorum) config(): u32 = DEFAULT_APPROVAL_QUORUM;
-
-        /// Minimum amount of a balance to be staked in order to make a proposal.
-        MinStake get(min_stake) config(): BalanceOf<T> =
-            BalanceOf::<T>::from(DEFAULT_MIN_STAKE);
-
-        /// A fee to be slashed (burn) in case a proposer decides to cancel a proposal.
-        CancellationFee get(cancellation_fee) config(): BalanceOf<T> =
-            BalanceOf::<T>::from(DEFAULT_CANCELLATION_FEE);
-
-        /// A fee to be slashed (burn) in case a proposal was rejected.
-        RejectionFee get(rejection_fee) config(): BalanceOf<T> =
-            BalanceOf::<T>::from(DEFAULT_REJECTION_FEE);
-
-        /// Max duration of proposal in blocks until it will be expired if not enough votes.
-        VotingPeriod get(voting_period) config(): T::BlockNumber =
-            T::BlockNumber::from(DEFAULT_VOTING_PERIOD_IN_SECS /
-            (<T as timestamp::Trait>::MinimumPeriod::get().saturated_into::<u32>() * 2));
-
-        NameMaxLen get(name_max_len) config(): u32 = DEFAULT_NAME_MAX_LEN;
-        DescriptionMaxLen get(description_max_len) config(): u32 = DEFAULT_DESCRIPTION_MAX_LEN;
-        WasmCodeMaxLen get(wasm_code_max_len) config(): u32 = DEFAULT_WASM_CODE_MAX_LEN;
-
-        // Persistent state (always relevant, changes constantly):
-
-        /// Count of all proposals that have been created.
-        ProposalCount get(proposal_count): u32;
-
-        /// Get proposal details by its id.
-        Proposals get(proposals): map u32 => RuntimeUpgradeProposal<T::AccountId, BalanceOf<T>, T::BlockNumber, T::Hash>;
-
-        /// Ids of proposals that are open for voting (have not been finalized yet).
-        ActiveProposalIds get(active_proposal_ids): Vec<u32> = vec![];
-
-        /// Get WASM code of runtime upgrade by hash of its content.
-        WasmCodeByHash get(wasm_code_by_hash): map T::Hash => Vec<u8>;
-
-        VotesByProposal get(votes_by_proposal): map u32 => Vec<(T::AccountId, VoteKind)>;
-
-        // TODO Rethink: this can be replaced with: votes_by_proposal.find(|vote| vote.0 == proposer)
-        VoteByAccountAndProposal get(vote_by_account_and_proposal): map (T::AccountId, u32) => VoteKind;
-
-        TallyResults get(tally_results): map u32 => TallyResult<T::BlockNumber>;
-    }
-}
-
-decl_module! {
-    pub struct Module<T: Trait> for enum Call where origin: T::Origin {
-
-        fn deposit_event() = default;
-
-        /// Use next code to create a proposal from Substrate UI's web console:
-        /// ```js
-        /// post({ sender: runtime.indices.ss58Decode('F7Gh'), call: calls.proposals.createProposal(2500, "0x123", "0x456", "0x789") }).tie(console.log)
-        /// ```
-        fn create_proposal(
-            origin,
-            stake: BalanceOf<T>,
-            name: Vec<u8>,
-            description: Vec<u8>,
-            wasm_code: Vec<u8>
-        ) {
-
-            let proposer = ensure_signed(origin)?;
-            ensure!(Self::can_participate(&proposer), MSG_ONLY_MEMBERS_CAN_PROPOSE);
-            ensure!(stake >= Self::min_stake(), MSG_STAKE_IS_TOO_LOW);
-
-            ensure!(!name.is_empty(), MSG_EMPTY_NAME_PROVIDED);
-            ensure!(name.len() as u32 <= Self::name_max_len(), MSG_TOO_LONG_NAME);
-
-            ensure!(!description.is_empty(), MSG_EMPTY_DESCRIPTION_PROVIDED);
-            ensure!(description.len() as u32 <= Self::description_max_len(), MSG_TOO_LONG_DESCRIPTION);
-
-            ensure!(!wasm_code.is_empty(), MSG_EMPTY_WASM_CODE_PROVIDED);
-            ensure!(wasm_code.len() as u32 <= Self::wasm_code_max_len(), MSG_TOO_LONG_WASM_CODE);
-
-            // Lock proposer's stake:
-            T::Currency::reserve(&proposer, stake)
-                .map_err(|_| MSG_STAKE_IS_GREATER_THAN_BALANCE)?;
-
-            let proposal_id = Self::proposal_count() + 1;
-            ProposalCount::put(proposal_id);
-
-            // See in substrate repo @ srml/contract/src/wasm/code_cache.rs:73
-            let wasm_hash = T::Hashing::hash(&wasm_code);
-
-            let new_proposal = RuntimeUpgradeProposal {
-                id: proposal_id,
-                proposer: proposer.clone(),
-                stake,
-                name,
-                description,
-                wasm_hash,
-                proposed_at: Self::current_block(),
-                status: Active
-            };
-
-            if !<WasmCodeByHash<T>>::exists(wasm_hash) {
-              <WasmCodeByHash<T>>::insert(wasm_hash, wasm_code);
-            }
-            <Proposals<T>>::insert(proposal_id, new_proposal);
-            ActiveProposalIds::mutate(|ids| ids.push(proposal_id));
-            Self::deposit_event(RawEvent::ProposalCreated(proposer.clone(), proposal_id));
-
-            // Auto-vote with Approve if proposer is a councilor:
-            if Self::is_councilor(&proposer) {
-                Self::_process_vote(proposer, proposal_id, Approve)?;
-            }
-        }
-
-        /// Use next code to create a proposal from Substrate UI's web console:
-        /// ```js
-        /// post({ sender: runtime.indices.ss58Decode('F7Gh'), call: calls.proposals.voteOnProposal(1, { option: "Approve", _type: "VoteKind" }) }).tie(console.log)
-        /// ```
-        fn vote_on_proposal(origin, proposal_id: u32, vote: VoteKind) {
-            let voter = ensure_signed(origin)?;
-            ensure!(Self::is_councilor(&voter), MSG_ONLY_COUNCILORS_CAN_VOTE);
-
-            ensure!(<Proposals<T>>::exists(proposal_id), MSG_PROPOSAL_NOT_FOUND);
-            let proposal = Self::proposals(proposal_id);
-
-            ensure!(proposal.status == Active, MSG_PROPOSAL_FINALIZED);
-
-            let not_expired = !Self::is_voting_period_expired(proposal.proposed_at);
-            ensure!(not_expired, MSG_PROPOSAL_EXPIRED);
-
-            let did_not_vote_before = !<VoteByAccountAndProposal<T>>::exists((voter.clone(), proposal_id));
-            ensure!(did_not_vote_before, MSG_YOU_ALREADY_VOTED);
-
-            Self::_process_vote(voter, proposal_id, vote)?;
-        }
-
-        // TODO add 'reason' why a proposer wants to cancel (UX + feedback)?
-        /// Cancel a proposal by its original proposer. Some fee will be withdrawn from his balance.
-        fn cancel_proposal(origin, proposal_id: u32) {
-            let proposer = ensure_signed(origin)?;
-
-            ensure!(<Proposals<T>>::exists(proposal_id), MSG_PROPOSAL_NOT_FOUND);
-            let proposal = Self::proposals(proposal_id);
-
-            ensure!(proposer == proposal.proposer, MSG_YOU_DONT_OWN_THIS_PROPOSAL);
-            ensure!(proposal.status == Active, MSG_PROPOSAL_FINALIZED);
-
-            // Spend some minimum fee on proposer's balance for canceling a proposal
-            let fee = Self::cancellation_fee();
-            let _ = T::Currency::slash_reserved(&proposer, fee);
-
-            // Return unspent part of remaining staked deposit (after taking some fee)
-            let left_stake = proposal.stake - fee;
-            let _ = T::Currency::unreserve(&proposer, left_stake);
-
-            Self::_update_proposal_status(proposal_id, Cancelled)?;
-            Self::deposit_event(RawEvent::ProposalCanceled(proposer, proposal_id));
-        }
-
-        // Called on every block
-        fn on_finalize(n: T::BlockNumber) {
-            if let Err(e) = Self::end_block(n) {
-                print(e);
-            }
-        }
-
-        /// Cancel a proposal and return stake without slashing
-        fn veto_proposal(origin, proposal_id: u32) {
-            ensure_root(origin)?;
-            ensure!(<Proposals<T>>::exists(proposal_id), MSG_PROPOSAL_NOT_FOUND);
-            let proposal = Self::proposals(proposal_id);
-            ensure!(proposal.status == Active, MSG_PROPOSAL_FINALIZED);
-
-            let _ = T::Currency::unreserve(&proposal.proposer, proposal.stake);
-
-            Self::_update_proposal_status(proposal_id, Cancelled)?;
-
-            Self::deposit_event(RawEvent::ProposalVetoed(proposal_id));
-        }
-
-        fn set_approval_quorum(origin, new_value: u32) {
-            ensure_root(origin)?;
-            ensure!(new_value > 0, "approval quorom must be greater than zero");
-            ApprovalQuorum::put(new_value);
-        }
-    }
-}
-
-impl<T: Trait> Module<T> {
-    fn current_block() -> T::BlockNumber {
-        <system::Module<T>>::block_number()
-    }
-
-    fn can_participate(sender: &T::AccountId) -> bool {
-        !T::Currency::free_balance(sender).is_zero()
-            && <membership::members::Module<T>>::is_member_account(sender)
-    }
-
-    fn is_councilor(sender: &T::AccountId) -> bool {
-        <council::Module<T>>::is_councilor(sender)
-    }
-
-    fn councilors_count() -> u32 {
-        <council::Module<T>>::active_council().len() as u32
-    }
-
-    fn approval_quorum_seats() -> u32 {
-        (Self::approval_quorum() * Self::councilors_count()) / 100
-    }
-
-    fn is_voting_period_expired(proposed_at: T::BlockNumber) -> bool {
-        Self::current_block() >= proposed_at + Self::voting_period()
-    }
-
-    fn _process_vote(voter: T::AccountId, proposal_id: u32, vote: VoteKind) -> dispatch::Result {
-        let new_vote = (voter.clone(), vote.clone());
-        if <VotesByProposal<T>>::exists(proposal_id) {
-            // Append a new vote to other votes on this proposal:
-            <VotesByProposal<T>>::mutate(proposal_id, |votes| votes.push(new_vote));
-        } else {
-            // This is the first vote on this proposal:
-            <VotesByProposal<T>>::insert(proposal_id, vec![new_vote]);
-        }
-        <VoteByAccountAndProposal<T>>::insert((voter.clone(), proposal_id), &vote);
-        Self::deposit_event(RawEvent::Voted(voter, proposal_id, vote));
-        Ok(())
-    }
-
-    fn end_block(_now: T::BlockNumber) -> dispatch::Result {
-        // TODO refactor this method
-
-        // TODO iterate over not expired proposals and tally
-
-        Self::tally()?;
-        // TODO approve or reject a proposal
-
-        Ok(())
-    }
-
-    /// Get the voters for the current proposal.
-    pub fn tally() -> dispatch::Result {
-        let councilors: u32 = Self::councilors_count();
-        let quorum: u32 = Self::approval_quorum_seats();
-
-        for &proposal_id in Self::active_proposal_ids().iter() {
-            let votes = Self::votes_by_proposal(proposal_id);
-            let mut abstentions: u32 = 0;
-            let mut approvals: u32 = 0;
-            let mut rejections: u32 = 0;
-            let mut slashes: u32 = 0;
-
-            for (_, vote) in votes.iter() {
-                match vote {
-                    Abstain => abstentions += 1,
-                    Approve => approvals += 1,
-                    Reject => rejections += 1,
-                    Slash => slashes += 1,
-                }
-            }
-
-            let proposal = Self::proposals(proposal_id);
-            let is_expired = Self::is_voting_period_expired(proposal.proposed_at);
-
-            // We need to check that the council is not empty because otherwise,
-            // if there is no votes on a proposal it will be counted as if
-            // all 100% (zero) councilors voted on the proposal and should be approved.
-
-            let non_empty_council = councilors > 0;
-            let all_councilors_voted = non_empty_council && votes.len() as u32 == councilors;
-            let all_councilors_slashed = non_empty_council && slashes == councilors;
-            let quorum_reached = quorum > 0 && approvals >= quorum;
-
-            // Don't approve a proposal right after quorum reached
-            // if not all councilors casted their votes.
-            // Instead let other councilors cast their vote
-            // up until the proposal's expired.
-
-            let new_status: Option<ProposalStatus> = if all_councilors_slashed {
-                Some(Slashed)
-            } else if all_councilors_voted {
-                if quorum_reached {
-                    Some(Approved)
-                } else {
-                    Some(Rejected)
-                }
-            } else if is_expired {
-                if quorum_reached {
-                    Some(Approved)
-                } else {
-                    // Proposal has been expired and quorum not reached.
-                    Some(Expired)
-                }
-            } else {
-                // Councilors still have time to vote on this proposal.
-                None
-            };
-
-            // TODO move next block outside of tally to 'end_block'
-            if let Some(status) = new_status {
-                Self::_update_proposal_status(proposal_id, status.clone())?;
-                let tally_result = TallyResult {
-                    proposal_id,
-                    abstentions,
-                    approvals,
-                    rejections,
-                    slashes,
-                    status,
-                    finalized_at: Self::current_block(),
-                };
-                <TallyResults<T>>::insert(proposal_id, &tally_result);
-                Self::deposit_event(RawEvent::TallyFinalized(tally_result));
-            }
-        }
-
-        Ok(())
-    }
-
-    /// Updates proposal status and removes proposal from active ids.
-    fn _update_proposal_status(proposal_id: u32, new_status: ProposalStatus) -> dispatch::Result {
-        let all_active_ids = Self::active_proposal_ids();
-        let all_len = all_active_ids.len();
-        let other_active_ids: Vec<u32> = all_active_ids
-            .into_iter()
-            .filter(|&id| id != proposal_id)
-            .collect();
-
-        let not_found_in_active = other_active_ids.len() == all_len;
-        if not_found_in_active {
-            // Seems like this proposal's status has been updated and removed from active.
-            Err(MSG_PROPOSAL_STATUS_ALREADY_UPDATED)
-        } else {
-            let pid = proposal_id.clone();
-            match new_status {
-                Slashed => Self::_slash_proposal(pid)?,
-                Rejected | Expired => Self::_reject_proposal(pid)?,
-                Approved => Self::_approve_proposal(pid)?,
-                Active | Cancelled => { /* nothing */ }
-            }
-            ActiveProposalIds::put(other_active_ids);
-            <Proposals<T>>::mutate(proposal_id, |p| p.status = new_status.clone());
-            Self::deposit_event(RawEvent::ProposalStatusUpdated(proposal_id, new_status));
-            Ok(())
-        }
-    }
-
-    /// Slash a proposal. The staked deposit will be slashed.
-    fn _slash_proposal(proposal_id: u32) -> dispatch::Result {
-        let proposal = Self::proposals(proposal_id);
-
-        // Slash proposer's stake:
-        let _ = T::Currency::slash_reserved(&proposal.proposer, proposal.stake);
-
-        Ok(())
-    }
-
-    /// Reject a proposal. The staked deposit will be returned to a proposer.
-    fn _reject_proposal(proposal_id: u32) -> dispatch::Result {
-        let proposal = Self::proposals(proposal_id);
-        let proposer = proposal.proposer;
-
-        // Spend some minimum fee on proposer's balance to prevent spamming attacks:
-        let fee = Self::rejection_fee();
-        let _ = T::Currency::slash_reserved(&proposer, fee);
-
-        // Return unspent part of remaining staked deposit (after taking some fee):
-        let left_stake = proposal.stake - fee;
-        let _ = T::Currency::unreserve(&proposer, left_stake);
-
-        Ok(())
-    }
-
-    /// Approve a proposal. The staked deposit will be returned.
-    fn _approve_proposal(proposal_id: u32) -> dispatch::Result {
-        let proposal = Self::proposals(proposal_id);
-        let wasm_code = Self::wasm_code_by_hash(proposal.wasm_hash);
-
-        // Return staked deposit to proposer:
-        let _ = T::Currency::unreserve(&proposal.proposer, proposal.stake);
-
-        // Update wasm code of node's runtime:
-        <system::Module<T>>::set_code(system::RawOrigin::Root.into(), wasm_code)?;
-
-        Self::deposit_event(RawEvent::RuntimeUpdated(proposal_id, proposal.wasm_hash));
-
-        Ok(())
-    }
-}
-
-#[cfg(test)]
-mod tests {
-
-    use super::*;
-    use primitives::H256;
-    // The testing primitives are very useful for avoiding having to work with signatures
-    // or public keys. `u64` is used as the `AccountId` and no `Signature`s are requried.
-    use sr_primitives::{
-        testing::Header,
-        traits::{BlakeTwo256, IdentityLookup},
-        Perbill,
-    };
-    use srml_support::*;
-
-    impl_outer_origin! {
-        pub enum Origin for Test {}
-    }
-
-    // Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted.
-    #[derive(Clone, PartialEq, Eq, Debug)]
-    pub struct Test;
-
-    parameter_types! {
-        pub const BlockHashCount: u64 = 250;
-        pub const MaximumBlockWeight: u32 = 1024;
-        pub const MaximumBlockLength: u32 = 2 * 1024;
-        pub const AvailableBlockRatio: Perbill = Perbill::one();
-        pub const MinimumPeriod: u64 = 5;
-    }
-
-    impl system::Trait for Test {
-        type Origin = Origin;
-        type Index = u64;
-        type BlockNumber = u64;
-        type Call = ();
-        type Hash = H256;
-        type Hashing = BlakeTwo256;
-        type AccountId = u64;
-        type Lookup = IdentityLookup<Self::AccountId>;
-        type Header = Header;
-        type Event = ();
-        type BlockHashCount = BlockHashCount;
-        type MaximumBlockWeight = MaximumBlockWeight;
-        type MaximumBlockLength = MaximumBlockLength;
-        type AvailableBlockRatio = AvailableBlockRatio;
-        type Version = ();
-    }
-
-    impl timestamp::Trait for Test {
-        type Moment = u64;
-        type OnTimestampSet = ();
-        type MinimumPeriod = MinimumPeriod;
-    }
-
-    parameter_types! {
-        pub const ExistentialDeposit: u32 = 0;
-        pub const TransferFee: u32 = 0;
-        pub const CreationFee: u32 = 0;
-        pub const TransactionBaseFee: u32 = 1;
-        pub const TransactionByteFee: u32 = 0;
-        pub const InitialMembersBalance: u32 = 0;
-    }
-
-    impl balances::Trait for Test {
-        /// The type for recording an account's balance.
-        type Balance = u64;
-        /// What to do if an account's free balance gets zeroed.
-        type OnFreeBalanceZero = ();
-        /// What to do if a new account is created.
-        type OnNewAccount = ();
-        /// The ubiquitous event type.
-        type Event = ();
-
-        type DustRemoval = ();
-        type TransferPayment = ();
-        type ExistentialDeposit = ExistentialDeposit;
-        type TransferFee = TransferFee;
-        type CreationFee = CreationFee;
-    }
-
-    impl council::Trait for Test {
-        type Event = ();
-        type CouncilTermEnded = ();
-    }
-
-    impl GovernanceCurrency for Test {
-        type Currency = balances::Module<Self>;
-    }
-
-    impl membership::members::Trait for Test {
-        type Event = ();
-        type MemberId = u32;
-        type PaidTermId = u32;
-        type SubscriptionId = u32;
-        type ActorId = u32;
-        type InitialMembersBalance = InitialMembersBalance;
-    }
-
-    impl Trait for Test {
-        type Event = ();
-    }
-
-    type System = system::Module<Test>;
-    type Balances = balances::Module<Test>;
-    type Proposals = Module<Test>;
-
-    const COUNCILOR1: u64 = 1;
-    const COUNCILOR2: u64 = 2;
-    const COUNCILOR3: u64 = 3;
-    const COUNCILOR4: u64 = 4;
-    const COUNCILOR5: u64 = 5;
-
-    const PROPOSER1: u64 = 11;
-    const PROPOSER2: u64 = 12;
-
-    const NOT_COUNCILOR: u64 = 22;
-
-    const ALL_COUNCILORS: [u64; 5] = [COUNCILOR1, COUNCILOR2, COUNCILOR3, COUNCILOR4, COUNCILOR5];
-
-    // TODO Figure out how to test Events in test... (low priority)
-    // mod proposals {
-    //     pub use ::Event;
-    // }
-    // impl_outer_event!{
-    //     pub enum TestEvent for Test {
-    //         balances<T>,system<T>,proposals<T>,
-    //     }
-    // }
-
-    // This function basically just builds a genesis storage key/value store according to
-    // our desired mockup.
-    fn new_test_ext() -> runtime_io::TestExternalities {
-        let mut t = system::GenesisConfig::default()
-            .build_storage::<Test>()
-            .unwrap();
-
-        // balances doesn't contain GenesisConfig anymore
-        // // We use default for brevity, but you can configure as desired if needed.
-        // balances::GenesisConfig::<Test>::default()
-        //     .assimilate_storage(&mut t)
-        //     .unwrap();
-
-        let council_mock: council::Seats<u64, u64> = ALL_COUNCILORS
-            .iter()
-            .map(|&c| council::Seat {
-                member: c,
-                stake: 0u64,
-                backers: vec![],
-            })
-            .collect();
-
-        council::GenesisConfig::<Test> {
-            active_council: council_mock,
-            term_ends_at: 0,
-        }
-        .assimilate_storage(&mut t)
-        .unwrap();
-
-        membership::members::GenesisConfig::<Test> {
-            default_paid_membership_fee: 0,
-            members: vec![
-                (PROPOSER1, "alice".into(), "".into(), "".into()),
-                (PROPOSER2, "bobby".into(), "".into(), "".into()),
-                (COUNCILOR1, "councilor1".into(), "".into(), "".into()),
-                (COUNCILOR2, "councilor2".into(), "".into(), "".into()),
-                (COUNCILOR3, "councilor3".into(), "".into(), "".into()),
-                (COUNCILOR4, "councilor4".into(), "".into(), "".into()),
-                (COUNCILOR5, "councilor5".into(), "".into(), "".into()),
-            ],
-        }
-        .assimilate_storage(&mut t)
-        .unwrap();
-        // t.extend(GenesisConfig::<Test>{
-        //     // Here we can override defaults.
-        // }.build_storage().unwrap().0);
-
-        t.into()
-    }
-
-    /// A shortcut to get minimum stake in tests.
-    fn min_stake() -> u64 {
-        Proposals::min_stake()
-    }
-
-    /// A shortcut to get cancellation fee in tests.
-    fn cancellation_fee() -> u64 {
-        Proposals::cancellation_fee()
-    }
-
-    /// A shortcut to get rejection fee in tests.
-    fn rejection_fee() -> u64 {
-        Proposals::rejection_fee()
-    }
-
-    /// Initial balance of Proposer 1.
-    fn initial_balance() -> u64 {
-        (min_stake() as f64 * 2.5) as u64
-    }
-
-    fn name() -> Vec<u8> {
-        b"Proposal Name".to_vec()
-    }
-
-    fn description() -> Vec<u8> {
-        b"Proposal Description".to_vec()
-    }
-
-    fn wasm_code() -> Vec<u8> {
-        b"Proposal Wasm Code".to_vec()
-    }
-
-    fn _create_default_proposal() -> dispatch::Result {
-        _create_proposal(None, None, None, None, None)
-    }
-
-    fn _create_proposal(
-        origin: Option<u64>,
-        stake: Option<u64>,
-        name: Option<Vec<u8>>,
-        description: Option<Vec<u8>>,
-        wasm_code: Option<Vec<u8>>,
-    ) -> dispatch::Result {
-        Proposals::create_proposal(
-            Origin::signed(origin.unwrap_or(PROPOSER1)),
-            stake.unwrap_or(min_stake()),
-            name.unwrap_or(self::name()),
-            description.unwrap_or(self::description()),
-            wasm_code.unwrap_or(self::wasm_code()),
-        )
-    }
-
-    fn get_runtime_code() -> Option<Vec<u8>> {
-        storage::unhashed::get_raw(well_known_keys::CODE)
-    }
-
-    macro_rules! assert_runtime_code_empty {
-        () => {
-            assert_eq!(get_runtime_code(), Some(vec![]))
-        };
-    }
-
-    macro_rules! assert_runtime_code {
-        ($code:expr) => {
-            assert_eq!(get_runtime_code(), Some($code))
-        };
-    }
-
-    #[test]
-    fn check_default_values() {
-        new_test_ext().execute_with(|| {
-            assert_eq!(Proposals::approval_quorum(), DEFAULT_APPROVAL_QUORUM);
-            assert_eq!(
-                Proposals::min_stake(),
-                BalanceOf::<Test>::from(DEFAULT_MIN_STAKE)
-            );
-            assert_eq!(
-                Proposals::cancellation_fee(),
-                BalanceOf::<Test>::from(DEFAULT_CANCELLATION_FEE)
-            );
-            assert_eq!(
-                Proposals::rejection_fee(),
-                BalanceOf::<Test>::from(DEFAULT_REJECTION_FEE)
-            );
-            assert_eq!(Proposals::name_max_len(), DEFAULT_NAME_MAX_LEN);
-            assert_eq!(
-                Proposals::description_max_len(),
-                DEFAULT_DESCRIPTION_MAX_LEN
-            );
-            assert_eq!(Proposals::wasm_code_max_len(), DEFAULT_WASM_CODE_MAX_LEN);
-            assert_eq!(Proposals::proposal_count(), 0);
-            assert!(Proposals::active_proposal_ids().is_empty());
-        });
-    }
-
-    #[test]
-    fn member_create_proposal() {
-        new_test_ext().execute_with(|| {
-            let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
-
-            assert_ok!(_create_default_proposal());
-            assert_eq!(Proposals::active_proposal_ids().len(), 1);
-            assert_eq!(Proposals::active_proposal_ids()[0], 1);
-
-            let wasm_hash = BlakeTwo256::hash(&wasm_code());
-            let expected_proposal = RuntimeUpgradeProposal {
-                id: 1,
-                proposer: PROPOSER1,
-                stake: min_stake(),
-                name: name(),
-                description: description(),
-                wasm_hash,
-                proposed_at: 1,
-                status: Active,
-            };
-            assert_eq!(Proposals::proposals(1), expected_proposal);
-
-            // Check that stake amount has been locked on proposer's balance:
-            assert_eq!(
-                Balances::free_balance(PROPOSER1),
-                initial_balance() - min_stake()
-            );
-            assert_eq!(Balances::reserved_balance(PROPOSER1), min_stake());
-
-            // TODO expect event ProposalCreated(AccountId, u32)
-        });
-    }
-
-    #[test]
-    fn not_member_cannot_create_proposal() {
-        new_test_ext().execute_with(|| {
-            // In this test a proposer has an empty balance
-            // thus he is not considered as a member.
-            assert_eq!(
-                _create_default_proposal(),
-                Err(MSG_ONLY_MEMBERS_CAN_PROPOSE)
-            );
-        });
-    }
-
-    #[test]
-    fn cannot_create_proposal_with_small_stake() {
-        new_test_ext().execute_with(|| {
-            let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
-
-            assert_eq!(
-                _create_proposal(None, Some(min_stake() - 1), None, None, None),
-                Err(MSG_STAKE_IS_TOO_LOW)
-            );
-
-            // Check that balances remain unchanged afer a failed attempt to create a proposal:
-            assert_eq!(Balances::free_balance(PROPOSER1), initial_balance());
-            assert_eq!(Balances::reserved_balance(PROPOSER1), 0);
-        });
-    }
-
-    #[test]
-    fn cannot_create_proposal_when_stake_is_greater_than_balance() {
-        new_test_ext().execute_with(|| {
-            let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
-
-            assert_eq!(
-                _create_proposal(None, Some(initial_balance() + 1), None, None, None),
-                Err(MSG_STAKE_IS_GREATER_THAN_BALANCE)
-            );
-
-            // Check that balances remain unchanged afer a failed attempt to create a proposal:
-            assert_eq!(Balances::free_balance(PROPOSER1), initial_balance());
-            assert_eq!(Balances::reserved_balance(PROPOSER1), 0);
-        });
-    }
-
-    #[test]
-    fn cannot_create_proposal_with_empty_values() {
-        new_test_ext().execute_with(|| {
-            let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
-
-            // Empty name:
-            assert_eq!(
-                _create_proposal(None, None, Some(vec![]), None, None),
-                Err(MSG_EMPTY_NAME_PROVIDED)
-            );
-
-            // Empty description:
-            assert_eq!(
-                _create_proposal(None, None, None, Some(vec![]), None),
-                Err(MSG_EMPTY_DESCRIPTION_PROVIDED)
-            );
-
-            // Empty WASM code:
-            assert_eq!(
-                _create_proposal(None, None, None, None, Some(vec![])),
-                Err(MSG_EMPTY_WASM_CODE_PROVIDED)
-            );
-        });
-    }
-
-    #[test]
-    fn cannot_create_proposal_with_too_long_values() {
-        new_test_ext().execute_with(|| {
-            let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
-
-            // Too long name:
-            assert_eq!(
-                _create_proposal(None, None, Some(too_long_name()), None, None),
-                Err(MSG_TOO_LONG_NAME)
-            );
-
-            // Too long description:
-            assert_eq!(
-                _create_proposal(None, None, None, Some(too_long_description()), None),
-                Err(MSG_TOO_LONG_DESCRIPTION)
-            );
-
-            // Too long WASM code:
-            assert_eq!(
-                _create_proposal(None, None, None, None, Some(too_long_wasm_code())),
-                Err(MSG_TOO_LONG_WASM_CODE)
-            );
-        });
-    }
-
-    fn too_long_name() -> Vec<u8> {
-        vec![65; Proposals::name_max_len() as usize + 1]
-    }
-
-    fn too_long_description() -> Vec<u8> {
-        vec![65; Proposals::description_max_len() as usize + 1]
-    }
-
-    fn too_long_wasm_code() -> Vec<u8> {
-        vec![65; Proposals::wasm_code_max_len() as usize + 1]
-    }
-
-    // -------------------------------------------------------------------
-    // Cancellation
-
-    #[test]
-    fn owner_cancel_proposal() {
-        new_test_ext().execute_with(|| {
-            let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
-
-            assert_ok!(_create_default_proposal());
-            assert_ok!(Proposals::cancel_proposal(Origin::signed(PROPOSER1), 1));
-            assert_eq!(Proposals::proposals(1).status, Cancelled);
-            assert!(Proposals::active_proposal_ids().is_empty());
-
-            // Check that proposer's balance reduced by cancellation fee and other part of his stake returned to his balance:
-            assert_eq!(
-                Balances::free_balance(PROPOSER1),
-                initial_balance() - cancellation_fee()
-            );
-            assert_eq!(Balances::reserved_balance(PROPOSER1), 0);
-
-            // TODO expect event ProposalCancelled(AccountId, u32)
-        });
-    }
-
-    #[test]
-    fn owner_cannot_cancel_proposal_if_its_finalized() {
-        new_test_ext().execute_with(|| {
-            let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
-
-            assert_ok!(_create_default_proposal());
-            assert_ok!(Proposals::cancel_proposal(Origin::signed(PROPOSER1), 1));
-            assert_eq!(Proposals::proposals(1).status, Cancelled);
-
-            // Get balances updated after cancelling a proposal:
-            let updated_free_balance = Balances::free_balance(PROPOSER1);
-            let updated_reserved_balance = Balances::reserved_balance(PROPOSER1);
-
-            assert_eq!(
-                Proposals::cancel_proposal(Origin::signed(PROPOSER1), 1),
-                Err(MSG_PROPOSAL_FINALIZED)
-            );
-
-            // Check that proposer's balance and locked stake haven't been changed:
-            assert_eq!(Balances::free_balance(PROPOSER1), updated_free_balance);
-            assert_eq!(
-                Balances::reserved_balance(PROPOSER1),
-                updated_reserved_balance
-            );
-        });
-    }
-
-    #[test]
-    fn not_owner_cannot_cancel_proposal() {
-        new_test_ext().execute_with(|| {
-            let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
-            let _ = Balances::deposit_creating(&PROPOSER2, initial_balance());
-            assert_ok!(_create_default_proposal());
-            assert_eq!(
-                Proposals::cancel_proposal(Origin::signed(PROPOSER2), 1),
-                Err(MSG_YOU_DONT_OWN_THIS_PROPOSAL)
-            );
-        });
-    }
-
-    // -------------------------------------------------------------------
-    // Voting
-
-    #[test]
-    fn councilor_vote_on_proposal() {
-        new_test_ext().execute_with(|| {
-            let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
-
-            assert_ok!(_create_default_proposal());
-
-            assert_ok!(Proposals::vote_on_proposal(
-                Origin::signed(COUNCILOR1),
-                1,
-                Approve
-            ));
-
-            // Check that a vote has been saved:
-            assert_eq!(Proposals::votes_by_proposal(1), vec![(COUNCILOR1, Approve)]);
-            assert_eq!(
-                Proposals::vote_by_account_and_proposal((COUNCILOR1, 1)),
-                Approve
-            );
-
-            // TODO expect event Voted(PROPOSER1, 1, Approve)
-        });
-    }
-
-    #[test]
-    fn councilor_cannot_vote_on_proposal_twice() {
-        new_test_ext().execute_with(|| {
-            let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
-
-            assert_ok!(_create_default_proposal());
-
-            assert_ok!(Proposals::vote_on_proposal(
-                Origin::signed(COUNCILOR1),
-                1,
-                Approve
-            ));
-            assert_eq!(
-                Proposals::vote_on_proposal(Origin::signed(COUNCILOR1), 1, Approve),
-                Err(MSG_YOU_ALREADY_VOTED)
-            );
-        });
-    }
-
-    #[test]
-    fn autovote_with_approve_when_councilor_creates_proposal() {
-        new_test_ext().execute_with(|| {
-            let _ = Balances::deposit_creating(&COUNCILOR1, initial_balance());
-
-            assert_ok!(_create_proposal(Some(COUNCILOR1), None, None, None, None));
-
-            // Check that a vote has been sent automatically,
-            // such as the proposer is a councilor:
-            assert_eq!(Proposals::votes_by_proposal(1), vec![(COUNCILOR1, Approve)]);
-            assert_eq!(
-                Proposals::vote_by_account_and_proposal((COUNCILOR1, 1)),
-                Approve
-            );
-        });
-    }
-
-    #[test]
-    fn not_councilor_cannot_vote_on_proposal() {
-        new_test_ext().execute_with(|| {
-            let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
-
-            assert_ok!(_create_default_proposal());
-            assert_eq!(
-                Proposals::vote_on_proposal(Origin::signed(NOT_COUNCILOR), 1, Approve),
-                Err(MSG_ONLY_COUNCILORS_CAN_VOTE)
-            );
-        });
-    }
-
-    #[test]
-    fn councilor_cannot_vote_on_proposal_if_it_has_been_cancelled() {
-        new_test_ext().execute_with(|| {
-            let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
-
-            assert_ok!(_create_default_proposal());
-            assert_ok!(Proposals::cancel_proposal(Origin::signed(PROPOSER1), 1));
-            assert_eq!(
-                Proposals::vote_on_proposal(Origin::signed(COUNCILOR1), 1, Approve),
-                Err(MSG_PROPOSAL_FINALIZED)
-            );
-        });
-    }
-
-    #[test]
-    fn councilor_cannot_vote_on_proposal_if_tally_has_been_finalized() {
-        new_test_ext().execute_with(|| {
-            let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
-
-            assert_ok!(_create_default_proposal());
-
-            // All councilors vote with 'Approve' on proposal:
-            let mut expected_votes: Vec<(u64, VoteKind)> = vec![];
-            for &councilor in ALL_COUNCILORS.iter() {
-                expected_votes.push((councilor, Approve));
-                assert_ok!(Proposals::vote_on_proposal(
-                    Origin::signed(councilor),
-                    1,
-                    Approve
-                ));
-                assert_eq!(
-                    Proposals::vote_by_account_and_proposal((councilor, 1)),
-                    Approve
-                );
-            }
-            assert_eq!(Proposals::votes_by_proposal(1), expected_votes);
-
-            System::set_block_number(2);
-            let _ = Proposals::end_block(2);
-
-            assert!(Proposals::active_proposal_ids().is_empty());
-            assert_eq!(Proposals::proposals(1).status, Approved);
-
-            // Try to vote on finalized proposal:
-            assert_eq!(
-                Proposals::vote_on_proposal(Origin::signed(COUNCILOR1), 1, Reject),
-                Err(MSG_PROPOSAL_FINALIZED)
-            );
-        });
-    }
-
-    // -------------------------------------------------------------------
-    // Tally + Outcome:
-
-    #[test]
-    fn approve_proposal_when_all_councilors_approved_it() {
-        new_test_ext().execute_with(|| {
-            let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
-
-            assert_ok!(_create_default_proposal());
-
-            // All councilors approved:
-            let mut expected_votes: Vec<(u64, VoteKind)> = vec![];
-            for &councilor in ALL_COUNCILORS.iter() {
-                expected_votes.push((councilor, Approve));
-                assert_ok!(Proposals::vote_on_proposal(
-                    Origin::signed(councilor),
-                    1,
-                    Approve
-                ));
-                assert_eq!(
-                    Proposals::vote_by_account_and_proposal((councilor, 1)),
-                    Approve
-                );
-            }
-            assert_eq!(Proposals::votes_by_proposal(1), expected_votes);
-
-            assert_runtime_code_empty!();
-
-            System::set_block_number(2);
-            let _ = Proposals::end_block(2);
-
-            // Check that runtime code has been updated after proposal approved.
-            assert_runtime_code!(wasm_code());
-
-            assert!(Proposals::active_proposal_ids().is_empty());
-            assert_eq!(Proposals::proposals(1).status, Approved);
-            assert_eq!(
-                Proposals::tally_results(1),
-                TallyResult {
-                    proposal_id: 1,
-                    abstentions: 0,
-                    approvals: ALL_COUNCILORS.len() as u32,
-                    rejections: 0,
-                    slashes: 0,
-                    status: Approved,
-                    finalized_at: 2
-                }
-            );
-
-            // Check that proposer's stake has been added back to his balance:
-            assert_eq!(Balances::free_balance(PROPOSER1), initial_balance());
-            assert_eq!(Balances::reserved_balance(PROPOSER1), 0);
-
-            // TODO expect event ProposalStatusUpdated(1, Approved)
-        });
-    }
-
-    #[test]
-    fn approve_proposal_when_all_councilors_voted_and_only_quorum_approved() {
-        new_test_ext().execute_with(|| {
-            let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
-
-            assert_ok!(_create_default_proposal());
-
-            // Only a quorum of councilors approved, others rejected:
-            let councilors = Proposals::councilors_count();
-            let approvals = Proposals::approval_quorum_seats();
-            let rejections = councilors - approvals;
-            for i in 0..councilors as usize {
-                let vote = if (i as u32) < approvals {
-                    Approve
-                } else {
-                    Reject
-                };
-                assert_ok!(Proposals::vote_on_proposal(
-                    Origin::signed(ALL_COUNCILORS[i]),
-                    1,
-                    vote
-                ));
-            }
-            assert_eq!(Proposals::votes_by_proposal(1).len() as u32, councilors);
-
-            assert_runtime_code_empty!();
-
-            System::set_block_number(2);
-            let _ = Proposals::end_block(2);
-
-            // Check that runtime code has been updated after proposal approved.
-            assert_runtime_code!(wasm_code());
-
-            assert!(Proposals::active_proposal_ids().is_empty());
-            assert_eq!(Proposals::proposals(1).status, Approved);
-            assert_eq!(
-                Proposals::tally_results(1),
-                TallyResult {
-                    proposal_id: 1,
-                    abstentions: 0,
-                    approvals: approvals,
-                    rejections: rejections,
-                    slashes: 0,
-                    status: Approved,
-                    finalized_at: 2
-                }
-            );
-
-            // Check that proposer's stake has been added back to his balance:
-            assert_eq!(Balances::free_balance(PROPOSER1), initial_balance());
-            assert_eq!(Balances::reserved_balance(PROPOSER1), 0);
-
-            // TODO expect event ProposalStatusUpdated(1, Approved)
-        });
-    }
-
-    #[test]
-    fn approve_proposal_when_voting_period_expired_if_only_quorum_voted() {
-        new_test_ext().execute_with(|| {
-            let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
-
-            assert_ok!(_create_default_proposal());
-
-            // Only quorum of councilors approved, other councilors didn't vote:
-            let approvals = Proposals::approval_quorum_seats();
-            for i in 0..approvals as usize {
-                let vote = if (i as u32) < approvals {
-                    Approve
-                } else {
-                    Slash
-                };
-                assert_ok!(Proposals::vote_on_proposal(
-                    Origin::signed(ALL_COUNCILORS[i]),
-                    1,
-                    vote
-                ));
-            }
-            assert_eq!(Proposals::votes_by_proposal(1).len() as u32, approvals);
-
-            assert_runtime_code_empty!();
-
-            let expiration_block = System::block_number() + Proposals::voting_period();
-            System::set_block_number(2);
-            let _ = Proposals::end_block(2);
-
-            // Check that runtime code has NOT been updated yet,
-            // because not all councilors voted and voting period is not expired yet.
-            assert_runtime_code_empty!();
-
-            System::set_block_number(expiration_block);
-            let _ = Proposals::end_block(expiration_block);
-
-            // Check that runtime code has been updated after proposal approved.
-            assert_runtime_code!(wasm_code());
-
-            assert!(Proposals::active_proposal_ids().is_empty());
-            assert_eq!(Proposals::proposals(1).status, Approved);
-            assert_eq!(
-                Proposals::tally_results(1),
-                TallyResult {
-                    proposal_id: 1,
-                    abstentions: 0,
-                    approvals: approvals,
-                    rejections: 0,
-                    slashes: 0,
-                    status: Approved,
-                    finalized_at: expiration_block
-                }
-            );
-
-            // Check that proposer's stake has been added back to his balance:
-            assert_eq!(Balances::free_balance(PROPOSER1), initial_balance());
-            assert_eq!(Balances::reserved_balance(PROPOSER1), 0);
-
-            // TODO expect event ProposalStatusUpdated(1, Approved)
-        });
-    }
-
-    #[test]
-    fn reject_proposal_when_all_councilors_voted_and_quorum_not_reached() {
-        new_test_ext().execute_with(|| {
-            let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
-
-            assert_ok!(_create_default_proposal());
-
-            // Less than a quorum of councilors approved, while others abstained:
-            let councilors = Proposals::councilors_count();
-            let approvals = Proposals::approval_quorum_seats() - 1;
-            let abstentions = councilors - approvals;
-            for i in 0..councilors as usize {
-                let vote = if (i as u32) < approvals {
-                    Approve
-                } else {
-                    Abstain
-                };
-                assert_ok!(Proposals::vote_on_proposal(
-                    Origin::signed(ALL_COUNCILORS[i]),
-                    1,
-                    vote
-                ));
-            }
-            assert_eq!(Proposals::votes_by_proposal(1).len() as u32, councilors);
-
-            assert_runtime_code_empty!();
-
-            System::set_block_number(2);
-            let _ = Proposals::end_block(2);
-
-            // Check that runtime code has NOT been updated after proposal slashed.
-            assert_runtime_code_empty!();
-
-            assert!(Proposals::active_proposal_ids().is_empty());
-            assert_eq!(Proposals::proposals(1).status, Rejected);
-            assert_eq!(
-                Proposals::tally_results(1),
-                TallyResult {
-                    proposal_id: 1,
-                    abstentions: abstentions,
-                    approvals: approvals,
-                    rejections: 0,
-                    slashes: 0,
-                    status: Rejected,
-                    finalized_at: 2
-                }
-            );
-
-            // Check that proposer's balance reduced by burnt stake:
-            assert_eq!(
-                Balances::free_balance(PROPOSER1),
-                initial_balance() - rejection_fee()
-            );
-            assert_eq!(Balances::reserved_balance(PROPOSER1), 0);
-
-            // TODO expect event ProposalStatusUpdated(1, Rejected)
-        });
-    }
-
-    #[test]
-    fn reject_proposal_when_all_councilors_rejected_it() {
-        new_test_ext().execute_with(|| {
-            let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
-
-            assert_ok!(_create_default_proposal());
-
-            // All councilors rejected:
-            let mut expected_votes: Vec<(u64, VoteKind)> = vec![];
-            for &councilor in ALL_COUNCILORS.iter() {
-                expected_votes.push((councilor, Reject));
-                assert_ok!(Proposals::vote_on_proposal(
-                    Origin::signed(councilor),
-                    1,
-                    Reject
-                ));
-                assert_eq!(
-                    Proposals::vote_by_account_and_proposal((councilor, 1)),
-                    Reject
-                );
-            }
-            assert_eq!(Proposals::votes_by_proposal(1), expected_votes);
-
-            assert_runtime_code_empty!();
-
-            System::set_block_number(2);
-            let _ = Proposals::end_block(2);
-
-            // Check that runtime code has NOT been updated after proposal rejected.
-            assert_runtime_code_empty!();
-
-            assert!(Proposals::active_proposal_ids().is_empty());
-            assert_eq!(Proposals::proposals(1).status, Rejected);
-            assert_eq!(
-                Proposals::tally_results(1),
-                TallyResult {
-                    proposal_id: 1,
-                    abstentions: 0,
-                    approvals: 0,
-                    rejections: ALL_COUNCILORS.len() as u32,
-                    slashes: 0,
-                    status: Rejected,
-                    finalized_at: 2
-                }
-            );
-
-            // Check that proposer's balance reduced by burnt stake:
-            assert_eq!(
-                Balances::free_balance(PROPOSER1),
-                initial_balance() - rejection_fee()
-            );
-            assert_eq!(Balances::reserved_balance(PROPOSER1), 0);
-
-            // TODO expect event ProposalStatusUpdated(1, Rejected)
-        });
-    }
-
-    #[test]
-    fn slash_proposal_when_all_councilors_slashed_it() {
-        new_test_ext().execute_with(|| {
-            let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
-
-            assert_ok!(_create_default_proposal());
-
-            // All councilors slashed:
-            let mut expected_votes: Vec<(u64, VoteKind)> = vec![];
-            for &councilor in ALL_COUNCILORS.iter() {
-                expected_votes.push((councilor, Slash));
-                assert_ok!(Proposals::vote_on_proposal(
-                    Origin::signed(councilor),
-                    1,
-                    Slash
-                ));
-                assert_eq!(
-                    Proposals::vote_by_account_and_proposal((councilor, 1)),
-                    Slash
-                );
-            }
-            assert_eq!(Proposals::votes_by_proposal(1), expected_votes);
-
-            assert_runtime_code_empty!();
-
-            System::set_block_number(2);
-            let _ = Proposals::end_block(2);
-
-            // Check that runtime code has NOT been updated after proposal slashed.
-            assert_runtime_code_empty!();
-
-            assert!(Proposals::active_proposal_ids().is_empty());
-            assert_eq!(Proposals::proposals(1).status, Slashed);
-            assert_eq!(
-                Proposals::tally_results(1),
-                TallyResult {
-                    proposal_id: 1,
-                    abstentions: 0,
-                    approvals: 0,
-                    rejections: 0,
-                    slashes: ALL_COUNCILORS.len() as u32,
-                    status: Slashed,
-                    finalized_at: 2
-                }
-            );
-
-            // Check that proposer's balance reduced by burnt stake:
-            assert_eq!(
-                Balances::free_balance(PROPOSER1),
-                initial_balance() - min_stake()
-            );
-            assert_eq!(Balances::reserved_balance(PROPOSER1), 0);
-
-            // TODO expect event ProposalStatusUpdated(1, Slashed)
-            // TODO fix: event log assertion doesn't work and return empty event in every record
-            // assert_eq!(*System::events().last().unwrap(),
-            //     EventRecord {
-            //         phase: Phase::ApplyExtrinsic(0),
-            //         event: RawEvent::ProposalStatusUpdated(1, Slashed),
-            //     }
-            // );
-        });
-    }
-
-    // In this case a proposal will be marked as 'Expired'
-    // and it will be processed in the same way as if it has been rejected.
-    #[test]
-    fn expire_proposal_when_not_all_councilors_voted_and_quorum_not_reached() {
-        new_test_ext().execute_with(|| {
-            let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
-
-            assert_ok!(_create_default_proposal());
-
-            // Less than a quorum of councilors approved:
-            let approvals = Proposals::approval_quorum_seats() - 1;
-            for i in 0..approvals as usize {
-                let vote = if (i as u32) < approvals {
-                    Approve
-                } else {
-                    Slash
-                };
-                assert_ok!(Proposals::vote_on_proposal(
-                    Origin::signed(ALL_COUNCILORS[i]),
-                    1,
-                    vote
-                ));
-            }
-            assert_eq!(Proposals::votes_by_proposal(1).len() as u32, approvals);
-
-            assert_runtime_code_empty!();
-
-            let expiration_block = System::block_number() + Proposals::voting_period();
-            System::set_block_number(expiration_block);
-            let _ = Proposals::end_block(expiration_block);
-
-            // Check that runtime code has NOT been updated after proposal slashed.
-            assert_runtime_code_empty!();
-
-            assert!(Proposals::active_proposal_ids().is_empty());
-            assert_eq!(Proposals::proposals(1).status, Expired);
-            assert_eq!(
-                Proposals::tally_results(1),
-                TallyResult {
-                    proposal_id: 1,
-                    abstentions: 0,
-                    approvals: approvals,
-                    rejections: 0,
-                    slashes: 0,
-                    status: Expired,
-                    finalized_at: expiration_block
-                }
-            );
-
-            // Check that proposer's balance reduced by burnt stake:
-            assert_eq!(
-                Balances::free_balance(PROPOSER1),
-                initial_balance() - rejection_fee()
-            );
-            assert_eq!(Balances::reserved_balance(PROPOSER1), 0);
-
-            // TODO expect event ProposalStatusUpdated(1, Rejected)
-        });
-    }
-}

+ 0 - 2
runtime-modules/hiring/src/lib.rs

@@ -27,7 +27,6 @@ use mockall::*;
 use stake::{InitiateUnstakingError, Stake, StakeActionError, StakingError, Trait as StakeTrait};
 
 use codec::Codec;
-use system;
 
 use runtime_primitives::traits::Zero;
 use runtime_primitives::traits::{MaybeSerialize, Member, One, SimpleArithmetic};
@@ -52,7 +51,6 @@ mod mock;
 mod test;
 
 pub use hiring::*;
-use stake;
 
 /// Main trait of hiring substrate module
 pub trait Trait: system::Trait + stake::Trait + Sized {

+ 1 - 1
runtime-modules/membership/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 name = 'substrate-membership-module'
-version = '1.0.0'
+version = '1.0.1'
 authors = ['Joystream contributors']
 edition = '2018'
 

+ 1 - 1
runtime-modules/membership/src/lib.rs

@@ -5,5 +5,5 @@ pub mod genesis;
 pub mod members;
 pub mod role_types;
 
-mod mock;
+pub(crate) mod mock;
 mod tests;

+ 17 - 12
runtime-modules/membership/src/members.rs

@@ -1,13 +1,17 @@
+// Clippy linter requirement
+#![allow(clippy::redundant_closure_call)] // disable it because of the substrate lib design
+                                          // example:  pub PaidMembershipTermsById get(paid_membership_terms_by_id) build(|config: &GenesisConfig<T>| {}
+
 use codec::{Codec, Decode, Encode};
 use common::currency::{BalanceOf, GovernanceCurrency};
 
+use rstd::borrow::ToOwned;
 use rstd::prelude::*;
 use sr_primitives::traits::{MaybeSerialize, Member, One, SimpleArithmetic};
 use srml_support::traits::{Currency, Get};
 use srml_support::{decl_event, decl_module, decl_storage, dispatch, ensure, Parameter};
 
 use system::{self, ensure_root, ensure_signed};
-use timestamp;
 
 pub use super::role_types::*;
 
@@ -252,7 +256,7 @@ decl_module! {
             let _ = T::Currency::slash(&who, terms.fee);
             let member_id = Self::insert_member(&who, &user_info, EntryMethod::Paid(paid_terms_id));
 
-            Self::deposit_event(RawEvent::MemberRegistered(member_id, who.clone()));
+            Self::deposit_event(RawEvent::MemberRegistered(member_id, who));
         }
 
         /// Change member's about text
@@ -448,12 +452,13 @@ impl<T: Trait> Module<T> {
         Ok(terms)
     }
 
+    #[allow(clippy::ptr_arg)] // cannot change to the "&[u8]" suggested by clippy
     fn ensure_unique_handle(handle: &Vec<u8>) -> dispatch::Result {
         ensure!(!<Handles<T>>::exists(handle), "handle already registered");
         Ok(())
     }
 
-    fn validate_handle(handle: &Vec<u8>) -> dispatch::Result {
+    fn validate_handle(handle: &[u8]) -> dispatch::Result {
         ensure!(
             handle.len() >= Self::min_handle_length() as usize,
             "handle too short"
@@ -465,13 +470,13 @@ impl<T: Trait> Module<T> {
         Ok(())
     }
 
-    fn validate_text(text: &Vec<u8>) -> Vec<u8> {
-        let mut text = text.clone();
+    fn validate_text(text: &[u8]) -> Vec<u8> {
+        let mut text = text.to_owned();
         text.truncate(Self::max_about_text_length() as usize);
         text
     }
 
-    fn validate_avatar(uri: &Vec<u8>) -> dispatch::Result {
+    fn validate_avatar(uri: &[u8]) -> dispatch::Result {
         ensure!(
             uri.len() <= Self::max_avatar_uri_length() as usize,
             "avatar uri too long"
@@ -533,7 +538,7 @@ impl<T: Trait> Module<T> {
         new_member_id
     }
 
-    fn _change_member_about_text(id: T::MemberId, text: &Vec<u8>) -> dispatch::Result {
+    fn _change_member_about_text(id: T::MemberId, text: &[u8]) -> dispatch::Result {
         let mut profile = Self::ensure_profile(id)?;
         let text = Self::validate_text(text);
         profile.about = text;
@@ -542,10 +547,10 @@ impl<T: Trait> Module<T> {
         Ok(())
     }
 
-    fn _change_member_avatar(id: T::MemberId, uri: &Vec<u8>) -> dispatch::Result {
+    fn _change_member_avatar(id: T::MemberId, uri: &[u8]) -> dispatch::Result {
         let mut profile = Self::ensure_profile(id)?;
         Self::validate_avatar(uri)?;
-        profile.avatar_uri = uri.clone();
+        profile.avatar_uri = uri.to_owned();
         Self::deposit_event(RawEvent::MemberUpdatedAvatar(id));
         <MemberProfile<T>>::insert(id, profile);
         Ok(())
@@ -593,7 +598,7 @@ impl<T: Trait> Module<T> {
             ensure_signed(origin).map_err(|_| MemberControllerAccountDidNotSign::UnsignedOrigin)?;
 
         // Ensure member exists
-        let profile = Self::ensure_profile(member_id.clone())
+        let profile = Self::ensure_profile(*member_id)
             .map_err(|_| MemberControllerAccountDidNotSign::MemberIdInvalid)?;
 
         ensure!(
@@ -609,7 +614,7 @@ impl<T: Trait> Module<T> {
         member_id: &T::MemberId,
     ) -> Result<(), MemberControllerAccountMismatch> {
         // Ensure member exists
-        let profile = Self::ensure_profile(member_id.clone())
+        let profile = Self::ensure_profile(*member_id)
             .map_err(|_| MemberControllerAccountMismatch::MemberIdInvalid)?;
 
         ensure!(
@@ -625,7 +630,7 @@ impl<T: Trait> Module<T> {
         member_id: &T::MemberId,
     ) -> Result<(), MemberRootAccountMismatch> {
         // Ensure member exists
-        let profile = Self::ensure_profile(member_id.clone())
+        let profile = Self::ensure_profile(*member_id)
             .map_err(|_| MemberRootAccountMismatch::MemberIdInvalid)?;
 
         ensure!(

+ 6 - 0
runtime-modules/membership/src/role_types.rs

@@ -1,7 +1,13 @@
+#![allow(clippy::new_without_default)] // disable because Default for enums doesn't make sense
+
 use codec::{Decode, Encode};
 use rstd::collections::btree_set::BTreeSet;
 use rstd::prelude::*;
 
+#[cfg(feature = "std")]
+use serde::{Deserialize, Serialize};
+
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Debug)]
 pub enum Role {
     StorageProvider,

+ 184 - 0
runtime-modules/proposals/codex/Cargo.toml

@@ -0,0 +1,184 @@
+[package]
+name = 'substrate-proposals-codex-module'
+version = '2.0.0'
+authors = ['Joystream contributors']
+edition = '2018'
+
+[features]
+default = ['std']
+no_std = []
+std = [
+    'codec/std',
+    'rstd/std',
+    'srml-support/std',
+    'primitives/std',
+    'sr-primitives/std',
+    'system/std',
+    'timestamp/std',
+    'staking/std',
+    'serde',
+    'proposal_engine/std',
+    'proposal_discussion/std',
+    'stake/std',
+    'balances/std',
+    'membership/std',
+    'governance/std',
+    'mint/std',
+    'roles/std',
+    'common/std',
+    'content_working_group/std',
+]
+
+
+[dependencies.num_enum]
+default_features = false
+version = "0.4.2"
+
+[dependencies.serde]
+features = ['derive']
+optional = true
+version = '1.0.101'
+
+[dependencies.codec]
+default-features = false
+features = ['derive']
+package = 'parity-scale-codec'
+version = '1.0.0'
+
+[dependencies.primitives]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'substrate-primitives'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.rstd]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-std'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.sr-primitives]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-primitives'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.srml-support]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-support'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.system]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-system'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.timestamp]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-timestamp'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.balances]
+package = 'srml-balances'
+default-features = false
+git = 'https://github.com/paritytech/substrate.git'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.staking]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-staking'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.stake]
+default_features = false
+package = 'substrate-stake-module'
+path = '../../stake'
+
+[dependencies.membership]
+default_features = false
+package = 'substrate-membership-module'
+path = '../../membership'
+
+[dependencies.governance]
+default_features = false
+package = 'substrate-governance-module'
+path = '../../governance'
+
+[dependencies.mint]
+default_features = false
+package = 'substrate-token-mint-module'
+path = '../../token-minting'
+
+[dependencies.proposal_engine]
+default_features = false
+package = 'substrate-proposals-engine-module'
+path = '../engine'
+
+[dependencies.proposal_discussion]
+default_features = false
+package = 'substrate-proposals-discussion-module'
+path = '../discussion'
+
+[dependencies.common]
+default_features = false
+package = 'substrate-common-module'
+path = '../../common'
+
+[dependencies.content_working_group]
+default_features = false
+package = 'substrate-content-working-group-module'
+path = '../../content-working-group'
+
+[dependencies.roles]
+default_features = false
+package = 'substrate-roles-module'
+path = '../../roles'
+
+[dev-dependencies.hiring]
+default_features = false
+package = 'substrate-hiring-module'
+path = '../../hiring'
+
+[dev-dependencies.versioned_store]
+default_features = false
+package ='substrate-versioned-store'
+path = '../../versioned-store'
+
+[dependencies.versioned_store]
+default_features = false
+package ='substrate-versioned-store'
+path = '../../versioned-store'
+
+[dev-dependencies.versioned_store_permissions]
+default_features = false
+package = 'substrate-versioned-store-permissions-module'
+path = '../../versioned-store-permissions'
+
+[dev-dependencies.recurring_rewards]
+default_features = false
+package = 'substrate-recurring-reward-module'
+path = '../../recurring-reward'
+
+[dev-dependencies.sr-staking-primitives]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-staking-primitives'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dev-dependencies.runtime-io]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-io'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+# don't rename the dependency it is causing some strange compiler error:
+# https://github.com/rust-lang/rust/issues/64450
+[dev-dependencies.srml-staking-reward-curve]
+package = 'srml-staking-reward-curve'
+git = 'https://github.com/paritytech/substrate.git'
+default_features = false
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'

+ 1090 - 0
runtime-modules/proposals/codex/src/lib.rs

@@ -0,0 +1,1090 @@
+//! # Proposals codex module
+//! Proposals `codex` module for the Joystream platform. Version 2.
+//! Component of the proposals system. It contains preset proposal types.
+//!
+//! ## Overview
+//!
+//! The proposals codex module serves as a facade and entry point of the proposals system. It uses
+//! proposals `engine` module to maintain a lifecycle of the proposal and to execute proposals.
+//! During the proposal creation, `codex` also create a discussion thread using the `discussion`
+//! proposals module. `Codex` uses predefined parameters (eg.:`voting_period`) for each proposal and
+//! encodes extrinsic calls from dependency modules in order to create proposals inside the `engine`
+//! module. For each proposal, [its crucial details](./enum.ProposalDetails.html) are saved to the
+//! `ProposalDetailsByProposalId` map.
+//!
+//! ### Supported extrinsics (proposal types)
+//! - [create_text_proposal](./struct.Module.html#method.create_text_proposal)
+//! - [create_runtime_upgrade_proposal](./struct.Module.html#method.create_runtime_upgrade_proposal)
+//! - [create_set_election_parameters_proposal](./struct.Module.html#method.create_set_election_parameters_proposal)
+//! - [create_set_content_working_group_mint_capacity_proposal](./struct.Module.html#method.create_set_content_working_group_mint_capacity_proposal)
+//! - [create_spending_proposal](./struct.Module.html#method.create_spending_proposal)
+//! - [create_set_lead_proposal](./struct.Module.html#method.create_set_lead_proposal)
+//! - [create_evict_storage_provider_proposal](./struct.Module.html#method.create_evict_storage_provider_proposal)
+//! - [create_set_validator_count_proposal](./struct.Module.html#method.create_set_validator_count_proposal)
+//! - [create_set_storage_role_parameters_proposal](./struct.Module.html#method.create_set_storage_role_parameters_proposal)
+//!
+//! ### Proposal implementations of this module
+//! - execute_text_proposal - prints the proposal to the log
+//! - execute_runtime_upgrade_proposal - sets the runtime code
+//!
+//! ### Dependencies:
+//! - [proposals engine](../substrate_proposals_engine_module/index.html)
+//! - [proposals discussion](../substrate_proposals_discussion_module/index.html)
+//! - [membership](../substrate_membership_module/index.html)
+//! - [governance](../substrate_governance_module/index.html)
+//! - [content_working_group](../substrate_content_working_group_module/index.html)
+//!
+//! ### Notes
+//! The module uses [ProposalEncoder](./trait.ProposalEncoder.html) to encode the proposal using
+//! its details. Encoded byte vector is passed to the _proposals engine_ as serialized executable code.
+
+// Clippy linter warning. TODO: remove after the Constaninople release
+#![allow(clippy::type_complexity)]
+// disable it because of possible frontend API break
+
+// Clippy linter warning. TODO: refactor "this function has too many argument"
+#![allow(clippy::too_many_arguments)] // disable it because of possible API break
+
+// Ensure we're `no_std` when compiling for Wasm.
+#![cfg_attr(not(feature = "std"), no_std)]
+
+// Do not delete! Cannot be uncommented by default, because of Parity decl_module! issue.
+// #![warn(missing_docs)]
+
+mod proposal_types;
+
+#[cfg(test)]
+mod tests;
+
+use common::origin_validator::ActorOriginValidator;
+use governance::election_params::ElectionParameters;
+use proposal_engine::ProposalParameters;
+use roles::actors::RoleParameters;
+use rstd::clone::Clone;
+use rstd::prelude::*;
+use rstd::str::from_utf8;
+use rstd::vec::Vec;
+use sr_primitives::traits::Zero;
+use srml_support::dispatch::DispatchResult;
+use srml_support::traits::{Currency, Get};
+use srml_support::{decl_error, decl_module, decl_storage, ensure, print};
+use system::{ensure_root, RawOrigin};
+
+pub use crate::proposal_types::ProposalsConfigParameters;
+pub use proposal_types::{ProposalDetails, ProposalDetailsOf, ProposalEncoder};
+
+// 'Set working group mint capacity' proposal limit
+const CONTENT_WORKING_GROUP_MINT_CAPACITY_MAX_VALUE: u32 = 1_000_000;
+// Max allowed value for 'spending' proposal
+const MAX_SPENDING_PROPOSAL_VALUE: u32 = 2_000_000_u32;
+// Max validator count for the 'set validator count' proposal
+const MAX_VALIDATOR_COUNT: u32 = 100;
+// min_actors min value for the 'set storage role parameters' proposal
+const ROLE_PARAMETERS_MIN_ACTORS_MAX_VALUE: u32 = 2;
+// max_actors min value for the 'set storage role parameters' proposal
+const ROLE_PARAMETERS_MAX_ACTORS_MIN_VALUE: u32 = 2;
+// max_actors max value for the 'set storage role parameters' proposal
+const ROLE_PARAMETERS_MAX_ACTORS_MAX_VALUE: u32 = 100;
+// reward_period min value for the 'set storage role parameters' proposal
+const ROLE_PARAMETERS_REWARD_PERIOD_MIN_VALUE: u32 = 600;
+// reward_period max value for the 'set storage role parameters' proposal
+const ROLE_PARAMETERS_REWARD_PERIOD_MAX_VALUE: u32 = 3600;
+// bonding_period min value for the 'set storage role parameters' proposal
+const ROLE_PARAMETERS_BONDING_PERIOD_MIN_VALUE: u32 = 600;
+// bonding_period max value for the 'set storage role parameters' proposal
+const ROLE_PARAMETERS_BONDING_PERIOD_MAX_VALUE: u32 = 28800;
+// unbonding_period min value for the 'set storage role parameters' proposal
+const ROLE_PARAMETERS_UNBONDING_PERIOD_MIN_VALUE: u32 = 600;
+// unbonding_period max value for the 'set storage role parameters' proposal
+const ROLE_PARAMETERS_UNBONDING_PERIOD_MAX_VALUE: u32 = 28800;
+// min_service_period min value for the 'set storage role parameters' proposal
+const ROLE_PARAMETERS_MIN_SERVICE_PERIOD_MIN_VALUE: u32 = 600;
+// min_service_period max value for the 'set storage role parameters' proposal
+const ROLE_PARAMETERS_MIN_SERVICE_PERIOD_MAX_VALUE: u32 = 28800;
+// startup_grace_period min value for the 'set storage role parameters' proposal
+const ROLE_PARAMETERS_STARTUP_GRACE_PERIOD_MIN_VALUE: u32 = 600;
+// startup_grace_period max value for the 'set storage role parameters' proposal
+const ROLE_PARAMETERS_STARTUP_GRACE_PERIOD_MAX_VALUE: u32 = 28800;
+// min_stake min value for the 'set storage role parameters' proposal
+const ROLE_PARAMETERS_MIN_STAKE_MIN_VALUE: u32 = 0;
+// min_stake max value for the 'set storage role parameters' proposal
+const ROLE_PARAMETERS_MIN_STAKE_MAX_VALUE: u32 = 10_000_000;
+// entry_request_fee min value for the 'set storage role parameters' proposal
+const ROLE_PARAMETERS_ENTRY_REQUEST_FEE_MIN_VALUE: u32 = 0;
+// entry_request_fee max value for the 'set storage role parameters' proposal
+const ROLE_PARAMETERS_ENTRY_REQUEST_FEE_MAX_VALUE: u32 = 100_000;
+// reward min value for the 'set storage role parameters' proposal
+const ROLE_PARAMETERS_REWARD_MIN_VALUE: u32 = 0;
+// reward max value for the 'set storage role parameters' proposal
+const ROLE_PARAMETERS_REWARD_MAX_VALUE: u32 = 1000;
+// council_size min value for the 'set election parameters' proposal
+const ELECTION_PARAMETERS_COUNCIL_SIZE_MIN_VALUE: u32 = 4;
+// council_size max value for the 'set election parameters' proposal
+const ELECTION_PARAMETERS_COUNCIL_SIZE_MAX_VALUE: u32 = 20;
+// candidacy_limit min value for the 'set election parameters' proposal
+const ELECTION_PARAMETERS_CANDIDACY_LIMIT_MIN_VALUE: u32 = 25;
+// candidacy_limit max value for the 'set election parameters' proposal
+const ELECTION_PARAMETERS_CANDIDACY_LIMIT_MAX_VALUE: u32 = 100;
+// min_voting_stake min value for the 'set election parameters' proposal
+const ELECTION_PARAMETERS_MIN_STAKE_MIN_VALUE: u32 = 1;
+// min_voting_stake max value for the 'set election parameters' proposal
+const ELECTION_PARAMETERS_MIN_STAKE_MAX_VALUE: u32 = 100_000_u32;
+// new_term_duration min value for the 'set election parameters' proposal
+const ELECTION_PARAMETERS_NEW_TERM_DURATION_MIN_VALUE: u32 = 14400;
+// new_term_duration max value for the 'set election parameters' proposal
+const ELECTION_PARAMETERS_NEW_TERM_DURATION_MAX_VALUE: u32 = 432_000;
+// revealing_period min value for the 'set election parameters' proposal
+const ELECTION_PARAMETERS_REVEALING_PERIOD_MIN_VALUE: u32 = 14400;
+// revealing_period max value for the 'set election parameters' proposal
+const ELECTION_PARAMETERS_REVEALING_PERIOD_MAX_VALUE: u32 = 28800;
+// voting_period min value for the 'set election parameters' proposal
+const ELECTION_PARAMETERS_VOTING_PERIOD_MIN_VALUE: u32 = 14400;
+// voting_period max value for the 'set election parameters' proposal
+const ELECTION_PARAMETERS_VOTING_PERIOD_MAX_VALUE: u32 = 28800;
+// announcing_period min value for the 'set election parameters' proposal
+const ELECTION_PARAMETERS_ANNOUNCING_PERIOD_MIN_VALUE: u32 = 14400;
+// announcing_period max value for the 'set election parameters' proposal
+const ELECTION_PARAMETERS_ANNOUNCING_PERIOD_MAX_VALUE: u32 = 43200;
+// min_council_stake min value for the 'set election parameters' proposal
+const ELECTION_PARAMETERS_MIN_COUNCIL_STAKE_MIN_VALUE: u32 = 1;
+// min_council_stake max value for the 'set election parameters' proposal
+const ELECTION_PARAMETERS_MIN_COUNCIL_STAKE_MAX_VALUE: u32 = 100_000_u32;
+
+/// 'Proposals codex' substrate module Trait
+pub trait Trait:
+    system::Trait
+    + proposal_engine::Trait
+    + proposal_discussion::Trait
+    + membership::members::Trait
+    + governance::election::Trait
+    + content_working_group::Trait
+    + roles::actors::Trait
+    + staking::Trait
+{
+    /// Defines max allowed text proposal length.
+    type TextProposalMaxLength: Get<u32>;
+
+    /// Defines max wasm code length of the runtime upgrade proposal.
+    type RuntimeUpgradeWasmProposalMaxLength: Get<u32>;
+
+    /// Validates member id and origin combination
+    type MembershipOriginValidator: ActorOriginValidator<
+        Self::Origin,
+        MemberId<Self>,
+        Self::AccountId,
+    >;
+
+    /// Encodes the proposal usint its details
+    type ProposalEncoder: ProposalEncoder<Self>;
+}
+
+/// Balance alias for `stake` module
+pub type BalanceOf<T> =
+    <<T as stake::Trait>::Currency as Currency<<T as system::Trait>::AccountId>>::Balance;
+
+/// Currency alias for `stake` module
+pub type CurrencyOf<T> = <T as stake::Trait>::Currency;
+
+/// Balance alias for GovernanceCurrency from `common` module. TODO: replace with BalanceOf
+pub type BalanceOfGovernanceCurrency<T> =
+    <<T as common::currency::GovernanceCurrency>::Currency as Currency<
+        <T as system::Trait>::AccountId,
+    >>::Balance;
+
+/// Balance alias for token mint balance from `token mint` module. TODO: replace with BalanceOf
+pub type BalanceOfMint<T> =
+    <<T as mint::Trait>::Currency as Currency<<T as system::Trait>::AccountId>>::Balance;
+
+/// Negative imbalance alias for staking
+pub type NegativeImbalance<T> =
+    <<T as stake::Trait>::Currency as Currency<<T as system::Trait>::AccountId>>::NegativeImbalance;
+
+type MemberId<T> = <T as membership::members::Trait>::MemberId;
+
+decl_error! {
+    /// Codex module predefined errors
+    pub enum Error {
+        /// The size of the provided text for text proposal exceeded the limit
+        TextProposalSizeExceeded,
+
+        /// Provided text for text proposal is empty
+        TextProposalIsEmpty,
+
+        /// The size of the provided WASM code for the runtime upgrade proposal exceeded the limit
+        RuntimeProposalSizeExceeded,
+
+        /// Provided WASM code for the runtime upgrade proposal is empty
+        RuntimeProposalIsEmpty,
+
+        /// Invalid balance value for the spending proposal
+        InvalidSpendingProposalBalance,
+
+        /// Invalid validator count for the 'set validator count' proposal
+        InvalidValidatorCount,
+
+        /// Require root origin in extrinsics
+        RequireRootOrigin,
+
+        /// Invalid storage role parameter - min_actors
+        InvalidStorageRoleParameterMinActors,
+
+        /// Invalid storage role parameter - max_actors
+        InvalidStorageRoleParameterMaxActors,
+
+        /// Invalid storage role parameter - reward_period
+        InvalidStorageRoleParameterRewardPeriod,
+
+        /// Invalid storage role parameter - bonding_period
+        InvalidStorageRoleParameterBondingPeriod,
+
+        /// Invalid storage role parameter - unbonding_period
+        InvalidStorageRoleParameterUnbondingPeriod,
+
+        /// Invalid storage role parameter - min_service_period
+        InvalidStorageRoleParameterMinServicePeriod,
+
+        /// Invalid storage role parameter - startup_grace_period
+        InvalidStorageRoleParameterStartupGracePeriod,
+
+        /// Invalid council election parameter - council_size
+        InvalidCouncilElectionParameterCouncilSize,
+
+        /// Invalid council election parameter - candidacy-limit
+        InvalidCouncilElectionParameterCandidacyLimit,
+
+        /// Invalid council election parameter - min-voting_stake
+        InvalidCouncilElectionParameterMinVotingStake,
+
+        /// Invalid council election parameter - new_term_duration
+        InvalidCouncilElectionParameterNewTermDuration,
+
+        /// Invalid council election parameter - min_council_stake
+        InvalidCouncilElectionParameterMinCouncilStake,
+
+        /// Invalid council election parameter - revealing_period
+        InvalidCouncilElectionParameterRevealingPeriod,
+
+        /// Invalid council election parameter - voting_period
+        InvalidCouncilElectionParameterVotingPeriod,
+
+        /// Invalid council election parameter - announcing_period
+        InvalidCouncilElectionParameterAnnouncingPeriod,
+
+        /// Invalid council election parameter - min_stake
+        InvalidStorageRoleParameterMinStake,
+
+        /// Invalid council election parameter - reward
+        InvalidStorageRoleParameterReward,
+
+        /// Invalid council election parameter - entry_request_fee
+        InvalidStorageRoleParameterEntryRequestFee,
+
+        /// Invalid working group mint capacity parameter
+        InvalidStorageWorkingGroupMintCapacity,
+
+        /// Invalid 'set lead proposal' parameter - proposed lead cannot be a councilor
+        InvalidSetLeadParameterCannotBeCouncilor
+    }
+}
+
+impl From<system::Error> for Error {
+    fn from(error: system::Error) -> Self {
+        match error {
+            system::Error::Other(msg) => Error::Other(msg),
+            system::Error::RequireRootOrigin => Error::RequireRootOrigin,
+            _ => Error::Other(error.into()),
+        }
+    }
+}
+
+impl From<proposal_engine::Error> for Error {
+    fn from(error: proposal_engine::Error) -> Self {
+        match error {
+            proposal_engine::Error::Other(msg) => Error::Other(msg),
+            proposal_engine::Error::RequireRootOrigin => Error::RequireRootOrigin,
+            _ => Error::Other(error.into()),
+        }
+    }
+}
+
+impl From<proposal_discussion::Error> for Error {
+    fn from(error: proposal_discussion::Error) -> Self {
+        match error {
+            proposal_discussion::Error::Other(msg) => Error::Other(msg),
+            proposal_discussion::Error::RequireRootOrigin => Error::RequireRootOrigin,
+            _ => Error::Other(error.into()),
+        }
+    }
+}
+
+// Storage for the proposals codex module
+decl_storage! {
+    pub trait Store for Module<T: Trait> as ProposalCodex{
+        /// Map proposal id to its discussion thread id
+        pub ThreadIdByProposalId get(fn thread_id_by_proposal_id):
+            map T::ProposalId => T::ThreadId;
+
+        /// Map proposal id to proposal details
+        pub ProposalDetailsByProposalId get(fn proposal_details_by_proposal_id):
+            map T::ProposalId => ProposalDetails<
+                BalanceOfMint<T>,
+                BalanceOfGovernanceCurrency<T>,
+                T::BlockNumber,
+                T::AccountId,
+                T::MemberId
+            >;
+
+        /// Voting period for the 'set validator count' proposal
+        pub SetValidatorCountProposalVotingPeriod get(set_validator_count_proposal_voting_period)
+            config(): T::BlockNumber;
+
+        /// Grace period for the 'set validator count' proposal
+        pub SetValidatorCountProposalGracePeriod get(set_validator_count_proposal_grace_period)
+            config(): T::BlockNumber;
+
+        /// Voting period for the 'runtime upgrade' proposal
+        pub RuntimeUpgradeProposalVotingPeriod get(runtime_upgrade_proposal_voting_period)
+            config(): T::BlockNumber;
+
+        /// Grace period for the 'runtime upgrade' proposal
+        pub RuntimeUpgradeProposalGracePeriod get(runtime_upgrade_proposal_grace_period)
+            config(): T::BlockNumber;
+
+        /// Voting period for the 'set election parameters' proposal
+        pub SetElectionParametersProposalVotingPeriod get(set_election_parameters_proposal_voting_period)
+            config(): T::BlockNumber;
+
+        /// Grace period for the 'set election parameters' proposal
+        pub SetElectionParametersProposalGracePeriod get(set_election_parameters_proposal_grace_period)
+            config(): T::BlockNumber;
+
+        /// Voting period for the 'text' proposal
+        pub TextProposalVotingPeriod get(text_proposal_voting_period) config(): T::BlockNumber;
+
+        /// Grace period for the 'text' proposal
+        pub TextProposalGracePeriod get(text_proposal_grace_period) config(): T::BlockNumber;
+
+        /// Voting period for the 'set content working group mint capacity' proposal
+        pub SetContentWorkingGroupMintCapacityProposalVotingPeriod get(set_content_working_group_mint_capacity_proposal_voting_period)
+            config(): T::BlockNumber;
+
+        /// Grace period for the 'set content working group mint capacity' proposal
+        pub SetContentWorkingGroupMintCapacityProposalGracePeriod get(set_content_working_group_mint_capacity_proposal_grace_period)
+            config(): T::BlockNumber;
+
+        /// Voting period for the 'set lead' proposal
+        pub SetLeadProposalVotingPeriod get(set_lead_proposal_voting_period)
+            config(): T::BlockNumber;
+
+        /// Grace period for the 'set lead' proposal
+        pub SetLeadProposalGracePeriod get(set_lead_proposal_grace_period)
+            config(): T::BlockNumber;
+
+        /// Voting period for the 'spending' proposal
+        pub SpendingProposalVotingPeriod get(spending_proposal_voting_period) config(): T::BlockNumber;
+
+        /// Grace period for the 'spending' proposal
+        pub SpendingProposalGracePeriod get(spending_proposal_grace_period) config(): T::BlockNumber;
+
+        /// Voting period for the 'evict storage provider' proposal
+        pub EvictStorageProviderProposalVotingPeriod get(evict_storage_provider_proposal_voting_period)
+            config(): T::BlockNumber;
+
+        /// Grace period for the 'evict storage provider' proposal
+        pub EvictStorageProviderProposalGracePeriod get(evict_storage_provider_proposal_grace_period)
+            config(): T::BlockNumber;
+
+        /// Voting period for the 'set storage role parameters' proposal
+        pub SetStorageRoleParametersProposalVotingPeriod get(set_storage_role_parameters_proposal_voting_period)
+            config(): T::BlockNumber;
+
+        /// Grace period for the 'set storage role parameters' proposal
+        pub SetStorageRoleParametersProposalGracePeriod get(set_storage_role_parameters_proposal_grace_period)
+            config(): T::BlockNumber;
+    }
+}
+
+decl_module! {
+    /// Proposal codex substrate module Call
+    pub struct Module<T: Trait> for enum Call where origin: T::Origin {
+        /// Predefined errors
+        type Error = Error;
+
+        /// Exports max allowed text proposal length const.
+        const TextProposalMaxLength: u32 = T::TextProposalMaxLength::get();
+
+        /// Exports max wasm code length of the runtime upgrade proposal const.
+        const RuntimeUpgradeWasmProposalMaxLength: u32 = T::RuntimeUpgradeWasmProposalMaxLength::get();
+
+        /// Create 'Text (signal)' proposal type.
+        pub fn create_text_proposal(
+            origin,
+            member_id: MemberId<T>,
+            title: Vec<u8>,
+            description: Vec<u8>,
+            stake_balance: Option<BalanceOf<T>>,
+            text: Vec<u8>,
+        ) {
+            ensure!(!text.is_empty(), Error::TextProposalIsEmpty);
+            ensure!(text.len() as u32 <=  T::TextProposalMaxLength::get(),
+                Error::TextProposalSizeExceeded);
+
+            let proposal_parameters = proposal_types::parameters::text_proposal::<T>();
+            let proposal_details = ProposalDetails::<BalanceOfMint<T>, BalanceOfGovernanceCurrency<T>, T::BlockNumber, T::AccountId, MemberId<T>>::Text(text);
+            let proposal_code = T::ProposalEncoder::encode_proposal(proposal_details.clone());
+
+            Self::create_proposal(
+                origin,
+                member_id,
+                title,
+                description,
+                stake_balance,
+                proposal_code,
+                proposal_parameters,
+                proposal_details,
+            )?;
+        }
+
+        /// Create 'Runtime upgrade' proposal type. Runtime upgrade can be initiated only by
+        /// members from the hardcoded list `RuntimeUpgradeProposalAllowedProposers`
+        pub fn create_runtime_upgrade_proposal(
+            origin,
+            member_id: MemberId<T>,
+            title: Vec<u8>,
+            description: Vec<u8>,
+            stake_balance: Option<BalanceOf<T>>,
+            wasm: Vec<u8>,
+        ) {
+            ensure!(!wasm.is_empty(), Error::RuntimeProposalIsEmpty);
+            ensure!(wasm.len() as u32 <= T::RuntimeUpgradeWasmProposalMaxLength::get(),
+                Error::RuntimeProposalSizeExceeded);
+
+            let proposal_parameters = proposal_types::parameters::runtime_upgrade_proposal::<T>();
+            let proposal_details = ProposalDetails::RuntimeUpgrade(wasm);
+            let proposal_code = T::ProposalEncoder::encode_proposal(proposal_details.clone());
+
+            Self::create_proposal(
+                origin,
+                member_id,
+                title,
+                description,
+                stake_balance,
+                proposal_code,
+                proposal_parameters,
+                proposal_details,
+            )?;
+        }
+
+        /// Create 'Set election parameters' proposal type. This proposal uses `set_election_parameters()`
+        /// extrinsic from the `governance::election module`.
+        pub fn create_set_election_parameters_proposal(
+            origin,
+            member_id: MemberId<T>,
+            title: Vec<u8>,
+            description: Vec<u8>,
+            stake_balance: Option<BalanceOf<T>>,
+            election_parameters: ElectionParameters<BalanceOfGovernanceCurrency<T>, T::BlockNumber>,
+        ) {
+            election_parameters.ensure_valid()?;
+
+            Self::ensure_council_election_parameters_valid(&election_parameters)?;
+
+            let proposal_details = ProposalDetails::SetElectionParameters(election_parameters);
+            let proposal_code = T::ProposalEncoder::encode_proposal(proposal_details.clone());
+            let proposal_parameters =
+                proposal_types::parameters::set_election_parameters_proposal::<T>();
+
+            Self::create_proposal(
+                origin,
+                member_id,
+                title,
+                description,
+                stake_balance,
+                proposal_code,
+                proposal_parameters,
+                proposal_details,
+            )?;
+        }
+
+        /// Create 'Set content working group mint capacity' proposal type.
+        /// This proposal uses `set_mint_capacity()` extrinsic from the `content-working-group`  module.
+        pub fn create_set_content_working_group_mint_capacity_proposal(
+            origin,
+            member_id: MemberId<T>,
+            title: Vec<u8>,
+            description: Vec<u8>,
+            stake_balance: Option<BalanceOf<T>>,
+            mint_balance: BalanceOfMint<T>,
+        ) {
+            ensure!(
+                mint_balance <= <BalanceOfMint<T>>::from(CONTENT_WORKING_GROUP_MINT_CAPACITY_MAX_VALUE),
+                Error::InvalidStorageWorkingGroupMintCapacity
+            );
+
+            let proposal_parameters =
+                proposal_types::parameters::set_content_working_group_mint_capacity_proposal::<T>();
+            let proposal_details = ProposalDetails::SetContentWorkingGroupMintCapacity(mint_balance);
+            let proposal_code = T::ProposalEncoder::encode_proposal(proposal_details.clone());
+
+            Self::create_proposal(
+                origin,
+                member_id,
+                title,
+                description,
+                stake_balance,
+                proposal_code,
+                proposal_parameters,
+                proposal_details,
+            )?;
+        }
+
+        /// Create 'Spending' proposal type.
+        /// This proposal uses `spend_from_council_mint()` extrinsic from the `governance::council`  module.
+        pub fn create_spending_proposal(
+            origin,
+            member_id: MemberId<T>,
+            title: Vec<u8>,
+            description: Vec<u8>,
+            stake_balance: Option<BalanceOf<T>>,
+            balance: BalanceOfMint<T>,
+            destination: T::AccountId,
+        ) {
+            ensure!(balance != BalanceOfMint::<T>::zero(), Error::InvalidSpendingProposalBalance);
+            ensure!(
+                balance <= <BalanceOfMint<T>>::from(MAX_SPENDING_PROPOSAL_VALUE),
+                Error::InvalidSpendingProposalBalance
+            );
+
+            let proposal_parameters =
+                proposal_types::parameters::spending_proposal::<T>();
+            let proposal_details = ProposalDetails::Spending(balance, destination);
+            let proposal_code = T::ProposalEncoder::encode_proposal(proposal_details.clone());
+
+            Self::create_proposal(
+                origin,
+                member_id,
+                title,
+                description,
+                stake_balance,
+                proposal_code,
+                proposal_parameters,
+                proposal_details,
+            )?;
+        }
+
+        /// Create 'Set lead' proposal type.
+        /// This proposal uses `replace_lead()` extrinsic from the `content_working_group`  module.
+        pub fn create_set_lead_proposal(
+            origin,
+            member_id: MemberId<T>,
+            title: Vec<u8>,
+            description: Vec<u8>,
+            stake_balance: Option<BalanceOf<T>>,
+            new_lead: Option<(T::MemberId, T::AccountId)>
+        ) {
+            if let Some(lead) = new_lead.clone() {
+                let account_id = lead.1;
+                ensure!(
+                    !<governance::council::Module<T>>::is_councilor(&account_id),
+                    Error::InvalidSetLeadParameterCannotBeCouncilor
+                );
+            }
+
+            let proposal_parameters =
+                proposal_types::parameters::set_lead_proposal::<T>();
+            let proposal_details = ProposalDetails::SetLead(new_lead);
+            let proposal_code = T::ProposalEncoder::encode_proposal(proposal_details.clone());
+
+            Self::create_proposal(
+                origin,
+                member_id,
+                title,
+                description,
+                stake_balance,
+                proposal_code,
+                proposal_parameters,
+                proposal_details,
+            )?;
+        }
+
+        /// Create 'Evict storage provider' proposal type.
+        /// This proposal uses `remove_actor()` extrinsic from the `roles::actors`  module.
+        pub fn create_evict_storage_provider_proposal(
+            origin,
+            member_id: MemberId<T>,
+            title: Vec<u8>,
+            description: Vec<u8>,
+            stake_balance: Option<BalanceOf<T>>,
+            actor_account: T::AccountId,
+        ) {
+            let proposal_parameters =
+                proposal_types::parameters::evict_storage_provider_proposal::<T>();
+            let proposal_details = ProposalDetails::EvictStorageProvider(actor_account);
+            let proposal_code = T::ProposalEncoder::encode_proposal(proposal_details.clone());
+
+            Self::create_proposal(
+                origin,
+                member_id,
+                title,
+                description,
+                stake_balance,
+                proposal_code,
+                proposal_parameters,
+                proposal_details,
+            )?;
+        }
+
+        /// Create 'Evict storage provider' proposal type.
+        /// This proposal uses `set_validator_count()` extrinsic from the Substrate `staking`  module.
+        pub fn create_set_validator_count_proposal(
+            origin,
+            member_id: MemberId<T>,
+            title: Vec<u8>,
+            description: Vec<u8>,
+            stake_balance: Option<BalanceOf<T>>,
+            new_validator_count: u32,
+        ) {
+            ensure!(
+                new_validator_count >= <staking::Module<T>>::minimum_validator_count(),
+                Error::InvalidValidatorCount
+            );
+
+            ensure!(
+                new_validator_count <= MAX_VALIDATOR_COUNT,
+                Error::InvalidValidatorCount
+            );
+
+            let proposal_parameters =
+                proposal_types::parameters::set_validator_count_proposal::<T>();
+            let proposal_details = ProposalDetails::SetValidatorCount(new_validator_count);
+            let proposal_code = T::ProposalEncoder::encode_proposal(proposal_details.clone());
+
+            Self::create_proposal(
+                origin,
+                member_id,
+                title,
+                description,
+                stake_balance,
+                proposal_code,
+                proposal_parameters,
+                proposal_details,
+            )?;
+        }
+
+        /// Create 'Set storage roles parameters' proposal type.
+        /// This proposal uses `set_role_parameters()` extrinsic from the Substrate `roles::actors`  module.
+        pub fn create_set_storage_role_parameters_proposal(
+            origin,
+            member_id: MemberId<T>,
+            title: Vec<u8>,
+            description: Vec<u8>,
+            stake_balance: Option<BalanceOf<T>>,
+            role_parameters: RoleParameters<BalanceOfGovernanceCurrency<T>, T::BlockNumber>
+        ) {
+            Self::ensure_storage_role_parameters_valid(&role_parameters)?;
+
+            let proposal_parameters =
+                proposal_types::parameters::set_storage_role_parameters_proposal::<T>();
+            let proposal_details =  ProposalDetails::SetStorageRoleParameters(role_parameters);
+            let proposal_code = T::ProposalEncoder::encode_proposal(proposal_details.clone());
+
+            Self::create_proposal(
+                origin,
+                member_id,
+                title,
+                description,
+                stake_balance,
+                proposal_code,
+                proposal_parameters,
+                proposal_details,
+            )?;
+        }
+
+// *************** Extrinsic to execute
+
+        /// Text proposal extrinsic. Should be used as callable object to pass to the `engine` module.
+        pub fn execute_text_proposal(
+            origin,
+            text: Vec<u8>,
+        ) {
+            ensure_root(origin)?;
+            print("Text proposal: ");
+            let text_string_result = from_utf8(text.as_slice());
+            if let Ok(text_string) = text_string_result{
+                print(text_string);
+            }
+        }
+
+        /// Runtime upgrade proposal extrinsic.
+        /// Should be used as callable object to pass to the `engine` module.
+        pub fn execute_runtime_upgrade_proposal(
+            origin,
+            wasm: Vec<u8>,
+        ) {
+            let (cloned_origin1, cloned_origin2) =  Self::double_origin(origin);
+            ensure_root(cloned_origin1)?;
+
+            print("Runtime upgrade proposal execution started.");
+
+            <system::Module<T>>::set_code(cloned_origin2, wasm)?;
+
+            print("Runtime upgrade proposal execution finished.");
+        }
+    }
+}
+
+impl<T: Trait> Module<T> {
+    // Multiplies the T::Origin.
+    // In our current substrate version system::Origin doesn't support clone(),
+    // but it will be supported in latest up-to-date substrate version.
+    // TODO: delete when T::Origin will support the clone()
+    fn double_origin(origin: T::Origin) -> (T::Origin, T::Origin) {
+        let coerced_origin = origin.into().ok().unwrap_or(RawOrigin::None);
+
+        let (cloned_origin1, cloned_origin2) = match coerced_origin {
+            RawOrigin::None => (RawOrigin::None, RawOrigin::None),
+            RawOrigin::Root => (RawOrigin::Root, RawOrigin::Root),
+            RawOrigin::Signed(account_id) => (
+                RawOrigin::Signed(account_id.clone()),
+                RawOrigin::Signed(account_id),
+            ),
+        };
+
+        (cloned_origin1.into(), cloned_origin2.into())
+    }
+
+    // Generic template proposal builder
+    fn create_proposal(
+        origin: T::Origin,
+        member_id: MemberId<T>,
+        title: Vec<u8>,
+        description: Vec<u8>,
+        stake_balance: Option<BalanceOf<T>>,
+        proposal_code: Vec<u8>,
+        proposal_parameters: ProposalParameters<T::BlockNumber, BalanceOf<T>>,
+        proposal_details: ProposalDetails<
+            BalanceOfMint<T>,
+            BalanceOfGovernanceCurrency<T>,
+            T::BlockNumber,
+            T::AccountId,
+            T::MemberId,
+        >,
+    ) -> DispatchResult<Error> {
+        let account_id = T::MembershipOriginValidator::ensure_actor_origin(origin, member_id)?;
+
+        <proposal_engine::Module<T>>::ensure_create_proposal_parameters_are_valid(
+            &proposal_parameters,
+            &title,
+            &description,
+            stake_balance,
+        )?;
+
+        <proposal_discussion::Module<T>>::ensure_can_create_thread(member_id, &title)?;
+
+        let discussion_thread_id =
+            <proposal_discussion::Module<T>>::create_thread(member_id, title.clone())?;
+
+        let proposal_id = <proposal_engine::Module<T>>::create_proposal(
+            account_id,
+            member_id,
+            proposal_parameters,
+            title,
+            description,
+            stake_balance,
+            proposal_code,
+        )?;
+
+        <ThreadIdByProposalId<T>>::insert(proposal_id, discussion_thread_id);
+        <ProposalDetailsByProposalId<T>>::insert(proposal_id, proposal_details);
+
+        Ok(())
+    }
+
+    // validates storage role parameters for the 'Set storage role parameters' proposal
+    fn ensure_storage_role_parameters_valid(
+        role_parameters: &RoleParameters<BalanceOfGovernanceCurrency<T>, T::BlockNumber>,
+    ) -> Result<(), Error> {
+        ensure!(
+            role_parameters.min_actors < ROLE_PARAMETERS_MIN_ACTORS_MAX_VALUE,
+            Error::InvalidStorageRoleParameterMinActors
+        );
+
+        ensure!(
+            role_parameters.max_actors >= ROLE_PARAMETERS_MAX_ACTORS_MIN_VALUE,
+            Error::InvalidStorageRoleParameterMaxActors
+        );
+
+        ensure!(
+            role_parameters.max_actors < ROLE_PARAMETERS_MAX_ACTORS_MAX_VALUE,
+            Error::InvalidStorageRoleParameterMaxActors
+        );
+
+        ensure!(
+            role_parameters.reward_period
+                >= T::BlockNumber::from(ROLE_PARAMETERS_REWARD_PERIOD_MIN_VALUE),
+            Error::InvalidStorageRoleParameterRewardPeriod
+        );
+
+        ensure!(
+            role_parameters.reward_period
+                <= T::BlockNumber::from(ROLE_PARAMETERS_REWARD_PERIOD_MAX_VALUE),
+            Error::InvalidStorageRoleParameterRewardPeriod
+        );
+
+        ensure!(
+            role_parameters.bonding_period
+                >= T::BlockNumber::from(ROLE_PARAMETERS_BONDING_PERIOD_MIN_VALUE),
+            Error::InvalidStorageRoleParameterBondingPeriod
+        );
+
+        ensure!(
+            role_parameters.bonding_period
+                <= T::BlockNumber::from(ROLE_PARAMETERS_BONDING_PERIOD_MAX_VALUE),
+            Error::InvalidStorageRoleParameterBondingPeriod
+        );
+
+        ensure!(
+            role_parameters.unbonding_period
+                >= T::BlockNumber::from(ROLE_PARAMETERS_UNBONDING_PERIOD_MIN_VALUE),
+            Error::InvalidStorageRoleParameterUnbondingPeriod
+        );
+
+        ensure!(
+            role_parameters.unbonding_period
+                <= T::BlockNumber::from(ROLE_PARAMETERS_UNBONDING_PERIOD_MAX_VALUE),
+            Error::InvalidStorageRoleParameterUnbondingPeriod
+        );
+
+        ensure!(
+            role_parameters.min_service_period
+                >= T::BlockNumber::from(ROLE_PARAMETERS_MIN_SERVICE_PERIOD_MIN_VALUE),
+            Error::InvalidStorageRoleParameterMinServicePeriod
+        );
+
+        ensure!(
+            role_parameters.min_service_period
+                <= T::BlockNumber::from(ROLE_PARAMETERS_MIN_SERVICE_PERIOD_MAX_VALUE),
+            Error::InvalidStorageRoleParameterMinServicePeriod
+        );
+
+        ensure!(
+            role_parameters.startup_grace_period
+                >= T::BlockNumber::from(ROLE_PARAMETERS_STARTUP_GRACE_PERIOD_MIN_VALUE),
+            Error::InvalidStorageRoleParameterStartupGracePeriod
+        );
+
+        ensure!(
+            role_parameters.startup_grace_period
+                <= T::BlockNumber::from(ROLE_PARAMETERS_STARTUP_GRACE_PERIOD_MAX_VALUE),
+            Error::InvalidStorageRoleParameterStartupGracePeriod
+        );
+
+        ensure!(
+            role_parameters.min_stake
+                > <BalanceOfGovernanceCurrency<T>>::from(ROLE_PARAMETERS_MIN_STAKE_MIN_VALUE),
+            Error::InvalidStorageRoleParameterMinStake
+        );
+
+        ensure!(
+            role_parameters.min_stake
+                <= <BalanceOfGovernanceCurrency<T>>::from(ROLE_PARAMETERS_MIN_STAKE_MAX_VALUE),
+            Error::InvalidStorageRoleParameterMinStake
+        );
+
+        ensure!(
+            role_parameters.entry_request_fee
+                > <BalanceOfGovernanceCurrency<T>>::from(
+                    ROLE_PARAMETERS_ENTRY_REQUEST_FEE_MIN_VALUE
+                ),
+            Error::InvalidStorageRoleParameterEntryRequestFee
+        );
+
+        ensure!(
+            role_parameters.entry_request_fee
+                <= <BalanceOfGovernanceCurrency<T>>::from(
+                    ROLE_PARAMETERS_ENTRY_REQUEST_FEE_MAX_VALUE
+                ),
+            Error::InvalidStorageRoleParameterEntryRequestFee
+        );
+
+        ensure!(
+            role_parameters.reward
+                > <BalanceOfGovernanceCurrency<T>>::from(ROLE_PARAMETERS_REWARD_MIN_VALUE),
+            Error::InvalidStorageRoleParameterReward
+        );
+
+        ensure!(
+            role_parameters.reward
+                < <BalanceOfGovernanceCurrency<T>>::from(ROLE_PARAMETERS_REWARD_MAX_VALUE),
+            Error::InvalidStorageRoleParameterReward
+        );
+
+        Ok(())
+    }
+
+    /*
+    entry_request_fee [tJOY]	>0	<1%	NA
+    * Not enforced by runtime. Should not be displayed in the UI, or at least grayed out.
+    ** Should not be displayed in the UI, or at least grayed out.
+        */
+
+    // validates council election parameters for the 'Set election parameters' proposal
+    pub(crate) fn ensure_council_election_parameters_valid(
+        election_parameters: &ElectionParameters<BalanceOfGovernanceCurrency<T>, T::BlockNumber>,
+    ) -> Result<(), Error> {
+        ensure!(
+            election_parameters.council_size >= ELECTION_PARAMETERS_COUNCIL_SIZE_MIN_VALUE,
+            Error::InvalidCouncilElectionParameterCouncilSize
+        );
+
+        ensure!(
+            election_parameters.council_size <= ELECTION_PARAMETERS_COUNCIL_SIZE_MAX_VALUE,
+            Error::InvalidCouncilElectionParameterCouncilSize
+        );
+
+        ensure!(
+            election_parameters.candidacy_limit >= ELECTION_PARAMETERS_CANDIDACY_LIMIT_MIN_VALUE,
+            Error::InvalidCouncilElectionParameterCandidacyLimit
+        );
+
+        ensure!(
+            election_parameters.candidacy_limit <= ELECTION_PARAMETERS_CANDIDACY_LIMIT_MAX_VALUE,
+            Error::InvalidCouncilElectionParameterCandidacyLimit
+        );
+
+        ensure!(
+            election_parameters.min_voting_stake
+                >= <BalanceOfGovernanceCurrency<T>>::from(ELECTION_PARAMETERS_MIN_STAKE_MIN_VALUE),
+            Error::InvalidCouncilElectionParameterMinVotingStake
+        );
+
+        ensure!(
+            election_parameters.min_voting_stake
+                <= <BalanceOfGovernanceCurrency<T>>::from(ELECTION_PARAMETERS_MIN_STAKE_MAX_VALUE),
+            Error::InvalidCouncilElectionParameterMinVotingStake
+        );
+
+        ensure!(
+            election_parameters.new_term_duration
+                >= T::BlockNumber::from(ELECTION_PARAMETERS_NEW_TERM_DURATION_MIN_VALUE),
+            Error::InvalidCouncilElectionParameterNewTermDuration
+        );
+
+        ensure!(
+            election_parameters.new_term_duration
+                <= T::BlockNumber::from(ELECTION_PARAMETERS_NEW_TERM_DURATION_MAX_VALUE),
+            Error::InvalidCouncilElectionParameterNewTermDuration
+        );
+
+        ensure!(
+            election_parameters.revealing_period
+                >= T::BlockNumber::from(ELECTION_PARAMETERS_REVEALING_PERIOD_MIN_VALUE),
+            Error::InvalidCouncilElectionParameterRevealingPeriod
+        );
+
+        ensure!(
+            election_parameters.revealing_period
+                <= T::BlockNumber::from(ELECTION_PARAMETERS_REVEALING_PERIOD_MAX_VALUE),
+            Error::InvalidCouncilElectionParameterRevealingPeriod
+        );
+
+        ensure!(
+            election_parameters.voting_period
+                >= T::BlockNumber::from(ELECTION_PARAMETERS_VOTING_PERIOD_MIN_VALUE),
+            Error::InvalidCouncilElectionParameterVotingPeriod
+        );
+
+        ensure!(
+            election_parameters.voting_period
+                <= T::BlockNumber::from(ELECTION_PARAMETERS_VOTING_PERIOD_MAX_VALUE),
+            Error::InvalidCouncilElectionParameterVotingPeriod
+        );
+
+        ensure!(
+            election_parameters.announcing_period
+                >= T::BlockNumber::from(ELECTION_PARAMETERS_ANNOUNCING_PERIOD_MIN_VALUE),
+            Error::InvalidCouncilElectionParameterAnnouncingPeriod
+        );
+
+        ensure!(
+            election_parameters.announcing_period
+                <= T::BlockNumber::from(ELECTION_PARAMETERS_ANNOUNCING_PERIOD_MAX_VALUE),
+            Error::InvalidCouncilElectionParameterAnnouncingPeriod
+        );
+
+        ensure!(
+            election_parameters.min_council_stake
+                >= <BalanceOfGovernanceCurrency<T>>::from(
+                    ELECTION_PARAMETERS_MIN_COUNCIL_STAKE_MIN_VALUE
+                ),
+            Error::InvalidCouncilElectionParameterMinCouncilStake
+        );
+
+        ensure!(
+            election_parameters.min_council_stake
+                <= <BalanceOfGovernanceCurrency<T>>::from(
+                    ELECTION_PARAMETERS_MIN_COUNCIL_STAKE_MAX_VALUE
+                ),
+            Error::InvalidCouncilElectionParameterMinCouncilStake
+        );
+
+        Ok(())
+    }
+
+    /// Sets default config values for the proposals.
+    /// Should be called on the migration to the new runtime version.
+    pub fn set_default_config_values() {
+        let p = ProposalsConfigParameters::default();
+
+        <SetValidatorCountProposalVotingPeriod<T>>::put(T::BlockNumber::from(
+            p.set_validator_count_proposal_voting_period,
+        ));
+        <SetValidatorCountProposalGracePeriod<T>>::put(T::BlockNumber::from(
+            p.set_validator_count_proposal_grace_period,
+        ));
+        <RuntimeUpgradeProposalVotingPeriod<T>>::put(T::BlockNumber::from(
+            p.runtime_upgrade_proposal_voting_period,
+        ));
+        <RuntimeUpgradeProposalGracePeriod<T>>::put(T::BlockNumber::from(
+            p.runtime_upgrade_proposal_grace_period,
+        ));
+        <TextProposalVotingPeriod<T>>::put(T::BlockNumber::from(p.text_proposal_voting_period));
+        <TextProposalGracePeriod<T>>::put(T::BlockNumber::from(p.text_proposal_grace_period));
+        <SetElectionParametersProposalVotingPeriod<T>>::put(T::BlockNumber::from(
+            p.set_election_parameters_proposal_voting_period,
+        ));
+        <SetElectionParametersProposalGracePeriod<T>>::put(T::BlockNumber::from(
+            p.set_election_parameters_proposal_grace_period,
+        ));
+        <SetContentWorkingGroupMintCapacityProposalVotingPeriod<T>>::put(T::BlockNumber::from(
+            p.set_content_working_group_mint_capacity_proposal_voting_period,
+        ));
+        <SetContentWorkingGroupMintCapacityProposalGracePeriod<T>>::put(T::BlockNumber::from(
+            p.set_content_working_group_mint_capacity_proposal_grace_period,
+        ));
+        <SetLeadProposalVotingPeriod<T>>::put(T::BlockNumber::from(
+            p.set_lead_proposal_voting_period,
+        ));
+        <SetLeadProposalGracePeriod<T>>::put(T::BlockNumber::from(
+            p.set_lead_proposal_grace_period,
+        ));
+        <SpendingProposalVotingPeriod<T>>::put(T::BlockNumber::from(
+            p.spending_proposal_voting_period,
+        ));
+        <SpendingProposalGracePeriod<T>>::put(T::BlockNumber::from(
+            p.spending_proposal_grace_period,
+        ));
+        <EvictStorageProviderProposalVotingPeriod<T>>::put(T::BlockNumber::from(
+            p.evict_storage_provider_proposal_voting_period,
+        ));
+        <EvictStorageProviderProposalGracePeriod<T>>::put(T::BlockNumber::from(
+            p.evict_storage_provider_proposal_grace_period,
+        ));
+        <SetStorageRoleParametersProposalVotingPeriod<T>>::put(T::BlockNumber::from(
+            p.set_storage_role_parameters_proposal_voting_period,
+        ));
+        <SetStorageRoleParametersProposalGracePeriod<T>>::put(T::BlockNumber::from(
+            p.set_storage_role_parameters_proposal_grace_period,
+        ));
+    }
+}

+ 148 - 0
runtime-modules/proposals/codex/src/proposal_types/mod.rs

@@ -0,0 +1,148 @@
+#![warn(missing_docs)]
+
+pub(crate) mod parameters;
+
+use codec::{Decode, Encode};
+use rstd::vec::Vec;
+#[cfg(feature = "std")]
+use serde::{Deserialize, Serialize};
+
+use crate::ElectionParameters;
+use roles::actors::RoleParameters;
+
+/// Encodes proposal using its details information.
+pub trait ProposalEncoder<T: crate::Trait> {
+    /// Encodes proposal using its details information.
+    fn encode_proposal(proposal_details: ProposalDetailsOf<T>) -> Vec<u8>;
+}
+
+/// _ProposalDetails_ alias for type simplification
+pub type ProposalDetailsOf<T> = ProposalDetails<
+    crate::BalanceOfMint<T>,
+    crate::BalanceOfGovernanceCurrency<T>,
+    <T as system::Trait>::BlockNumber,
+    <T as system::Trait>::AccountId,
+    crate::MemberId<T>,
+>;
+
+/// Proposal details provide voters the information required for the perceived voting.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Clone, PartialEq, Debug)]
+pub enum ProposalDetails<MintedBalance, CurrencyBalance, BlockNumber, AccountId, MemberId> {
+    /// The text of the `text` proposal
+    Text(Vec<u8>),
+
+    /// The wasm code for the `runtime upgrade` proposal
+    RuntimeUpgrade(Vec<u8>),
+
+    /// Election parameters for the `set election parameters` proposal
+    SetElectionParameters(ElectionParameters<CurrencyBalance, BlockNumber>),
+
+    /// Balance and destination account for the `spending` proposal
+    Spending(MintedBalance, AccountId),
+
+    /// New leader memberId and account_id for the `set lead` proposal
+    SetLead(Option<(MemberId, AccountId)>),
+
+    /// Balance for the `set content working group mint capacity` proposal
+    SetContentWorkingGroupMintCapacity(MintedBalance),
+
+    /// AccountId for the `evict storage provider` proposal
+    EvictStorageProvider(AccountId),
+
+    /// Validator count for the `set validator count` proposal
+    SetValidatorCount(u32),
+
+    /// Role parameters for the `set storage role parameters` proposal
+    SetStorageRoleParameters(RoleParameters<CurrencyBalance, BlockNumber>),
+}
+
+impl<MintedBalance, CurrencyBalance, BlockNumber, AccountId, MemberId> Default
+    for ProposalDetails<MintedBalance, CurrencyBalance, BlockNumber, AccountId, MemberId>
+{
+    fn default() -> Self {
+        ProposalDetails::Text(b"invalid proposal details".to_vec())
+    }
+}
+
+/// Contains proposal config parameters. Default values are used by migration and genesis config.
+pub struct ProposalsConfigParameters {
+    /// 'Set validator count' proposal voting period
+    pub set_validator_count_proposal_voting_period: u32,
+
+    /// 'Set validator count' proposal grace period
+    pub set_validator_count_proposal_grace_period: u32,
+
+    /// 'Runtime upgrade' proposal voting period
+    pub runtime_upgrade_proposal_voting_period: u32,
+
+    /// 'Runtime upgrade' proposal grace period
+    pub runtime_upgrade_proposal_grace_period: u32,
+
+    /// 'Text' proposal voting period
+    pub text_proposal_voting_period: u32,
+
+    /// 'Text' proposal grace period
+    pub text_proposal_grace_period: u32,
+
+    /// 'Set election parameters' proposal voting period
+    pub set_election_parameters_proposal_voting_period: u32,
+
+    /// 'Set election parameters' proposal grace period
+    pub set_election_parameters_proposal_grace_period: u32,
+
+    /// 'Set content working group mint capacity' proposal voting period
+    pub set_content_working_group_mint_capacity_proposal_voting_period: u32,
+
+    /// 'Set content working group mint capacity' proposal grace period
+    pub set_content_working_group_mint_capacity_proposal_grace_period: u32,
+
+    /// 'Set lead' proposal voting period
+    pub set_lead_proposal_voting_period: u32,
+
+    /// 'Set lead' proposal grace period
+    pub set_lead_proposal_grace_period: u32,
+
+    /// 'Spending' proposal voting period
+    pub spending_proposal_voting_period: u32,
+
+    /// 'Spending' proposal grace period
+    pub spending_proposal_grace_period: u32,
+
+    /// 'Evict storage provider' proposal voting period
+    pub evict_storage_provider_proposal_voting_period: u32,
+
+    /// 'Evict storage provider' proposal grace period
+    pub evict_storage_provider_proposal_grace_period: u32,
+
+    /// 'Set storage role parameters' proposal voting period
+    pub set_storage_role_parameters_proposal_voting_period: u32,
+
+    /// 'Set storage role parameters' proposal grace period
+    pub set_storage_role_parameters_proposal_grace_period: u32,
+}
+
+impl Default for ProposalsConfigParameters {
+    fn default() -> Self {
+        ProposalsConfigParameters {
+            set_validator_count_proposal_voting_period: 43200u32,
+            set_validator_count_proposal_grace_period: 0u32,
+            runtime_upgrade_proposal_voting_period: 72000u32,
+            runtime_upgrade_proposal_grace_period: 72000u32,
+            text_proposal_voting_period: 72000u32,
+            text_proposal_grace_period: 0u32,
+            set_election_parameters_proposal_voting_period: 72000u32,
+            set_election_parameters_proposal_grace_period: 201_601_u32,
+            set_content_working_group_mint_capacity_proposal_voting_period: 43200u32,
+            set_content_working_group_mint_capacity_proposal_grace_period: 0u32,
+            set_lead_proposal_voting_period: 43200u32,
+            set_lead_proposal_grace_period: 0u32,
+            spending_proposal_voting_period: 72000u32,
+            spending_proposal_grace_period: 14400u32,
+            evict_storage_provider_proposal_voting_period: 43200u32,
+            evict_storage_provider_proposal_grace_period: 0u32,
+            set_storage_role_parameters_proposal_voting_period: 43200u32,
+            set_storage_role_parameters_proposal_grace_period: 14400u32,
+        }
+    }
+}

+ 127 - 0
runtime-modules/proposals/codex/src/proposal_types/parameters.rs

@@ -0,0 +1,127 @@
+use crate::{BalanceOf, Module, ProposalParameters};
+
+// Proposal parameters for the 'Set validator count' proposal
+pub(crate) fn set_validator_count_proposal<T: crate::Trait>(
+) -> ProposalParameters<T::BlockNumber, BalanceOf<T>> {
+    ProposalParameters {
+        voting_period: <Module<T>>::set_validator_count_proposal_voting_period(),
+        grace_period: <Module<T>>::set_validator_count_proposal_grace_period(),
+        approval_quorum_percentage: 66,
+        approval_threshold_percentage: 80,
+        slashing_quorum_percentage: 60,
+        slashing_threshold_percentage: 80,
+        required_stake: Some(<BalanceOf<T>>::from(100_000_u32)),
+    }
+}
+
+// Proposal parameters for the upgrade runtime proposal
+pub(crate) fn runtime_upgrade_proposal<T: crate::Trait>(
+) -> ProposalParameters<T::BlockNumber, BalanceOf<T>> {
+    ProposalParameters {
+        voting_period: <Module<T>>::runtime_upgrade_proposal_voting_period(),
+        grace_period: <Module<T>>::runtime_upgrade_proposal_grace_period(),
+        approval_quorum_percentage: 80,
+        approval_threshold_percentage: 100,
+        slashing_quorum_percentage: 60,
+        slashing_threshold_percentage: 80,
+        required_stake: Some(<BalanceOf<T>>::from(1_000_000_u32)),
+    }
+}
+
+// Proposal parameters for the text proposal
+pub(crate) fn text_proposal<T: crate::Trait>() -> ProposalParameters<T::BlockNumber, BalanceOf<T>> {
+    ProposalParameters {
+        voting_period: <Module<T>>::text_proposal_voting_period(),
+        grace_period: <Module<T>>::text_proposal_grace_period(),
+        approval_quorum_percentage: 60,
+        approval_threshold_percentage: 80,
+        slashing_quorum_percentage: 60,
+        slashing_threshold_percentage: 80,
+        required_stake: Some(<BalanceOf<T>>::from(25000u32)),
+    }
+}
+
+// Proposal parameters for the 'Set Election Parameters' proposal
+pub(crate) fn set_election_parameters_proposal<T: crate::Trait>(
+) -> ProposalParameters<T::BlockNumber, BalanceOf<T>> {
+    ProposalParameters {
+        voting_period: <Module<T>>::set_election_parameters_proposal_voting_period(),
+        grace_period: <Module<T>>::set_election_parameters_proposal_grace_period(),
+        approval_quorum_percentage: 66,
+        approval_threshold_percentage: 80,
+        slashing_quorum_percentage: 60,
+        slashing_threshold_percentage: 80,
+        required_stake: Some(<BalanceOf<T>>::from(200_000_u32)),
+    }
+}
+
+// Proposal parameters for the 'Set content working group mint capacity' proposal
+pub(crate) fn set_content_working_group_mint_capacity_proposal<T: crate::Trait>(
+) -> ProposalParameters<T::BlockNumber, BalanceOf<T>> {
+    ProposalParameters {
+        voting_period: <Module<T>>::set_content_working_group_mint_capacity_proposal_voting_period(
+        ),
+        grace_period: <Module<T>>::set_content_working_group_mint_capacity_proposal_grace_period(),
+        approval_quorum_percentage: 60,
+        approval_threshold_percentage: 75,
+        slashing_quorum_percentage: 60,
+        slashing_threshold_percentage: 80,
+        required_stake: Some(<BalanceOf<T>>::from(50000u32)),
+    }
+}
+
+// Proposal parameters for the 'Spending' proposal
+pub(crate) fn spending_proposal<T: crate::Trait>(
+) -> ProposalParameters<T::BlockNumber, BalanceOf<T>> {
+    ProposalParameters {
+        voting_period: <Module<T>>::spending_proposal_voting_period(),
+        grace_period: <Module<T>>::spending_proposal_grace_period(),
+        approval_quorum_percentage: 60,
+        approval_threshold_percentage: 80,
+        slashing_quorum_percentage: 60,
+        slashing_threshold_percentage: 80,
+        required_stake: Some(<BalanceOf<T>>::from(25000u32)),
+    }
+}
+
+// Proposal parameters for the 'Set lead' proposal
+pub(crate) fn set_lead_proposal<T: crate::Trait>(
+) -> ProposalParameters<T::BlockNumber, BalanceOf<T>> {
+    ProposalParameters {
+        voting_period: <Module<T>>::set_lead_proposal_voting_period(),
+        grace_period: <Module<T>>::set_lead_proposal_grace_period(),
+        approval_quorum_percentage: 60,
+        approval_threshold_percentage: 75,
+        slashing_quorum_percentage: 60,
+        slashing_threshold_percentage: 80,
+        required_stake: Some(<BalanceOf<T>>::from(50000u32)),
+    }
+}
+
+// Proposal parameters for the 'Evict storage provider' proposal
+pub(crate) fn evict_storage_provider_proposal<T: crate::Trait>(
+) -> ProposalParameters<T::BlockNumber, BalanceOf<T>> {
+    ProposalParameters {
+        voting_period: <Module<T>>::evict_storage_provider_proposal_voting_period(),
+        grace_period: <Module<T>>::evict_storage_provider_proposal_grace_period(),
+        approval_quorum_percentage: 50,
+        approval_threshold_percentage: 75,
+        slashing_quorum_percentage: 60,
+        slashing_threshold_percentage: 80,
+        required_stake: Some(<BalanceOf<T>>::from(25000u32)),
+    }
+}
+
+// Proposal parameters for the 'Set storage role parameters' proposal
+pub(crate) fn set_storage_role_parameters_proposal<T: crate::Trait>(
+) -> ProposalParameters<T::BlockNumber, BalanceOf<T>> {
+    ProposalParameters {
+        voting_period: <Module<T>>::set_storage_role_parameters_proposal_voting_period(),
+        grace_period: <Module<T>>::set_storage_role_parameters_proposal_grace_period(),
+        approval_quorum_percentage: 66,
+        approval_threshold_percentage: 80,
+        slashing_quorum_percentage: 60,
+        slashing_threshold_percentage: 80,
+        required_stake: Some(<BalanceOf<T>>::from(100_000_u32)),
+    }
+}

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

@@ -0,0 +1,296 @@
+#![cfg(test)]
+// srml_staking_reward_curve::build! - substrate macro produces a warning.
+// TODO: remove after post-Rome substrate upgrade
+#![allow(array_into_iter)]
+
+use crate::{ProposalDetailsOf, ProposalEncoder};
+pub use primitives::{Blake2Hasher, H256};
+use proposal_engine::VotersParameters;
+use sr_primitives::curve::PiecewiseLinear;
+pub use sr_primitives::{
+    testing::{Digest, DigestItem, Header, UintAuthorityId},
+    traits::{BlakeTwo256, Convert, IdentityLookup, OnFinalize},
+    weights::Weight,
+    BuildStorage, DispatchError, Perbill,
+};
+use sr_staking_primitives::SessionIndex;
+use srml_support::{impl_outer_dispatch, impl_outer_origin, parameter_types};
+pub use system;
+
+impl_outer_origin! {
+    pub enum Origin for Test {}
+}
+
+// Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted.
+#[derive(Clone, PartialEq, Eq, Debug)]
+pub struct Test;
+parameter_types! {
+    pub const BlockHashCount: u64 = 250;
+    pub const MaximumBlockWeight: u32 = 1024;
+    pub const MaximumBlockLength: u32 = 2 * 1024;
+    pub const AvailableBlockRatio: Perbill = Perbill::one();
+    pub const MinimumPeriod: u64 = 5;
+    pub const StakePoolId: [u8; 8] = *b"joystake";
+}
+
+impl_outer_dispatch! {
+    pub enum Call for Test where origin: Origin {
+        codex::ProposalCodex,
+        proposals::ProposalsEngine,
+    }
+}
+
+impl common::currency::GovernanceCurrency for Test {
+    type Currency = balances::Module<Self>;
+}
+
+impl membership::members::Trait for Test {
+    type Event = ();
+    type MemberId = u64;
+    type PaidTermId = u64;
+    type SubscriptionId = u64;
+    type ActorId = u64;
+    type InitialMembersBalance = ();
+}
+
+parameter_types! {
+    pub const ExistentialDeposit: u32 = 0;
+    pub const TransferFee: u32 = 0;
+    pub const CreationFee: u32 = 0;
+}
+
+impl balances::Trait for Test {
+    /// The type for recording an account's balance.
+    type Balance = u64;
+    /// What to do if an account's free balance gets zeroed.
+    type OnFreeBalanceZero = ();
+    /// What to do if a new account is created.
+    type OnNewAccount = ();
+
+    type Event = ();
+
+    type DustRemoval = ();
+    type TransferPayment = ();
+    type ExistentialDeposit = ExistentialDeposit;
+    type TransferFee = TransferFee;
+    type CreationFee = CreationFee;
+}
+
+impl stake::Trait for Test {
+    type Currency = Balances;
+    type StakePoolId = StakePoolId;
+    type StakingEventsHandler = ();
+    type StakeId = u64;
+    type SlashId = u64;
+}
+
+parameter_types! {
+    pub const CancellationFee: u64 = 5;
+    pub const RejectionFee: u64 = 3;
+    pub const TitleMaxLength: u32 = 100;
+    pub const DescriptionMaxLength: u32 = 10000;
+    pub const MaxActiveProposalLimit: u32 = 100;
+}
+
+impl proposal_engine::Trait for Test {
+    type Event = ();
+    type ProposerOriginValidator = ();
+    type VoterOriginValidator = ();
+    type TotalVotersCounter = MockVotersParameters;
+    type ProposalId = u32;
+    type StakeHandlerProvider = proposal_engine::DefaultStakeHandlerProvider;
+    type CancellationFee = CancellationFee;
+    type RejectionFee = RejectionFee;
+    type TitleMaxLength = TitleMaxLength;
+    type DescriptionMaxLength = DescriptionMaxLength;
+    type MaxActiveProposalLimit = MaxActiveProposalLimit;
+    type DispatchableCallCode = crate::Call<Test>;
+}
+
+impl Default for crate::Call<Test> {
+    fn default() -> Self {
+        panic!("shouldn't call default for Call");
+    }
+}
+
+impl mint::Trait for Test {
+    type Currency = Balances;
+    type MintId = u64;
+}
+
+impl governance::council::Trait for Test {
+    type Event = ();
+    type CouncilTermEnded = ();
+}
+
+impl common::origin_validator::ActorOriginValidator<Origin, u64, u64> for () {
+    fn ensure_actor_origin(origin: Origin, _: u64) -> Result<u64, &'static str> {
+        let account_id = system::ensure_signed(origin)?;
+
+        Ok(account_id)
+    }
+}
+
+parameter_types! {
+    pub const MaxPostEditionNumber: u32 = 5;
+    pub const MaxThreadInARowNumber: u32 = 3;
+    pub const ThreadTitleLengthLimit: u32 = 200;
+    pub const PostLengthLimit: u32 = 2000;
+}
+
+impl proposal_discussion::Trait for Test {
+    type Event = ();
+    type PostAuthorOriginValidator = ();
+    type ThreadId = u64;
+    type PostId = u64;
+    type MaxPostEditionNumber = MaxPostEditionNumber;
+    type ThreadTitleLengthLimit = ThreadTitleLengthLimit;
+    type PostLengthLimit = PostLengthLimit;
+    type MaxThreadInARowNumber = MaxThreadInARowNumber;
+}
+
+pub struct MockVotersParameters;
+impl VotersParameters for MockVotersParameters {
+    fn total_voters_count() -> u32 {
+        4
+    }
+}
+
+parameter_types! {
+    pub const TextProposalMaxLength: u32 = 20_000;
+    pub const RuntimeUpgradeWasmProposalMaxLength: u32 = 20_000;
+}
+
+impl governance::election::Trait for Test {
+    type Event = ();
+    type CouncilElected = ();
+}
+
+impl content_working_group::Trait for Test {
+    type Event = ();
+}
+
+impl recurring_rewards::Trait for Test {
+    type PayoutStatusHandler = ();
+    type RecipientId = u64;
+    type RewardRelationshipId = u64;
+}
+
+impl versioned_store_permissions::Trait for Test {
+    type Credential = u64;
+    type CredentialChecker = ();
+    type CreateClassPermissionsChecker = ();
+}
+
+impl versioned_store::Trait for Test {
+    type Event = ();
+}
+
+impl hiring::Trait for Test {
+    type OpeningId = u64;
+    type ApplicationId = u64;
+    type ApplicationDeactivatedHandler = ();
+    type StakeHandlerProvider = hiring::Module<Self>;
+}
+
+impl roles::actors::Trait for Test {
+    type Event = ();
+    type OnActorRemoved = ();
+}
+
+impl roles::actors::ActorRemoved<Test> for () {
+    fn actor_removed(_: &u64) {}
+}
+
+srml_staking_reward_curve::build! {
+    const I_NPOS: PiecewiseLinear<'static> = curve!(
+        min_inflation: 0_025_000,
+        max_inflation: 0_100_000,
+        ideal_stake: 0_500_000,
+        falloff: 0_050_000,
+        max_piece_count: 40,
+        test_precision: 0_005_000,
+    );
+}
+
+parameter_types! {
+    pub const SessionsPerEra: SessionIndex = 3;
+    pub const BondingDuration: staking::EraIndex = 3;
+    pub const RewardCurve: &'static PiecewiseLinear<'static> = &I_NPOS;
+}
+impl staking::Trait for Test {
+    type Currency = balances::Module<Self>;
+    type Time = timestamp::Module<Self>;
+    type CurrencyToVote = ();
+    type RewardRemainder = ();
+    type Event = ();
+    type Slash = ();
+    type Reward = ();
+    type SessionsPerEra = SessionsPerEra;
+    type BondingDuration = BondingDuration;
+    type SessionInterface = Self;
+    type RewardCurve = RewardCurve;
+}
+
+impl staking::SessionInterface<u64> for Test {
+    fn disable_validator(_: &u64) -> Result<bool, ()> {
+        unimplemented!()
+    }
+
+    fn validators() -> Vec<u64> {
+        unimplemented!()
+    }
+
+    fn prune_historical_up_to(_: u32) {
+        unimplemented!()
+    }
+}
+
+impl crate::Trait for Test {
+    type TextProposalMaxLength = TextProposalMaxLength;
+    type RuntimeUpgradeWasmProposalMaxLength = RuntimeUpgradeWasmProposalMaxLength;
+    type MembershipOriginValidator = ();
+    type ProposalEncoder = ();
+}
+
+impl ProposalEncoder<Test> for () {
+    fn encode_proposal(_proposal_details: ProposalDetailsOf<Test>) -> Vec<u8> {
+        Vec::new()
+    }
+}
+
+impl system::Trait for Test {
+    type Origin = Origin;
+    type Index = u64;
+    type BlockNumber = u64;
+    type Call = ();
+    type Hash = H256;
+    type Hashing = BlakeTwo256;
+    type AccountId = u64;
+    type Lookup = IdentityLookup<Self::AccountId>;
+    type Header = Header;
+    type Event = ();
+    type BlockHashCount = BlockHashCount;
+    type MaximumBlockWeight = MaximumBlockWeight;
+    type MaximumBlockLength = MaximumBlockLength;
+    type AvailableBlockRatio = AvailableBlockRatio;
+    type Version = ();
+}
+
+impl timestamp::Trait for Test {
+    type Moment = u64;
+    type OnTimestampSet = ();
+    type MinimumPeriod = MinimumPeriod;
+}
+
+pub fn initial_test_ext() -> runtime_io::TestExternalities {
+    let t = system::GenesisConfig::default()
+        .build_storage::<Test>()
+        .unwrap();
+
+    t.into()
+}
+
+pub type ProposalCodex = crate::Module<Test>;
+pub type ProposalsEngine = proposal_engine::Module<Test>;
+pub type Balances = balances::Module<Test>;

+ 1147 - 0
runtime-modules/proposals/codex/src/tests/mod.rs

@@ -0,0 +1,1147 @@
+mod mock;
+
+use governance::election_params::ElectionParameters;
+use srml_support::traits::Currency;
+use srml_support::StorageMap;
+use system::RawOrigin;
+
+use crate::*;
+use crate::{BalanceOf, Error, ProposalDetails};
+use proposal_engine::ProposalParameters;
+use roles::actors::RoleParameters;
+use srml_support::dispatch::DispatchResult;
+
+use crate::proposal_types::ProposalsConfigParameters;
+pub use mock::*;
+
+pub(crate) fn increase_total_balance_issuance(balance: u64) {
+    increase_total_balance_issuance_using_account_id(999, balance);
+}
+
+pub(crate) fn increase_total_balance_issuance_using_account_id(account_id: u64, balance: u64) {
+    let initial_balance = Balances::total_issuance();
+    {
+        let _ = <Test as stake::Trait>::Currency::deposit_creating(&account_id, balance);
+    }
+    assert_eq!(Balances::total_issuance(), initial_balance + balance);
+}
+
+struct ProposalTestFixture<InsufficientRightsCall, EmptyStakeCall, InvalidStakeCall, SuccessfulCall>
+where
+    InsufficientRightsCall: Fn() -> DispatchResult<crate::Error>,
+    EmptyStakeCall: Fn() -> DispatchResult<crate::Error>,
+    InvalidStakeCall: Fn() -> DispatchResult<crate::Error>,
+    SuccessfulCall: Fn() -> DispatchResult<crate::Error>,
+{
+    insufficient_rights_call: InsufficientRightsCall,
+    empty_stake_call: EmptyStakeCall,
+    invalid_stake_call: InvalidStakeCall,
+    successful_call: SuccessfulCall,
+    proposal_parameters: ProposalParameters<u64, u64>,
+    proposal_details: ProposalDetails<u64, u64, u64, u64, u64>,
+}
+
+impl<InsufficientRightsCall, EmptyStakeCall, InvalidStakeCall, SuccessfulCall>
+    ProposalTestFixture<InsufficientRightsCall, EmptyStakeCall, InvalidStakeCall, SuccessfulCall>
+where
+    InsufficientRightsCall: Fn() -> DispatchResult<crate::Error>,
+    EmptyStakeCall: Fn() -> DispatchResult<crate::Error>,
+    InvalidStakeCall: Fn() -> DispatchResult<crate::Error>,
+    SuccessfulCall: Fn() -> DispatchResult<crate::Error>,
+{
+    fn check_for_invalid_stakes(&self) {
+        assert_eq!((self.empty_stake_call)(), Err(Error::Other("EmptyStake")));
+
+        assert_eq!(
+            (self.invalid_stake_call)(),
+            Err(Error::Other("StakeDiffersFromRequired"))
+        );
+    }
+
+    fn check_call_for_insufficient_rights(&self) {
+        assert_eq!(
+            (self.insufficient_rights_call)(),
+            Err(Error::Other("RequireSignedOrigin"))
+        );
+    }
+
+    fn check_for_successful_call(&self) {
+        let account_id = 1;
+        let _imbalance = <Test as stake::Trait>::Currency::deposit_creating(&account_id, 50000);
+
+        assert_eq!((self.successful_call)(), Ok(()));
+
+        // a discussion was created
+        let thread_id = <crate::ThreadIdByProposalId<Test>>::get(1);
+        assert_eq!(thread_id, 1);
+
+        let proposal_id = 1;
+        let proposal = ProposalsEngine::proposals(proposal_id);
+        // check for correct proposal parameters
+        assert_eq!(proposal.parameters, self.proposal_parameters);
+
+        // proposal details was set
+        let details = <crate::ProposalDetailsByProposalId<Test>>::get(proposal_id);
+        assert_eq!(details, self.proposal_details);
+    }
+
+    pub fn check_all(&self) {
+        self.check_call_for_insufficient_rights();
+        self.check_for_invalid_stakes();
+        self.check_for_successful_call();
+    }
+}
+
+#[test]
+fn create_text_proposal_common_checks_succeed() {
+    initial_test_ext().execute_with(|| {
+        increase_total_balance_issuance(500000);
+
+        let proposal_fixture = ProposalTestFixture {
+            insufficient_rights_call: || {
+                ProposalCodex::create_text_proposal(
+                    RawOrigin::None.into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    b"text".to_vec(),
+                )
+            },
+            empty_stake_call: || {
+                ProposalCodex::create_text_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    b"text".to_vec(),
+                )
+            },
+            invalid_stake_call: || {
+                ProposalCodex::create_text_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(5000u32)),
+                    b"text".to_vec(),
+                )
+            },
+            successful_call: || {
+                ProposalCodex::create_text_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(25000u32)),
+                    b"text".to_vec(),
+                )
+            },
+            proposal_parameters: crate::proposal_types::parameters::text_proposal::<Test>(),
+            proposal_details: ProposalDetails::Text(b"text".to_vec()),
+        };
+        proposal_fixture.check_all();
+    });
+}
+
+#[test]
+fn create_text_proposal_codex_call_fails_with_incorrect_text_size() {
+    initial_test_ext().execute_with(|| {
+        let origin = RawOrigin::Signed(1).into();
+
+        let long_text = [0u8; 30000].to_vec();
+        assert_eq!(
+            ProposalCodex::create_text_proposal(
+                origin,
+                1,
+                b"title".to_vec(),
+                b"body".to_vec(),
+                None,
+                long_text,
+            ),
+            Err(Error::TextProposalSizeExceeded)
+        );
+
+        assert_eq!(
+            ProposalCodex::create_text_proposal(
+                RawOrigin::Signed(1).into(),
+                1,
+                b"title".to_vec(),
+                b"body".to_vec(),
+                None,
+                Vec::new(),
+            ),
+            Err(Error::TextProposalIsEmpty)
+        );
+    });
+}
+
+#[test]
+fn create_runtime_upgrade_common_checks_succeed() {
+    initial_test_ext().execute_with(|| {
+        increase_total_balance_issuance_using_account_id(1, 5000000);
+
+        let proposal_fixture = ProposalTestFixture {
+            insufficient_rights_call: || {
+                ProposalCodex::create_runtime_upgrade_proposal(
+                    RawOrigin::None.into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    b"wasm".to_vec(),
+                )
+            },
+            empty_stake_call: || {
+                ProposalCodex::create_runtime_upgrade_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    b"wasm".to_vec(),
+                )
+            },
+            invalid_stake_call: || {
+                ProposalCodex::create_runtime_upgrade_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(500u32)),
+                    b"wasm".to_vec(),
+                )
+            },
+            successful_call: || {
+                ProposalCodex::create_runtime_upgrade_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(1_000_000_u32)),
+                    b"wasm".to_vec(),
+                )
+            },
+            proposal_parameters: crate::proposal_types::parameters::runtime_upgrade_proposal::<Test>(),
+            proposal_details: ProposalDetails::RuntimeUpgrade(b"wasm".to_vec()),
+        };
+        proposal_fixture.check_all();
+    });
+}
+
+#[test]
+fn create_upgrade_runtime_proposal_codex_call_fails_with_incorrect_wasm_size() {
+    initial_test_ext().execute_with(|| {
+        let origin = RawOrigin::Signed(1).into();
+
+        let long_wasm = [0u8; 30000].to_vec();
+        assert_eq!(
+            ProposalCodex::create_runtime_upgrade_proposal(
+                origin,
+                1,
+                b"title".to_vec(),
+                b"body".to_vec(),
+                None,
+                long_wasm,
+            ),
+            Err(Error::RuntimeProposalSizeExceeded)
+        );
+
+        assert_eq!(
+            ProposalCodex::create_runtime_upgrade_proposal(
+                RawOrigin::Signed(1).into(),
+                1,
+                b"title".to_vec(),
+                b"body".to_vec(),
+                None,
+                Vec::new(),
+            ),
+            Err(Error::RuntimeProposalIsEmpty)
+        );
+    });
+}
+
+#[test]
+fn create_set_election_parameters_proposal_common_checks_succeed() {
+    initial_test_ext().execute_with(|| {
+        increase_total_balance_issuance_using_account_id(1, 500000);
+
+        let proposal_fixture = ProposalTestFixture {
+            insufficient_rights_call: || {
+                ProposalCodex::create_set_election_parameters_proposal(
+                    RawOrigin::None.into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    get_valid_election_parameters(),
+                )
+            },
+            empty_stake_call: || {
+                ProposalCodex::create_set_election_parameters_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    get_valid_election_parameters(),
+                )
+            },
+            invalid_stake_call: || {
+                ProposalCodex::create_set_election_parameters_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(50000u32)),
+                    get_valid_election_parameters(),
+                )
+            },
+            successful_call: || {
+                ProposalCodex::create_set_election_parameters_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(200_000_u32)),
+                    get_valid_election_parameters(),
+                )
+            },
+            proposal_parameters:
+                crate::proposal_types::parameters::set_election_parameters_proposal::<Test>(),
+            proposal_details: ProposalDetails::SetElectionParameters(
+                get_valid_election_parameters(),
+            ),
+        };
+        proposal_fixture.check_all();
+    });
+}
+
+fn assert_failed_election_parameters_call(
+    election_parameters: ElectionParameters<u64, u64>,
+    error: Error,
+) {
+    assert_eq!(
+        ProposalCodex::create_set_election_parameters_proposal(
+            RawOrigin::Signed(1).into(),
+            1,
+            b"title".to_vec(),
+            b"body".to_vec(),
+            Some(<BalanceOf<Test>>::from(3750u32)),
+            election_parameters,
+        ),
+        Err(error)
+    );
+}
+
+fn get_valid_election_parameters() -> ElectionParameters<u64, u64> {
+    ElectionParameters {
+        announcing_period: 14400,
+        voting_period: 14400,
+        revealing_period: 14400,
+        council_size: 4,
+        candidacy_limit: 25,
+        new_term_duration: 14400,
+        min_council_stake: 1,
+        min_voting_stake: 1,
+    }
+}
+
+#[test]
+fn create_set_election_parameters_call_fails_with_incorrect_parameters() {
+    initial_test_ext().execute_with(|| {
+        increase_total_balance_issuance_using_account_id(1, 500000);
+
+        let mut election_parameters = get_valid_election_parameters();
+        election_parameters.council_size = 2;
+        assert_failed_election_parameters_call(
+            election_parameters,
+            Error::InvalidCouncilElectionParameterCouncilSize,
+        );
+
+        election_parameters.council_size = 21;
+        assert_failed_election_parameters_call(
+            election_parameters,
+            Error::InvalidCouncilElectionParameterCouncilSize,
+        );
+
+        election_parameters = get_valid_election_parameters();
+        election_parameters.candidacy_limit = 22;
+        assert_failed_election_parameters_call(
+            election_parameters,
+            Error::InvalidCouncilElectionParameterCandidacyLimit,
+        );
+
+        election_parameters = get_valid_election_parameters();
+        election_parameters.candidacy_limit = 122;
+        assert_failed_election_parameters_call(
+            election_parameters,
+            Error::InvalidCouncilElectionParameterCandidacyLimit,
+        );
+
+        election_parameters = get_valid_election_parameters();
+        election_parameters.min_voting_stake = 0;
+        assert_failed_election_parameters_call(
+            election_parameters,
+            Error::InvalidCouncilElectionParameterMinVotingStake,
+        );
+
+        election_parameters = get_valid_election_parameters();
+        election_parameters.min_voting_stake = 200000;
+        assert_failed_election_parameters_call(
+            election_parameters,
+            Error::InvalidCouncilElectionParameterMinVotingStake,
+        );
+
+        election_parameters = get_valid_election_parameters();
+        election_parameters.new_term_duration = 10000;
+        assert_failed_election_parameters_call(
+            election_parameters,
+            Error::InvalidCouncilElectionParameterNewTermDuration,
+        );
+
+        election_parameters = get_valid_election_parameters();
+        election_parameters.new_term_duration = 500000;
+        assert_failed_election_parameters_call(
+            election_parameters,
+            Error::InvalidCouncilElectionParameterNewTermDuration,
+        );
+
+        election_parameters = get_valid_election_parameters();
+        election_parameters.min_council_stake = 0;
+        assert_failed_election_parameters_call(
+            election_parameters,
+            Error::InvalidCouncilElectionParameterMinCouncilStake,
+        );
+
+        election_parameters = get_valid_election_parameters();
+        election_parameters.min_council_stake = 200000;
+        assert_failed_election_parameters_call(
+            election_parameters,
+            Error::InvalidCouncilElectionParameterMinCouncilStake,
+        );
+
+        election_parameters = get_valid_election_parameters();
+        election_parameters.voting_period = 10000;
+        assert_failed_election_parameters_call(
+            election_parameters,
+            Error::InvalidCouncilElectionParameterVotingPeriod,
+        );
+
+        election_parameters = get_valid_election_parameters();
+        election_parameters.voting_period = 50000;
+        assert_failed_election_parameters_call(
+            election_parameters,
+            Error::InvalidCouncilElectionParameterVotingPeriod,
+        );
+
+        election_parameters = get_valid_election_parameters();
+        election_parameters.revealing_period = 10000;
+        assert_failed_election_parameters_call(
+            election_parameters,
+            Error::InvalidCouncilElectionParameterRevealingPeriod,
+        );
+
+        election_parameters = get_valid_election_parameters();
+        election_parameters.revealing_period = 50000;
+        assert_failed_election_parameters_call(
+            election_parameters,
+            Error::InvalidCouncilElectionParameterRevealingPeriod,
+        );
+
+        election_parameters = get_valid_election_parameters();
+        election_parameters.announcing_period = 10000;
+        assert_failed_election_parameters_call(
+            election_parameters,
+            Error::InvalidCouncilElectionParameterAnnouncingPeriod,
+        );
+
+        election_parameters = get_valid_election_parameters();
+        election_parameters.announcing_period = 50000;
+        assert_failed_election_parameters_call(
+            election_parameters,
+            Error::InvalidCouncilElectionParameterAnnouncingPeriod,
+        );
+    });
+}
+
+#[test]
+fn create_working_group_mint_capacity_proposal_fails_with_invalid_parameters() {
+    initial_test_ext().execute_with(|| {
+        increase_total_balance_issuance_using_account_id(1, 500000);
+
+        assert_eq!(
+            ProposalCodex::create_set_content_working_group_mint_capacity_proposal(
+                RawOrigin::Signed(1).into(),
+                1,
+                b"title".to_vec(),
+                b"body".to_vec(),
+                Some(<BalanceOf<Test>>::from(50000u32)),
+                (crate::CONTENT_WORKING_GROUP_MINT_CAPACITY_MAX_VALUE + 1) as u64,
+            ),
+            Err(Error::InvalidStorageWorkingGroupMintCapacity)
+        );
+    });
+}
+
+#[test]
+fn create_set_content_working_group_mint_capacity_proposal_common_checks_succeed() {
+    initial_test_ext().execute_with(|| {
+        increase_total_balance_issuance(500000);
+
+        let proposal_fixture = ProposalTestFixture {
+            insufficient_rights_call: || {
+                ProposalCodex::create_set_content_working_group_mint_capacity_proposal(
+                    RawOrigin::None.into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    0,
+                )
+            },
+            empty_stake_call: || {
+                ProposalCodex::create_set_content_working_group_mint_capacity_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    0,
+                )
+            },
+            invalid_stake_call: || {
+                ProposalCodex::create_set_content_working_group_mint_capacity_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(5000u32)),
+                    0,
+                )
+            },
+            successful_call: || {
+                ProposalCodex::create_set_content_working_group_mint_capacity_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(50000u32)),
+                    10,
+                )
+            },
+            proposal_parameters: crate::proposal_types::parameters::set_content_working_group_mint_capacity_proposal::<Test>(),
+            proposal_details: ProposalDetails::SetContentWorkingGroupMintCapacity(10),
+        };
+        proposal_fixture.check_all();
+    });
+}
+
+#[test]
+fn create_spending_proposal_common_checks_succeed() {
+    initial_test_ext().execute_with(|| {
+        increase_total_balance_issuance(500000);
+
+        let proposal_fixture = ProposalTestFixture {
+            insufficient_rights_call: || {
+                ProposalCodex::create_spending_proposal(
+                    RawOrigin::None.into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    20,
+                    10,
+                )
+            },
+            empty_stake_call: || {
+                ProposalCodex::create_spending_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    20,
+                    10,
+                )
+            },
+            invalid_stake_call: || {
+                ProposalCodex::create_spending_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(5000u32)),
+                    20,
+                    10,
+                )
+            },
+            successful_call: || {
+                ProposalCodex::create_spending_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(25000u32)),
+                    100,
+                    2,
+                )
+            },
+            proposal_parameters: crate::proposal_types::parameters::spending_proposal::<Test>(),
+            proposal_details: ProposalDetails::Spending(100, 2),
+        };
+        proposal_fixture.check_all();
+    });
+}
+
+#[test]
+fn create_spending_proposal_call_fails_with_incorrect_balance() {
+    initial_test_ext().execute_with(|| {
+        increase_total_balance_issuance_using_account_id(500000, 1);
+
+        assert_eq!(
+            ProposalCodex::create_spending_proposal(
+                RawOrigin::Signed(1).into(),
+                1,
+                b"title".to_vec(),
+                b"body".to_vec(),
+                Some(<BalanceOf<Test>>::from(1250u32)),
+                0,
+                2,
+            ),
+            Err(Error::InvalidSpendingProposalBalance)
+        );
+
+        assert_eq!(
+            ProposalCodex::create_spending_proposal(
+                RawOrigin::Signed(1).into(),
+                1,
+                b"title".to_vec(),
+                b"body".to_vec(),
+                Some(<BalanceOf<Test>>::from(1250u32)),
+                2000001,
+                2,
+            ),
+            Err(Error::InvalidSpendingProposalBalance)
+        );
+    });
+}
+
+#[test]
+fn create_set_lead_proposal_fails_with_proposed_councilor() {
+    initial_test_ext().execute_with(|| {
+        increase_total_balance_issuance_using_account_id(1, 500000);
+
+        let lead_account_id = 20;
+        <governance::council::Module<Test>>::set_council(
+            RawOrigin::Root.into(),
+            vec![lead_account_id],
+        )
+        .unwrap();
+
+        assert_eq!(
+            ProposalCodex::create_set_lead_proposal(
+                RawOrigin::Signed(1).into(),
+                1,
+                b"title".to_vec(),
+                b"body".to_vec(),
+                Some(<BalanceOf<Test>>::from(1250u32)),
+                Some((20, lead_account_id)),
+            ),
+            Err(Error::InvalidSetLeadParameterCannotBeCouncilor)
+        );
+    });
+}
+
+#[test]
+fn create_set_lead_proposal_common_checks_succeed() {
+    initial_test_ext().execute_with(|| {
+        increase_total_balance_issuance(500000);
+
+        let proposal_fixture = ProposalTestFixture {
+            insufficient_rights_call: || {
+                ProposalCodex::create_set_lead_proposal(
+                    RawOrigin::None.into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    Some((20, 10)),
+                )
+            },
+            empty_stake_call: || {
+                ProposalCodex::create_set_lead_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    Some((20, 10)),
+                )
+            },
+            invalid_stake_call: || {
+                ProposalCodex::create_set_lead_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(5000u32)),
+                    Some((20, 10)),
+                )
+            },
+            successful_call: || {
+                ProposalCodex::create_set_lead_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(50000u32)),
+                    Some((20, 10)),
+                )
+            },
+            proposal_parameters: crate::proposal_types::parameters::set_lead_proposal::<Test>(),
+            proposal_details: ProposalDetails::SetLead(Some((20, 10))),
+        };
+        proposal_fixture.check_all();
+    });
+}
+
+#[test]
+fn create_evict_storage_provider_proposal_common_checks_succeed() {
+    initial_test_ext().execute_with(|| {
+        increase_total_balance_issuance(500000);
+
+        let proposal_fixture = ProposalTestFixture {
+            insufficient_rights_call: || {
+                ProposalCodex::create_evict_storage_provider_proposal(
+                    RawOrigin::None.into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    1,
+                )
+            },
+            empty_stake_call: || {
+                ProposalCodex::create_evict_storage_provider_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    1,
+                )
+            },
+            invalid_stake_call: || {
+                ProposalCodex::create_evict_storage_provider_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(5000u32)),
+                    1,
+                )
+            },
+            successful_call: || {
+                ProposalCodex::create_evict_storage_provider_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(25000u32)),
+                    1,
+                )
+            },
+            proposal_parameters: crate::proposal_types::parameters::evict_storage_provider_proposal::<Test>(),
+            proposal_details: ProposalDetails::EvictStorageProvider(1),
+        };
+        proposal_fixture.check_all();
+    });
+}
+
+#[test]
+fn create_set_validator_count_proposal_common_checks_succeed() {
+    initial_test_ext().execute_with(|| {
+        increase_total_balance_issuance_using_account_id(1, 500000);
+
+        let proposal_fixture = ProposalTestFixture {
+            insufficient_rights_call: || {
+                ProposalCodex::create_set_validator_count_proposal(
+                    RawOrigin::None.into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    4,
+                )
+            },
+            empty_stake_call: || {
+                ProposalCodex::create_set_validator_count_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    4,
+                )
+            },
+            invalid_stake_call: || {
+                ProposalCodex::create_set_validator_count_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(5000u32)),
+                    4,
+                )
+            },
+            successful_call: || {
+                ProposalCodex::create_set_validator_count_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(100_000_u32)),
+                    4,
+                )
+            },
+            proposal_parameters: crate::proposal_types::parameters::set_validator_count_proposal::<
+                Test,
+            >(),
+            proposal_details: ProposalDetails::SetValidatorCount(4),
+        };
+        proposal_fixture.check_all();
+    });
+}
+
+#[test]
+fn create_set_validator_count_proposal_failed_with_invalid_validator_count() {
+    initial_test_ext().execute_with(|| {
+        assert_eq!(
+            ProposalCodex::create_set_validator_count_proposal(
+                RawOrigin::Signed(1).into(),
+                1,
+                b"title".to_vec(),
+                b"body".to_vec(),
+                Some(<BalanceOf<Test>>::from(500u32)),
+                3,
+            ),
+            Err(Error::InvalidValidatorCount)
+        );
+
+        assert_eq!(
+            ProposalCodex::create_set_validator_count_proposal(
+                RawOrigin::Signed(1).into(),
+                1,
+                b"title".to_vec(),
+                b"body".to_vec(),
+                Some(<BalanceOf<Test>>::from(1001u32)),
+                3,
+            ),
+            Err(Error::InvalidValidatorCount)
+        );
+    });
+}
+
+#[test]
+fn create_set_storage_role_parameters_proposal_common_checks_succeed() {
+    initial_test_ext().execute_with(|| {
+        increase_total_balance_issuance_using_account_id(1, 500000);
+        let role_parameters = RoleParameters {
+            min_actors: 1,
+            ..RoleParameters::default()
+        };
+        let proposal_fixture = ProposalTestFixture {
+            insufficient_rights_call: || {
+                ProposalCodex::create_set_storage_role_parameters_proposal(
+                    RawOrigin::None.into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    role_parameters.clone(),
+                )
+            },
+            empty_stake_call: || {
+                ProposalCodex::create_set_storage_role_parameters_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    role_parameters.clone(),
+                )
+            },
+            invalid_stake_call: || {
+                ProposalCodex::create_set_storage_role_parameters_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(5000u32)),
+                    role_parameters.clone(),
+                )
+            },
+            successful_call: || {
+                ProposalCodex::create_set_storage_role_parameters_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(100_000_u32)),
+                    role_parameters.clone(),
+                )
+            },
+            proposal_parameters:
+                crate::proposal_types::parameters::set_storage_role_parameters_proposal::<Test>(),
+            proposal_details: ProposalDetails::SetStorageRoleParameters(role_parameters),
+        };
+        proposal_fixture.check_all();
+    });
+}
+
+fn assert_failed_set_storage_parameters_call(
+    role_parameters: RoleParameters<u64, u64>,
+    error: Error,
+) {
+    assert_eq!(
+        ProposalCodex::create_set_storage_role_parameters_proposal(
+            RawOrigin::Signed(1).into(),
+            1,
+            b"title".to_vec(),
+            b"body".to_vec(),
+            Some(<BalanceOf<Test>>::from(100_000_u32)),
+            role_parameters,
+        ),
+        Err(error)
+    );
+}
+
+#[test]
+fn create_set_storage_role_parameters_proposal_fails_with_invalid_parameters() {
+    initial_test_ext().execute_with(|| {
+        increase_total_balance_issuance_using_account_id(1, 500000);
+
+        let working_role_parameters = RoleParameters {
+            min_actors: 1,
+            ..RoleParameters::default()
+        };
+        let mut role_parameters = working_role_parameters.clone();
+        role_parameters.min_actors = 2;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterMinActors,
+        );
+
+        role_parameters = working_role_parameters.clone();
+        role_parameters.max_actors = 1;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterMaxActors,
+        );
+
+        role_parameters = working_role_parameters.clone();
+        role_parameters.max_actors = 100;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterMaxActors,
+        );
+
+        role_parameters = working_role_parameters.clone();
+        role_parameters.reward_period = 599;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterRewardPeriod,
+        );
+
+        role_parameters.reward_period = 28801;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterRewardPeriod,
+        );
+
+        role_parameters = working_role_parameters.clone();
+        role_parameters.bonding_period = 599;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterBondingPeriod,
+        );
+
+        role_parameters = working_role_parameters.clone();
+        role_parameters.bonding_period = 28801;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterBondingPeriod,
+        );
+
+        role_parameters = working_role_parameters.clone();
+        role_parameters.unbonding_period = 599;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterUnbondingPeriod,
+        );
+
+        role_parameters = working_role_parameters.clone();
+        role_parameters.unbonding_period = 28801;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterUnbondingPeriod,
+        );
+
+        role_parameters = working_role_parameters.clone();
+        role_parameters.min_service_period = 599;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterMinServicePeriod,
+        );
+
+        role_parameters = working_role_parameters.clone();
+        role_parameters.min_service_period = 28801;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterMinServicePeriod,
+        );
+
+        role_parameters = working_role_parameters.clone();
+        role_parameters.startup_grace_period = 599;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterStartupGracePeriod,
+        );
+
+        role_parameters = working_role_parameters.clone();
+        role_parameters.startup_grace_period = 28801;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterStartupGracePeriod,
+        );
+
+        role_parameters = working_role_parameters.clone();
+        role_parameters.min_stake = 0;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterMinStake,
+        );
+
+        role_parameters = working_role_parameters.clone();
+        role_parameters.min_stake = 10000001;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterMinStake,
+        );
+
+        role_parameters = working_role_parameters.clone();
+        role_parameters.entry_request_fee = 0;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterEntryRequestFee,
+        );
+
+        role_parameters = working_role_parameters.clone();
+        role_parameters.entry_request_fee = 100001;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterEntryRequestFee,
+        );
+
+        role_parameters = working_role_parameters.clone();
+        role_parameters.reward = 0;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterReward,
+        );
+
+        role_parameters = working_role_parameters;
+        role_parameters.reward = 1001;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterReward,
+        );
+    });
+}
+
+#[test]
+fn set_default_proposal_parameters_succeeded() {
+    initial_test_ext().execute_with(|| {
+        let p = ProposalsConfigParameters::default();
+
+        // nothing is set
+        assert_eq!(<SetValidatorCountProposalVotingPeriod<Test>>::get(), 0);
+
+        ProposalCodex::set_default_config_values();
+
+        assert_eq!(
+            <SetValidatorCountProposalVotingPeriod<Test>>::get(),
+            p.set_validator_count_proposal_voting_period as u64
+        );
+        assert_eq!(
+            <SetValidatorCountProposalGracePeriod<Test>>::get(),
+            p.set_validator_count_proposal_grace_period as u64
+        );
+        assert_eq!(
+            <RuntimeUpgradeProposalVotingPeriod<Test>>::get(),
+            p.runtime_upgrade_proposal_voting_period as u64
+        );
+        assert_eq!(
+            <RuntimeUpgradeProposalGracePeriod<Test>>::get(),
+            p.runtime_upgrade_proposal_grace_period as u64
+        );
+        assert_eq!(
+            <TextProposalVotingPeriod<Test>>::get(),
+            p.text_proposal_voting_period as u64
+        );
+        assert_eq!(
+            <TextProposalGracePeriod<Test>>::get(),
+            p.text_proposal_grace_period as u64
+        );
+        assert_eq!(
+            <SetElectionParametersProposalVotingPeriod<Test>>::get(),
+            p.set_election_parameters_proposal_voting_period as u64
+        );
+        assert_eq!(
+            <SetElectionParametersProposalGracePeriod<Test>>::get(),
+            p.set_election_parameters_proposal_grace_period as u64
+        );
+        assert_eq!(
+            <SetContentWorkingGroupMintCapacityProposalVotingPeriod<Test>>::get(),
+            p.set_content_working_group_mint_capacity_proposal_voting_period as u64
+        );
+        assert_eq!(
+            <SetContentWorkingGroupMintCapacityProposalGracePeriod<Test>>::get(),
+            p.set_content_working_group_mint_capacity_proposal_grace_period as u64
+        );
+        assert_eq!(
+            <SetLeadProposalVotingPeriod<Test>>::get(),
+            p.set_lead_proposal_voting_period as u64
+        );
+        assert_eq!(
+            <SetLeadProposalGracePeriod<Test>>::get(),
+            p.set_lead_proposal_grace_period as u64
+        );
+        assert_eq!(
+            <SpendingProposalVotingPeriod<Test>>::get(),
+            p.spending_proposal_voting_period as u64
+        );
+        assert_eq!(
+            <SpendingProposalGracePeriod<Test>>::get(),
+            p.spending_proposal_grace_period as u64
+        );
+        assert_eq!(
+            <EvictStorageProviderProposalVotingPeriod<Test>>::get(),
+            p.evict_storage_provider_proposal_voting_period as u64
+        );
+        assert_eq!(
+            <EvictStorageProviderProposalGracePeriod<Test>>::get(),
+            p.evict_storage_provider_proposal_grace_period as u64
+        );
+        assert_eq!(
+            <SetStorageRoleParametersProposalVotingPeriod<Test>>::get(),
+            p.set_storage_role_parameters_proposal_voting_period as u64
+        );
+        assert_eq!(
+            <SetStorageRoleParametersProposalGracePeriod<Test>>::get(),
+            p.set_storage_role_parameters_proposal_grace_period as u64
+        );
+    });
+}

+ 94 - 0
runtime-modules/proposals/discussion/Cargo.toml

@@ -0,0 +1,94 @@
+[package]
+name = 'substrate-proposals-discussion-module'
+version = '2.0.0'
+authors = ['Joystream contributors']
+edition = '2018'
+
+[features]
+default = ['std']
+no_std = []
+std = [
+    'codec/std',
+    'rstd/std',
+    'srml-support/std',
+    'primitives/std',
+    'sr-primitives/std',
+    'system/std',
+    'timestamp/std',
+    'serde',
+    'membership/std',
+    'common/std',
+]
+
+[dependencies.num_enum]
+default_features = false
+version = "0.4.2"
+
+[dependencies.serde]
+features = ['derive']
+optional = true
+version = '1.0.101'
+
+[dependencies.codec]
+default-features = false
+features = ['derive']
+package = 'parity-scale-codec'
+version = '1.0.0'
+
+[dependencies.primitives]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'substrate-primitives'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.rstd]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-std'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.sr-primitives]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-primitives'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.srml-support]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-support'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.system]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-system'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.timestamp]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-timestamp'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.membership]
+default_features = false
+package = 'substrate-membership-module'
+path = '../../membership'
+
+[dependencies.common]
+default_features = false
+package = 'substrate-common-module'
+path = '../../common'
+
+[dev-dependencies.runtime-io]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-io'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dev-dependencies.balances]
+package = 'srml-balances'
+default-features = false
+git = 'https://github.com/paritytech/substrate.git'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'

+ 364 - 0
runtime-modules/proposals/discussion/src/lib.rs

@@ -0,0 +1,364 @@
+//! # Proposals discussion module
+//! Proposals `discussion` module for the Joystream platform. Version 2.
+//! It contains discussion subsystem of the proposals.
+//!
+//! ## Overview
+//!
+//! The proposals discussion module is used by the codex module to provide a platform for discussions
+//! about different proposals. It allows to create discussion threads and then add and update related
+//! posts.
+//!
+//! ## Supported extrinsics
+//! - [add_post](./struct.Module.html#method.add_post) - adds a post to an existing discussion thread
+//! - [update_post](./struct.Module.html#method.update_post) - updates existing post
+//!
+//! ## Public API methods
+//! - [create_thread](./struct.Module.html#method.create_thread) - creates a discussion thread
+//! - [ensure_can_create_thread](./struct.Module.html#method.ensure_can_create_thread) - ensures safe thread creation
+//!
+//! ## Usage
+//!
+//! ```
+//! use srml_support::{decl_module, dispatch::Result};
+//! use system::ensure_root;
+//! use substrate_proposals_discussion_module::{self as discussions};
+//!
+//! pub trait Trait: discussions::Trait + membership::members::Trait {}
+//!
+//! decl_module! {
+//!     pub struct Module<T: Trait> for enum Call where origin: T::Origin {
+//!         pub fn create_discussion(origin, title: Vec<u8>, author_id : T::MemberId) -> Result {
+//!             ensure_root(origin)?;
+//!             <discussions::Module<T>>::ensure_can_create_thread(author_id, &title)?;
+//!             <discussions::Module<T>>::create_thread(author_id, title)?;
+//!             Ok(())
+//!         }
+//!     }
+//! }
+//! # fn main() {}
+//! ```
+
+//!
+
+// Ensure we're `no_std` when compiling for Wasm.
+#![cfg_attr(not(feature = "std"), no_std)]
+
+// Do not delete! Cannot be uncommented by default, because of Parity decl_module! issue.
+//#![warn(missing_docs)]
+
+#[cfg(test)]
+mod tests;
+mod types;
+
+use rstd::clone::Clone;
+use rstd::prelude::*;
+use rstd::vec::Vec;
+use srml_support::{decl_error, decl_event, decl_module, decl_storage, ensure, Parameter};
+
+use srml_support::traits::Get;
+use types::{Post, Thread, ThreadCounter};
+
+use common::origin_validator::ActorOriginValidator;
+use srml_support::dispatch::DispatchResult;
+
+type MemberId<T> = <T as membership::members::Trait>::MemberId;
+
+decl_event!(
+    /// Proposals engine events
+    pub enum Event<T>
+    where
+        <T as Trait>::ThreadId,
+        MemberId = MemberId<T>,
+        <T as Trait>::PostId,
+    {
+        /// Emits on thread creation.
+        ThreadCreated(ThreadId, MemberId),
+
+        /// Emits on post creation.
+        PostCreated(PostId, MemberId),
+
+        /// Emits on post update.
+        PostUpdated(PostId, MemberId),
+    }
+);
+
+/// 'Proposal discussion' substrate module Trait
+pub trait Trait: system::Trait + membership::members::Trait {
+    /// Engine event type.
+    type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
+
+    /// Validates post author id and origin combination
+    type PostAuthorOriginValidator: ActorOriginValidator<
+        Self::Origin,
+        MemberId<Self>,
+        Self::AccountId,
+    >;
+
+    /// Discussion thread Id type
+    type ThreadId: From<u64> + Into<u64> + Parameter + Default + Copy;
+
+    /// Post Id type
+    type PostId: From<u64> + Parameter + Default + Copy;
+
+    /// Defines post edition number limit.
+    type MaxPostEditionNumber: Get<u32>;
+
+    /// Defines thread title length limit.
+    type ThreadTitleLengthLimit: Get<u32>;
+
+    /// Defines post length limit.
+    type PostLengthLimit: Get<u32>;
+
+    /// Defines max thread by same author in a row number limit.
+    type MaxThreadInARowNumber: Get<u32>;
+}
+
+decl_error! {
+    /// Discussion module predefined errors
+    pub enum Error {
+        /// Author should match the post creator
+        NotAuthor,
+
+        ///  Post edition limit reached
+        PostEditionNumberExceeded,
+
+        /// Discussion cannot have an empty title
+        EmptyTitleProvided,
+
+        /// Title is too long
+        TitleIsTooLong,
+
+        /// Thread doesn't exist
+        ThreadDoesntExist,
+
+        /// Post doesn't exist
+        PostDoesntExist,
+
+        /// Post cannot be empty
+        EmptyPostProvided,
+
+        /// Post is too long
+        PostIsTooLong,
+
+        /// Max number of threads by same author in a row limit exceeded
+        MaxThreadInARowLimitExceeded,
+
+        /// Require root origin in extrinsics
+        RequireRootOrigin,
+    }
+}
+
+impl From<system::Error> for Error {
+    fn from(error: system::Error) -> Self {
+        match error {
+            system::Error::Other(msg) => Error::Other(msg),
+            system::Error::RequireRootOrigin => Error::RequireRootOrigin,
+            _ => Error::Other(error.into()),
+        }
+    }
+}
+
+// Storage for the proposals discussion module
+decl_storage! {
+    pub trait Store for Module<T: Trait> as ProposalDiscussion {
+        /// Map thread identifier to corresponding thread.
+        pub ThreadById get(thread_by_id): map T::ThreadId =>
+            Thread<MemberId<T>, T::BlockNumber>;
+
+        /// Count of all threads that have been created.
+        pub ThreadCount get(fn thread_count): u64;
+
+        /// Map thread id and post id to corresponding post.
+        pub PostThreadIdByPostId: double_map T::ThreadId, twox_128(T::PostId) =>
+             Post<MemberId<T>, T::BlockNumber, T::ThreadId>;
+
+        /// Count of all posts that have been created.
+        pub PostCount get(fn post_count): u64;
+
+        /// Last author thread counter (part of the antispam mechanism)
+        pub LastThreadAuthorCounter get(fn last_thread_author_counter):
+            Option<ThreadCounter<MemberId<T>>>;
+    }
+}
+
+decl_module! {
+    /// 'Proposal discussion' substrate module
+    pub struct Module<T: Trait> for enum Call where origin: T::Origin {
+        /// Predefined errors
+        type Error = Error;
+
+        /// Emits an event. Default substrate implementation.
+        fn deposit_event() = default;
+
+        /// Exports post edition number limit const.
+        const MaxPostEditionNumber: u32 = T::MaxPostEditionNumber::get();
+
+        /// Exports thread title length limit const.
+        const ThreadTitleLengthLimit: u32 = T::ThreadTitleLengthLimit::get();
+
+        /// Exports post length limit const.
+        const PostLengthLimit: u32 = T::PostLengthLimit::get();
+
+        /// Exports max thread by same author in a row number limit const.
+        const MaxThreadInARowNumber: u32 = T::MaxThreadInARowNumber::get();
+
+        /// Adds a post with author origin check.
+        pub fn add_post(
+            origin,
+            post_author_id: MemberId<T>,
+            thread_id : T::ThreadId,
+            text : Vec<u8>
+        ) {
+            T::PostAuthorOriginValidator::ensure_actor_origin(
+                origin,
+                post_author_id,
+            )?;
+            ensure!(<ThreadById<T>>::exists(thread_id), Error::ThreadDoesntExist);
+
+            ensure!(!text.is_empty(),Error::EmptyPostProvided);
+            ensure!(
+                text.len() as u32 <= T::PostLengthLimit::get(),
+                Error::PostIsTooLong
+            );
+
+            // mutation
+
+            let next_post_count_value = Self::post_count() + 1;
+            let new_post_id = next_post_count_value;
+
+            let new_post = Post {
+                text,
+                created_at: Self::current_block(),
+                updated_at: Self::current_block(),
+                author_id: post_author_id,
+                edition_number : 0,
+                thread_id,
+            };
+
+            let post_id = T::PostId::from(new_post_id);
+            <PostThreadIdByPostId<T>>::insert(thread_id, post_id, new_post);
+            PostCount::put(next_post_count_value);
+            Self::deposit_event(RawEvent::PostCreated(post_id, post_author_id));
+       }
+
+        /// Updates a post with author origin check. Update attempts number is limited.
+        pub fn update_post(
+            origin,
+            post_author_id: MemberId<T>,
+            thread_id: T::ThreadId,
+            post_id : T::PostId,
+            text : Vec<u8>
+        ){
+            T::PostAuthorOriginValidator::ensure_actor_origin(
+                origin,
+                post_author_id,
+            )?;
+
+            ensure!(<ThreadById<T>>::exists(thread_id), Error::ThreadDoesntExist);
+            ensure!(<PostThreadIdByPostId<T>>::exists(thread_id, post_id), Error::PostDoesntExist);
+
+            ensure!(!text.is_empty(), Error::EmptyPostProvided);
+            ensure!(
+                text.len() as u32 <= T::PostLengthLimit::get(),
+                Error::PostIsTooLong
+            );
+
+            let post = <PostThreadIdByPostId<T>>::get(&thread_id, &post_id);
+
+            ensure!(post.author_id == post_author_id, Error::NotAuthor);
+            ensure!(post.edition_number < T::MaxPostEditionNumber::get(),
+                Error::PostEditionNumberExceeded);
+
+            let new_post = Post {
+                text,
+                updated_at: Self::current_block(),
+                edition_number: post.edition_number + 1,
+                ..post
+            };
+
+            // mutation
+
+            <PostThreadIdByPostId<T>>::insert(thread_id, post_id, new_post);
+            Self::deposit_event(RawEvent::PostUpdated(post_id, post_author_id));
+       }
+    }
+}
+
+impl<T: Trait> Module<T> {
+    /// Create the discussion thread. Cannot add more threads than 'predefined limit = MaxThreadInARowNumber'
+    /// times in a row by the same author.
+    pub fn create_thread(
+        thread_author_id: MemberId<T>,
+        title: Vec<u8>,
+    ) -> Result<T::ThreadId, Error> {
+        Self::ensure_can_create_thread(thread_author_id, &title)?;
+
+        let next_thread_count_value = Self::thread_count() + 1;
+        let new_thread_id = next_thread_count_value;
+
+        let new_thread = Thread {
+            title,
+            created_at: Self::current_block(),
+            author_id: thread_author_id,
+        };
+
+        // get new 'threads in a row' counter for the author
+        let current_thread_counter = Self::get_updated_thread_counter(thread_author_id);
+
+        // mutation
+
+        let thread_id = T::ThreadId::from(new_thread_id);
+        <ThreadById<T>>::insert(thread_id, new_thread);
+        ThreadCount::put(next_thread_count_value);
+        <LastThreadAuthorCounter<T>>::put(current_thread_counter);
+        Self::deposit_event(RawEvent::ThreadCreated(thread_id, thread_author_id));
+
+        Ok(thread_id)
+    }
+
+    /// Ensures thread can be created.
+    /// Checks:
+    /// - title is valid
+    /// - max thread in a row by the same author
+    pub fn ensure_can_create_thread(
+        thread_author_id: MemberId<T>,
+        title: &[u8],
+    ) -> DispatchResult<Error> {
+        ensure!(!title.is_empty(), Error::EmptyTitleProvided);
+        ensure!(
+            title.len() as u32 <= T::ThreadTitleLengthLimit::get(),
+            Error::TitleIsTooLong
+        );
+
+        // get new 'threads in a row' counter for the author
+        let current_thread_counter = Self::get_updated_thread_counter(thread_author_id);
+
+        ensure!(
+            current_thread_counter.counter as u32 <= T::MaxThreadInARowNumber::get(),
+            Error::MaxThreadInARowLimitExceeded
+        );
+
+        Ok(())
+    }
+}
+
+impl<T: Trait> Module<T> {
+    // Wrapper-function over system::block_number()
+    fn current_block() -> T::BlockNumber {
+        <system::Module<T>>::block_number()
+    }
+
+    // returns incremented thread counter if last thread author equals with provided parameter
+    fn get_updated_thread_counter(author_id: MemberId<T>) -> ThreadCounter<MemberId<T>> {
+        // if thread counter exists
+        if let Some(last_thread_author_counter) = Self::last_thread_author_counter() {
+            // if last(previous) author is the same as current author
+            if last_thread_author_counter.author_id == author_id {
+                return last_thread_author_counter.increment();
+            }
+        }
+
+        // else return new counter (set with 1 thread number)
+        ThreadCounter::new(author_id)
+    }
+}

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

@@ -0,0 +1,145 @@
+#![cfg(test)]
+
+pub use system;
+
+pub use primitives::{Blake2Hasher, H256};
+pub use sr_primitives::{
+    testing::{Digest, DigestItem, Header, UintAuthorityId},
+    traits::{BlakeTwo256, Convert, IdentityLookup, OnFinalize},
+    weights::Weight,
+    BuildStorage, Perbill,
+};
+
+use crate::ActorOriginValidator;
+use srml_support::{impl_outer_event, impl_outer_origin, parameter_types};
+
+impl_outer_origin! {
+    pub enum Origin for Test {}
+}
+
+// Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted.
+#[derive(Clone, PartialEq, Eq, Debug)]
+pub struct Test;
+parameter_types! {
+    pub const BlockHashCount: u64 = 250;
+    pub const MaximumBlockWeight: u32 = 1024;
+    pub const MaximumBlockLength: u32 = 2 * 1024;
+    pub const AvailableBlockRatio: Perbill = Perbill::one();
+    pub const MinimumPeriod: u64 = 5;
+    pub const StakePoolId: [u8; 8] = *b"joystake";
+}
+
+parameter_types! {
+    pub const MaxPostEditionNumber: u32 = 5;
+    pub const MaxThreadInARowNumber: u32 = 3;
+    pub const ThreadTitleLengthLimit: u32 = 200;
+    pub const PostLengthLimit: u32 = 2000;
+}
+
+mod discussion {
+    pub use crate::Event;
+}
+
+mod membership_mod {
+    pub use membership::members::Event;
+}
+
+impl_outer_event! {
+    pub enum TestEvent for Test {
+        discussion<T>,
+        balances<T>,
+        membership_mod<T>,
+    }
+}
+
+parameter_types! {
+    pub const ExistentialDeposit: u32 = 0;
+    pub const TransferFee: u32 = 0;
+    pub const CreationFee: u32 = 0;
+}
+
+impl balances::Trait for Test {
+    type Balance = u64;
+    /// What to do if an account's free balance gets zeroed.
+    type OnFreeBalanceZero = ();
+    type OnNewAccount = ();
+    type TransferPayment = ();
+    type DustRemoval = ();
+    type Event = TestEvent;
+    type ExistentialDeposit = ExistentialDeposit;
+    type TransferFee = TransferFee;
+    type CreationFee = CreationFee;
+}
+
+impl common::currency::GovernanceCurrency for Test {
+    type Currency = balances::Module<Self>;
+}
+
+impl membership::members::Trait for Test {
+    type Event = TestEvent;
+    type MemberId = u64;
+    type PaidTermId = u64;
+    type SubscriptionId = u64;
+    type ActorId = u64;
+    type InitialMembersBalance = ();
+}
+
+impl crate::Trait for Test {
+    type Event = TestEvent;
+    type PostAuthorOriginValidator = ();
+    type ThreadId = u64;
+    type PostId = u64;
+    type MaxPostEditionNumber = MaxPostEditionNumber;
+    type ThreadTitleLengthLimit = ThreadTitleLengthLimit;
+    type PostLengthLimit = PostLengthLimit;
+    type MaxThreadInARowNumber = MaxThreadInARowNumber;
+}
+
+impl ActorOriginValidator<Origin, u64, u64> for () {
+    fn ensure_actor_origin(origin: Origin, actor_id: u64) -> Result<u64, &'static str> {
+        if system::ensure_none(origin).is_ok() {
+            return Ok(1);
+        }
+
+        if actor_id == 1 {
+            return Ok(1);
+        }
+
+        Err("Invalid author")
+    }
+}
+
+impl system::Trait for Test {
+    type Origin = Origin;
+    type Call = ();
+    type Index = u64;
+    type BlockNumber = u64;
+    type Hash = H256;
+    type Hashing = BlakeTwo256;
+    type AccountId = u64;
+    type Lookup = IdentityLookup<Self::AccountId>;
+    type Header = Header;
+    type Event = TestEvent;
+    type BlockHashCount = BlockHashCount;
+    type MaximumBlockWeight = MaximumBlockWeight;
+    type MaximumBlockLength = MaximumBlockLength;
+    type AvailableBlockRatio = AvailableBlockRatio;
+    type Version = ();
+}
+
+impl timestamp::Trait for Test {
+    type Moment = u64;
+    type OnTimestampSet = ();
+    type MinimumPeriod = MinimumPeriod;
+}
+
+pub fn initial_test_ext() -> runtime_io::TestExternalities {
+    let t = system::GenesisConfig::default()
+        .build_storage::<Test>()
+        .unwrap();
+
+    t.into()
+}
+
+pub type Discussions = crate::Module<Test>;
+pub type System = system::Module<Test>;

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

@@ -0,0 +1,417 @@
+mod mock;
+
+use mock::*;
+
+use crate::*;
+use system::RawOrigin;
+use system::{EventRecord, Phase};
+
+struct EventFixture;
+impl EventFixture {
+    fn assert_events(expected_raw_events: Vec<RawEvent<u64, u64, u64>>) {
+        let expected_events = expected_raw_events
+            .iter()
+            .map(|ev| EventRecord {
+                phase: Phase::ApplyExtrinsic(0),
+                event: TestEvent::discussion(ev.clone()),
+                topics: vec![],
+            })
+            .collect::<Vec<EventRecord<_, _>>>();
+
+        assert_eq!(System::events(), expected_events);
+    }
+}
+
+struct TestPostEntry {
+    pub post_id: u64,
+    pub text: Vec<u8>,
+    pub edition_number: u32,
+}
+
+struct TestThreadEntry {
+    pub thread_id: u64,
+    pub title: Vec<u8>,
+}
+
+fn assert_thread_content(thread_entry: TestThreadEntry, post_entries: Vec<TestPostEntry>) {
+    assert!(<ThreadById<Test>>::exists(thread_entry.thread_id));
+
+    let actual_thread = <ThreadById<Test>>::get(thread_entry.thread_id);
+    let expected_thread = Thread {
+        title: thread_entry.title,
+        created_at: 1,
+        author_id: 1,
+    };
+    assert_eq!(actual_thread, expected_thread);
+
+    for post_entry in post_entries {
+        let actual_post =
+            <PostThreadIdByPostId<Test>>::get(thread_entry.thread_id, post_entry.post_id);
+        let expected_post = Post {
+            text: post_entry.text,
+            created_at: 1,
+            updated_at: 1,
+            author_id: 1,
+            thread_id: thread_entry.thread_id,
+            edition_number: post_entry.edition_number,
+        };
+
+        assert_eq!(actual_post, expected_post);
+    }
+}
+
+struct DiscussionFixture {
+    pub title: Vec<u8>,
+    pub origin: RawOrigin<u64>,
+    pub author_id: u64,
+}
+
+impl Default for DiscussionFixture {
+    fn default() -> Self {
+        DiscussionFixture {
+            title: b"title".to_vec(),
+            origin: RawOrigin::Signed(1),
+            author_id: 1,
+        }
+    }
+}
+
+impl DiscussionFixture {
+    fn with_title(self, title: Vec<u8>) -> Self {
+        DiscussionFixture { title, ..self }
+    }
+
+    fn create_discussion_and_assert(&self, result: Result<u64, Error>) -> Option<u64> {
+        let create_discussion_result =
+            Discussions::create_thread(self.author_id, self.title.clone());
+
+        assert_eq!(create_discussion_result, result);
+
+        create_discussion_result.ok()
+    }
+}
+
+struct PostFixture {
+    pub text: Vec<u8>,
+    pub origin: RawOrigin<u64>,
+    pub thread_id: u64,
+    pub post_id: Option<u64>,
+    pub author_id: u64,
+}
+
+impl PostFixture {
+    fn default_for_thread(thread_id: u64) -> Self {
+        PostFixture {
+            text: b"text".to_vec(),
+            author_id: 1,
+            thread_id,
+            origin: RawOrigin::Signed(1),
+            post_id: None,
+        }
+    }
+
+    fn with_text(self, text: Vec<u8>) -> Self {
+        PostFixture { text, ..self }
+    }
+
+    fn with_origin(self, origin: RawOrigin<u64>) -> Self {
+        PostFixture { origin, ..self }
+    }
+
+    fn with_author(self, author_id: u64) -> Self {
+        PostFixture { author_id, ..self }
+    }
+
+    fn change_thread_id(self, thread_id: u64) -> Self {
+        PostFixture { thread_id, ..self }
+    }
+
+    fn change_post_id(self, post_id: u64) -> Self {
+        PostFixture {
+            post_id: Some(post_id),
+            ..self
+        }
+    }
+
+    fn add_post_and_assert(&mut self, result: Result<(), Error>) -> Option<u64> {
+        let add_post_result = Discussions::add_post(
+            self.origin.clone().into(),
+            self.author_id,
+            self.thread_id,
+            self.text.clone(),
+        );
+
+        assert_eq!(add_post_result, result);
+
+        if result.is_ok() {
+            self.post_id = Some(<PostCount>::get());
+        }
+
+        self.post_id
+    }
+
+    fn update_post_with_text_and_assert(&mut self, new_text: Vec<u8>, result: Result<(), Error>) {
+        let add_post_result = Discussions::update_post(
+            self.origin.clone().into(),
+            self.author_id,
+            self.thread_id,
+            self.post_id.unwrap(),
+            new_text,
+        );
+
+        assert_eq!(add_post_result, result);
+    }
+
+    fn update_post_and_assert(&mut self, result: Result<(), Error>) {
+        self.update_post_with_text_and_assert(self.text.clone(), result);
+    }
+}
+
+#[test]
+fn create_discussion_call_succeeds() {
+    initial_test_ext().execute_with(|| {
+        let discussion_fixture = DiscussionFixture::default();
+
+        discussion_fixture.create_discussion_and_assert(Ok(1));
+    });
+}
+
+#[test]
+fn create_post_call_succeeds() {
+    initial_test_ext().execute_with(|| {
+        let discussion_fixture = DiscussionFixture::default();
+
+        let thread_id = discussion_fixture
+            .create_discussion_and_assert(Ok(1))
+            .unwrap();
+
+        let mut post_fixture = PostFixture::default_for_thread(thread_id);
+
+        post_fixture.add_post_and_assert(Ok(()));
+    });
+}
+
+#[test]
+fn update_post_call_succeeds() {
+    initial_test_ext().execute_with(|| {
+        let discussion_fixture = DiscussionFixture::default();
+
+        let thread_id = discussion_fixture
+            .create_discussion_and_assert(Ok(1))
+            .unwrap();
+
+        let mut post_fixture = PostFixture::default_for_thread(thread_id);
+
+        post_fixture.add_post_and_assert(Ok(()));
+        post_fixture.update_post_and_assert(Ok(()));
+
+        EventFixture::assert_events(vec![
+            RawEvent::ThreadCreated(1, 1),
+            RawEvent::PostCreated(1, 1),
+            RawEvent::PostUpdated(1, 1),
+        ]);
+    });
+}
+
+#[test]
+fn update_post_call_fails_because_of_post_edition_limit() {
+    initial_test_ext().execute_with(|| {
+        let discussion_fixture = DiscussionFixture::default();
+
+        let thread_id = discussion_fixture
+            .create_discussion_and_assert(Ok(1))
+            .unwrap();
+
+        let mut post_fixture = PostFixture::default_for_thread(thread_id);
+
+        post_fixture.add_post_and_assert(Ok(()));
+
+        for _ in 1..6 {
+            post_fixture.update_post_and_assert(Ok(()));
+        }
+
+        post_fixture.update_post_and_assert(Err(Error::PostEditionNumberExceeded));
+    });
+}
+
+#[test]
+fn update_post_call_fails_because_of_the_wrong_author() {
+    initial_test_ext().execute_with(|| {
+        let discussion_fixture = DiscussionFixture::default();
+
+        let thread_id = discussion_fixture
+            .create_discussion_and_assert(Ok(1))
+            .unwrap();
+
+        let mut post_fixture = PostFixture::default_for_thread(thread_id);
+
+        post_fixture.add_post_and_assert(Ok(()));
+
+        post_fixture = post_fixture.with_author(2);
+
+        post_fixture.update_post_and_assert(Err(Error::Other("Invalid author")));
+
+        post_fixture = post_fixture.with_origin(RawOrigin::None).with_author(2);
+
+        post_fixture.update_post_and_assert(Err(Error::NotAuthor));
+    });
+}
+
+#[test]
+fn thread_content_check_succeeded() {
+    initial_test_ext().execute_with(|| {
+        let discussion_fixture = DiscussionFixture::default();
+
+        let thread_id = discussion_fixture
+            .create_discussion_and_assert(Ok(1))
+            .unwrap();
+
+        let mut post_fixture1 = PostFixture::default_for_thread(thread_id);
+        let post_id1 = post_fixture1.add_post_and_assert(Ok(())).unwrap();
+
+        let mut post_fixture2 = PostFixture::default_for_thread(thread_id);
+        let post_id2 = post_fixture2.add_post_and_assert(Ok(())).unwrap();
+        post_fixture1.update_post_with_text_and_assert(b"new_text".to_vec(), Ok(()));
+
+        assert_thread_content(
+            TestThreadEntry {
+                thread_id,
+                title: b"title".to_vec(),
+            },
+            vec![
+                TestPostEntry {
+                    post_id: post_id1,
+                    text: b"new_text".to_vec(),
+                    edition_number: 1,
+                },
+                TestPostEntry {
+                    post_id: post_id2,
+                    text: b"text".to_vec(),
+                    edition_number: 0,
+                },
+            ],
+        );
+    });
+}
+
+#[test]
+fn create_discussion_call_with_bad_title_failed() {
+    initial_test_ext().execute_with(|| {
+        let mut discussion_fixture = DiscussionFixture::default().with_title(Vec::new());
+        discussion_fixture.create_discussion_and_assert(Err(Error::EmptyTitleProvided));
+
+        discussion_fixture = DiscussionFixture::default().with_title([0; 201].to_vec());
+        discussion_fixture.create_discussion_and_assert(Err(Error::TitleIsTooLong));
+    });
+}
+
+#[test]
+fn add_post_call_with_invalid_thread_failed() {
+    initial_test_ext().execute_with(|| {
+        let discussion_fixture = DiscussionFixture::default();
+        discussion_fixture
+            .create_discussion_and_assert(Ok(1))
+            .unwrap();
+
+        let mut post_fixture = PostFixture::default_for_thread(2);
+        post_fixture.add_post_and_assert(Err(Error::ThreadDoesntExist));
+    });
+}
+
+#[test]
+fn update_post_call_with_invalid_post_failed() {
+    initial_test_ext().execute_with(|| {
+        let discussion_fixture = DiscussionFixture::default();
+        let thread_id = discussion_fixture
+            .create_discussion_and_assert(Ok(1))
+            .unwrap();
+
+        let mut post_fixture1 = PostFixture::default_for_thread(thread_id);
+        post_fixture1.add_post_and_assert(Ok(())).unwrap();
+
+        let mut post_fixture2 = post_fixture1.change_post_id(2);
+        post_fixture2.update_post_and_assert(Err(Error::PostDoesntExist));
+    });
+}
+
+#[test]
+fn update_post_call_with_invalid_thread_failed() {
+    initial_test_ext().execute_with(|| {
+        let discussion_fixture = DiscussionFixture::default();
+        let thread_id = discussion_fixture
+            .create_discussion_and_assert(Ok(1))
+            .unwrap();
+
+        let mut post_fixture1 = PostFixture::default_for_thread(thread_id);
+        post_fixture1.add_post_and_assert(Ok(())).unwrap();
+
+        let mut post_fixture2 = post_fixture1.change_thread_id(2);
+        post_fixture2.update_post_and_assert(Err(Error::ThreadDoesntExist));
+    });
+}
+
+#[test]
+fn add_post_call_with_invalid_text_failed() {
+    initial_test_ext().execute_with(|| {
+        let discussion_fixture = DiscussionFixture::default();
+        let thread_id = discussion_fixture
+            .create_discussion_and_assert(Ok(1))
+            .unwrap();
+
+        let mut post_fixture1 = PostFixture::default_for_thread(thread_id).with_text(Vec::new());
+        post_fixture1.add_post_and_assert(Err(Error::EmptyPostProvided));
+
+        let mut post_fixture2 =
+            PostFixture::default_for_thread(thread_id).with_text([0; 2001].to_vec());
+        post_fixture2.add_post_and_assert(Err(Error::PostIsTooLong));
+    });
+}
+
+#[test]
+fn update_post_call_with_invalid_text_failed() {
+    initial_test_ext().execute_with(|| {
+        let discussion_fixture = DiscussionFixture::default();
+        let thread_id = discussion_fixture
+            .create_discussion_and_assert(Ok(1))
+            .unwrap();
+
+        let mut post_fixture1 = PostFixture::default_for_thread(thread_id);
+        post_fixture1.add_post_and_assert(Ok(()));
+
+        let mut post_fixture2 = post_fixture1.with_text(Vec::new());
+        post_fixture2.update_post_and_assert(Err(Error::EmptyPostProvided));
+
+        let mut post_fixture3 = post_fixture2.with_text([0; 2001].to_vec());
+        post_fixture3.update_post_and_assert(Err(Error::PostIsTooLong));
+    });
+}
+
+#[test]
+fn add_discussion_thread_fails_because_of_max_thread_by_same_author_in_a_row_limit_exceeded() {
+    initial_test_ext().execute_with(|| {
+        let discussion_fixture = DiscussionFixture::default();
+        for idx in 1..=3 {
+            discussion_fixture
+                .create_discussion_and_assert(Ok(idx))
+                .unwrap();
+        }
+
+        discussion_fixture.create_discussion_and_assert(Err(Error::MaxThreadInARowLimitExceeded));
+    });
+}
+
+#[test]
+fn discussion_thread_and_post_counters_are_valid() {
+    initial_test_ext().execute_with(|| {
+        let discussion_fixture = DiscussionFixture::default();
+        let thread_id = discussion_fixture
+            .create_discussion_and_assert(Ok(1))
+            .unwrap();
+
+        let mut post_fixture1 = PostFixture::default_for_thread(thread_id);
+        let _ = post_fixture1.add_post_and_assert(Ok(())).unwrap();
+
+        assert_eq!(Discussions::thread_count(), 1);
+        assert_eq!(Discussions::post_count(), 1);
+    });
+}

+ 102 - 0
runtime-modules/proposals/discussion/src/types.rs

@@ -0,0 +1,102 @@
+#![warn(missing_docs)]
+
+use codec::{Decode, Encode};
+#[cfg(feature = "std")]
+use serde::{Deserialize, Serialize};
+
+use rstd::prelude::*;
+
+/// Represents a discussion thread
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
+pub struct Thread<ThreadAuthorId, BlockNumber> {
+    /// Title
+    pub title: Vec<u8>,
+
+    /// When thread was established.
+    pub created_at: BlockNumber,
+
+    /// Author of the thread.
+    pub author_id: ThreadAuthorId,
+}
+
+/// Post for the discussion thread
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
+pub struct Post<PostAuthorId, BlockNumber, ThreadId> {
+    /// Text
+    pub text: Vec<u8>,
+
+    /// When post was added.
+    pub created_at: BlockNumber,
+
+    /// When post was updated last time.
+    pub updated_at: BlockNumber,
+
+    /// Author of the post.
+    pub author_id: PostAuthorId,
+
+    /// Parent thread id for this post
+    pub thread_id: ThreadId,
+
+    /// Defines how many times this post was edited. Zero on creation.
+    pub edition_number: u32,
+}
+
+/// Post for the discussion thread
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
+#[derive(Encode, Decode, Default, Clone, Copy, PartialEq, Eq)]
+pub struct ThreadCounter<ThreadAuthorId> {
+    /// Author of the threads.
+    pub author_id: ThreadAuthorId,
+
+    /// ThreadCount
+    pub counter: u32,
+}
+
+impl<ThreadAuthorId: Clone> ThreadCounter<ThreadAuthorId> {
+    /// Increments existing counter
+    pub fn increment(&self) -> Self {
+        ThreadCounter {
+            counter: self.counter + 1,
+            author_id: self.author_id.clone(),
+        }
+    }
+
+    /// Creates new counter by author_id. Counter instantiated with 1.
+    pub fn new(author_id: ThreadAuthorId) -> Self {
+        ThreadCounter {
+            author_id,
+            counter: 1,
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::types::ThreadCounter;
+
+    #[test]
+    fn thread_counter_increment_works() {
+        let test = ThreadCounter {
+            author_id: 56,
+            counter: 56,
+        };
+        let expected = ThreadCounter {
+            author_id: 56,
+            counter: 57,
+        };
+
+        assert_eq!(expected, test.increment());
+    }
+
+    #[test]
+    fn thread_counter_new_works() {
+        let expected = ThreadCounter {
+            author_id: 56,
+            counter: 1,
+        };
+
+        assert_eq!(expected, ThreadCounter::new(56));
+    }
+}

+ 106 - 0
runtime-modules/proposals/engine/Cargo.toml

@@ -0,0 +1,106 @@
+[package]
+name = 'substrate-proposals-engine-module'
+version = '2.0.0'
+authors = ['Joystream contributors']
+edition = '2018'
+
+[features]
+default = ['std']
+no_std = []
+std = [
+    'codec/std',
+    'rstd/std',
+    'srml-support/std',
+    'primitives/std',
+    'system/std',
+    'timestamp/std',
+    'serde',
+    'stake/std',
+    'balances/std',
+    'sr-primitives/std',
+    'membership/std',
+    'common/std',
+
+]
+
+
+[dependencies.num_enum]
+default_features = false
+version = "0.4.2"
+
+[dependencies.serde]
+features = ['derive']
+optional = true
+version = '1.0.101'
+
+[dependencies.codec]
+default-features = false
+features = ['derive']
+package = 'parity-scale-codec'
+version = '1.0.0'
+
+[dependencies.primitives]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'substrate-primitives'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.rstd]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-std'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.srml-support]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-support'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.system]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-system'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.timestamp]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-timestamp'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.balances]
+package = 'srml-balances'
+default-features = false
+git = 'https://github.com/paritytech/substrate.git'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.sr-primitives]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-primitives'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.stake]
+default_features = false
+package = 'substrate-stake-module'
+path = '../../stake'
+
+[dependencies.membership]
+default_features = false
+package = 'substrate-membership-module'
+path = '../../membership'
+
+[dependencies.common]
+default_features = false
+package = 'substrate-common-module'
+path = '../../common'
+
+[dev-dependencies]
+mockall = "0.6.0"
+
+[dev-dependencies.runtime-io]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-io'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'

+ 827 - 0
runtime-modules/proposals/engine/src/lib.rs

@@ -0,0 +1,827 @@
+//! # Proposals engine module
+//! Proposals `engine` module for the Joystream platform. Version 2.
+//! The main component of the proposals system. Provides methods and extrinsics to create and
+//! vote for proposals, inspired by Parity **Democracy module**.
+//!
+//! ## Overview
+//! Proposals `engine` module provides an abstract mechanism to work with proposals: creation, voting,
+//! execution, canceling, etc. Proposal execution demands serialized _Dispatchable_ proposal code.
+//! It could be any _Dispatchable_ + _Parameter_ type, but most likely, it would be serialized (via
+//! Parity _codec_ crate) extrisic call. A proposal stage can be described by its [status](./enum.ProposalStatus.html).
+//!
+//! ## Proposal lifecycle
+//! When a proposal passes [checks](./struct.Module.html#method.ensure_create_proposal_parameters_are_valid)
+//! for its [parameters](./struct.ProposalParameters.html) - it can be [created](./struct.Module.html#method.create_proposal).
+//! The newly created proposal has _Active_ status. The proposal can be voted on or canceled during its
+//! _voting period_. Votes can be [different](./enum.VoteKind.html). When the proposal gets enough votes
+//! to be slashed or approved or _voting period_ ends - the proposal becomes _Finalized_. If the proposal
+//! got approved and _grace period_ passed - the  `engine` module tries to execute the proposal.
+//! The final [approved status](./enum.ApprovedProposalStatus.html) of the proposal defines
+//! an overall proposal outcome.
+//!
+//! ### Notes
+//!
+//! - The proposal can be [vetoed](./struct.Module.html#method.veto_proposal)
+//! anytime before the proposal execution by the _sudo_.
+//! - When the proposal is created with some stake - refunding on proposal finalization with
+//! different statuses should be accomplished from the external handler from the _stake module_
+//! (_StakingEventsHandler_). Such a handler should call
+//! [refund_proposal_stake](./struct.Module.html#method.refund_proposal_stake) callback function.
+//! - If the _council_ got reelected during the proposal _voting period_ the external handler calls
+//! [reset_active_proposals](./trait.Module.html#method.reset_active_proposals) function and
+//! all voting results get cleared.
+//!
+//! ### Important abstract types to be implemented
+//! Proposals `engine` module has several abstractions to be implemented in order to work correctly.
+//! - _VoterOriginValidator_ - ensure valid voter identity. Voters should have permissions to vote:
+//! they should be council members.
+//! - [VotersParameters](./trait.VotersParameters.html) - defines total voter number, which is
+//! the council size
+//! - _ProposerOriginValidator_ - ensure valid proposer identity. Proposers should have permissions
+//! to create a proposal: they should be members of the Joystream.
+//! - [StakeHandlerProvider](./trait.StakeHandlerProvider.html) - defines an interface for the staking.
+//!
+//! A full list of the abstractions can be found [here](./trait.Trait.html).
+//!
+//! ### Supported extrinsics
+//! - [vote](./struct.Module.html#method.vote) - registers a vote for the proposal
+//! - [cancel_proposal](./struct.Module.html#method.cancel_proposal) - cancels the proposal (can be canceled only by owner)
+//! - [veto_proposal](./struct.Module.html#method.veto_proposal) - vetoes the proposal
+//!
+//! ### Public API
+//! - [create_proposal](./struct.Module.html#method.create_proposal) - creates proposal using provided parameters
+//! - [ensure_create_proposal_parameters_are_valid](./struct.Module.html#method.ensure_create_proposal_parameters_are_valid) - ensures that we can create the proposal
+//! - [refund_proposal_stake](./struct.Module.html#method.refund_proposal_stake) - a callback for _StakingHandlerEvents_
+//! - [reset_active_proposals](./trait.Module.html#method.reset_active_proposals) - resets voting results for active proposals
+//!
+//! ## Usage
+//!
+//! ```
+//! use srml_support::{decl_module, dispatch::Result, print};
+//! use system::ensure_signed;
+//! use codec::Encode;
+//! use substrate_proposals_engine_module::{self as engine, ProposalParameters};
+//!
+//! pub trait Trait: engine::Trait + membership::members::Trait {}
+//!
+//! decl_module! {
+//!     pub struct Module<T: Trait> for enum Call where origin: T::Origin {
+//!         fn executable_proposal(origin) {
+//!             print("executed!");
+//!         }
+//!
+//!         pub fn create_spending_proposal(
+//!             origin,
+//!             proposer_id: T::MemberId,
+//!         ) -> Result {
+//!             let account_id = ensure_signed(origin)?;
+//!             let parameters = ProposalParameters::default();
+//!             let title = b"Spending proposal".to_vec();
+//!             let description = b"We need to spend some tokens to support the working group lead."
+//!                 .to_vec();
+//!             let encoded_proposal_code = <Call<T>>::executable_proposal().encode();
+//!
+//!             <engine::Module<T>>::ensure_create_proposal_parameters_are_valid(
+//!                 &parameters,
+//!                 &title,
+//!                 &description,
+//!                 None
+//!             )?;
+//!             <engine::Module<T>>::create_proposal(
+//!                 account_id,
+//!                 proposer_id,
+//!                 parameters,
+//!                 title,
+//!                 description,
+//!                 None,
+//!                 encoded_proposal_code
+//!             )?;
+//!             Ok(())
+//!         }
+//!     }
+//! }
+//! # fn main() {}
+//! ```
+
+// Ensure we're `no_std` when compiling for Wasm.
+#![cfg_attr(not(feature = "std"), no_std)]
+
+// Do not delete! Cannot be uncommented by default, because of Parity decl_module! issue.
+//#![warn(missing_docs)]
+
+use types::FinalizedProposalData;
+use types::ProposalStakeManager;
+pub use types::{
+    ActiveStake, ApprovedProposalStatus, FinalizationData, Proposal, ProposalDecisionStatus,
+    ProposalParameters, ProposalStatus, VotingResults,
+};
+pub use types::{BalanceOf, CurrencyOf, NegativeImbalance};
+pub use types::{DefaultStakeHandlerProvider, StakeHandler, StakeHandlerProvider};
+pub use types::{ProposalCodeDecoder, ProposalExecutable};
+pub use types::{VoteKind, VotersParameters};
+
+pub(crate) mod types;
+
+#[cfg(test)]
+mod tests;
+
+use codec::Decode;
+use rstd::prelude::*;
+use sr_primitives::traits::{DispatchResult, Zero};
+use srml_support::traits::{Currency, Get};
+use srml_support::{
+    decl_error, decl_event, decl_module, decl_storage, ensure, print, Parameter, StorageDoubleMap,
+};
+use system::{ensure_root, RawOrigin};
+
+use crate::types::ApprovedProposalData;
+use common::origin_validator::ActorOriginValidator;
+use srml_support::dispatch::Dispatchable;
+
+type MemberId<T> = <T as membership::members::Trait>::MemberId;
+
+/// Proposals engine trait.
+pub trait Trait:
+    system::Trait + timestamp::Trait + stake::Trait + membership::members::Trait
+{
+    /// Engine event type.
+    type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
+
+    /// Validates proposer id and origin combination
+    type ProposerOriginValidator: ActorOriginValidator<
+        Self::Origin,
+        MemberId<Self>,
+        Self::AccountId,
+    >;
+
+    /// Validates voter id and origin combination
+    type VoterOriginValidator: ActorOriginValidator<Self::Origin, MemberId<Self>, Self::AccountId>;
+
+    /// Provides data for voting. Defines maximum voters count for the proposal.
+    type TotalVotersCounter: VotersParameters;
+
+    /// Proposal Id type
+    type ProposalId: From<u32> + Parameter + Default + Copy;
+
+    /// Provides stake logic implementation. Can be used to mock stake logic.
+    type StakeHandlerProvider: StakeHandlerProvider<Self>;
+
+    /// The fee is applied when cancel the proposal. A fee would be slashed (burned).
+    type CancellationFee: Get<BalanceOf<Self>>;
+
+    /// The fee is applied when the proposal gets rejected. A fee would be slashed (burned).
+    type RejectionFee: Get<BalanceOf<Self>>;
+
+    /// Defines max allowed proposal title length.
+    type TitleMaxLength: Get<u32>;
+
+    /// Defines max allowed proposal description length.
+    type DescriptionMaxLength: Get<u32>;
+
+    /// Defines max simultaneous active proposals number.
+    type MaxActiveProposalLimit: Get<u32>;
+
+    /// Proposals executable code. Can be instantiated by external module Call enum members.
+    type DispatchableCallCode: Parameter + Dispatchable<Origin = Self::Origin> + Default;
+}
+
+decl_event!(
+    /// Proposals engine events
+    pub enum Event<T>
+    where
+        <T as Trait>::ProposalId,
+        MemberId = MemberId<T>,
+        <T as system::Trait>::BlockNumber,
+        <T as system::Trait>::AccountId,
+        <T as stake::Trait>::StakeId,
+    {
+        /// Emits on proposal creation.
+        /// Params:
+        /// - Member id of a proposer.
+        /// - Id of a newly created proposal after it was saved in storage.
+        ProposalCreated(MemberId, ProposalId),
+
+        /// Emits on proposal status change.
+        /// Params:
+        /// - Id of a updated proposal.
+        /// - New proposal status
+        ProposalStatusUpdated(ProposalId, ProposalStatus<BlockNumber, StakeId, AccountId>),
+
+        /// Emits on voting for the proposal
+        /// Params:
+        /// - Voter - member id of a voter.
+        /// - Id of a proposal.
+        /// - Kind of vote.
+        Voted(MemberId, ProposalId, VoteKind),
+    }
+);
+
+decl_error! {
+    /// Engine module predefined errors
+    pub enum Error {
+        /// Proposal cannot have an empty title"
+        EmptyTitleProvided,
+
+        /// Proposal cannot have an empty body
+        EmptyDescriptionProvided,
+
+        /// Title is too long
+        TitleIsTooLong,
+
+        /// Description is too long
+        DescriptionIsTooLong,
+
+        /// The proposal does not exist
+        ProposalNotFound,
+
+        /// Proposal is finalized already
+        ProposalFinalized,
+
+        /// The proposal have been already voted on
+        AlreadyVoted,
+
+        /// Not an author
+        NotAuthor,
+
+        /// Max active proposals number exceeded
+        MaxActiveProposalNumberExceeded,
+
+        /// Stake cannot be empty with this proposal
+        EmptyStake,
+
+        /// Stake should be empty for this proposal
+        StakeShouldBeEmpty,
+
+        /// Stake differs from the proposal requirements
+        StakeDiffersFromRequired,
+
+        /// Approval threshold cannot be zero
+        InvalidParameterApprovalThreshold,
+
+        /// Slashing threshold cannot be zero
+        InvalidParameterSlashingThreshold,
+
+        /// Require root origin in extrinsics
+        RequireRootOrigin,
+    }
+}
+
+impl From<system::Error> for Error {
+    fn from(error: system::Error) -> Self {
+        match error {
+            system::Error::Other(msg) => Error::Other(msg),
+            system::Error::RequireRootOrigin => Error::RequireRootOrigin,
+            _ => Error::Other(error.into()),
+        }
+    }
+}
+
+// Storage for the proposals engine module
+decl_storage! {
+    pub trait Store for Module<T: Trait> as ProposalEngine{
+        /// Map proposal by its id.
+        pub Proposals get(fn proposals): map T::ProposalId => ProposalOf<T>;
+
+        /// Count of all proposals that have been created.
+        pub ProposalCount get(fn proposal_count): u32;
+
+        /// Map proposal executable code by proposal id.
+        pub DispatchableCallCode get(fn proposal_codes): map T::ProposalId =>  Vec<u8>;
+
+        /// Count of active proposals.
+        pub ActiveProposalCount get(fn active_proposal_count): u32;
+
+        /// Ids of proposals that are open for voting (have not been finalized yet).
+        pub ActiveProposalIds get(fn active_proposal_ids): linked_map T::ProposalId=> ();
+
+        /// Ids of proposals that were approved and theirs grace period was not expired.
+        pub PendingExecutionProposalIds get(fn pending_proposal_ids): linked_map T::ProposalId=> ();
+
+        /// Double map for preventing duplicate votes. Should be cleaned after usage.
+        pub VoteExistsByProposalByVoter get(fn vote_by_proposal_by_voter):
+            double_map T::ProposalId, twox_256(MemberId<T>) => VoteKind;
+
+        /// Map proposal id by stake id. Required by StakingEventsHandler callback call
+        pub StakesProposals get(fn stakes_proposals): map T::StakeId =>  T::ProposalId;
+    }
+}
+
+decl_module! {
+    /// 'Proposal engine' substrate module
+    pub struct Module<T: Trait> for enum Call where origin: T::Origin {
+        /// Predefined errors
+        type Error = Error;
+
+        /// Emits an event. Default substrate implementation.
+        fn deposit_event() = default;
+
+        /// Exports const - the fee is applied when cancel the proposal. A fee would be slashed (burned).
+        const CancellationFee: BalanceOf<T> = T::CancellationFee::get();
+
+        /// Exports const -  the fee is applied when the proposal gets rejected. A fee would be slashed (burned).
+        const RejectionFee: BalanceOf<T> = T::RejectionFee::get();
+
+        /// Exports const -  max allowed proposal title length.
+        const TitleMaxLength: u32 = T::TitleMaxLength::get();
+
+        /// Exports const -  max allowed proposal description length.
+        const DescriptionMaxLength: u32 = T::DescriptionMaxLength::get();
+
+        /// Exports const -  max simultaneous active proposals number.
+        const MaxActiveProposalLimit: u32 = T::MaxActiveProposalLimit::get();
+
+        /// Vote extrinsic. Conditions:  origin must allow votes.
+        pub fn vote(origin, voter_id: MemberId<T>, proposal_id: T::ProposalId, vote: VoteKind)  {
+            T::VoterOriginValidator::ensure_actor_origin(
+                origin,
+                voter_id,
+            )?;
+
+            ensure!(<Proposals<T>>::exists(proposal_id), Error::ProposalNotFound);
+            let mut proposal = Self::proposals(proposal_id);
+
+            ensure!(matches!(proposal.status, ProposalStatus::Active{..}), Error::ProposalFinalized);
+
+            let did_not_vote_before = !<VoteExistsByProposalByVoter<T>>::exists(
+                proposal_id,
+                voter_id,
+            );
+
+            ensure!(did_not_vote_before, Error::AlreadyVoted);
+
+            proposal.voting_results.add_vote(vote.clone());
+
+            // mutation
+
+            <Proposals<T>>::insert(proposal_id, proposal);
+            <VoteExistsByProposalByVoter<T>>::insert( proposal_id, voter_id, vote.clone());
+            Self::deposit_event(RawEvent::Voted(voter_id, proposal_id, vote));
+        }
+
+        /// Cancel a proposal by its original proposer.
+        pub fn cancel_proposal(origin, proposer_id: MemberId<T>, proposal_id: T::ProposalId) {
+            T::ProposerOriginValidator::ensure_actor_origin(
+                origin,
+                proposer_id,
+            )?;
+
+            ensure!(<Proposals<T>>::exists(proposal_id), Error::ProposalNotFound);
+            let proposal = Self::proposals(proposal_id);
+
+            ensure!(proposer_id == proposal.proposer_id, Error::NotAuthor);
+            ensure!(matches!(proposal.status, ProposalStatus::Active{..}), Error::ProposalFinalized);
+
+            // mutation
+
+            Self::finalize_proposal(proposal_id, ProposalDecisionStatus::Canceled);
+        }
+
+        /// Veto a proposal. Must be root.
+        pub fn veto_proposal(origin, proposal_id: T::ProposalId) {
+            ensure_root(origin)?;
+
+            ensure!(<Proposals<T>>::exists(proposal_id), Error::ProposalNotFound);
+            let proposal = Self::proposals(proposal_id);
+
+            // mutation
+
+            if <PendingExecutionProposalIds<T>>::exists(proposal_id) {
+                Self::veto_pending_execution_proposal(proposal_id, proposal);
+            } else {
+                ensure!(matches!(proposal.status, ProposalStatus::Active{..}), Error::ProposalFinalized);
+                Self::finalize_proposal(proposal_id, ProposalDecisionStatus::Vetoed);
+            }
+        }
+
+        /// Block finalization. Perform voting period check, vote result tally, approved proposals
+        /// grace period checks, and proposal execution.
+        fn on_finalize(_n: T::BlockNumber) {
+            let finalized_proposals = Self::get_finalized_proposals();
+
+            // mutation
+
+            // Check vote results. Approved proposals with zero grace period will be
+            // transitioned to the PendingExecution status.
+            for  proposal_data in finalized_proposals {
+                <Proposals<T>>::insert(proposal_data.proposal_id, proposal_data.proposal);
+                Self::finalize_proposal(proposal_data.proposal_id, proposal_data.status);
+            }
+
+            let executable_proposals =
+                Self::get_approved_proposal_with_expired_grace_period();
+
+            // Execute approved proposals with expired grace period
+            for approved_proosal in executable_proposals {
+                Self::execute_proposal(approved_proosal);
+            }
+        }
+    }
+}
+
+impl<T: Trait> Module<T> {
+    /// Create proposal. Requires 'proposal origin' membership.
+    pub fn create_proposal(
+        account_id: T::AccountId,
+        proposer_id: MemberId<T>,
+        parameters: ProposalParameters<T::BlockNumber, types::BalanceOf<T>>,
+        title: Vec<u8>,
+        description: Vec<u8>,
+        stake_balance: Option<types::BalanceOf<T>>,
+        encoded_dispatchable_call_code: Vec<u8>,
+    ) -> Result<T::ProposalId, Error> {
+        Self::ensure_create_proposal_parameters_are_valid(
+            &parameters,
+            &title,
+            &description,
+            stake_balance,
+        )?;
+
+        // checks passed
+        // mutation
+
+        let next_proposal_count_value = Self::proposal_count() + 1;
+        let new_proposal_id = next_proposal_count_value;
+        let proposal_id = T::ProposalId::from(new_proposal_id);
+
+        // Check stake_balance for value and create stake if value exists, else take None
+        // If create_stake() returns error - return error from extrinsic
+        let stake_id_result = stake_balance
+            .map(|stake_amount| {
+                ProposalStakeManager::<T>::create_stake(stake_amount, account_id.clone())
+            })
+            .transpose()?;
+
+        let mut stake_data = None;
+        if let Some(stake_id) = stake_id_result {
+            stake_data = Some(ActiveStake {
+                stake_id,
+                source_account_id: account_id,
+            });
+
+            <StakesProposals<T>>::insert(stake_id, proposal_id);
+        }
+
+        let new_proposal = Proposal {
+            created_at: Self::current_block(),
+            parameters,
+            title,
+            description,
+            proposer_id,
+            status: ProposalStatus::Active(stake_data),
+            voting_results: VotingResults::default(),
+        };
+
+        <Proposals<T>>::insert(proposal_id, new_proposal);
+        <DispatchableCallCode<T>>::insert(proposal_id, encoded_dispatchable_call_code);
+        <ActiveProposalIds<T>>::insert(proposal_id, ());
+        ProposalCount::put(next_proposal_count_value);
+        Self::increase_active_proposal_counter();
+
+        Self::deposit_event(RawEvent::ProposalCreated(proposer_id, proposal_id));
+
+        Ok(proposal_id)
+    }
+
+    /// Performs all checks for the proposal creation:
+    /// - title, body lengths
+    /// - max active proposal
+    /// - provided parameters: approval_threshold_percentage and slashing_threshold_percentage > 0
+    /// - provided stake balance and parameters.required_stake are valid
+    pub fn ensure_create_proposal_parameters_are_valid(
+        parameters: &ProposalParameters<T::BlockNumber, types::BalanceOf<T>>,
+        title: &[u8],
+        description: &[u8],
+        stake_balance: Option<types::BalanceOf<T>>,
+    ) -> DispatchResult<Error> {
+        ensure!(!title.is_empty(), Error::EmptyTitleProvided);
+        ensure!(
+            title.len() as u32 <= T::TitleMaxLength::get(),
+            Error::TitleIsTooLong
+        );
+
+        ensure!(!description.is_empty(), Error::EmptyDescriptionProvided);
+        ensure!(
+            description.len() as u32 <= T::DescriptionMaxLength::get(),
+            Error::DescriptionIsTooLong
+        );
+
+        ensure!(
+            (Self::active_proposal_count()) < T::MaxActiveProposalLimit::get(),
+            Error::MaxActiveProposalNumberExceeded
+        );
+
+        ensure!(
+            parameters.approval_threshold_percentage > 0,
+            Error::InvalidParameterApprovalThreshold
+        );
+
+        ensure!(
+            parameters.slashing_threshold_percentage > 0,
+            Error::InvalidParameterSlashingThreshold
+        );
+
+        // check stake parameters
+        if let Some(required_stake) = parameters.required_stake {
+            if let Some(staked_balance) = stake_balance {
+                ensure!(
+                    required_stake == staked_balance,
+                    Error::StakeDiffersFromRequired
+                );
+            } else {
+                return Err(Error::EmptyStake);
+            }
+        }
+
+        if stake_balance.is_some() && parameters.required_stake.is_none() {
+            return Err(Error::StakeShouldBeEmpty);
+        }
+
+        Ok(())
+    }
+
+    /// Callback from StakingEventsHandler. Refunds unstaked imbalance back to the source account.
+    /// There can be a lot of invariant breaks in the scope of this proposal.
+    /// Such situations are handled by adding error messages to the log.
+    pub fn refund_proposal_stake(stake_id: T::StakeId, imbalance: NegativeImbalance<T>) {
+        if <StakesProposals<T>>::exists(stake_id) {
+            let proposal_id = Self::stakes_proposals(stake_id);
+
+            if <Proposals<T>>::exists(proposal_id) {
+                let proposal = Self::proposals(proposal_id);
+
+                if let ProposalStatus::Active(active_stake_result) = proposal.status {
+                    if let Some(active_stake) = active_stake_result {
+                        let refunding_result = CurrencyOf::<T>::resolve_into_existing(
+                            &active_stake.source_account_id,
+                            imbalance,
+                        );
+
+                        if refunding_result.is_err() {
+                            print("Broken invariant: cannot refund");
+                        }
+                    }
+                } else {
+                    print("Broken invariant: proposal status is not Active");
+                }
+            } else {
+                print("Broken invariant: proposal doesn't exist");
+            }
+        } else {
+            print("Broken invariant: stake doesn't exist");
+        }
+    }
+
+    /// Resets voting results for active proposals.
+    /// Possible application includes new council elections.
+    pub fn reset_active_proposals() {
+        <ActiveProposalIds<T>>::enumerate().for_each(|(proposal_id, _)| {
+            <Proposals<T>>::mutate(proposal_id, |proposal| {
+                proposal.reset_proposal();
+                <VoteExistsByProposalByVoter<T>>::remove_prefix(&proposal_id);
+            });
+        });
+    }
+}
+
+impl<T: Trait> Module<T> {
+    // Wrapper-function over system::block_number()
+    fn current_block() -> T::BlockNumber {
+        <system::Module<T>>::block_number()
+    }
+
+    // Enumerates through active proposals. Tally Voting results.
+    // Returns proposals with finalized status and id
+    fn get_finalized_proposals() -> Vec<FinalizedProposal<T>> {
+        // Enumerate active proposals id and gather finalization data.
+        // Skip proposals with unfinished voting.
+        <ActiveProposalIds<T>>::enumerate()
+            .filter_map(|(proposal_id, _)| {
+                // load current proposal
+                let proposal = Self::proposals(proposal_id);
+
+                // Calculates votes, takes in account voting period expiration.
+                // If voting process is in progress, then decision status is None.
+                let decision_status = proposal.define_proposal_decision_status(
+                    T::TotalVotersCounter::total_voters_count(),
+                    Self::current_block(),
+                );
+
+                // map to FinalizedProposalData if decision for the proposal is made or return None
+                decision_status.map(|status| FinalizedProposalData {
+                    proposal_id,
+                    proposal,
+                    status,
+                    finalized_at: Self::current_block(),
+                })
+            })
+            .collect() // compose output vector
+    }
+
+    // Veto approved proposal during its grace period. Saves a new proposal status and removes
+    // proposal id from the 'PendingExecutionProposalIds'
+    fn veto_pending_execution_proposal(proposal_id: T::ProposalId, proposal: ProposalOf<T>) {
+        <PendingExecutionProposalIds<T>>::remove(proposal_id);
+
+        let vetoed_proposal_status = ProposalStatus::finalized(
+            ProposalDecisionStatus::Vetoed,
+            None,
+            None,
+            Self::current_block(),
+        );
+
+        <Proposals<T>>::insert(
+            proposal_id,
+            Proposal {
+                status: vetoed_proposal_status,
+                ..proposal
+            },
+        );
+    }
+
+    // Executes approved proposal code
+    fn execute_proposal(approved_proposal: ApprovedProposal<T>) {
+        let proposal_code = Self::proposal_codes(approved_proposal.proposal_id);
+
+        let proposal_code_result = T::DispatchableCallCode::decode(&mut &proposal_code[..]);
+
+        let approved_proposal_status = match proposal_code_result {
+            Ok(proposal_code) => {
+                if let Err(error) = proposal_code.dispatch(T::Origin::from(RawOrigin::Root)) {
+                    ApprovedProposalStatus::failed_execution(
+                        error.into().message.unwrap_or("Dispatch error"),
+                    )
+                } else {
+                    ApprovedProposalStatus::Executed
+                }
+            }
+            Err(error) => ApprovedProposalStatus::failed_execution(error.what()),
+        };
+
+        let proposal_execution_status = approved_proposal
+            .finalisation_status_data
+            .create_approved_proposal_status(approved_proposal_status);
+
+        let mut proposal = approved_proposal.proposal;
+        proposal.status = proposal_execution_status.clone();
+        <Proposals<T>>::insert(approved_proposal.proposal_id, proposal);
+
+        Self::deposit_event(RawEvent::ProposalStatusUpdated(
+            approved_proposal.proposal_id,
+            proposal_execution_status,
+        ));
+
+        <PendingExecutionProposalIds<T>>::remove(&approved_proposal.proposal_id);
+    }
+
+    // Performs all actions on proposal finalization:
+    // - clean active proposal cache
+    // - update proposal status fields (status, finalized_at)
+    // - add to pending execution proposal cache if approved
+    // - slash and unstake proposal stake if stake exists
+    // - decrease active proposal counter
+    // - fire an event
+    // It prints an error message in case of an attempt to finalize the non-active proposal.
+    fn finalize_proposal(proposal_id: T::ProposalId, decision_status: ProposalDecisionStatus) {
+        Self::decrease_active_proposal_counter();
+        <ActiveProposalIds<T>>::remove(&proposal_id.clone());
+
+        let mut proposal = Self::proposals(proposal_id);
+
+        if let ProposalStatus::Active(active_stake) = proposal.status.clone() {
+            if let ProposalDecisionStatus::Approved { .. } = decision_status {
+                <PendingExecutionProposalIds<T>>::insert(proposal_id, ());
+            }
+
+            // deal with stakes if necessary
+            let slash_balance =
+                Self::calculate_slash_balance(&decision_status, &proposal.parameters);
+            let slash_and_unstake_result =
+                Self::slash_and_unstake(active_stake.clone(), slash_balance);
+
+            // create finalized proposal status with error if any
+            let new_proposal_status = ProposalStatus::finalized(
+                decision_status,
+                slash_and_unstake_result.err(),
+                active_stake,
+                Self::current_block(),
+            );
+
+            proposal.status = new_proposal_status.clone();
+            <Proposals<T>>::insert(proposal_id, proposal);
+
+            Self::deposit_event(RawEvent::ProposalStatusUpdated(
+                proposal_id,
+                new_proposal_status,
+            ));
+        } else {
+            print("Broken invariant: proposal cannot be non-active during the finalisation");
+        }
+    }
+
+    // Slashes the stake and perform unstake only in case of existing stake
+    fn slash_and_unstake(
+        current_stake_data: Option<ActiveStake<T::StakeId, T::AccountId>>,
+        slash_balance: BalanceOf<T>,
+    ) -> Result<(), &'static str> {
+        // only if stake exists
+        if let Some(stake_data) = current_stake_data {
+            if !slash_balance.is_zero() {
+                ProposalStakeManager::<T>::slash(stake_data.stake_id, slash_balance)?;
+            }
+
+            ProposalStakeManager::<T>::remove_stake(stake_data.stake_id)?;
+        }
+
+        Ok(())
+    }
+
+    // Calculates required slash based on finalization ProposalDecisionStatus and proposal parameters.
+    // Method visibility allows testing.
+    pub(crate) fn calculate_slash_balance(
+        decision_status: &ProposalDecisionStatus,
+        proposal_parameters: &ProposalParameters<T::BlockNumber, types::BalanceOf<T>>,
+    ) -> types::BalanceOf<T> {
+        match decision_status {
+            ProposalDecisionStatus::Rejected | ProposalDecisionStatus::Expired => {
+                T::RejectionFee::get()
+            }
+            ProposalDecisionStatus::Approved { .. } | ProposalDecisionStatus::Vetoed => {
+                BalanceOf::<T>::zero()
+            }
+            ProposalDecisionStatus::Canceled => T::CancellationFee::get(),
+            ProposalDecisionStatus::Slashed => proposal_parameters
+                .required_stake
+                .clone()
+                .unwrap_or_else(BalanceOf::<T>::zero), // stake if set or zero
+        }
+    }
+
+    // Enumerates approved proposals and checks their grace period expiration
+    fn get_approved_proposal_with_expired_grace_period() -> Vec<ApprovedProposal<T>> {
+        <PendingExecutionProposalIds<T>>::enumerate()
+            .filter_map(|(proposal_id, _)| {
+                let proposal = Self::proposals(proposal_id);
+
+                if proposal.is_grace_period_expired(Self::current_block()) {
+                    // this should be true, because it was tested inside is_grace_period_expired()
+                    if let ProposalStatus::Finalized(finalisation_data) = proposal.status.clone() {
+                        Some(ApprovedProposalData {
+                            proposal_id,
+                            proposal,
+                            finalisation_status_data: finalisation_data,
+                        })
+                    } else {
+                        None
+                    }
+                } else {
+                    None
+                }
+            })
+            .collect()
+    }
+
+    // Increases active proposal counter.
+    fn increase_active_proposal_counter() {
+        let next_active_proposal_count_value = Self::active_proposal_count() + 1;
+        ActiveProposalCount::put(next_active_proposal_count_value);
+    }
+
+    // Decreases active proposal counter down to zero. Decreasing below zero has no effect.
+    fn decrease_active_proposal_counter() {
+        let current_active_proposal_counter = Self::active_proposal_count();
+
+        if current_active_proposal_counter > 0 {
+            let next_active_proposal_count_value = current_active_proposal_counter - 1;
+            ActiveProposalCount::put(next_active_proposal_count_value);
+        };
+    }
+}
+
+// Simplification of the 'FinalizedProposalData' type
+type FinalizedProposal<T> = FinalizedProposalData<
+    <T as Trait>::ProposalId,
+    <T as system::Trait>::BlockNumber,
+    MemberId<T>,
+    types::BalanceOf<T>,
+    <T as stake::Trait>::StakeId,
+    <T as system::Trait>::AccountId,
+>;
+
+// Simplification of the 'ApprovedProposalData' type
+type ApprovedProposal<T> = ApprovedProposalData<
+    <T as Trait>::ProposalId,
+    <T as system::Trait>::BlockNumber,
+    MemberId<T>,
+    types::BalanceOf<T>,
+    <T as stake::Trait>::StakeId,
+    <T as system::Trait>::AccountId,
+>;
+
+// Simplification of the 'Proposal' type
+type ProposalOf<T> = Proposal<
+    <T as system::Trait>::BlockNumber,
+    MemberId<T>,
+    types::BalanceOf<T>,
+    <T as stake::Trait>::StakeId,
+    <T as system::Trait>::AccountId,
+>;

+ 33 - 0
runtime-modules/proposals/engine/src/tests/mock/balance_manager.rs

@@ -0,0 +1,33 @@
+#![cfg(test)]
+
+pub use sr_primitives::traits::Zero;
+use srml_support::traits::{Currency, Imbalance};
+
+use super::*;
+
+/// StakingEventsHandler implementation for the stake::Trait. Restores balances after the unstaking
+/// and slashes balances if necessary.
+pub struct BalanceManagerStakingEventsHandler;
+impl stake::StakingEventsHandler<Test> for BalanceManagerStakingEventsHandler {
+    fn unstaked(
+        _id: &u64,
+        _unstaked_amount: stake::BalanceOf<Test>,
+        imbalance: stake::NegativeImbalance<Test>,
+    ) -> stake::NegativeImbalance<Test> {
+        let default_account_id = 1;
+
+        <Test as stake::Trait>::Currency::resolve_creating(&default_account_id, imbalance);
+
+        stake::NegativeImbalance::<Test>::zero()
+    }
+
+    fn slashed(
+        _id: &u64,
+        _slash_id: Option<<Test as stake::Trait>::SlashId>,
+        _slashed_amount: stake::BalanceOf<Test>,
+        _remaining_stake: stake::BalanceOf<Test>,
+        imbalance: stake::NegativeImbalance<Test>,
+    ) -> stake::NegativeImbalance<Test> {
+        imbalance
+    }
+}

+ 186 - 0
runtime-modules/proposals/engine/src/tests/mock/mod.rs

@@ -0,0 +1,186 @@
+//! Mock runtime for the module testing.
+//!
+//! Submodules:
+//! - stakes: contains support for mocking external 'stake' module
+//! - balance_restorator: restores balances after unstaking
+//! - proposals: provides types for proposal execution tests
+//!
+
+#![cfg(test)]
+pub use primitives::{Blake2Hasher, H256};
+pub use sr_primitives::{
+    testing::{Digest, DigestItem, Header, UintAuthorityId},
+    traits::{BlakeTwo256, Convert, IdentityLookup, OnFinalize, Zero},
+    weights::Weight,
+    BuildStorage, DispatchError, Perbill,
+};
+use srml_support::{impl_outer_event, impl_outer_origin, parameter_types};
+pub use system;
+
+mod balance_manager;
+pub(crate) mod proposals;
+mod stakes;
+
+use balance_manager::*;
+pub use proposals::*;
+pub use stakes::*;
+
+// Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted.
+#[derive(Clone, PartialEq, Eq, Debug)]
+pub struct Test;
+
+impl_outer_origin! {
+    pub enum Origin for Test {}
+}
+
+mod engine {
+    pub use crate::Event;
+}
+
+mod membership_mod {
+    pub use membership::members::Event;
+}
+
+impl_outer_event! {
+    pub enum TestEvent for Test {
+        balances<T>,
+        engine<T>,
+        membership_mod<T>,
+    }
+}
+
+parameter_types! {
+    pub const ExistentialDeposit: u32 = 0;
+    pub const TransferFee: u32 = 0;
+    pub const CreationFee: u32 = 0;
+}
+
+impl balances::Trait for Test {
+    /// The type for recording an account's balance.
+    type Balance = u64;
+    /// What to do if an account's free balance gets zeroed.
+    type OnFreeBalanceZero = ();
+    /// What to do if a new account is created.
+    type OnNewAccount = ();
+
+    type TransferPayment = ();
+
+    type DustRemoval = ();
+    type Event = TestEvent;
+    type ExistentialDeposit = ExistentialDeposit;
+    type TransferFee = TransferFee;
+    type CreationFee = CreationFee;
+}
+
+impl common::currency::GovernanceCurrency for Test {
+    type Currency = balances::Module<Self>;
+}
+
+impl proposals::Trait for Test {}
+
+impl stake::Trait for Test {
+    type Currency = Balances;
+    type StakePoolId = StakePoolId;
+    type StakingEventsHandler = BalanceManagerStakingEventsHandler;
+    type StakeId = u64;
+    type SlashId = u64;
+}
+
+parameter_types! {
+    pub const CancellationFee: u64 = 5;
+    pub const RejectionFee: u64 = 3;
+    pub const TitleMaxLength: u32 = 100;
+    pub const DescriptionMaxLength: u32 = 10000;
+    pub const MaxActiveProposalLimit: u32 = 100;
+}
+
+impl membership::members::Trait for Test {
+    type Event = TestEvent;
+    type MemberId = u64;
+    type PaidTermId = u64;
+    type SubscriptionId = u64;
+    type ActorId = u64;
+    type InitialMembersBalance = ();
+}
+
+impl crate::Trait for Test {
+    type Event = TestEvent;
+    type ProposerOriginValidator = ();
+    type VoterOriginValidator = ();
+    type TotalVotersCounter = ();
+    type ProposalId = u32;
+    type StakeHandlerProvider = stakes::TestStakeHandlerProvider;
+    type CancellationFee = CancellationFee;
+    type RejectionFee = RejectionFee;
+    type TitleMaxLength = TitleMaxLength;
+    type DescriptionMaxLength = DescriptionMaxLength;
+    type MaxActiveProposalLimit = MaxActiveProposalLimit;
+    type DispatchableCallCode = proposals::Call<Test>;
+}
+
+impl Default for proposals::Call<Test> {
+    fn default() -> Self {
+        panic!("shouldn't call default for Call");
+    }
+}
+
+impl common::origin_validator::ActorOriginValidator<Origin, u64, u64> for () {
+    fn ensure_actor_origin(origin: Origin, _account_id: u64) -> Result<u64, &'static str> {
+        let signed_account_id = system::ensure_signed(origin)?;
+
+        Ok(signed_account_id)
+    }
+}
+
+// If changing count is required, we can upgrade the implementation as shown here:
+// https://substrate.dev/recipes/3-entrees/testing/externalities.html
+impl crate::VotersParameters for () {
+    fn total_voters_count() -> u32 {
+        4
+    }
+}
+
+parameter_types! {
+    pub const BlockHashCount: u64 = 250;
+    pub const MaximumBlockWeight: u32 = 1024;
+    pub const MaximumBlockLength: u32 = 2 * 1024;
+    pub const AvailableBlockRatio: Perbill = Perbill::one();
+    pub const MinimumPeriod: u64 = 5;
+    pub const StakePoolId: [u8; 8] = *b"joystake";
+}
+
+impl system::Trait for Test {
+    type Origin = Origin;
+    type Call = ();
+    type Index = u64;
+    type BlockNumber = u64;
+    type Hash = H256;
+    type Hashing = BlakeTwo256;
+    type AccountId = u64;
+    type Lookup = IdentityLookup<Self::AccountId>;
+    type Header = Header;
+    type Event = TestEvent;
+    type BlockHashCount = BlockHashCount;
+    type MaximumBlockWeight = MaximumBlockWeight;
+    type MaximumBlockLength = MaximumBlockLength;
+    type AvailableBlockRatio = AvailableBlockRatio;
+    type Version = ();
+}
+
+impl timestamp::Trait for Test {
+    type Moment = u64;
+    type OnTimestampSet = ();
+    type MinimumPeriod = MinimumPeriod;
+}
+
+pub fn initial_test_ext() -> runtime_io::TestExternalities {
+    let t = system::GenesisConfig::default()
+        .build_storage::<Test>()
+        .unwrap();
+
+    t.into()
+}
+
+pub type ProposalsEngine = crate::Module<Test>;
+pub type System = system::Module<Test>;
+pub type Balances = balances::Module<Test>;

+ 18 - 0
runtime-modules/proposals/engine/src/tests/mock/proposals.rs

@@ -0,0 +1,18 @@
+//! Contains executable proposal extrinsic mocks
+
+use rstd::prelude::*;
+use rstd::vec::Vec;
+use srml_support::decl_module;
+pub trait Trait: system::Trait {}
+
+decl_module! {
+    pub struct Module<T: Trait> for enum Call where origin: T::Origin {
+        /// Working extrinsic test
+        pub fn dummy_proposal(_origin, _title: Vec<u8>, _description: Vec<u8>) {}
+
+        /// Broken extrinsic test
+        pub fn faulty_proposal(_origin, _title: Vec<u8>, _description: Vec<u8>,) {
+             Err("ExecutionFailed")?
+        }
+    }
+}

+ 66 - 0
runtime-modules/proposals/engine/src/tests/mock/stakes.rs

@@ -0,0 +1,66 @@
+#![cfg(test)]
+
+use rstd::marker::PhantomData;
+use std::cell::RefCell;
+use std::panic;
+use std::rc::Rc;
+
+use super::Test;
+
+// Intercepts panic method
+// Returns: whether panic occurred
+pub(crate) fn panics<F: std::panic::RefUnwindSafe + Fn()>(could_panic_func: F) -> bool {
+    {
+        let default_hook = panic::take_hook();
+        panic::set_hook(Box::new(|info| {
+            println!("{}", info);
+        }));
+
+        // intercept panic
+        let result = panic::catch_unwind(|| could_panic_func());
+
+        //restore default behaviour
+        panic::set_hook(default_hook);
+
+        result.is_err()
+    }
+}
+
+// Test StakeHandlerProvider implementation based on local thread static variables
+pub struct TestStakeHandlerProvider;
+impl crate::StakeHandlerProvider<Test> for TestStakeHandlerProvider {
+    /// Returns StakeHandler. Mock entry point for stake module.
+    fn stakes() -> Rc<dyn crate::StakeHandler<Test>> {
+        THREAD_LOCAL_STAKE_HANDLER.with(|f| f.borrow().clone())
+    }
+}
+
+// 1. RefCell - thread_local! mutation pattern
+// 2. Rc - ability to have multiple references
+thread_local! {
+    pub static THREAD_LOCAL_STAKE_HANDLER:
+      RefCell<Rc<dyn crate::StakeHandler<Test>>> = RefCell::new(Rc::new(crate::types::DefaultStakeHandler{marker: PhantomData::<Test>}));
+}
+
+// Sets stake handler implementation. Mockall framework integration.
+pub(crate) fn set_stake_handler_impl(mock: Rc<dyn crate::StakeHandler<Test>>) {
+    THREAD_LOCAL_STAKE_HANDLER.with(|f| {
+        *f.borrow_mut() = mock.clone();
+    });
+}
+
+// Tests mock expectation and restores default behaviour
+pub(crate) fn test_expectation_and_clear_mock() {
+    set_stake_handler_impl(Rc::new(crate::types::DefaultStakeHandler {
+        marker: PhantomData::<Test>,
+    }));
+}
+
+// Intercepts panic in provided function, test mock expectation and restores default behaviour
+pub(crate) fn handle_mock<F: std::panic::RefUnwindSafe + Fn()>(func: F) {
+    let panicked = panics(func);
+
+    test_expectation_and_clear_mock();
+
+    assert!(!panicked);
+}

+ 1581 - 0
runtime-modules/proposals/engine/src/tests/mod.rs

@@ -0,0 +1,1581 @@
+pub(crate) mod mock;
+
+use crate::*;
+use mock::*;
+
+use codec::Encode;
+use rstd::rc::Rc;
+use sr_primitives::traits::{DispatchResult, OnFinalize, OnInitialize};
+use srml_support::{StorageDoubleMap, StorageMap, StorageValue};
+use system::RawOrigin;
+use system::{EventRecord, Phase};
+
+use srml_support::traits::Currency;
+
+pub(crate) fn increase_total_balance_issuance_using_account_id(account_id: u64, balance: u64) {
+    let initial_balance = Balances::total_issuance();
+    {
+        let _ = <Test as stake::Trait>::Currency::deposit_creating(&account_id, balance);
+    }
+    assert_eq!(Balances::total_issuance(), initial_balance + balance);
+}
+
+struct ProposalParametersFixture {
+    parameters: ProposalParameters<u64, u64>,
+}
+
+impl ProposalParametersFixture {
+    fn with_required_stake(&self, required_stake: BalanceOf<Test>) -> Self {
+        ProposalParametersFixture {
+            parameters: ProposalParameters {
+                required_stake: Some(required_stake),
+                ..self.parameters
+            },
+        }
+    }
+    fn with_grace_period(&self, grace_period: u64) -> Self {
+        ProposalParametersFixture {
+            parameters: ProposalParameters {
+                grace_period,
+                ..self.parameters
+            },
+        }
+    }
+
+    fn params(&self) -> ProposalParameters<u64, u64> {
+        self.parameters.clone()
+    }
+}
+
+impl Default for ProposalParametersFixture {
+    fn default() -> Self {
+        ProposalParametersFixture {
+            parameters: ProposalParameters {
+                voting_period: 3,
+                approval_quorum_percentage: 60,
+                approval_threshold_percentage: 60,
+                slashing_quorum_percentage: 60,
+                slashing_threshold_percentage: 60,
+                grace_period: 0,
+                required_stake: None,
+            },
+        }
+    }
+}
+
+#[derive(Clone)]
+struct DummyProposalFixture {
+    parameters: ProposalParameters<u64, u64>,
+    account_id: u64,
+    proposer_id: u64,
+    proposal_code: Vec<u8>,
+    title: Vec<u8>,
+    description: Vec<u8>,
+    stake_balance: Option<BalanceOf<Test>>,
+}
+
+impl Default for DummyProposalFixture {
+    fn default() -> Self {
+        let title = b"title".to_vec();
+        let description = b"description".to_vec();
+        let dummy_proposal =
+            mock::proposals::Call::<Test>::dummy_proposal(title.clone(), description.clone());
+
+        DummyProposalFixture {
+            parameters: ProposalParameters {
+                voting_period: 3,
+                approval_quorum_percentage: 60,
+                approval_threshold_percentage: 60,
+                slashing_quorum_percentage: 60,
+                slashing_threshold_percentage: 60,
+                grace_period: 0,
+                required_stake: None,
+            },
+            account_id: 1,
+            proposer_id: 1,
+            proposal_code: dummy_proposal.encode(),
+            title,
+            description,
+            stake_balance: None,
+        }
+    }
+}
+
+impl DummyProposalFixture {
+    fn with_title_and_body(self, title: Vec<u8>, description: Vec<u8>) -> Self {
+        DummyProposalFixture {
+            title,
+            description,
+            ..self
+        }
+    }
+
+    fn with_parameters(self, parameters: ProposalParameters<u64, u64>) -> Self {
+        DummyProposalFixture { parameters, ..self }
+    }
+
+    fn with_account_id(self, account_id: u64) -> Self {
+        DummyProposalFixture { account_id, ..self }
+    }
+
+    fn with_stake(self, stake_balance: BalanceOf<Test>) -> Self {
+        DummyProposalFixture {
+            stake_balance: Some(stake_balance),
+            ..self
+        }
+    }
+
+    fn with_proposal_code(self, proposal_code: Vec<u8>) -> Self {
+        DummyProposalFixture {
+            proposal_code,
+            ..self
+        }
+    }
+
+    fn create_proposal_and_assert(self, result: Result<u32, Error>) -> Option<u32> {
+        let proposal_id_result = ProposalsEngine::create_proposal(
+            self.account_id,
+            self.proposer_id,
+            self.parameters,
+            self.title,
+            self.description,
+            self.stake_balance,
+            self.proposal_code,
+        );
+        assert_eq!(proposal_id_result, result);
+
+        proposal_id_result.ok()
+    }
+}
+
+struct CancelProposalFixture {
+    origin: RawOrigin<u64>,
+    proposal_id: u32,
+    proposer_id: u64,
+}
+
+impl CancelProposalFixture {
+    fn new(proposal_id: u32) -> Self {
+        CancelProposalFixture {
+            proposal_id,
+            origin: RawOrigin::Signed(1),
+            proposer_id: 1,
+        }
+    }
+
+    fn with_origin(self, origin: RawOrigin<u64>) -> Self {
+        CancelProposalFixture { origin, ..self }
+    }
+
+    fn with_proposer(self, proposer_id: u64) -> Self {
+        CancelProposalFixture {
+            proposer_id,
+            ..self
+        }
+    }
+
+    fn cancel_and_assert(self, expected_result: DispatchResult<Error>) {
+        assert_eq!(
+            ProposalsEngine::cancel_proposal(
+                self.origin.into(),
+                self.proposer_id,
+                self.proposal_id
+            ),
+            expected_result
+        );
+    }
+}
+struct VetoProposalFixture {
+    origin: RawOrigin<u64>,
+    proposal_id: u32,
+}
+
+impl VetoProposalFixture {
+    fn new(proposal_id: u32) -> Self {
+        VetoProposalFixture {
+            proposal_id,
+            origin: RawOrigin::Root,
+        }
+    }
+
+    fn with_origin(self, origin: RawOrigin<u64>) -> Self {
+        VetoProposalFixture { origin, ..self }
+    }
+
+    fn veto_and_assert(self, expected_result: DispatchResult<Error>) {
+        assert_eq!(
+            ProposalsEngine::veto_proposal(self.origin.into(), self.proposal_id,),
+            expected_result
+        );
+    }
+}
+
+struct VoteGenerator {
+    proposal_id: u32,
+    current_account_id: u64,
+    current_voter_id: u64,
+    pub auto_increment_voter_id: bool,
+}
+
+impl VoteGenerator {
+    fn new(proposal_id: u32) -> Self {
+        VoteGenerator {
+            proposal_id,
+            current_voter_id: 0,
+            current_account_id: 0,
+            auto_increment_voter_id: true,
+        }
+    }
+    fn vote_and_assert_ok(&mut self, vote_kind: VoteKind) {
+        self.vote_and_assert(vote_kind, Ok(()));
+    }
+
+    fn vote_and_assert(&mut self, vote_kind: VoteKind, expected_result: DispatchResult<Error>) {
+        assert_eq!(self.vote(vote_kind.clone()), expected_result);
+    }
+
+    fn vote(&mut self, vote_kind: VoteKind) -> DispatchResult<Error> {
+        if self.auto_increment_voter_id {
+            self.current_account_id += 1;
+            self.current_voter_id += 1;
+        }
+
+        ProposalsEngine::vote(
+            system::RawOrigin::Signed(self.current_account_id).into(),
+            self.current_voter_id,
+            self.proposal_id,
+            vote_kind,
+        )
+    }
+}
+
+struct EventFixture;
+impl EventFixture {
+    fn assert_events(expected_raw_events: Vec<RawEvent<u32, u64, u64, u64, u64>>) {
+        let expected_events = expected_raw_events
+            .iter()
+            .map(|ev| EventRecord {
+                phase: Phase::ApplyExtrinsic(0),
+                event: TestEvent::engine(ev.clone()),
+                topics: vec![],
+            })
+            .collect::<Vec<EventRecord<_, _>>>();
+
+        assert_eq!(System::events(), expected_events);
+    }
+}
+
+// Recommendation from Parity on testing on_finalize
+// https://substrate.dev/docs/en/next/development/module/tests
+fn run_to_block(n: u64) {
+    while System::block_number() < n {
+        <System as OnFinalize<u64>>::on_finalize(System::block_number());
+        <ProposalsEngine as OnFinalize<u64>>::on_finalize(System::block_number());
+        System::set_block_number(System::block_number() + 1);
+        <System as OnInitialize<u64>>::on_initialize(System::block_number());
+        <ProposalsEngine as OnInitialize<u64>>::on_initialize(System::block_number());
+    }
+}
+
+fn run_to_block_and_finalize(n: u64) {
+    run_to_block(n);
+    <ProposalsEngine as OnFinalize<u64>>::on_finalize(n);
+}
+
+#[test]
+fn create_dummy_proposal_succeeds() {
+    initial_test_ext().execute_with(|| {
+        let dummy_proposal = DummyProposalFixture::default();
+
+        dummy_proposal.create_proposal_and_assert(Ok(1));
+    });
+}
+
+#[test]
+fn vote_succeeds() {
+    initial_test_ext().execute_with(|| {
+        let dummy_proposal = DummyProposalFixture::default();
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        let mut vote_generator = VoteGenerator::new(proposal_id);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+    });
+}
+
+#[test]
+fn vote_fails_with_insufficient_rights() {
+    initial_test_ext().execute_with(|| {
+        assert_eq!(
+            ProposalsEngine::vote(system::RawOrigin::None.into(), 1, 1, VoteKind::Approve),
+            Err(Error::Other("RequireSignedOrigin"))
+        );
+    });
+}
+
+#[test]
+fn proposal_execution_succeeds() {
+    initial_test_ext().execute_with(|| {
+        let parameters_fixture = ProposalParametersFixture::default();
+        let dummy_proposal =
+            DummyProposalFixture::default().with_parameters(parameters_fixture.params());
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        // internal active proposal counter check
+        assert_eq!(<ActiveProposalCount>::get(), 1);
+
+        let mut vote_generator = VoteGenerator::new(proposal_id);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+
+        run_to_block_and_finalize(1);
+
+        let proposal = <crate::Proposals<Test>>::get(proposal_id);
+
+        assert_eq!(
+            proposal,
+            Proposal {
+                parameters: parameters_fixture.params(),
+                proposer_id: 1,
+                created_at: 1,
+                status: ProposalStatus::approved(ApprovedProposalStatus::Executed, 1),
+                title: b"title".to_vec(),
+                description: b"description".to_vec(),
+                voting_results: VotingResults {
+                    abstentions: 0,
+                    approvals: 4,
+                    rejections: 0,
+                    slashes: 0,
+                },
+            }
+        );
+
+        // internal active proposal counter check
+        assert_eq!(<ActiveProposalCount>::get(), 0);
+    });
+}
+
+#[test]
+fn proposal_execution_failed() {
+    initial_test_ext().execute_with(|| {
+        let parameters_fixture = ProposalParametersFixture::default();
+
+        let faulty_proposal = mock::proposals::Call::<Test>::faulty_proposal(
+            b"title".to_vec(),
+            b"description".to_vec(),
+        );
+
+        let dummy_proposal = DummyProposalFixture::default()
+            .with_parameters(parameters_fixture.params())
+            .with_proposal_code(faulty_proposal.encode());
+
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        let mut vote_generator = VoteGenerator::new(proposal_id);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+
+        run_to_block_and_finalize(2);
+
+        let proposal = <crate::Proposals<Test>>::get(proposal_id);
+
+        assert_eq!(
+            proposal,
+            Proposal {
+                parameters: parameters_fixture.params(),
+                proposer_id: 1,
+                created_at: 1,
+                status: ProposalStatus::approved(
+                    ApprovedProposalStatus::failed_execution("ExecutionFailed"),
+                    1
+                ),
+                title: b"title".to_vec(),
+                description: b"description".to_vec(),
+                voting_results: VotingResults {
+                    abstentions: 0,
+                    approvals: 4,
+                    rejections: 0,
+                    slashes: 0,
+                },
+            }
+        )
+    });
+}
+
+#[test]
+fn voting_results_calculation_succeeds() {
+    initial_test_ext().execute_with(|| {
+        let parameters = ProposalParameters {
+            voting_period: 3,
+            approval_quorum_percentage: 50,
+            approval_threshold_percentage: 50,
+            slashing_quorum_percentage: 60,
+            slashing_threshold_percentage: 60,
+            grace_period: 0,
+            required_stake: None,
+        };
+        let dummy_proposal = DummyProposalFixture::default().with_parameters(parameters);
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        let mut vote_generator = VoteGenerator::new(proposal_id);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert_ok(VoteKind::Reject);
+        vote_generator.vote_and_assert_ok(VoteKind::Abstain);
+
+        run_to_block_and_finalize(2);
+
+        let proposal = <crate::Proposals<Test>>::get(proposal_id);
+
+        assert_eq!(
+            proposal.voting_results,
+            VotingResults {
+                abstentions: 1,
+                approvals: 2,
+                rejections: 1,
+                slashes: 0,
+            }
+        )
+    });
+}
+
+#[test]
+fn rejected_voting_results_and_remove_proposal_id_from_active_succeeds() {
+    initial_test_ext().execute_with(|| {
+        // internal active proposal counter check
+        assert_eq!(<ActiveProposalCount>::get(), 0);
+
+        let dummy_proposal = DummyProposalFixture::default();
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        let mut vote_generator = VoteGenerator::new(proposal_id);
+        vote_generator.vote_and_assert_ok(VoteKind::Reject);
+        vote_generator.vote_and_assert_ok(VoteKind::Reject);
+        vote_generator.vote_and_assert_ok(VoteKind::Abstain);
+        vote_generator.vote_and_assert_ok(VoteKind::Abstain);
+
+        assert!(<ActiveProposalIds<Test>>::exists(proposal_id));
+
+        // internal active proposal counter check
+        assert_eq!(<ActiveProposalCount>::get(), 1);
+
+        run_to_block_and_finalize(2);
+
+        let proposal = <Proposals<Test>>::get(proposal_id);
+
+        assert_eq!(
+            proposal.voting_results,
+            VotingResults {
+                abstentions: 2,
+                approvals: 0,
+                rejections: 2,
+                slashes: 0,
+            }
+        );
+
+        assert_eq!(
+            proposal.status,
+            ProposalStatus::finalized_successfully(ProposalDecisionStatus::Rejected, 1),
+        );
+        assert!(!<ActiveProposalIds<Test>>::exists(proposal_id));
+
+        // internal active proposal counter check
+        assert_eq!(<ActiveProposalCount>::get(), 0);
+    });
+}
+
+#[test]
+fn create_proposal_fails_with_invalid_body_or_title() {
+    initial_test_ext().execute_with(|| {
+        let mut dummy_proposal =
+            DummyProposalFixture::default().with_title_and_body(Vec::new(), b"body".to_vec());
+        dummy_proposal.create_proposal_and_assert(Err(Error::EmptyTitleProvided.into()));
+
+        dummy_proposal =
+            DummyProposalFixture::default().with_title_and_body(b"title".to_vec(), Vec::new());
+        dummy_proposal.create_proposal_and_assert(Err(Error::EmptyDescriptionProvided.into()));
+
+        let too_long_title = vec![0; 200];
+        dummy_proposal =
+            DummyProposalFixture::default().with_title_and_body(too_long_title, b"body".to_vec());
+        dummy_proposal.create_proposal_and_assert(Err(Error::TitleIsTooLong.into()));
+
+        let too_long_body = vec![0; 11000];
+        dummy_proposal =
+            DummyProposalFixture::default().with_title_and_body(b"title".to_vec(), too_long_body);
+        dummy_proposal.create_proposal_and_assert(Err(Error::DescriptionIsTooLong.into()));
+    });
+}
+
+#[test]
+fn vote_fails_with_expired_voting_period() {
+    initial_test_ext().execute_with(|| {
+        let dummy_proposal = DummyProposalFixture::default();
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        run_to_block_and_finalize(6);
+
+        let mut vote_generator = VoteGenerator::new(proposal_id);
+        vote_generator.vote_and_assert(VoteKind::Approve, Err(Error::ProposalFinalized));
+    });
+}
+
+#[test]
+fn vote_fails_with_not_active_proposal() {
+    initial_test_ext().execute_with(|| {
+        let dummy_proposal = DummyProposalFixture::default();
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        let mut vote_generator = VoteGenerator::new(proposal_id);
+        vote_generator.vote_and_assert_ok(VoteKind::Reject);
+        vote_generator.vote_and_assert_ok(VoteKind::Reject);
+        vote_generator.vote_and_assert_ok(VoteKind::Abstain);
+        vote_generator.vote_and_assert_ok(VoteKind::Abstain);
+
+        run_to_block_and_finalize(2);
+
+        let mut vote_generator_to_fail = VoteGenerator::new(proposal_id);
+        vote_generator_to_fail.vote_and_assert(VoteKind::Approve, Err(Error::ProposalFinalized));
+    });
+}
+
+#[test]
+fn vote_fails_with_absent_proposal() {
+    initial_test_ext().execute_with(|| {
+        let mut vote_generator = VoteGenerator::new(2);
+        vote_generator.vote_and_assert(VoteKind::Approve, Err(Error::ProposalNotFound));
+    });
+}
+
+#[test]
+fn vote_fails_on_double_voting() {
+    initial_test_ext().execute_with(|| {
+        let dummy_proposal = DummyProposalFixture::default();
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        let mut vote_generator = VoteGenerator::new(proposal_id);
+        vote_generator.auto_increment_voter_id = false;
+
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert(VoteKind::Approve, Err(Error::AlreadyVoted));
+    });
+}
+
+#[test]
+fn cancel_proposal_succeeds() {
+    initial_test_ext().execute_with(|| {
+        let parameters_fixture = ProposalParametersFixture::default();
+        let dummy_proposal =
+            DummyProposalFixture::default().with_parameters(parameters_fixture.params());
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        // internal active proposal counter check
+        assert_eq!(<ActiveProposalCount>::get(), 1);
+
+        let cancel_proposal = CancelProposalFixture::new(proposal_id);
+        cancel_proposal.cancel_and_assert(Ok(()));
+
+        // internal active proposal counter check
+        assert_eq!(<ActiveProposalCount>::get(), 0);
+
+        let proposal = <crate::Proposals<Test>>::get(proposal_id);
+
+        assert_eq!(
+            proposal,
+            Proposal {
+                parameters: parameters_fixture.params(),
+                proposer_id: 1,
+                created_at: 1,
+                status: ProposalStatus::finalized_successfully(ProposalDecisionStatus::Canceled, 1),
+                title: b"title".to_vec(),
+                description: b"description".to_vec(),
+                voting_results: VotingResults::default(),
+            }
+        )
+    });
+}
+
+#[test]
+fn cancel_proposal_fails_with_not_active_proposal() {
+    initial_test_ext().execute_with(|| {
+        let dummy_proposal = DummyProposalFixture::default();
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        run_to_block_and_finalize(6);
+
+        let cancel_proposal = CancelProposalFixture::new(proposal_id);
+        cancel_proposal.cancel_and_assert(Err(Error::ProposalFinalized));
+    });
+}
+
+#[test]
+fn cancel_proposal_fails_with_not_existing_proposal() {
+    initial_test_ext().execute_with(|| {
+        let cancel_proposal = CancelProposalFixture::new(2);
+        cancel_proposal.cancel_and_assert(Err(Error::ProposalNotFound));
+    });
+}
+
+#[test]
+fn cancel_proposal_fails_with_insufficient_rights() {
+    initial_test_ext().execute_with(|| {
+        let dummy_proposal = DummyProposalFixture::default();
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        let cancel_proposal = CancelProposalFixture::new(proposal_id)
+            .with_origin(RawOrigin::Signed(2))
+            .with_proposer(2);
+        cancel_proposal.cancel_and_assert(Err(Error::NotAuthor));
+    });
+}
+
+#[test]
+fn veto_proposal_succeeds() {
+    initial_test_ext().execute_with(|| {
+        // internal active proposal counter check
+        assert_eq!(<ActiveProposalCount>::get(), 0);
+
+        let parameters_fixture = ProposalParametersFixture::default();
+        let dummy_proposal =
+            DummyProposalFixture::default().with_parameters(parameters_fixture.params());
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        // internal active proposal counter check
+        assert_eq!(<ActiveProposalCount>::get(), 1);
+
+        let veto_proposal = VetoProposalFixture::new(proposal_id);
+        veto_proposal.veto_and_assert(Ok(()));
+
+        let proposal = <crate::Proposals<Test>>::get(proposal_id);
+
+        assert_eq!(
+            proposal,
+            Proposal {
+                parameters: parameters_fixture.params(),
+                proposer_id: 1,
+                created_at: 1,
+                status: ProposalStatus::finalized_successfully(ProposalDecisionStatus::Vetoed, 1),
+                title: b"title".to_vec(),
+                description: b"description".to_vec(),
+                voting_results: VotingResults::default(),
+            }
+        );
+
+        // internal active proposal counter check
+        assert_eq!(<ActiveProposalCount>::get(), 0);
+    });
+}
+
+#[test]
+fn veto_proposal_fails_with_not_active_proposal() {
+    initial_test_ext().execute_with(|| {
+        let dummy_proposal = DummyProposalFixture::default();
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        run_to_block_and_finalize(6);
+
+        let veto_proposal = VetoProposalFixture::new(proposal_id);
+        veto_proposal.veto_and_assert(Err(Error::ProposalFinalized));
+    });
+}
+
+#[test]
+fn veto_proposal_fails_with_not_existing_proposal() {
+    initial_test_ext().execute_with(|| {
+        let veto_proposal = VetoProposalFixture::new(2);
+        veto_proposal.veto_and_assert(Err(Error::ProposalNotFound));
+    });
+}
+
+#[test]
+fn veto_proposal_fails_with_insufficient_rights() {
+    initial_test_ext().execute_with(|| {
+        let dummy_proposal = DummyProposalFixture::default();
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        let veto_proposal = VetoProposalFixture::new(proposal_id).with_origin(RawOrigin::Signed(2));
+        veto_proposal.veto_and_assert(Err(Error::RequireRootOrigin));
+    });
+}
+
+#[test]
+fn create_proposal_event_emitted() {
+    initial_test_ext().execute_with(|| {
+        let dummy_proposal = DummyProposalFixture::default();
+        dummy_proposal.create_proposal_and_assert(Ok(1));
+
+        EventFixture::assert_events(vec![RawEvent::ProposalCreated(1, 1)]);
+    });
+}
+
+#[test]
+fn veto_proposal_event_emitted() {
+    initial_test_ext().execute_with(|| {
+        let dummy_proposal = DummyProposalFixture::default();
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        let veto_proposal = VetoProposalFixture::new(proposal_id);
+        veto_proposal.veto_and_assert(Ok(()));
+
+        EventFixture::assert_events(vec![
+            RawEvent::ProposalCreated(1, 1),
+            RawEvent::ProposalStatusUpdated(
+                1,
+                ProposalStatus::finalized_successfully(ProposalDecisionStatus::Vetoed, 1),
+            ),
+        ]);
+    });
+}
+
+#[test]
+fn cancel_proposal_event_emitted() {
+    initial_test_ext().execute_with(|| {
+        let dummy_proposal = DummyProposalFixture::default();
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        let cancel_proposal = CancelProposalFixture::new(proposal_id);
+        cancel_proposal.cancel_and_assert(Ok(()));
+
+        EventFixture::assert_events(vec![
+            RawEvent::ProposalCreated(1, 1),
+            RawEvent::ProposalStatusUpdated(
+                1,
+                ProposalStatus::Finalized(FinalizationData {
+                    proposal_status: ProposalDecisionStatus::Canceled,
+                    encoded_unstaking_error_due_to_broken_runtime: None,
+                    stake_data_after_unstaking_error: None,
+                    finalized_at: 1,
+                }),
+            ),
+        ]);
+    });
+}
+
+#[test]
+fn vote_proposal_event_emitted() {
+    initial_test_ext().execute_with(|| {
+        let dummy_proposal = DummyProposalFixture::default();
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        let mut vote_generator = VoteGenerator::new(proposal_id);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+
+        EventFixture::assert_events(vec![
+            RawEvent::ProposalCreated(1, 1),
+            RawEvent::Voted(1, 1, VoteKind::Approve),
+        ]);
+    });
+}
+
+#[test]
+fn create_proposal_and_expire_it() {
+    initial_test_ext().execute_with(|| {
+        let parameters_fixture = ProposalParametersFixture::default();
+        let dummy_proposal =
+            DummyProposalFixture::default().with_parameters(parameters_fixture.params());
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        run_to_block_and_finalize(8);
+
+        let proposal = <crate::Proposals<Test>>::get(proposal_id);
+
+        assert_eq!(
+            proposal,
+            Proposal {
+                parameters: parameters_fixture.params(),
+                proposer_id: 1,
+                created_at: 1,
+                status: ProposalStatus::finalized_successfully(ProposalDecisionStatus::Expired, 4),
+                title: b"title".to_vec(),
+                description: b"description".to_vec(),
+                voting_results: VotingResults::default(),
+            }
+        )
+    });
+}
+
+#[test]
+fn proposal_execution_postponed_because_of_grace_period() {
+    initial_test_ext().execute_with(|| {
+        let parameters_fixture = ProposalParametersFixture::default().with_grace_period(2);
+        let dummy_proposal =
+            DummyProposalFixture::default().with_parameters(parameters_fixture.params());
+
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        let mut vote_generator = VoteGenerator::new(proposal_id);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+
+        run_to_block_and_finalize(1);
+        run_to_block_and_finalize(2);
+
+        // check internal cache for proposal_id presense
+        assert!(<PendingExecutionProposalIds<Test>>::enumerate()
+            .find(|(x, _)| *x == proposal_id)
+            .is_some());
+
+        let proposal = <crate::Proposals<Test>>::get(proposal_id);
+
+        assert_eq!(
+            proposal,
+            Proposal {
+                parameters: parameters_fixture.params(),
+                proposer_id: 1,
+                created_at: 1,
+                status: ProposalStatus::approved(ApprovedProposalStatus::PendingExecution, 1),
+                title: b"title".to_vec(),
+                description: b"description".to_vec(),
+                voting_results: VotingResults {
+                    abstentions: 0,
+                    approvals: 4,
+                    rejections: 0,
+                    slashes: 0,
+                },
+            }
+        );
+    });
+}
+
+#[test]
+fn proposal_execution_vetoed_successfully_during_the_grace_period() {
+    initial_test_ext().execute_with(|| {
+        let parameters_fixture = ProposalParametersFixture::default().with_grace_period(2);
+        let dummy_proposal =
+            DummyProposalFixture::default().with_parameters(parameters_fixture.params());
+
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        let mut vote_generator = VoteGenerator::new(proposal_id);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+
+        run_to_block_and_finalize(1);
+        run_to_block_and_finalize(2);
+
+        // check internal cache for proposal_id presense
+        assert!(<PendingExecutionProposalIds<Test>>::enumerate()
+            .find(|(x, _)| *x == proposal_id)
+            .is_some());
+
+        let proposal = <crate::Proposals<Test>>::get(proposal_id);
+
+        assert_eq!(
+            proposal,
+            Proposal {
+                parameters: parameters_fixture.params(),
+                proposer_id: 1,
+                created_at: 1,
+                status: ProposalStatus::approved(ApprovedProposalStatus::PendingExecution, 1),
+                title: b"title".to_vec(),
+                description: b"description".to_vec(),
+                voting_results: VotingResults {
+                    abstentions: 0,
+                    approvals: 4,
+                    rejections: 0,
+                    slashes: 0,
+                },
+            }
+        );
+
+        let veto_proposal = VetoProposalFixture::new(proposal_id);
+        veto_proposal.veto_and_assert(Ok(()));
+
+        let proposal = <crate::Proposals<Test>>::get(proposal_id);
+
+        assert_eq!(
+            proposal,
+            Proposal {
+                parameters: parameters_fixture.params(),
+                proposer_id: 1,
+                created_at: 1,
+                status: ProposalStatus::finalized_successfully(ProposalDecisionStatus::Vetoed, 2),
+                title: b"title".to_vec(),
+                description: b"description".to_vec(),
+                voting_results: VotingResults {
+                    abstentions: 0,
+                    approvals: 4,
+                    rejections: 0,
+                    slashes: 0,
+                },
+            }
+        );
+
+        // check internal cache for proposal_id presense
+        assert!(<PendingExecutionProposalIds<Test>>::enumerate()
+            .find(|(x, _)| *x == proposal_id)
+            .is_none());
+    });
+}
+
+#[test]
+fn proposal_execution_succeeds_after_the_grace_period() {
+    initial_test_ext().execute_with(|| {
+        let parameters_fixture = ProposalParametersFixture::default().with_grace_period(1);
+        let dummy_proposal =
+            DummyProposalFixture::default().with_parameters(parameters_fixture.params());
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        let mut vote_generator = VoteGenerator::new(proposal_id);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+
+        run_to_block_and_finalize(1);
+
+        // check internal cache for proposal_id presence
+        assert!(<PendingExecutionProposalIds<Test>>::enumerate()
+            .find(|(x, _)| *x == proposal_id)
+            .is_some());
+
+        let mut proposal = <crate::Proposals<Test>>::get(proposal_id);
+
+        let mut expected_proposal = Proposal {
+            parameters: parameters_fixture.params(),
+            proposer_id: 1,
+            created_at: 1,
+            status: ProposalStatus::approved(ApprovedProposalStatus::PendingExecution, 1),
+            title: b"title".to_vec(),
+            description: b"description".to_vec(),
+            voting_results: VotingResults {
+                abstentions: 0,
+                approvals: 4,
+                rejections: 0,
+                slashes: 0,
+            },
+        };
+
+        assert_eq!(proposal, expected_proposal);
+
+        run_to_block_and_finalize(2);
+
+        proposal = <crate::Proposals<Test>>::get(proposal_id);
+
+        expected_proposal.status = ProposalStatus::approved(ApprovedProposalStatus::Executed, 1);
+
+        assert_eq!(proposal, expected_proposal);
+
+        // check internal cache for proposal_id absense
+        assert!(<PendingExecutionProposalIds<Test>>::enumerate()
+            .find(|(x, _)| *x == proposal_id)
+            .is_none());
+    });
+}
+
+#[test]
+fn create_proposal_fails_on_exceeding_max_active_proposals_count() {
+    initial_test_ext().execute_with(|| {
+        for idx in 1..101 {
+            let dummy_proposal = DummyProposalFixture::default();
+            dummy_proposal.create_proposal_and_assert(Ok(idx));
+            // internal active proposal counter check
+            assert_eq!(<ActiveProposalCount>::get(), idx);
+        }
+
+        let dummy_proposal = DummyProposalFixture::default();
+        dummy_proposal
+            .create_proposal_and_assert(Err(Error::MaxActiveProposalNumberExceeded.into()));
+        // internal active proposal counter check
+        assert_eq!(<ActiveProposalCount>::get(), 100);
+    });
+}
+
+#[test]
+fn voting_internal_cache_exists_after_proposal_finalization() {
+    initial_test_ext().execute_with(|| {
+        let dummy_proposal = DummyProposalFixture::default();
+        dummy_proposal.create_proposal_and_assert(Ok(1));
+
+        // last created proposal id equals current proposal count
+        let proposal_id = <ProposalCount>::get();
+
+        let mut vote_generator = VoteGenerator::new(proposal_id);
+        vote_generator.vote_and_assert_ok(VoteKind::Reject);
+        vote_generator.vote_and_assert_ok(VoteKind::Reject);
+        vote_generator.vote_and_assert_ok(VoteKind::Abstain);
+        vote_generator.vote_and_assert_ok(VoteKind::Abstain);
+
+        // cache exists
+        assert!(<crate::VoteExistsByProposalByVoter<Test>>::exists(
+            proposal_id,
+            1
+        ));
+
+        run_to_block_and_finalize(2);
+
+        // cache still exists and is not cleared
+        assert!(<crate::VoteExistsByProposalByVoter<Test>>::exists(
+            proposal_id,
+            1
+        ));
+    });
+}
+
+#[test]
+fn create_dummy_proposal_succeeds_with_stake() {
+    initial_test_ext().execute_with(|| {
+        let account_id = 1;
+
+        let required_stake = 200;
+        let parameters_fixture =
+            ProposalParametersFixture::default().with_required_stake(required_stake);
+
+        let dummy_proposal = DummyProposalFixture::default()
+            .with_parameters(parameters_fixture.params())
+            .with_account_id(account_id)
+            .with_stake(200);
+
+        let _imbalance = <Test as stake::Trait>::Currency::deposit_creating(&account_id, 500);
+
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        let proposal = <crate::Proposals<Test>>::get(proposal_id);
+
+        assert_eq!(
+            proposal,
+            Proposal {
+                parameters: parameters_fixture.params(),
+                proposer_id: 1,
+                created_at: 1,
+                status: ProposalStatus::Active(Some(ActiveStake {
+                    stake_id: 0, // valid stake_id
+                    source_account_id: 1
+                })),
+                title: b"title".to_vec(),
+                description: b"description".to_vec(),
+                voting_results: VotingResults::default(),
+            }
+        )
+    });
+}
+
+#[test]
+fn create_dummy_proposal_fail_with_stake_on_empty_account() {
+    initial_test_ext().execute_with(|| {
+        let account_id = 1;
+
+        let required_stake = 200;
+        let parameters_fixture =
+            ProposalParametersFixture::default().with_required_stake(required_stake);
+        let dummy_proposal = DummyProposalFixture::default()
+            .with_parameters(parameters_fixture.params())
+            .with_account_id(account_id)
+            .with_stake(required_stake);
+
+        dummy_proposal
+            .create_proposal_and_assert(Err(Error::Other("too few free funds in account")));
+    });
+}
+
+#[test]
+fn create_proposal_fais_with_invalid_stake_parameters() {
+    initial_test_ext().execute_with(|| {
+        let parameters_fixture = ProposalParametersFixture::default();
+
+        let mut dummy_proposal = DummyProposalFixture::default()
+            .with_parameters(parameters_fixture.params())
+            .with_stake(200);
+
+        dummy_proposal.create_proposal_and_assert(Err(Error::StakeShouldBeEmpty.into()));
+
+        let parameters_fixture_stake_200 = parameters_fixture.with_required_stake(200);
+        dummy_proposal =
+            DummyProposalFixture::default().with_parameters(parameters_fixture_stake_200.params());
+
+        dummy_proposal.create_proposal_and_assert(Err(Error::EmptyStake.into()));
+
+        let parameters_fixture_stake_300 = parameters_fixture.with_required_stake(300);
+        dummy_proposal = DummyProposalFixture::default()
+            .with_parameters(parameters_fixture_stake_300.params())
+            .with_stake(200);
+
+        dummy_proposal.create_proposal_and_assert(Err(Error::StakeDiffersFromRequired.into()));
+    });
+}
+
+#[test]
+fn finalize_expired_proposal_and_check_stake_removing_with_balance_checks_succeeds() {
+    initial_test_ext().execute_with(|| {
+        let account_id = 1;
+
+        let stake_amount = 200;
+        let parameters = ProposalParameters {
+            voting_period: 3,
+            approval_quorum_percentage: 50,
+            approval_threshold_percentage: 60,
+            slashing_quorum_percentage: 60,
+            slashing_threshold_percentage: 60,
+            grace_period: 5,
+            required_stake: Some(stake_amount),
+        };
+        let dummy_proposal = DummyProposalFixture::default()
+            .with_parameters(parameters)
+            .with_account_id(account_id)
+            .with_stake(stake_amount);
+
+        let account_balance = 500;
+        let _imbalance =
+            <Test as stake::Trait>::Currency::deposit_creating(&account_id, account_balance);
+
+        assert_eq!(
+            <Test as stake::Trait>::Currency::total_balance(&account_id),
+            account_balance
+        );
+
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+        assert_eq!(
+            <Test as stake::Trait>::Currency::total_balance(&account_id),
+            account_balance - stake_amount
+        );
+
+        let mut proposal = <crate::Proposals<Test>>::get(proposal_id);
+
+        let mut expected_proposal = Proposal {
+            parameters,
+            proposer_id: 1,
+            created_at: 1,
+            status: ProposalStatus::Active(Some(ActiveStake {
+                stake_id: 0,
+                source_account_id: 1,
+            })),
+            title: b"title".to_vec(),
+            description: b"description".to_vec(),
+            voting_results: VotingResults::default(),
+        };
+
+        assert_eq!(proposal, expected_proposal);
+
+        run_to_block_and_finalize(5);
+
+        proposal = <crate::Proposals<Test>>::get(proposal_id);
+
+        expected_proposal.status = ProposalStatus::Finalized(FinalizationData {
+            proposal_status: ProposalDecisionStatus::Expired,
+            finalized_at: 4,
+            encoded_unstaking_error_due_to_broken_runtime: None,
+            stake_data_after_unstaking_error: None,
+        });
+
+        assert_eq!(proposal, expected_proposal);
+
+        let rejection_fee = RejectionFee::get();
+        assert_eq!(
+            <Test as stake::Trait>::Currency::total_balance(&account_id),
+            account_balance - rejection_fee
+        );
+    });
+}
+
+#[test]
+fn proposal_cancellation_with_slashes_with_balance_checks_succeeds() {
+    initial_test_ext().execute_with(|| {
+        let account_id = 1;
+
+        let stake_amount = 200;
+        let parameters = ProposalParameters {
+            voting_period: 3,
+            approval_quorum_percentage: 50,
+            approval_threshold_percentage: 60,
+            slashing_quorum_percentage: 60,
+            slashing_threshold_percentage: 60,
+            grace_period: 5,
+            required_stake: Some(stake_amount),
+        };
+        let dummy_proposal = DummyProposalFixture::default()
+            .with_parameters(parameters)
+            .with_account_id(account_id.clone())
+            .with_stake(stake_amount);
+
+        let account_balance = 500;
+        let _imbalance =
+            <Test as stake::Trait>::Currency::deposit_creating(&account_id, account_balance);
+
+        assert_eq!(
+            <Test as stake::Trait>::Currency::total_balance(&account_id),
+            account_balance
+        );
+
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+        assert_eq!(
+            <Test as stake::Trait>::Currency::total_balance(&account_id),
+            account_balance - stake_amount
+        );
+
+        let mut proposal = <crate::Proposals<Test>>::get(proposal_id);
+
+        let mut expected_proposal = Proposal {
+            parameters,
+            proposer_id: 1,
+            created_at: 1,
+            status: ProposalStatus::Active(Some(ActiveStake {
+                stake_id: 0,
+                source_account_id: 1,
+            })),
+            title: b"title".to_vec(),
+            description: b"description".to_vec(),
+            voting_results: VotingResults::default(),
+        };
+
+        assert_eq!(proposal, expected_proposal);
+
+        let cancel_proposal_fixture = CancelProposalFixture::new(proposal_id);
+
+        cancel_proposal_fixture.cancel_and_assert(Ok(()));
+
+        proposal = <crate::Proposals<Test>>::get(proposal_id);
+
+        expected_proposal.status = ProposalStatus::Finalized(FinalizationData {
+            proposal_status: ProposalDecisionStatus::Canceled,
+            finalized_at: 1,
+            encoded_unstaking_error_due_to_broken_runtime: None,
+            stake_data_after_unstaking_error: None,
+        });
+
+        assert_eq!(proposal, expected_proposal);
+
+        let cancellation_fee = CancellationFee::get();
+        assert_eq!(
+            <Test as stake::Trait>::Currency::total_balance(&account_id),
+            account_balance - cancellation_fee
+        );
+    });
+}
+
+#[test]
+fn finalize_proposal_using_stake_mocks_succeeds() {
+    handle_mock(|| {
+        initial_test_ext().execute_with(|| {
+            let mock = {
+                let mut mock = crate::types::MockStakeHandler::<Test>::new();
+                mock.expect_create_stake().times(1).returning(|| Ok(1));
+
+                mock.expect_make_stake_imbalance()
+                    .times(1)
+                    .returning(|_, _| Ok(crate::types::NegativeImbalance::<Test>::new(200)));
+
+                mock.expect_stake().times(1).returning(|_, _| Ok(()));
+
+                mock.expect_remove_stake().times(1).returning(|_| Ok(()));
+
+                mock.expect_unstake().times(1).returning(|_| Ok(()));
+
+                mock.expect_slash().times(1).returning(|_, _| Ok(()));
+
+                Rc::new(mock)
+            };
+            set_stake_handler_impl(mock.clone());
+
+            let account_id = 1;
+
+            let stake_amount = 200;
+            let parameters_fixture =
+                ProposalParametersFixture::default().with_required_stake(stake_amount);
+            let dummy_proposal = DummyProposalFixture::default()
+                .with_parameters(parameters_fixture.params())
+                .with_account_id(account_id)
+                .with_stake(stake_amount);
+
+            let _proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+            run_to_block_and_finalize(5);
+        });
+    });
+}
+
+#[test]
+fn proposal_slashing_succeeds() {
+    initial_test_ext().execute_with(|| {
+        let dummy_proposal = DummyProposalFixture::default();
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        let mut vote_generator = VoteGenerator::new(proposal_id);
+        vote_generator.vote_and_assert_ok(VoteKind::Reject);
+        vote_generator.vote_and_assert_ok(VoteKind::Slash);
+        vote_generator.vote_and_assert_ok(VoteKind::Slash);
+        vote_generator.vote_and_assert_ok(VoteKind::Slash);
+
+        assert!(<ActiveProposalIds<Test>>::exists(proposal_id));
+
+        run_to_block_and_finalize(2);
+
+        let proposal = <Proposals<Test>>::get(proposal_id);
+
+        assert_eq!(
+            proposal.voting_results,
+            VotingResults {
+                abstentions: 0,
+                approvals: 0,
+                rejections: 1,
+                slashes: 3,
+            }
+        );
+
+        assert_eq!(
+            proposal.status,
+            ProposalStatus::Finalized(FinalizationData {
+                proposal_status: ProposalDecisionStatus::Slashed,
+                encoded_unstaking_error_due_to_broken_runtime: None,
+                finalized_at: 1,
+                stake_data_after_unstaking_error: None,
+            }),
+        );
+        assert!(!<ActiveProposalIds<Test>>::exists(proposal_id));
+    });
+}
+
+#[test]
+fn finalize_proposal_using_stake_mocks_failed() {
+    handle_mock(|| {
+        initial_test_ext().execute_with(|| {
+            let mock = {
+                let mut mock = crate::types::MockStakeHandler::<Test>::new();
+                mock.expect_create_stake().times(1).returning(|| Ok(1));
+
+                mock.expect_remove_stake()
+                    .times(1)
+                    .returning(|_| Err("Cannot remove stake"));
+
+                mock.expect_make_stake_imbalance()
+                    .times(1)
+                    .returning(|_, _| Ok(crate::types::NegativeImbalance::<Test>::new(200)));
+
+                mock.expect_stake().times(1).returning(|_, _| Ok(()));
+
+                mock.expect_unstake().times(1).returning(|_| Ok(()));
+
+                mock.expect_slash().times(1).returning(|_, _| Ok(()));
+
+                Rc::new(mock)
+            };
+            set_stake_handler_impl(mock.clone());
+
+            let account_id = 1;
+
+            let stake_amount = 200;
+            let parameters_fixture =
+                ProposalParametersFixture::default().with_required_stake(stake_amount);
+            let dummy_proposal = DummyProposalFixture::default()
+                .with_parameters(parameters_fixture.params())
+                .with_account_id(account_id)
+                .with_stake(stake_amount);
+
+            let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+            run_to_block_and_finalize(5);
+
+            let proposal = <Proposals<Test>>::get(proposal_id);
+            assert_eq!(
+                proposal,
+                Proposal {
+                    parameters: parameters_fixture.params(),
+                    proposer_id: 1,
+                    created_at: 1,
+                    status: ProposalStatus::finalized(
+                        ProposalDecisionStatus::Expired,
+                        Some("Cannot remove stake"),
+                        Some(ActiveStake {
+                            stake_id: 1,
+                            source_account_id: 1
+                        }),
+                        4,
+                    ),
+                    title: b"title".to_vec(),
+                    description: b"description".to_vec(),
+                    voting_results: VotingResults::default(),
+                }
+            );
+        });
+    });
+}
+
+#[test]
+fn create_proposal_fails_with_invalid_threshold_parameters() {
+    initial_test_ext().execute_with(|| {
+        let mut parameters = ProposalParameters {
+            voting_period: 3,
+            approval_quorum_percentage: 50,
+            approval_threshold_percentage: 0,
+            slashing_quorum_percentage: 60,
+            slashing_threshold_percentage: 60,
+            grace_period: 5,
+            required_stake: None,
+        };
+
+        let mut dummy_proposal = DummyProposalFixture::default().with_parameters(parameters);
+
+        dummy_proposal
+            .create_proposal_and_assert(Err(Error::InvalidParameterApprovalThreshold.into()));
+
+        parameters.approval_threshold_percentage = 60;
+        parameters.slashing_threshold_percentage = 0;
+        dummy_proposal = DummyProposalFixture::default().with_parameters(parameters);
+
+        dummy_proposal
+            .create_proposal_and_assert(Err(Error::InvalidParameterSlashingThreshold.into()));
+    });
+}
+
+#[test]
+fn proposal_reset_succeeds() {
+    initial_test_ext().execute_with(|| {
+        let dummy_proposal = DummyProposalFixture::default();
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        let mut vote_generator = VoteGenerator::new(proposal_id);
+        vote_generator.vote_and_assert_ok(VoteKind::Reject);
+        vote_generator.vote_and_assert_ok(VoteKind::Abstain);
+        vote_generator.vote_and_assert_ok(VoteKind::Slash);
+
+        assert!(<ActiveProposalIds<Test>>::exists(proposal_id));
+        assert_eq!(
+            <VoteExistsByProposalByVoter<Test>>::get(&proposal_id, &2),
+            VoteKind::Abstain
+        );
+
+        run_to_block_and_finalize(2);
+
+        let proposal = <Proposals<Test>>::get(proposal_id);
+
+        assert_eq!(
+            proposal.voting_results,
+            VotingResults {
+                abstentions: 1,
+                approvals: 0,
+                rejections: 1,
+                slashes: 1,
+            }
+        );
+
+        ProposalsEngine::reset_active_proposals();
+
+        let updated_proposal = <Proposals<Test>>::get(proposal_id);
+
+        assert_eq!(
+            updated_proposal.voting_results,
+            VotingResults {
+                abstentions: 0,
+                approvals: 0,
+                rejections: 0,
+                slashes: 0,
+            }
+        );
+
+        // whole double map prefix was removed (should return default value)
+        assert_eq!(
+            <VoteExistsByProposalByVoter<Test>>::get(&proposal_id, &2),
+            VoteKind::default()
+        );
+    });
+}
+
+#[test]
+fn proposal_counters_are_valid() {
+    initial_test_ext().execute_with(|| {
+        let mut dummy_proposal = DummyProposalFixture::default();
+        let _ = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        dummy_proposal = DummyProposalFixture::default();
+        let _ = dummy_proposal.create_proposal_and_assert(Ok(2)).unwrap();
+
+        dummy_proposal = DummyProposalFixture::default();
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(3)).unwrap();
+
+        assert_eq!(ActiveProposalCount::get(), 3);
+        assert_eq!(ProposalCount::get(), 3);
+
+        let cancel_proposal_fixture = CancelProposalFixture::new(proposal_id);
+        cancel_proposal_fixture.cancel_and_assert(Ok(()));
+
+        assert_eq!(ActiveProposalCount::get(), 2);
+        assert_eq!(ProposalCount::get(), 3);
+    });
+}
+
+#[test]
+fn proposal_stake_cache_is_valid() {
+    initial_test_ext().execute_with(|| {
+        increase_total_balance_issuance_using_account_id(1, 50000);
+
+        let stake = 250u32;
+        let parameters = ProposalParametersFixture::default().with_required_stake(stake.into());
+        let dummy_proposal = DummyProposalFixture::default()
+            .with_parameters(parameters.params())
+            .with_stake(stake as u64);
+
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+        let expected_stake_id = 0;
+        assert_eq!(
+            <StakesProposals<Test>>::get(&expected_stake_id),
+            proposal_id
+        );
+    });
+}
+
+#[test]
+fn slash_balance_is_calculated_correctly() {
+    initial_test_ext().execute_with(|| {
+        let vetoed_slash_balance = ProposalsEngine::calculate_slash_balance(
+            &ProposalDecisionStatus::Vetoed,
+            &ProposalParametersFixture::default().params(),
+        );
+
+        assert_eq!(vetoed_slash_balance, 0);
+
+        let approved_slash_balance = ProposalsEngine::calculate_slash_balance(
+            &ProposalDecisionStatus::Approved(ApprovedProposalStatus::Executed),
+            &ProposalParametersFixture::default().params(),
+        );
+
+        assert_eq!(approved_slash_balance, 0);
+
+        let rejection_fee = <Test as crate::Trait>::RejectionFee::get();
+
+        let rejected_slash_balance = ProposalsEngine::calculate_slash_balance(
+            &ProposalDecisionStatus::Rejected,
+            &ProposalParametersFixture::default().params(),
+        );
+
+        assert_eq!(rejected_slash_balance, rejection_fee);
+
+        let expired_slash_balance = ProposalsEngine::calculate_slash_balance(
+            &ProposalDecisionStatus::Expired,
+            &ProposalParametersFixture::default().params(),
+        );
+
+        assert_eq!(expired_slash_balance, rejection_fee);
+
+        let cancellation_fee = <Test as crate::Trait>::CancellationFee::get();
+
+        let cancellation_slash_balance = ProposalsEngine::calculate_slash_balance(
+            &ProposalDecisionStatus::Canceled,
+            &ProposalParametersFixture::default().params(),
+        );
+
+        assert_eq!(cancellation_slash_balance, cancellation_fee);
+
+        let slash_balance_with_no_stake = ProposalsEngine::calculate_slash_balance(
+            &ProposalDecisionStatus::Slashed,
+            &ProposalParametersFixture::default().params(),
+        );
+
+        assert_eq!(slash_balance_with_no_stake, 0);
+
+        let stake = 256;
+        let slash_balance_with_stake = ProposalsEngine::calculate_slash_balance(
+            &ProposalDecisionStatus::Slashed,
+            &ProposalParametersFixture::default()
+                .with_required_stake(stake)
+                .params(),
+        );
+
+        assert_eq!(slash_balance_with_stake, stake);
+    });
+}

+ 793 - 0
runtime-modules/proposals/engine/src/types/mod.rs

@@ -0,0 +1,793 @@
+//! Proposals types module for the Joystream platform. Version 2.
+//! Provides types for the proposal engine.
+
+#![warn(missing_docs)]
+
+use codec::{Decode, Encode};
+use rstd::cmp::PartialOrd;
+use rstd::ops::Add;
+use rstd::prelude::*;
+
+#[cfg(feature = "std")]
+use serde::{Deserialize, Serialize};
+use sr_primitives::Perbill;
+use srml_support::dispatch;
+use srml_support::traits::Currency;
+
+mod proposal_statuses;
+mod stakes;
+
+pub use proposal_statuses::{
+    ApprovedProposalStatus, FinalizationData, ProposalDecisionStatus, ProposalStatus,
+};
+pub(crate) use stakes::ProposalStakeManager;
+pub use stakes::{DefaultStakeHandlerProvider, StakeHandler, StakeHandlerProvider};
+
+#[cfg(test)]
+pub(crate) use stakes::DefaultStakeHandler;
+
+#[cfg(test)]
+pub(crate) use stakes::MockStakeHandler;
+
+/// Vote kind for the proposal. Sum of all votes defines proposal status.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)]
+pub enum VoteKind {
+    /// Pass, an alternative or a ranking, for binary, multiple choice
+    /// and ranked choice propositions, respectively.
+    Approve,
+
+    /// Against proposal.
+    Reject,
+
+    /// Reject proposal and slash it stake.
+    Slash,
+
+    /// Signals presence, but unwillingness to cast judgment on substance of vote.
+    Abstain,
+}
+
+impl Default for VoteKind {
+    fn default() -> Self {
+        VoteKind::Reject
+    }
+}
+
+/// Proposal parameters required to manage proposal risk.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Default, Clone, Copy, PartialEq, Eq, Debug)]
+pub struct ProposalParameters<BlockNumber, Balance> {
+    /// During this period, votes can be accepted
+    pub voting_period: BlockNumber,
+
+    /// A pause before execution of the approved proposal. Zero means approved proposal would be
+    /// executed immediately.
+    pub grace_period: BlockNumber,
+
+    /// Quorum percentage of approving voters required to pass the proposal.
+    pub approval_quorum_percentage: u32,
+
+    /// Approval votes percentage threshold to pass the proposal.
+    pub approval_threshold_percentage: u32,
+
+    /// Quorum percentage of voters required to slash the proposal.
+    pub slashing_quorum_percentage: u32,
+
+    /// Slashing votes percentage threshold to slash the proposal.
+    pub slashing_threshold_percentage: u32,
+
+    /// Proposal stake
+    pub required_stake: Option<Balance>,
+}
+
+/// Contains current voting results
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
+pub struct VotingResults {
+    /// 'Abstain' votes counter
+    pub abstentions: u32,
+
+    /// 'Approve' votes counter
+    pub approvals: u32,
+
+    /// 'Reject' votes counter
+    pub rejections: u32,
+
+    /// 'Slash' votes counter
+    pub slashes: u32,
+}
+
+impl VotingResults {
+    /// Add vote to the related counter
+    pub fn add_vote(&mut self, vote: VoteKind) {
+        match vote {
+            VoteKind::Abstain => self.abstentions += 1,
+            VoteKind::Approve => self.approvals += 1,
+            VoteKind::Reject => self.rejections += 1,
+            VoteKind::Slash => self.slashes += 1,
+        }
+    }
+
+    /// Calculates number of votes so far
+    pub fn votes_number(&self) -> u32 {
+        self.abstentions + self.approvals + self.rejections + self.slashes
+    }
+}
+
+/// Contains created stake id and source account for the stake balance
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Default, Clone, Copy, PartialEq, Eq, Debug)]
+pub struct ActiveStake<StakeId, AccountId> {
+    /// Created stake id for the proposal
+    pub stake_id: StakeId,
+
+    /// Source account of the stake balance. Refund if any will be provided using this account
+    pub source_account_id: AccountId,
+}
+
+/// 'Proposal' contains information necessary for the proposal system functioning.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
+pub struct Proposal<BlockNumber, ProposerId, Balance, StakeId, AccountId> {
+    /// Proposals parameter, characterize different proposal types.
+    pub parameters: ProposalParameters<BlockNumber, Balance>,
+
+    /// Identifier of member proposing.
+    pub proposer_id: ProposerId,
+
+    /// Proposal description
+    pub title: Vec<u8>,
+
+    /// Proposal body
+    pub description: Vec<u8>,
+
+    /// When it was created.
+    pub created_at: BlockNumber,
+
+    /// Current proposal status
+    pub status: ProposalStatus<BlockNumber, StakeId, AccountId>,
+
+    /// Curring voting result for the proposal
+    pub voting_results: VotingResults,
+}
+
+impl<BlockNumber, ProposerId, Balance, StakeId, AccountId>
+    Proposal<BlockNumber, ProposerId, Balance, StakeId, AccountId>
+where
+    BlockNumber: Add<Output = BlockNumber> + PartialOrd + Copy,
+    StakeId: Clone,
+    AccountId: Clone,
+{
+    /// Returns whether voting period expired by now
+    pub fn is_voting_period_expired(&self, now: BlockNumber) -> bool {
+        now >= self.created_at + self.parameters.voting_period
+    }
+
+    /// Returns whether grace period expired by now.
+    /// Grace period can be expired only if proposal is finalized with Approved status.
+    /// Returns false otherwise.
+    pub fn is_grace_period_expired(&self, now: BlockNumber) -> bool {
+        if let ProposalStatus::Finalized(finalized_status) = self.status.clone() {
+            if let ProposalDecisionStatus::Approved(_) = finalized_status.proposal_status {
+                return now >= finalized_status.finalized_at + self.parameters.grace_period;
+            }
+        }
+
+        false
+    }
+
+    /// Determines the finalized proposal status using voting results tally for current proposal.
+    /// Calculates votes, takes in account voting period expiration.
+    /// If voting process is in progress, then decision status is None.
+    /// Parameters: current time, total voters number involved (council size).
+    /// Returns the proposal finalized status if any.
+    pub fn define_proposal_decision_status(
+        &self,
+        total_voters_count: u32,
+        now: BlockNumber,
+    ) -> Option<ProposalDecisionStatus> {
+        let proposal_status_resolution = ProposalStatusResolution {
+            proposal: self,
+            approvals: self.voting_results.approvals,
+            slashes: self.voting_results.slashes,
+            now,
+            votes_count: self.voting_results.votes_number(),
+            total_voters_count,
+        };
+
+        if proposal_status_resolution.is_approval_quorum_reached()
+            && proposal_status_resolution.is_approval_threshold_reached()
+        {
+            Some(ProposalDecisionStatus::Approved(
+                ApprovedProposalStatus::PendingExecution,
+            ))
+        } else if proposal_status_resolution.is_slashing_quorum_reached()
+            && proposal_status_resolution.is_slashing_threshold_reached()
+        {
+            Some(ProposalDecisionStatus::Slashed)
+        } else if proposal_status_resolution.is_expired() {
+            Some(ProposalDecisionStatus::Expired)
+        } else if proposal_status_resolution.is_voting_completed() {
+            Some(ProposalDecisionStatus::Rejected)
+        } else {
+            None
+        }
+    }
+
+    /// Reset the proposal in Active status. Proposal with other status won't be changed.
+    /// Reset proposal operation clears voting results.
+    pub fn reset_proposal(&mut self) {
+        if let ProposalStatus::Active(_) = self.status.clone() {
+            self.voting_results = VotingResults::default();
+        }
+    }
+}
+
+/// Provides data for the voting.
+pub trait VotersParameters {
+    /// Defines maximum voters count for the proposal
+    fn total_voters_count() -> u32;
+}
+
+// Calculates quorum, votes threshold, expiration status
+struct ProposalStatusResolution<'a, BlockNumber, ProposerId, Balance, StakeId, AccountId> {
+    proposal: &'a Proposal<BlockNumber, ProposerId, Balance, StakeId, AccountId>,
+    now: BlockNumber,
+    votes_count: u32,
+    total_voters_count: u32,
+    approvals: u32,
+    slashes: u32,
+}
+
+impl<'a, BlockNumber, ProposerId, Balance, StakeId, AccountId>
+    ProposalStatusResolution<'a, BlockNumber, ProposerId, Balance, StakeId, AccountId>
+where
+    BlockNumber: Add<Output = BlockNumber> + PartialOrd + Copy,
+    StakeId: Clone,
+    AccountId: Clone,
+{
+    // Proposal has been expired and quorum not reached.
+    pub fn is_expired(&self) -> bool {
+        self.proposal.is_voting_period_expired(self.now)
+    }
+
+    // Approval quorum reached for the proposal. Compares predefined parameter with actual
+    // votes sum divided by total possible votes count.
+    pub fn is_approval_quorum_reached(&self) -> bool {
+        let actual_votes_fraction =
+            Perbill::from_rational_approximation(self.votes_count, self.total_voters_count);
+        let approval_quorum_fraction =
+            Perbill::from_percent(self.proposal.parameters.approval_quorum_percentage);
+
+        actual_votes_fraction.deconstruct() >= approval_quorum_fraction.deconstruct()
+    }
+
+    // Slashing quorum reached for the proposal. Compares predefined parameter with actual
+    // votes sum divided by total possible votes count.
+    pub fn is_slashing_quorum_reached(&self) -> bool {
+        let actual_votes_fraction =
+            Perbill::from_rational_approximation(self.votes_count, self.total_voters_count);
+        let slashing_quorum_fraction =
+            Perbill::from_percent(self.proposal.parameters.slashing_quorum_percentage);
+
+        actual_votes_fraction.deconstruct() >= slashing_quorum_fraction.deconstruct()
+    }
+
+    // Approval threshold reached for the proposal. Compares predefined parameter with 'approve'
+    // votes sum divided by actual votes count.
+    pub fn is_approval_threshold_reached(&self) -> bool {
+        let approval_votes_fraction =
+            Perbill::from_rational_approximation(self.approvals, self.votes_count);
+        let required_threshold_fraction =
+            Perbill::from_percent(self.proposal.parameters.approval_threshold_percentage);
+
+        approval_votes_fraction.deconstruct() >= required_threshold_fraction.deconstruct()
+    }
+
+    // Slashing threshold reached for the proposal. Compares predefined parameter with 'approve'
+    // votes sum divided by actual votes count.
+    pub fn is_slashing_threshold_reached(&self) -> bool {
+        let slashing_votes_fraction =
+            Perbill::from_rational_approximation(self.slashes, self.votes_count);
+        let required_threshold_fraction =
+            Perbill::from_percent(self.proposal.parameters.slashing_threshold_percentage);
+
+        slashing_votes_fraction.deconstruct() >= required_threshold_fraction.deconstruct()
+    }
+
+    // All voters had voted
+    pub fn is_voting_completed(&self) -> bool {
+        self.votes_count == self.total_voters_count
+    }
+}
+
+/// Proposal executable code wrapper
+pub trait ProposalExecutable {
+    /// Executes proposal code
+    fn execute(&self) -> dispatch::Result;
+}
+
+/// Proposal code binary converter
+pub trait ProposalCodeDecoder<T: system::Trait> {
+    /// Converts proposal code binary to executable representation
+    fn decode_proposal(
+        proposal_type: u32,
+        proposal_code: Vec<u8>,
+    ) -> Result<Box<dyn ProposalExecutable>, &'static str>;
+}
+
+/// Balance alias
+pub type BalanceOf<T> =
+    <<T as stake::Trait>::Currency as Currency<<T as system::Trait>::AccountId>>::Balance;
+
+/// Balance alias for staking
+pub type NegativeImbalance<T> =
+    <<T as stake::Trait>::Currency as Currency<<T as system::Trait>::AccountId>>::NegativeImbalance;
+
+/// Balance type of runtime
+pub type CurrencyOf<T> = <T as stake::Trait>::Currency;
+
+/// Data container for the finalized proposal results
+pub(crate) struct FinalizedProposalData<
+    ProposalId,
+    BlockNumber,
+    ProposerId,
+    Balance,
+    StakeId,
+    AccountId,
+> {
+    /// Proposal id
+    pub proposal_id: ProposalId,
+
+    /// Proposal to be finalized
+    pub proposal: Proposal<BlockNumber, ProposerId, Balance, StakeId, AccountId>,
+
+    /// Proposal finalization status
+    pub status: ProposalDecisionStatus,
+
+    /// Proposal finalization block number
+    pub finalized_at: BlockNumber,
+}
+
+/// Data container for the approved proposal results
+pub(crate) struct ApprovedProposalData<
+    ProposalId,
+    BlockNumber,
+    ProposerId,
+    Balance,
+    StakeId,
+    AccountId,
+> {
+    /// Proposal id
+    pub proposal_id: ProposalId,
+
+    /// Proposal to be finalized
+    pub proposal: Proposal<BlockNumber, ProposerId, Balance, StakeId, AccountId>,
+
+    /// Proposal finalisation status data
+    pub finalisation_status_data: FinalizationData<BlockNumber, StakeId, AccountId>,
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::types::ProposalStatusResolution;
+    use crate::*;
+
+    // Alias introduced for simplicity of changing Proposal exact types.
+    type ProposalObject = Proposal<u64, u64, u64, u64, u64>;
+
+    #[test]
+    fn proposal_voting_period_expired() {
+        let mut proposal = ProposalObject::default();
+
+        proposal.created_at = 1;
+        proposal.parameters.voting_period = 3;
+
+        assert!(proposal.is_voting_period_expired(4));
+    }
+
+    #[test]
+    fn proposal_voting_period_not_expired() {
+        let mut proposal = ProposalObject::default();
+
+        proposal.created_at = 1;
+        proposal.parameters.voting_period = 3;
+
+        assert!(!proposal.is_voting_period_expired(3));
+    }
+
+    #[test]
+    fn proposal_grace_period_expired() {
+        let mut proposal = ProposalObject::default();
+
+        proposal.parameters.grace_period = 3;
+        proposal.status = ProposalStatus::finalized_successfully(
+            ProposalDecisionStatus::Approved(ApprovedProposalStatus::PendingExecution),
+            0,
+        );
+
+        assert!(proposal.is_grace_period_expired(4));
+    }
+
+    #[test]
+    fn proposal_grace_period_auto_expired() {
+        let mut proposal = ProposalObject::default();
+
+        proposal.parameters.grace_period = 0;
+        proposal.status = ProposalStatus::finalized_successfully(
+            ProposalDecisionStatus::Approved(ApprovedProposalStatus::PendingExecution),
+            0,
+        );
+
+        assert!(proposal.is_grace_period_expired(1));
+    }
+
+    #[test]
+    fn proposal_grace_period_not_expired() {
+        let mut proposal = ProposalObject::default();
+
+        proposal.parameters.grace_period = 3;
+
+        assert!(!proposal.is_grace_period_expired(3));
+    }
+
+    #[test]
+    fn proposal_grace_period_not_expired_because_of_not_approved_proposal() {
+        let mut proposal = ProposalObject::default();
+
+        proposal.parameters.grace_period = 3;
+
+        assert!(!proposal.is_grace_period_expired(3));
+    }
+
+    #[test]
+    fn define_proposal_decision_status_returns_expired() {
+        let mut proposal = ProposalObject::default();
+        let now = 5;
+        proposal.created_at = 1;
+        proposal.parameters.voting_period = 3;
+        proposal.parameters.approval_quorum_percentage = 80;
+        proposal.parameters.approval_threshold_percentage = 40;
+        proposal.parameters.slashing_quorum_percentage = 50;
+        proposal.parameters.slashing_threshold_percentage = 50;
+
+        proposal.voting_results.add_vote(VoteKind::Reject);
+        proposal.voting_results.add_vote(VoteKind::Approve);
+        proposal.voting_results.add_vote(VoteKind::Approve);
+
+        assert_eq!(
+            proposal.voting_results,
+            VotingResults {
+                abstentions: 0,
+                approvals: 2,
+                rejections: 1,
+                slashes: 0,
+            }
+        );
+
+        let expected_proposal_status = proposal.define_proposal_decision_status(5, now);
+        assert_eq!(
+            expected_proposal_status,
+            Some(ProposalDecisionStatus::Expired)
+        );
+    }
+
+    #[test]
+    fn define_proposal_decision_status_returns_approved() {
+        let now = 2;
+        let mut proposal = ProposalObject::default();
+        proposal.created_at = 1;
+        proposal.parameters.voting_period = 3;
+        proposal.parameters.approval_quorum_percentage = 60;
+        proposal.parameters.slashing_quorum_percentage = 50;
+        proposal.parameters.slashing_threshold_percentage = 50;
+
+        proposal.voting_results.add_vote(VoteKind::Reject);
+        proposal.voting_results.add_vote(VoteKind::Approve);
+        proposal.voting_results.add_vote(VoteKind::Approve);
+        proposal.voting_results.add_vote(VoteKind::Approve);
+
+        assert_eq!(
+            proposal.voting_results,
+            VotingResults {
+                abstentions: 0,
+                approvals: 3,
+                rejections: 1,
+                slashes: 0,
+            }
+        );
+
+        let expected_proposal_status = proposal.define_proposal_decision_status(5, now);
+        assert_eq!(
+            expected_proposal_status,
+            Some(ProposalDecisionStatus::Approved(
+                ApprovedProposalStatus::PendingExecution
+            ))
+        );
+    }
+
+    #[test]
+    fn define_proposal_decision_status_returns_rejected() {
+        let mut proposal = ProposalObject::default();
+        let now = 2;
+
+        proposal.created_at = 1;
+        proposal.parameters.voting_period = 3;
+        proposal.parameters.approval_quorum_percentage = 50;
+        proposal.parameters.approval_threshold_percentage = 51;
+        proposal.parameters.slashing_quorum_percentage = 50;
+        proposal.parameters.slashing_threshold_percentage = 50;
+
+        proposal.voting_results.add_vote(VoteKind::Reject);
+        proposal.voting_results.add_vote(VoteKind::Reject);
+        proposal.voting_results.add_vote(VoteKind::Abstain);
+        proposal.voting_results.add_vote(VoteKind::Approve);
+
+        assert_eq!(
+            proposal.voting_results,
+            VotingResults {
+                abstentions: 1,
+                approvals: 1,
+                rejections: 2,
+                slashes: 0,
+            }
+        );
+
+        let expected_proposal_status = proposal.define_proposal_decision_status(4, now);
+        assert_eq!(
+            expected_proposal_status,
+            Some(ProposalDecisionStatus::Rejected)
+        );
+    }
+
+    #[test]
+    fn define_proposal_decision_status_returns_slashed() {
+        let mut proposal = ProposalObject::default();
+        let now = 2;
+
+        proposal.created_at = 1;
+        proposal.parameters.voting_period = 3;
+        proposal.parameters.approval_quorum_percentage = 50;
+        proposal.parameters.approval_threshold_percentage = 50;
+        proposal.parameters.slashing_quorum_percentage = 50;
+        proposal.parameters.slashing_threshold_percentage = 50;
+
+        proposal.voting_results.add_vote(VoteKind::Slash);
+        proposal.voting_results.add_vote(VoteKind::Reject);
+        proposal.voting_results.add_vote(VoteKind::Abstain);
+        proposal.voting_results.add_vote(VoteKind::Slash);
+
+        assert_eq!(
+            proposal.voting_results,
+            VotingResults {
+                abstentions: 1,
+                approvals: 0,
+                rejections: 1,
+                slashes: 2,
+            }
+        );
+
+        let expected_proposal_status = proposal.define_proposal_decision_status(4, now);
+        assert_eq!(
+            expected_proposal_status,
+            Some(ProposalDecisionStatus::Slashed)
+        );
+    }
+
+    #[test]
+    fn define_proposal_decision_status_returns_none() {
+        let mut proposal = ProposalObject::default();
+        let now = 2;
+
+        proposal.created_at = 1;
+        proposal.parameters.voting_period = 3;
+        proposal.parameters.approval_quorum_percentage = 60;
+        proposal.parameters.slashing_quorum_percentage = 50;
+
+        proposal.voting_results.add_vote(VoteKind::Abstain);
+        assert_eq!(
+            proposal.voting_results,
+            VotingResults {
+                abstentions: 1,
+                approvals: 0,
+                rejections: 0,
+                slashes: 0,
+            }
+        );
+
+        let expected_proposal_status = proposal.define_proposal_decision_status(5, now);
+        assert_eq!(expected_proposal_status, None);
+    }
+
+    #[test]
+    fn define_proposal_decision_status_returns_approved_before_slashing_before_rejection() {
+        let mut proposal = ProposalObject::default();
+        let now = 2;
+
+        proposal.created_at = 1;
+        proposal.parameters.voting_period = 3;
+        proposal.parameters.approval_quorum_percentage = 50;
+        proposal.parameters.approval_threshold_percentage = 30;
+        proposal.parameters.slashing_quorum_percentage = 50;
+        proposal.parameters.slashing_threshold_percentage = 30;
+
+        proposal.voting_results.add_vote(VoteKind::Approve);
+        proposal.voting_results.add_vote(VoteKind::Approve);
+        proposal.voting_results.add_vote(VoteKind::Reject);
+        proposal.voting_results.add_vote(VoteKind::Reject);
+        proposal.voting_results.add_vote(VoteKind::Slash);
+        proposal.voting_results.add_vote(VoteKind::Slash);
+
+        assert_eq!(
+            proposal.voting_results,
+            VotingResults {
+                abstentions: 0,
+                approvals: 2,
+                rejections: 2,
+                slashes: 2,
+            }
+        );
+
+        let expected_proposal_status = proposal.define_proposal_decision_status(6, now);
+
+        assert_eq!(
+            expected_proposal_status,
+            Some(ProposalDecisionStatus::Approved(
+                ApprovedProposalStatus::PendingExecution
+            ))
+        );
+    }
+
+    #[test]
+    fn define_proposal_decision_status_returns_slashed_before_rejection() {
+        let mut proposal = ProposalObject::default();
+        let now = 2;
+
+        proposal.created_at = 1;
+        proposal.parameters.voting_period = 3;
+        proposal.parameters.approval_quorum_percentage = 50;
+        proposal.parameters.approval_threshold_percentage = 30;
+        proposal.parameters.slashing_quorum_percentage = 50;
+        proposal.parameters.slashing_threshold_percentage = 30;
+
+        proposal.voting_results.add_vote(VoteKind::Abstain);
+        proposal.voting_results.add_vote(VoteKind::Approve);
+        proposal.voting_results.add_vote(VoteKind::Reject);
+        proposal.voting_results.add_vote(VoteKind::Reject);
+        proposal.voting_results.add_vote(VoteKind::Slash);
+        proposal.voting_results.add_vote(VoteKind::Slash);
+
+        assert_eq!(
+            proposal.voting_results,
+            VotingResults {
+                abstentions: 1,
+                approvals: 1,
+                rejections: 2,
+                slashes: 2,
+            }
+        );
+
+        let expected_proposal_status = proposal.define_proposal_decision_status(6, now);
+
+        assert_eq!(
+            expected_proposal_status,
+            Some(ProposalDecisionStatus::Slashed)
+        );
+    }
+
+    #[test]
+    fn proposal_status_resolution_approval_quorum_works_correctly() {
+        let no_approval_quorum_proposal: Proposal<u64, u64, u64, u64, u64> = Proposal {
+            parameters: ProposalParameters {
+                approval_quorum_percentage: 63,
+                slashing_threshold_percentage: 63,
+                ..ProposalParameters::default()
+            },
+            ..Proposal::default()
+        };
+        let no_approval_proposal_status_resolution = ProposalStatusResolution {
+            proposal: &no_approval_quorum_proposal,
+            now: 20,
+            votes_count: 314,
+            total_voters_count: 500,
+            approvals: 3,
+            slashes: 3,
+        };
+
+        assert!(!no_approval_proposal_status_resolution.is_approval_quorum_reached());
+
+        let approval_quorum_proposal_status_resolution = ProposalStatusResolution {
+            votes_count: 315,
+            ..no_approval_proposal_status_resolution
+        };
+
+        assert!(approval_quorum_proposal_status_resolution.is_approval_quorum_reached());
+    }
+
+    #[test]
+    fn proposal_status_resolution_slashing_quorum_works_correctly() {
+        let no_slashing_quorum_proposal: Proposal<u64, u64, u64, u64, u64> = Proposal {
+            parameters: ProposalParameters {
+                approval_quorum_percentage: 63,
+                slashing_quorum_percentage: 63,
+                ..ProposalParameters::default()
+            },
+            ..Proposal::default()
+        };
+        let no_slashing_proposal_status_resolution = ProposalStatusResolution {
+            proposal: &no_slashing_quorum_proposal,
+            now: 20,
+            votes_count: 314,
+            total_voters_count: 500,
+            approvals: 3,
+            slashes: 3,
+        };
+
+        assert!(!no_slashing_proposal_status_resolution.is_slashing_quorum_reached());
+
+        let slashing_quorum_proposal_status_resolution = ProposalStatusResolution {
+            votes_count: 315,
+            ..no_slashing_proposal_status_resolution
+        };
+
+        assert!(slashing_quorum_proposal_status_resolution.is_slashing_quorum_reached());
+    }
+
+    #[test]
+    fn proposal_status_resolution_approval_threshold_works_correctly() {
+        let no_approval_threshold_proposal: Proposal<u64, u64, u64, u64, u64> = Proposal {
+            parameters: ProposalParameters {
+                slashing_threshold_percentage: 63,
+                approval_threshold_percentage: 63,
+                ..ProposalParameters::default()
+            },
+            ..Proposal::default()
+        };
+        let no_approval_proposal_status_resolution = ProposalStatusResolution {
+            proposal: &no_approval_threshold_proposal,
+            now: 20,
+            votes_count: 500,
+            total_voters_count: 600,
+            approvals: 314,
+            slashes: 3,
+        };
+
+        assert!(!no_approval_proposal_status_resolution.is_approval_threshold_reached());
+
+        let approval_threshold_proposal_status_resolution = ProposalStatusResolution {
+            approvals: 315,
+            ..no_approval_proposal_status_resolution
+        };
+
+        assert!(approval_threshold_proposal_status_resolution.is_approval_threshold_reached());
+    }
+
+    #[test]
+    fn proposal_status_resolution_slashing_threshold_works_correctly() {
+        let no_slashing_threshold_proposal: Proposal<u64, u64, u64, u64, u64> = Proposal {
+            parameters: ProposalParameters {
+                slashing_threshold_percentage: 63,
+                approval_threshold_percentage: 63,
+                ..ProposalParameters::default()
+            },
+            ..Proposal::default()
+        };
+        let no_slashing_proposal_status_resolution = ProposalStatusResolution {
+            proposal: &no_slashing_threshold_proposal,
+            now: 20,
+            votes_count: 500,
+            total_voters_count: 600,
+            approvals: 3,
+            slashes: 314,
+        };
+
+        assert!(!no_slashing_proposal_status_resolution.is_slashing_threshold_reached());
+
+        let slashing_threshold_proposal_status_resolution = ProposalStatusResolution {
+            slashes: 315,
+            ..no_slashing_proposal_status_resolution
+        };
+
+        assert!(slashing_threshold_proposal_status_resolution.is_slashing_threshold_reached());
+    }
+}

+ 197 - 0
runtime-modules/proposals/engine/src/types/proposal_statuses.rs

@@ -0,0 +1,197 @@
+#![warn(missing_docs)]
+
+use codec::{Decode, Encode};
+use rstd::prelude::*;
+
+use crate::ActiveStake;
+#[cfg(feature = "std")]
+use serde::{Deserialize, Serialize};
+
+/// Current status of the proposal
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)]
+pub enum ProposalStatus<BlockNumber, StakeId, AccountId> {
+    /// A new proposal status that is available for voting (with optional stake data).
+    Active(Option<ActiveStake<StakeId, AccountId>>),
+
+    /// The proposal decision was made.
+    Finalized(FinalizationData<BlockNumber, StakeId, AccountId>),
+}
+
+impl<BlockNumber, StakeId, AccountId> Default for ProposalStatus<BlockNumber, StakeId, AccountId> {
+    fn default() -> Self {
+        ProposalStatus::Active(None)
+    }
+}
+
+impl<BlockNumber, StakeId, AccountId> ProposalStatus<BlockNumber, StakeId, AccountId> {
+    /// Creates finalized proposal status with provided ProposalDecisionStatus
+    pub fn finalized_successfully(
+        decision_status: ProposalDecisionStatus,
+        now: BlockNumber,
+    ) -> ProposalStatus<BlockNumber, StakeId, AccountId> {
+        Self::finalized(decision_status, None, None, now)
+    }
+
+    /// Creates finalized proposal status with provided ProposalDecisionStatus and error
+    pub fn finalized(
+        decision_status: ProposalDecisionStatus,
+        encoded_unstaking_error_due_to_broken_runtime: Option<&str>,
+        active_stake: Option<ActiveStake<StakeId, AccountId>>,
+        now: BlockNumber,
+    ) -> ProposalStatus<BlockNumber, StakeId, AccountId> {
+        // drop the stake information if there were no errors on unstaking
+        let actual_stake = if encoded_unstaking_error_due_to_broken_runtime.is_some() {
+            active_stake
+        } else {
+            None
+        };
+        ProposalStatus::Finalized(FinalizationData {
+            proposal_status: decision_status,
+            encoded_unstaking_error_due_to_broken_runtime:
+                encoded_unstaking_error_due_to_broken_runtime.map(|err| err.as_bytes().to_vec()),
+            finalized_at: now,
+            stake_data_after_unstaking_error: actual_stake,
+        })
+    }
+
+    /// Creates finalized and approved proposal status with provided ApprovedProposalStatus
+    pub fn approved(
+        approved_status: ApprovedProposalStatus,
+        now: BlockNumber,
+    ) -> ProposalStatus<BlockNumber, StakeId, AccountId> {
+        ProposalStatus::Finalized(FinalizationData {
+            proposal_status: ProposalDecisionStatus::Approved(approved_status),
+            encoded_unstaking_error_due_to_broken_runtime: None,
+            finalized_at: now,
+            stake_data_after_unstaking_error: None,
+        })
+    }
+}
+
+/// Final proposal status and potential error.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)]
+pub struct FinalizationData<BlockNumber, StakeId, AccountId> {
+    /// Final proposal status
+    pub proposal_status: ProposalDecisionStatus,
+
+    /// Proposal finalization block number
+    pub finalized_at: BlockNumber,
+
+    /// Error occured during the proposal finalization - unstaking failed in the stake module
+    pub encoded_unstaking_error_due_to_broken_runtime: Option<Vec<u8>>,
+
+    /// Stake data for the proposal, filled if the unstaking wasn't successful
+    pub stake_data_after_unstaking_error: Option<ActiveStake<StakeId, AccountId>>,
+}
+
+impl<BlockNumber, StakeId, AccountId> FinalizationData<BlockNumber, StakeId, AccountId> {
+    /// FinalizationData helper, creates ApprovedProposalStatus
+    pub fn create_approved_proposal_status(
+        self,
+        approved_status: ApprovedProposalStatus,
+    ) -> ProposalStatus<BlockNumber, StakeId, AccountId> {
+        ProposalStatus::Finalized(FinalizationData {
+            proposal_status: ProposalDecisionStatus::Approved(approved_status),
+            ..self
+        })
+    }
+}
+
+/// Status of the approved proposal. Defines execution stages.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)]
+pub enum ApprovedProposalStatus {
+    /// A proposal was approved and grace period is in effect
+    PendingExecution,
+
+    /// Proposal was successfully executed
+    Executed,
+
+    /// Proposal was executed and failed with an error
+    ExecutionFailed {
+        /// Error message
+        error: Vec<u8>,
+    },
+}
+
+impl ApprovedProposalStatus {
+    /// ApprovedProposalStatus helper, creates ExecutionFailed approved proposal status
+    pub fn failed_execution(err: &str) -> ApprovedProposalStatus {
+        ApprovedProposalStatus::ExecutionFailed {
+            error: err.as_bytes().to_vec(),
+        }
+    }
+}
+
+/// Status for the proposal with finalized decision
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)]
+pub enum ProposalDecisionStatus {
+    /// Proposal was withdrawn by its proposer.
+    Canceled,
+
+    /// Proposal was vetoed by root.
+    Vetoed,
+
+    /// A proposal was rejected
+    Rejected,
+
+    /// A proposal was rejected ans its stake should be slashed
+    Slashed,
+
+    /// Not enough votes and voting period expired.
+    Expired,
+
+    /// To clear the quorum requirement, the percentage of council members with revealed votes
+    /// must be no less than the quorum value for the given proposal type.
+    Approved(ApprovedProposalStatus),
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::{
+        ActiveStake, ApprovedProposalStatus, FinalizationData, ProposalDecisionStatus,
+        ProposalStatus,
+    };
+
+    #[test]
+    fn approved_proposal_status_helper_succeeds() {
+        let msg = "error";
+
+        assert_eq!(
+            ApprovedProposalStatus::failed_execution(&msg),
+            ApprovedProposalStatus::ExecutionFailed {
+                error: msg.as_bytes().to_vec()
+            }
+        );
+    }
+
+    #[test]
+    fn finalized_proposal_status_helper_succeeds() {
+        let msg = "error";
+        let block_number = 20;
+        let stake = ActiveStake {
+            stake_id: 50,
+            source_account_id: 2,
+        };
+
+        let proposal_status = ProposalStatus::finalized(
+            ProposalDecisionStatus::Slashed,
+            Some(msg),
+            Some(stake),
+            block_number,
+        );
+
+        assert_eq!(
+            ProposalStatus::Finalized(FinalizationData {
+                proposal_status: ProposalDecisionStatus::Slashed,
+                finalized_at: block_number,
+                encoded_unstaking_error_due_to_broken_runtime: Some(msg.as_bytes().to_vec()),
+                stake_data_after_unstaking_error: Some(stake)
+            }),
+            proposal_status
+        );
+    }
+}

+ 247 - 0
runtime-modules/proposals/engine/src/types/stakes.rs

@@ -0,0 +1,247 @@
+#![warn(missing_docs)]
+
+use super::{BalanceOf, CurrencyOf, NegativeImbalance};
+use crate::Trait;
+use rstd::convert::From;
+use rstd::marker::PhantomData;
+use rstd::rc::Rc;
+use srml_support::traits::{Currency, ExistenceRequirement, WithdrawReasons};
+
+// Mocking dependencies for testing
+#[cfg(test)]
+use mockall::predicate::*;
+#[cfg(test)]
+use mockall::*;
+
+/// Returns registered stake handler. This is scaffolds for the mocking of the stake module.
+pub trait StakeHandlerProvider<T: Trait> {
+    /// Returns stake logic handler
+    fn stakes() -> Rc<dyn StakeHandler<T>>;
+}
+
+/// Default implementation of the stake module logic provider. Returns actual implementation
+/// dependent on the stake module.
+pub struct DefaultStakeHandlerProvider;
+impl<T: Trait> StakeHandlerProvider<T> for DefaultStakeHandlerProvider {
+    /// Returns stake logic handler
+    fn stakes() -> Rc<dyn StakeHandler<T>> {
+        Rc::new(DefaultStakeHandler {
+            marker: PhantomData::<T>::default(),
+        })
+    }
+}
+
+/// Stake logic handler.
+#[cfg_attr(test, automock)] // attributes creates mocks in testing environment
+pub trait StakeHandler<T: Trait> {
+    /// Creates a stake. Returns created stake id or an error.
+    fn create_stake(&self) -> Result<T::StakeId, &'static str>;
+
+    /// Stake the imbalance
+    fn stake(
+        &self,
+        stake_id: &T::StakeId,
+        stake_imbalance: NegativeImbalance<T>,
+    ) -> Result<(), &'static str>;
+
+    /// Removes stake
+    fn remove_stake(&self, stake_id: T::StakeId) -> Result<(), &'static str>;
+
+    /// Execute unstaking
+    fn unstake(&self, stake_id: T::StakeId) -> Result<(), &'static str>;
+
+    /// Slash balance from the existing stake
+    fn slash(&self, stake_id: T::StakeId, slash_balance: BalanceOf<T>) -> Result<(), &'static str>;
+
+    /// Withdraw some balance from the source account and create stake imbalance
+    fn make_stake_imbalance(
+        &self,
+        balance: BalanceOf<T>,
+        source_account_id: &T::AccountId,
+    ) -> Result<NegativeImbalance<T>, &'static str>;
+}
+
+/// Default implementation of the stake logic. Uses actual stake module.
+/// 'marker' responsible for the 'Trait' binding.
+pub(crate) struct DefaultStakeHandler<T> {
+    pub marker: PhantomData<T>,
+}
+
+impl<T: Trait> StakeHandler<T> for DefaultStakeHandler<T> {
+    /// Creates a stake. Returns created stake id or an error.
+    fn create_stake(&self) -> Result<<T as stake::Trait>::StakeId, &'static str> {
+        Ok(stake::Module::<T>::create_stake())
+    }
+
+    /// Stake the imbalance
+    fn stake(
+        &self,
+        stake_id: &<T as stake::Trait>::StakeId,
+        stake_imbalance: NegativeImbalance<T>,
+    ) -> Result<(), &'static str> {
+        stake::Module::<T>::stake(&stake_id, stake_imbalance).map_err(WrappedError)?;
+
+        Ok(())
+    }
+
+    /// Removes stake
+    fn remove_stake(&self, stake_id: <T as stake::Trait>::StakeId) -> Result<(), &'static str> {
+        stake::Module::<T>::remove_stake(&stake_id).map_err(WrappedError)?;
+
+        Ok(())
+    }
+
+    /// Execute unstaking
+    fn unstake(&self, stake_id: <T as stake::Trait>::StakeId) -> Result<(), &'static str> {
+        stake::Module::<T>::initiate_unstaking(&stake_id, None).map_err(WrappedError)?;
+
+        Ok(())
+    }
+
+    /// Slash balance from the existing stake
+    fn slash(
+        &self,
+        stake_id: <T as stake::Trait>::StakeId,
+        slash_balance: BalanceOf<T>,
+    ) -> Result<(), &'static str> {
+        let _ignored_successful_result =
+            stake::Module::<T>::slash_immediate(&stake_id, slash_balance, false)
+                .map_err(WrappedError)?;
+
+        Ok(())
+    }
+
+    /// Withdraw some balance from the source account and create stake imbalance
+    fn make_stake_imbalance(
+        &self,
+        balance: BalanceOf<T>,
+        source_account_id: &T::AccountId,
+    ) -> Result<NegativeImbalance<T>, &'static str> {
+        CurrencyOf::<T>::withdraw(
+            source_account_id,
+            balance,
+            WithdrawReasons::all(),
+            ExistenceRequirement::AllowDeath,
+        )
+    }
+}
+
+/// Proposal implementation of the stake logic.
+/// 'marker' responsible for the 'Trait' binding.
+pub(crate) struct ProposalStakeManager<T> {
+    pub marker: PhantomData<T>,
+}
+
+impl<T: Trait> ProposalStakeManager<T> {
+    /// Creates a stake using stake balance and source account.
+    /// Returns created stake id or an error.
+    pub fn create_stake(
+        stake_balance: BalanceOf<T>,
+        source_account_id: T::AccountId,
+    ) -> Result<T::StakeId, &'static str> {
+        let stake_id = T::StakeHandlerProvider::stakes().create_stake()?;
+
+        let stake_imbalance = T::StakeHandlerProvider::stakes()
+            .make_stake_imbalance(stake_balance, &source_account_id)?;
+
+        T::StakeHandlerProvider::stakes().stake(&stake_id, stake_imbalance)?;
+
+        Ok(stake_id)
+    }
+
+    /// Execute unstaking and removes the stake
+    pub fn remove_stake(stake_id: T::StakeId) -> Result<(), &'static str> {
+        T::StakeHandlerProvider::stakes().unstake(stake_id)?;
+
+        T::StakeHandlerProvider::stakes().remove_stake(stake_id)?;
+
+        Ok(())
+    }
+
+    /// Slash balance from the existing stake
+    pub fn slash(stake_id: T::StakeId, slash_balance: BalanceOf<T>) -> Result<(), &'static str> {
+        T::StakeHandlerProvider::stakes().slash(stake_id, slash_balance)
+    }
+}
+
+// 'New type' pattern for the error
+// https://doc.rust-lang.org/book/ch19-03-advanced-traits.html#using-the-newtype-pattern-to-implement-external-traits-on-external-types
+struct WrappedError<E>(E);
+
+// error conversion for the Wrapped StakeActionError with the inner InitiateUnstakingError
+impl From<WrappedError<stake::StakeActionError<stake::InitiateUnstakingError>>> for &str {
+    fn from(wrapper: WrappedError<stake::StakeActionError<stake::InitiateUnstakingError>>) -> Self {
+        {
+            match wrapper.0 {
+                stake::StakeActionError::StakeNotFound => "StakeNotFound",
+                stake::StakeActionError::Error(err) => match err {
+                    stake::InitiateUnstakingError::UnstakingPeriodShouldBeGreaterThanZero => {
+                        "UnstakingPeriodShouldBeGreaterThanZero"
+                    }
+                    stake::InitiateUnstakingError::UnstakingError(e) => match e {
+                        stake::UnstakingError::NotStaked => "NotStaked",
+                        stake::UnstakingError::AlreadyUnstaking => "AlreadyUnstaking",
+                        stake::UnstakingError::CannotUnstakeWhileSlashesOngoing => {
+                            "CannotUnstakeWhileSlashesOngoing"
+                        }
+                    },
+                },
+            }
+        }
+    }
+}
+
+// error conversion for the Wrapped StakeActionError with the inner StakingError
+impl From<WrappedError<stake::StakeActionError<stake::StakingError>>> for &str {
+    fn from(wrapper: WrappedError<stake::StakeActionError<stake::StakingError>>) -> Self {
+        {
+            match wrapper.0 {
+                stake::StakeActionError::StakeNotFound => "StakeNotFound",
+                stake::StakeActionError::Error(err) => match err {
+                    stake::StakingError::CannotStakeZero => "CannotStakeZero",
+                    stake::StakingError::CannotStakeLessThanMinimumBalance => {
+                        "CannotStakeLessThanMinimumBalance"
+                    }
+                    stake::StakingError::AlreadyStaked => "AlreadyStaked",
+                },
+            }
+        }
+    }
+}
+
+// error conversion for the Wrapped StakeActionError with the inner InitiateSlashingError
+impl From<WrappedError<stake::StakeActionError<stake::InitiateSlashingError>>> for &str {
+    fn from(wrapper: WrappedError<stake::StakeActionError<stake::InitiateSlashingError>>) -> Self {
+        {
+            match wrapper.0 {
+                stake::StakeActionError::StakeNotFound => "StakeNotFound",
+                stake::StakeActionError::Error(err) => match err {
+                    stake::InitiateSlashingError::NotStaked => "NotStaked",
+                    stake::InitiateSlashingError::SlashPeriodShouldBeGreaterThanZero => {
+                        "SlashPeriodShouldBeGreaterThanZero"
+                    }
+                    stake::InitiateSlashingError::SlashAmountShouldBeGreaterThanZero => {
+                        "SlashAmountShouldBeGreaterThanZero"
+                    }
+                },
+            }
+        }
+    }
+}
+
+// error conversion for the Wrapped StakeActionError with the inner ImmediateSlashingError
+impl From<WrappedError<stake::StakeActionError<stake::ImmediateSlashingError>>> for &str {
+    fn from(wrapper: WrappedError<stake::StakeActionError<stake::ImmediateSlashingError>>) -> Self {
+        {
+            match wrapper.0 {
+                stake::StakeActionError::StakeNotFound => "StakeNotFound",
+                stake::StakeActionError::Error(err) => match err {
+                    stake::ImmediateSlashingError::NotStaked => "NotStaked",
+                    stake::ImmediateSlashingError::SlashAmountShouldBeGreaterThanZero => {
+                        "SlashAmountShouldBeGreaterThanZero"
+                    }
+                },
+            }
+        }
+    }
+}

+ 7 - 1
runtime-modules/recurring-reward/src/lib.rs

@@ -1,3 +1,10 @@
+// Clippy linter warning. TODO: remove after the Constaninople release
+#![allow(clippy::type_complexity)]
+// disable it because of possible frontend API break
+
+// Clippy linter warning. TODO: refactor the Option<Option<>>
+#![allow(clippy::option_option)] // disable it because of possible API break
+
 // Ensure we're `no_std` when compiling for Wasm.
 #![cfg_attr(not(feature = "std"), no_std)]
 use rstd::prelude::*;
@@ -7,7 +14,6 @@ use runtime_primitives::traits::{MaybeSerialize, Member, One, SimpleArithmetic,
 use srml_support::{decl_module, decl_storage, ensure, Parameter};
 
 use minting::{self, BalanceOf};
-use system;
 
 mod mock;
 mod tests;

+ 1 - 1
runtime-modules/roles/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 name = 'substrate-roles-module'
-version = '1.0.0'
+version = '1.0.1'
 authors = ['Joystream contributors']
 edition = '2018'
 

+ 14 - 6
runtime-modules/roles/src/actors.rs

@@ -1,3 +1,7 @@
+// Clippy linter warning
+#![allow(clippy::redundant_closure_call)] // disable it because of the substrate lib design
+                                          // example:  pub Parameters get(parameters) build(|config: &GenesisConfig| {..}
+
 use codec::{Decode, Encode};
 use common::currency::{BalanceOf, GovernanceCurrency};
 use rstd::prelude::*;
@@ -8,10 +12,14 @@ use srml_support::traits::{
 use srml_support::{decl_event, decl_module, decl_storage, ensure};
 use system::{self, ensure_root, ensure_signed};
 
+#[cfg(feature = "std")]
+use serde::{Deserialize, Serialize};
+
 pub use membership::members::Role;
 
 const STAKING_ID: LockIdentifier = *b"role_stk";
 
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Copy, Clone, Eq, PartialEq, Debug)]
 pub struct RoleParameters<Balance, BlockNumber> {
     // minium balance required to stake to enter a role
@@ -59,7 +67,7 @@ impl<Balance: From<u32>, BlockNumber: From<u32>> Default for RoleParameters<Bala
             entry_request_fee: Balance::from(50),
 
             // not currently used
-            min_actors: 5,
+            min_actors: 1,
             bonding_period: BlockNumber::from(600),
             min_service_period: BlockNumber::from(600),
             startup_grace_period: BlockNumber::from(600),
@@ -176,19 +184,19 @@ impl<T: Trait> Module<T> {
     fn remove_actor_from_service(actor_account: T::AccountId, role: Role, member_id: MemberId<T>) {
         let accounts: Vec<T::AccountId> = Self::account_ids_by_role(role)
             .into_iter()
-            .filter(|account| !(*account == actor_account))
+            .filter(|account| *account != actor_account)
             .collect();
         <AccountIdsByRole<T>>::insert(role, accounts);
 
         let accounts: Vec<T::AccountId> = Self::account_ids_by_member_id(&member_id)
             .into_iter()
-            .filter(|account| !(*account == actor_account))
+            .filter(|account| *account != actor_account)
             .collect();
         <AccountIdsByMemberId<T>>::insert(&member_id, accounts);
 
         let accounts: Vec<T::AccountId> = Self::actor_account_ids()
             .into_iter()
-            .filter(|account| !(*account == actor_account))
+            .filter(|account| *account != actor_account)
             .collect();
         <ActorAccountIds<T>>::put(accounts);
 
@@ -258,7 +266,7 @@ decl_module! {
                 if let Some(params) = Self::parameters(role) {
                     if !(now % params.reward_period == T::BlockNumber::zero()) { continue }
                     let accounts = Self::account_ids_by_role(role);
-                    for actor in accounts.into_iter().map(|account| Self::actor_by_account_id(account)) {
+                    for actor in accounts.into_iter().map(Self::actor_by_account_id) {
                         if let Some(actor) = actor {
                             if now > actor.joined_at + params.reward_period {
                                 // reward can top up balance if it is below minimum stake requirement
@@ -377,7 +385,7 @@ decl_module! {
 
         pub fn set_role_parameters(origin, role: Role, params: RoleParameters<BalanceOf<T>, T::BlockNumber>) {
             ensure_root(origin)?;
-            let new_stake = params.min_stake.clone();
+            let new_stake = params.min_stake;
             <Parameters<T>>::insert(role, params);
             // Update locks for all actors in the role. The lock for each account is already until max_value
             // It doesn't affect actors which are unbonding, they should have already been removed from AccountIdsByRole

+ 0 - 1
runtime-modules/roles/src/traits.rs

@@ -1,5 +1,4 @@
 use crate::actors;
-use system;
 
 // Roles
 pub trait Roles<T: system::Trait> {

+ 1 - 1
runtime-modules/service-discovery/src/discovery.rs

@@ -100,7 +100,7 @@ decl_module! {
                 expires_at: <system::Module<T>>::block_number() + ttl,
             });
 
-            Self::deposit_event(RawEvent::AccountInfoUpdated(sender.clone(), id.clone()));
+            Self::deposit_event(RawEvent::AccountInfoUpdated(sender, id));
         }
 
         pub fn unset_ipns_id(origin) {

+ 9 - 13
runtime-modules/stake/src/lib.rs

@@ -12,7 +12,6 @@ use srml_support::traits::{Currency, ExistenceRequirement, Get, Imbalance, Withd
 use srml_support::{decl_module, decl_storage, ensure, Parameter};
 
 use rstd::collections::btree_map::BTreeMap;
-use system;
 
 mod errors;
 pub use errors::*;
@@ -378,9 +377,9 @@ where
 
                     Ok((stake_to_reduce, staked_state.staked_amount))
                 }
-                _ => return Err(DecreasingStakeError::CannotDecreaseStakeWhileUnstaking),
+                _ => Err(DecreasingStakeError::CannotDecreaseStakeWhileUnstaking),
             },
-            _ => return Err(DecreasingStakeError::NotStaked),
+            _ => Err(DecreasingStakeError::NotStaked),
         }
     }
 
@@ -463,11 +462,9 @@ where
                 );
 
                 // pause Unstaking if unstaking is active
-                match staked_state.staked_status {
-                    StakedStatus::Unstaking(ref mut unstaking_state) => {
-                        unstaking_state.is_active = false;
-                    }
-                    _ => (),
+                if let StakedStatus::Unstaking(ref mut unstaking_state) = staked_state.staked_status
+                {
+                    unstaking_state.is_active = false;
                 }
 
                 Ok(slash_id)
@@ -523,11 +520,10 @@ where
 
                 // unpause unstaking on last ongoing slash cancelled
                 if staked_state.ongoing_slashes.is_empty() {
-                    match staked_state.staked_status {
-                        StakedStatus::Unstaking(ref mut unstaking_state) => {
-                            unstaking_state.is_active = true;
-                        }
-                        _ => (),
+                    if let StakedStatus::Unstaking(ref mut unstaking_state) =
+                        staked_state.staked_status
+                    {
+                        unstaking_state.is_active = true;
                     }
                 }
 

+ 7 - 7
runtime-modules/stake/src/tests.rs

@@ -321,7 +321,7 @@ fn decreasing_stake() {
                 Stake {
                     created: 0,
                     staking_status: StakingStatus::Staked(StakedState {
-                        staked_amount: staked_amount,
+                        staked_amount,
                         ongoing_slashes: BTreeMap::new(),
                         next_slash_id: 0,
                         staked_status: StakedStatus::Normal,
@@ -379,7 +379,7 @@ fn initiating_pausing_resuming_cancelling_slashes() {
             Stake {
                 created: System::block_number(),
                 staking_status: StakingStatus::Staked(StakedState {
-                    staked_amount: staked_amount,
+                    staked_amount,
                     ongoing_slashes: BTreeMap::new(),
                     next_slash_id: 0,
                     staked_status: StakedStatus::Unstaking(UnstakingState {
@@ -413,7 +413,7 @@ fn initiating_pausing_resuming_cancelling_slashes() {
             Stake {
                 created: System::block_number(),
                 staking_status: StakingStatus::Staked(StakedState {
-                    staked_amount: staked_amount,
+                    staked_amount,
                     ongoing_slashes: expected_ongoing_slashes.clone(),
                     next_slash_id: slash_id + 1,
                     staked_status: StakedStatus::Unstaking(UnstakingState {
@@ -449,7 +449,7 @@ fn initiating_pausing_resuming_cancelling_slashes() {
             Stake {
                 created: System::block_number(),
                 staking_status: StakingStatus::Staked(StakedState {
-                    staked_amount: staked_amount,
+                    staked_amount,
                     ongoing_slashes: expected_ongoing_slashes.clone(),
                     next_slash_id: slash_id + 1,
                     staked_status: StakedStatus::Unstaking(UnstakingState {
@@ -485,7 +485,7 @@ fn initiating_pausing_resuming_cancelling_slashes() {
             Stake {
                 created: System::block_number(),
                 staking_status: StakingStatus::Staked(StakedState {
-                    staked_amount: staked_amount,
+                    staked_amount,
                     ongoing_slashes: expected_ongoing_slashes.clone(),
                     next_slash_id: slash_id + 1,
                     staked_status: StakedStatus::Unstaking(UnstakingState {
@@ -512,7 +512,7 @@ fn initiating_pausing_resuming_cancelling_slashes() {
             Stake {
                 created: System::block_number(),
                 staking_status: StakingStatus::Staked(StakedState {
-                    staked_amount: staked_amount,
+                    staked_amount,
                     ongoing_slashes: BTreeMap::new(),
                     next_slash_id: slash_id + 1,
                     staked_status: StakedStatus::Unstaking(UnstakingState {
@@ -545,7 +545,7 @@ fn initiating_pausing_resuming_cancelling_slashes() {
             Stake {
                 created: System::block_number(),
                 staking_status: StakingStatus::Staked(StakedState {
-                    staked_amount: staked_amount,
+                    staked_amount,
                     ongoing_slashes: expected_ongoing_slashes.clone(),
                     next_slash_id: slash_id + 1,
                     staked_status: StakedStatus::Unstaking(UnstakingState {

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff