Browse Source

Merge pull request #190 from Joystream/monorepo

Migrate monorepo to development branch
Mokhtar Naamani 5 years ago
parent
commit
620094ef5f
100 changed files with 15346 additions and 4790 deletions
  1. 2 0
      .gitignore
  2. 0 4378
      Cargo.lock
  3. 18 303
      Cargo.toml
  4. 32 0
      runtime-modules/common/Cargo.toml
  5. 0 0
      runtime-modules/common/src/currency.rs
  6. 4 0
      runtime-modules/common/src/lib.rs
  7. 130 0
      runtime-modules/content-working-group/Cargo.toml
  8. 1 1
      runtime-modules/content-working-group/src/genesis.rs
  9. 10 7
      runtime-modules/content-working-group/src/lib.rs
  10. 9 5
      runtime-modules/content-working-group/src/mock.rs
  11. 0 0
      runtime-modules/content-working-group/src/mod.rs
  12. 57 56
      runtime-modules/content-working-group/src/tests.rs
  13. 50 0
      runtime-modules/forum/Cargo.toml
  14. 1318 0
      runtime-modules/forum/src/lib.rs
  15. 530 0
      runtime-modules/forum/src/mock.rs
  16. 981 0
      runtime-modules/forum/src/tests.rs
  17. 89 0
      runtime-modules/governance/Cargo.toml
  18. 2 2
      runtime-modules/governance/src/council.rs
  19. 2 4
      runtime-modules/governance/src/election.rs
  20. 3 0
      runtime-modules/governance/src/lib.rs
  21. 1 2
      runtime-modules/governance/src/mock.rs
  22. 6 6
      runtime-modules/governance/src/proposals.rs
  23. 0 0
      runtime-modules/governance/src/sealed_vote.rs
  24. 0 0
      runtime-modules/governance/src/stake.rs
  25. 60 0
      runtime-modules/hiring/Cargo.toml
  26. 163 0
      runtime-modules/hiring/src/hiring/application.rs
  27. 157 0
      runtime-modules/hiring/src/hiring/mod.rs
  28. 448 0
      runtime-modules/hiring/src/hiring/opening.rs
  29. 79 0
      runtime-modules/hiring/src/hiring/staking_policy.rs
  30. 1526 0
      runtime-modules/hiring/src/lib.rs
  31. 188 0
      runtime-modules/hiring/src/macroes.rs
  32. 204 0
      runtime-modules/hiring/src/mock.rs
  33. 27 0
      runtime-modules/hiring/src/test/mod.rs
  34. 437 0
      runtime-modules/hiring/src/test/public_api/add_application.rs
  35. 198 0
      runtime-modules/hiring/src/test/public_api/add_opening.rs
  36. 64 0
      runtime-modules/hiring/src/test/public_api/begin_accepting_applications.rs
  37. 55 0
      runtime-modules/hiring/src/test/public_api/begin_review.rs
  38. 431 0
      runtime-modules/hiring/src/test/public_api/cancel_opening.rs
  39. 382 0
      runtime-modules/hiring/src/test/public_api/deactivate_application.rs
  40. 365 0
      runtime-modules/hiring/src/test/public_api/ensure_can_add_application.rs
  41. 474 0
      runtime-modules/hiring/src/test/public_api/fill_opening.rs
  42. 50 0
      runtime-modules/hiring/src/test/public_api/mod.rs
  43. 177 0
      runtime-modules/hiring/src/test/public_api/on_finalize.rs
  44. 302 0
      runtime-modules/hiring/src/test/public_api/unstaked.rs
  45. 249 0
      runtime-modules/hiring/src/test/smoke.rs
  46. 83 0
      runtime-modules/hiring/src/test/staking_module/get_opt_stake_amount.rs
  47. 72 0
      runtime-modules/hiring/src/test/staking_module/infallible_stake_initiation_on_application.rs
  48. 111 0
      runtime-modules/hiring/src/test/staking_module/initiate_application_deactivations.rs
  49. 8 0
      runtime-modules/hiring/src/test/staking_module/mod.rs
  50. 39 0
      runtime-modules/hiring/src/test/staking_module/opt_infallible_unstake.rs
  51. 391 0
      runtime-modules/hiring/src/test/staking_module/try_to_initiate_application_deactivation.rs
  52. 221 0
      runtime-modules/hiring/src/test/staking_module/would_application_get_added.rs
  53. 83 0
      runtime-modules/membership/Cargo.toml
  54. 1 1
      runtime-modules/membership/src/genesis.rs
  55. 9 0
      runtime-modules/membership/src/lib.rs
  56. 1 1
      runtime-modules/membership/src/members.rs
  57. 1 1
      runtime-modules/membership/src/mock.rs
  58. 0 0
      runtime-modules/membership/src/role_types.rs
  59. 0 0
      runtime-modules/membership/src/tests.rs
  60. 58 0
      runtime-modules/memo/Cargo.toml
  61. 5 1
      runtime-modules/memo/src/lib.rs
  62. 56 0
      runtime-modules/recurring-reward/Cargo.toml
  63. 397 0
      runtime-modules/recurring-reward/src/lib.rs
  64. 103 0
      runtime-modules/recurring-reward/src/mock/mod.rs
  65. 64 0
      runtime-modules/recurring-reward/src/mock/status_handler.rs
  66. 259 0
      runtime-modules/recurring-reward/src/tests.rs
  67. 88 0
      runtime-modules/roles/Cargo.toml
  68. 1 2
      runtime-modules/roles/src/actors.rs
  69. 8 0
      runtime-modules/roles/src/lib.rs
  70. 1 2
      runtime-modules/roles/src/mock.rs
  71. 0 0
      runtime-modules/roles/src/tests.rs
  72. 26 0
      runtime-modules/roles/src/traits.rs
  73. 70 0
      runtime-modules/service-discovery/Cargo.toml
  74. 1 1
      runtime-modules/service-discovery/src/discovery.rs
  75. 7 0
      runtime-modules/service-discovery/src/lib.rs
  76. 2 2
      runtime-modules/service-discovery/src/mock.rs
  77. 0 0
      runtime-modules/service-discovery/src/tests.rs
  78. 50 0
      runtime-modules/stake/Cargo.toml
  79. 158 0
      runtime-modules/stake/src/errors.rs
  80. 1083 0
      runtime-modules/stake/src/lib.rs
  81. 19 0
      runtime-modules/stake/src/macroes.rs
  82. 115 0
      runtime-modules/stake/src/mock.rs
  83. 804 0
      runtime-modules/stake/src/tests.rs
  84. 96 0
      runtime-modules/storage/Cargo.toml
  85. 5 5
      runtime-modules/storage/src/data_directory.rs
  86. 5 4
      runtime-modules/storage/src/data_object_storage_registry.rs
  87. 1 1
      runtime-modules/storage/src/data_object_type_registry.rs
  88. 9 0
      runtime-modules/storage/src/lib.rs
  89. 4 5
      runtime-modules/storage/src/mock.rs
  90. 0 0
      runtime-modules/storage/src/tests.rs
  91. 20 0
      runtime-modules/storage/src/traits.rs
  92. 50 0
      runtime-modules/token-minting/Cargo.toml
  93. 260 0
      runtime-modules/token-minting/src/lib.rs
  94. 153 0
      runtime-modules/token-minting/src/mint.rs
  95. 90 0
      runtime-modules/token-minting/src/mock.rs
  96. 202 0
      runtime-modules/token-minting/src/tests.rs
  97. 44 0
      runtime-modules/versioned-store-permissions/Cargo.toml
  98. 28 0
      runtime-modules/versioned-store-permissions/src/constraint.rs
  99. 57 0
      runtime-modules/versioned-store-permissions/src/credentials.rs
  100. 646 0
      runtime-modules/versioned-store-permissions/src/lib.rs

+ 2 - 0
.gitignore

@@ -16,3 +16,5 @@ joystream_runtime.wasm
 
 # Vim
 .*.sw*
+
+**/Cargo.lock

+ 0 - 4378
Cargo.lock

@@ -1,4378 +0,0 @@
-# This file is automatically @generated by Cargo.
-# It is not intended for manual editing.
-[[package]]
-name = "adler32"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d2e7343e7fc9de883d1b0341e0b13970f764c14101234857d2ddafa1cb1cac2"
-
-[[package]]
-name = "aes-ctr"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2e5b0458ea3beae0d1d8c0f3946564f8e10f90646cf78c06b4351052058d1ee"
-dependencies = [
- "aes-soft",
- "aesni",
- "ctr",
- "stream-cipher",
-]
-
-[[package]]
-name = "aes-soft"
-version = "0.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cfd7e7ae3f9a1fb5c03b389fc6bb9a51400d0c13053f0dca698c832bfd893a0d"
-dependencies = [
- "block-cipher-trait",
- "byteorder 1.3.4",
- "opaque-debug",
-]
-
-[[package]]
-name = "aesni"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f70a6b5f971e473091ab7cfb5ffac6cde81666c4556751d8d5620ead8abf100"
-dependencies = [
- "block-cipher-trait",
- "opaque-debug",
- "stream-cipher",
-]
-
-[[package]]
-name = "ahash"
-version = "0.2.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f33b5018f120946c1dcf279194f238a9f146725593ead1c08fa47ff22b0b5d3"
-dependencies = [
- "const-random",
-]
-
-[[package]]
-name = "aho-corasick"
-version = "0.7.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d5e63fd144e18ba274ae7095c0197a870a7b9468abc801dd62f190d80817d2ec"
-dependencies = [
- "memchr",
-]
-
-[[package]]
-name = "arc-swap"
-version = "0.4.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7b8a9123b8027467bce0099fe556c628a53c8d83df0507084c31e9ba2e39aff"
-
-[[package]]
-name = "arrayref"
-version = "0.3.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
-
-[[package]]
-name = "arrayvec"
-version = "0.4.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9"
-dependencies = [
- "nodrop",
-]
-
-[[package]]
-name = "arrayvec"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8"
-
-[[package]]
-name = "asn1_der"
-version = "0.6.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6fce6b6a0ffdafebd82c87e79e3f40e8d2c523e5fea5566ff6b90509bf98d638"
-dependencies = [
- "asn1_der_derive",
-]
-
-[[package]]
-name = "asn1_der_derive"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d0864d84b8e07b145449be9a8537db86bf9de5ce03b913214694643b4743502"
-dependencies = [
- "quote 1.0.3",
- "syn 1.0.16",
-]
-
-[[package]]
-name = "autocfg"
-version = "0.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2"
-
-[[package]]
-name = "autocfg"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
-
-[[package]]
-name = "backtrace"
-version = "0.3.45"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad235dabf00f36301792cfe82499880ba54c6486be094d1047b02bacb67c14e8"
-dependencies = [
- "backtrace-sys",
- "cfg-if",
- "libc",
- "rustc-demangle",
-]
-
-[[package]]
-name = "backtrace-sys"
-version = "0.1.33"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e17b52e737c40a7d75abca20b29a19a0eb7ba9fc72c5a72dd282a0a3c2c0dc35"
-dependencies = [
- "cc",
- "libc",
-]
-
-[[package]]
-name = "base58"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5024ee8015f02155eee35c711107ddd9a9bf3cb689cf2a9089c97e79b6e1ae83"
-
-[[package]]
-name = "base64"
-version = "0.10.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e"
-dependencies = [
- "byteorder 1.3.4",
-]
-
-[[package]]
-name = "bitflags"
-version = "1.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
-
-[[package]]
-name = "bitmask"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5da9b3d9f6f585199287a473f4f8dfab6566cf827d15c00c219f53c645687ead"
-
-[[package]]
-name = "bitvec"
-version = "0.15.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a993f74b4c99c1908d156b8d2e0fb6277736b0ecbd833982fd1241d39b2766a6"
-
-[[package]]
-name = "blake2"
-version = "0.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94cb07b0da6a73955f8fb85d24c466778e70cda767a568229b104f0264089330"
-dependencies = [
- "byte-tools",
- "crypto-mac",
- "digest",
- "opaque-debug",
-]
-
-[[package]]
-name = "blake2-rfc"
-version = "0.2.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d6d530bdd2d52966a6d03b7a964add7ae1a288d25214066fd4b600f0f796400"
-dependencies = [
- "arrayvec 0.4.12",
- "constant_time_eq",
-]
-
-[[package]]
-name = "block-buffer"
-version = "0.7.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b"
-dependencies = [
- "block-padding",
- "byte-tools",
- "byteorder 1.3.4",
- "generic-array",
-]
-
-[[package]]
-name = "block-cipher-trait"
-version = "0.6.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1c924d49bd09e7c06003acda26cd9742e796e34282ec6c1189404dee0c1f4774"
-dependencies = [
- "generic-array",
-]
-
-[[package]]
-name = "block-padding"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5"
-dependencies = [
- "byte-tools",
-]
-
-[[package]]
-name = "bs58"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b170cd256a3f9fa6b9edae3e44a7dfdfc77e8124dbc3e2612d75f9c3e2396dae"
-
-[[package]]
-name = "bumpalo"
-version = "3.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f359dc14ff8911330a51ef78022d376f25ed00248912803b58f00cb1c27f742"
-
-[[package]]
-name = "byte-slice-cast"
-version = "0.3.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0a5e3906bcbf133e33c1d4d95afc664ad37fbdb9f6568d8043e7ea8c27d93d3"
-
-[[package]]
-name = "byte-tools"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
-
-[[package]]
-name = "byteorder"
-version = "0.5.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fc10e8cc6b2580fda3f36eb6dc5316657f812a3df879a44a66fc9f0fdbc4855"
-
-[[package]]
-name = "byteorder"
-version = "1.3.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
-
-[[package]]
-name = "bytes"
-version = "0.4.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c"
-dependencies = [
- "byteorder 1.3.4",
- "iovec",
-]
-
-[[package]]
-name = "bytes"
-version = "0.5.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "130aac562c0dd69c56b3b1cc8ffd2e17be31d0b6c25b61c96b76231aa23e39e1"
-
-[[package]]
-name = "c2-chacha"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "214238caa1bf3a496ec3392968969cab8549f96ff30652c9e56885329315f6bb"
-dependencies = [
- "ppv-lite86",
-]
-
-[[package]]
-name = "c_linked_list"
-version = "1.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4964518bd3b4a8190e832886cdc0da9794f12e8e6c1613a9e90ff331c4c8724b"
-
-[[package]]
-name = "cc"
-version = "1.0.50"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95e28fa049fda1c330bcf9d723be7663a899c4679724b34c81e9f5a326aab8cd"
-
-[[package]]
-name = "cfg-if"
-version = "0.1.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
-
-[[package]]
-name = "chrono"
-version = "0.4.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "31850b4a4d6bae316f7a09e691c944c28299298837edc0a03f755618c23cbc01"
-dependencies = [
- "num-integer",
- "num-traits",
- "time",
-]
-
-[[package]]
-name = "clear_on_drop"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97276801e127ffb46b66ce23f35cc96bd454fa311294bced4bbace7baa8b1d17"
-dependencies = [
- "cc",
-]
-
-[[package]]
-name = "cloudabi"
-version = "0.0.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
-dependencies = [
- "bitflags",
-]
-
-[[package]]
-name = "const-random"
-version = "0.1.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f1af9ac737b2dd2d577701e59fd09ba34822f6f2ebdb30a7647405d9e55e16a"
-dependencies = [
- "const-random-macro",
- "proc-macro-hack 0.5.11",
-]
-
-[[package]]
-name = "const-random-macro"
-version = "0.1.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25e4c606eb459dd29f7c57b2e0879f2b6f14ee130918c2b78ccb58a9624e6c7a"
-dependencies = [
- "getrandom",
- "proc-macro-hack 0.5.11",
-]
-
-[[package]]
-name = "constant_time_eq"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
-
-[[package]]
-name = "crc32fast"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1"
-dependencies = [
- "cfg-if",
-]
-
-[[package]]
-name = "crossbeam-channel"
-version = "0.3.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8ec7fcd21571dc78f96cc96243cab8d8f035247c3efd16c687be154c3fa9efa"
-dependencies = [
- "crossbeam-utils 0.6.6",
-]
-
-[[package]]
-name = "crossbeam-deque"
-version = "0.7.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f02af974daeee82218205558e51ec8768b48cf524bd01d550abe5573a608285"
-dependencies = [
- "crossbeam-epoch",
- "crossbeam-utils 0.7.2",
- "maybe-uninit",
-]
-
-[[package]]
-name = "crossbeam-epoch"
-version = "0.8.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace"
-dependencies = [
- "autocfg 1.0.0",
- "cfg-if",
- "crossbeam-utils 0.7.2",
- "lazy_static",
- "maybe-uninit",
- "memoffset",
- "scopeguard 1.1.0",
-]
-
-[[package]]
-name = "crossbeam-queue"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c695eeca1e7173472a32221542ae469b3e9aac3a4fc81f7696bcad82029493db"
-dependencies = [
- "cfg-if",
- "crossbeam-utils 0.7.2",
-]
-
-[[package]]
-name = "crossbeam-utils"
-version = "0.6.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04973fa96e96579258a5091af6003abde64af786b860f18622b82e026cca60e6"
-dependencies = [
- "cfg-if",
- "lazy_static",
-]
-
-[[package]]
-name = "crossbeam-utils"
-version = "0.7.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
-dependencies = [
- "autocfg 1.0.0",
- "cfg-if",
- "lazy_static",
-]
-
-[[package]]
-name = "crunchy"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
-
-[[package]]
-name = "crypto-mac"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5"
-dependencies = [
- "generic-array",
- "subtle 1.0.0",
-]
-
-[[package]]
-name = "ctr"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "022cd691704491df67d25d006fe8eca083098253c4d43516c2206479c58c6736"
-dependencies = [
- "block-cipher-trait",
- "stream-cipher",
-]
-
-[[package]]
-name = "cuckoofilter"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8dd43f7cfaffe0a386636a10baea2ee05cc50df3b77bea4a456c9572a939bf1f"
-dependencies = [
- "byteorder 0.5.3",
- "rand 0.3.23",
-]
-
-[[package]]
-name = "curve25519-dalek"
-version = "1.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b7dcd30ba50cdf88b55b033456138b7c0ac4afdc436d82e1b79f370f24cc66d"
-dependencies = [
- "byteorder 1.3.4",
- "clear_on_drop",
- "digest",
- "rand_core 0.3.1",
- "subtle 2.2.2",
-]
-
-[[package]]
-name = "curve25519-dalek"
-version = "2.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26778518a7f6cffa1d25a44b602b62b979bd88adb9e99ffec546998cf3404839"
-dependencies = [
- "byteorder 1.3.4",
- "digest",
- "rand_core 0.5.1",
- "subtle 2.2.2",
- "zeroize 1.1.0",
-]
-
-[[package]]
-name = "data-encoding"
-version = "2.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11c0346158a19b3627234e15596f5e465c360fcdb97d817bcb255e0510f5a788"
-
-[[package]]
-name = "derive_more"
-version = "0.15.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a141330240c921ec6d074a3e188a7c7ef95668bb95e7d44fa0e5778ec2a7afe"
-dependencies = [
- "lazy_static",
- "proc-macro2 0.4.30",
- "quote 0.6.13",
- "regex",
- "rustc_version",
- "syn 0.15.44",
-]
-
-[[package]]
-name = "digest"
-version = "0.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
-dependencies = [
- "generic-array",
-]
-
-[[package]]
-name = "dns-parser"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4d33be9473d06f75f58220f71f7a9317aca647dc061dbd3c361b0bef505fbea"
-dependencies = [
- "byteorder 1.3.4",
- "quick-error",
-]
-
-[[package]]
-name = "ed25519-dalek"
-version = "0.9.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d07e8b8a8386c3b89a7a4b329fdfa4cb545de2545e9e2ebbc3dd3929253e426"
-dependencies = [
- "clear_on_drop",
- "curve25519-dalek 1.2.3",
- "failure",
- "rand 0.6.5",
-]
-
-[[package]]
-name = "ed25519-dalek"
-version = "1.0.0-pre.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "978710b352437433c97b2bff193f2fb1dfd58a093f863dd95e225a19baa599a2"
-dependencies = [
- "clear_on_drop",
- "curve25519-dalek 2.0.0",
- "rand 0.7.3",
- "sha2",
-]
-
-[[package]]
-name = "either"
-version = "1.5.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3"
-
-[[package]]
-name = "elastic-array"
-version = "0.10.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "580f3768bd6465780d063f5b8213a2ebd506e139b345e4a81eb301ceae3d61e1"
-dependencies = [
- "heapsize",
-]
-
-[[package]]
-name = "environmental"
-version = "1.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "516aa8d7a71cb00a1c4146f0798549b93d083d4f189b3ced8f3de6b8f11ee6c4"
-
-[[package]]
-name = "erased-serde"
-version = "0.3.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd7d80305c9bd8cd78e3c753eb9fb110f83621e5211f1a3afffcc812b104daf9"
-dependencies = [
- "serde",
-]
-
-[[package]]
-name = "failure"
-version = "0.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8529c2421efa3066a5cbd8063d2244603824daccb6936b079010bb2aa89464b"
-dependencies = [
- "backtrace",
- "failure_derive",
-]
-
-[[package]]
-name = "failure_derive"
-version = "0.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "030a733c8287d6213886dd487564ff5c8f6aae10278b3588ed177f9d18f8d231"
-dependencies = [
- "proc-macro2 1.0.9",
- "quote 1.0.3",
- "syn 1.0.16",
- "synstructure",
-]
-
-[[package]]
-name = "fake-simd"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
-
-[[package]]
-name = "fixed-hash"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3367952ceb191f4ab95dd5685dc163ac539e36202f9fcfd0cb22f9f9c542fefc"
-dependencies = [
- "byteorder 1.3.4",
- "libc",
- "rand 0.7.3",
- "rustc-hex",
- "static_assertions",
-]
-
-[[package]]
-name = "flate2"
-version = "1.0.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6bd6d6f4752952feb71363cffc9ebac9411b75b87c6ab6058c40c8900cf43c0f"
-dependencies = [
- "cfg-if",
- "crc32fast",
- "futures",
- "libc",
- "libz-sys",
- "miniz_oxide",
- "tokio-io",
-]
-
-[[package]]
-name = "fnv"
-version = "1.0.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3"
-
-[[package]]
-name = "fuchsia-cprng"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
-
-[[package]]
-name = "fuchsia-zircon"
-version = "0.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
-dependencies = [
- "bitflags",
- "fuchsia-zircon-sys",
-]
-
-[[package]]
-name = "fuchsia-zircon-sys"
-version = "0.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
-
-[[package]]
-name = "futures"
-version = "0.1.29"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b980f2816d6ee8673b6517b52cb0e808a180efc92e5c19d02cdda79066703ef"
-
-[[package]]
-name = "futures-channel-preview"
-version = "0.3.0-alpha.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d5e5f4df964fa9c1c2f8bddeb5c3611631cacd93baf810fc8bb2fb4b495c263a"
-dependencies = [
- "futures-core-preview",
- "futures-sink-preview",
-]
-
-[[package]]
-name = "futures-core-preview"
-version = "0.3.0-alpha.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b35b6263fb1ef523c3056565fa67b1d16f0a8604ff12b11b08c25f28a734c60a"
-
-[[package]]
-name = "futures-cpupool"
-version = "0.1.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab90cde24b3319636588d0c35fe03b1333857621051837ed769faefb4c2162e4"
-dependencies = [
- "futures",
- "num_cpus",
-]
-
-[[package]]
-name = "futures-executor-preview"
-version = "0.3.0-alpha.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75236e88bd9fe88e5e8bfcd175b665d0528fe03ca4c5207fabc028c8f9d93e98"
-dependencies = [
- "futures-core-preview",
- "futures-util-preview",
- "num_cpus",
-]
-
-[[package]]
-name = "futures-io-preview"
-version = "0.3.0-alpha.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f4914ae450db1921a56c91bde97a27846287d062087d4a652efc09bb3a01ebda"
-
-[[package]]
-name = "futures-preview"
-version = "0.3.0-alpha.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b1dce2a0267ada5c6ff75a8ba864b4e679a9e2aa44262af7a3b5516d530d76e"
-dependencies = [
- "futures-channel-preview",
- "futures-core-preview",
- "futures-executor-preview",
- "futures-io-preview",
- "futures-sink-preview",
- "futures-util-preview",
-]
-
-[[package]]
-name = "futures-sink-preview"
-version = "0.3.0-alpha.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86f148ef6b69f75bb610d4f9a2336d4fc88c4b5b67129d1a340dd0fd362efeec"
-
-[[package]]
-name = "futures-timer"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "878f1d2fc31355fa02ed2372e741b0c17e58373341e6a122569b4623a14a7d33"
-dependencies = [
- "futures-core-preview",
- "futures-util-preview",
- "pin-utils",
-]
-
-[[package]]
-name = "futures-util-preview"
-version = "0.3.0-alpha.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ce968633c17e5f97936bd2797b6e38fb56cf16a7422319f7ec2e30d3c470e8d"
-dependencies = [
- "futures",
- "futures-channel-preview",
- "futures-core-preview",
- "futures-io-preview",
- "futures-sink-preview",
- "memchr",
- "pin-utils",
- "slab",
-]
-
-[[package]]
-name = "gcc"
-version = "0.3.55"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2"
-
-[[package]]
-name = "generic-array"
-version = "0.12.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec"
-dependencies = [
- "typenum",
-]
-
-[[package]]
-name = "get_if_addrs"
-version = "0.5.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "abddb55a898d32925f3148bd281174a68eeb68bbfd9a5938a57b18f506ee4ef7"
-dependencies = [
- "c_linked_list",
- "get_if_addrs-sys",
- "libc",
- "winapi 0.2.8",
-]
-
-[[package]]
-name = "get_if_addrs-sys"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d04f9fb746cf36b191c00f3ede8bde9c8e64f9f4b05ae2694a9ccf5e3f5ab48"
-dependencies = [
- "gcc",
- "libc",
-]
-
-[[package]]
-name = "getrandom"
-version = "0.1.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb"
-dependencies = [
- "cfg-if",
- "libc",
- "wasi",
-]
-
-[[package]]
-name = "hash-db"
-version = "0.15.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d23bd4e7b5eda0d0f3a307e8b381fdc8ba9000f26fbe912250c0a4cc3956364a"
-
-[[package]]
-name = "hash256-std-hasher"
-version = "0.15.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92c171d55b98633f4ed3860808f004099b36c1cc29c42cfc53aa8591b21efcf2"
-dependencies = [
- "crunchy",
-]
-
-[[package]]
-name = "hashbrown"
-version = "0.1.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3bae29b6653b3412c2e71e9d486db9f9df5d701941d86683005efb9f2d28e3da"
-dependencies = [
- "byteorder 1.3.4",
- "scopeguard 0.3.3",
-]
-
-[[package]]
-name = "hashbrown"
-version = "0.6.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e6073d0ca812575946eb5f35ff68dbe519907b25c42530389ff946dc84c6ead"
-dependencies = [
- "ahash",
- "autocfg 0.1.7",
-]
-
-[[package]]
-name = "heapsize"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1679e6ea370dee694f91f1dc469bf94cf8f52051d147aec3e1f9497c6fc22461"
-dependencies = [
- "winapi 0.3.8",
-]
-
-[[package]]
-name = "heck"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
-dependencies = [
- "unicode-segmentation",
-]
-
-[[package]]
-name = "hermit-abi"
-version = "0.1.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1010591b26bbfe835e9faeabeb11866061cc7dcebffd56ad7d0942d0e61aefd8"
-dependencies = [
- "libc",
-]
-
-[[package]]
-name = "hex"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35"
-
-[[package]]
-name = "hex-literal"
-version = "0.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ddc2928beef125e519d69ae1baa8c37ea2e0d3848545217f6db0179c5eb1d639"
-dependencies = [
- "hex-literal-impl 0.1.2",
- "proc-macro-hack 0.4.2",
-]
-
-[[package]]
-name = "hex-literal"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "961de220ec9a91af2e1e5bd80d02109155695e516771762381ef8581317066e0"
-dependencies = [
- "hex-literal-impl 0.2.1",
- "proc-macro-hack 0.5.11",
-]
-
-[[package]]
-name = "hex-literal-impl"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "520870c3213943eb8d7803e80180d12a6c7ceb4ae74602544529d1643dc4ddda"
-dependencies = [
- "proc-macro-hack 0.4.2",
-]
-
-[[package]]
-name = "hex-literal-impl"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d4c5c844e2fee0bf673d54c2c177f1713b3d2af2ff6e666b49cb7572e6cf42d"
-dependencies = [
- "proc-macro-hack 0.5.11",
-]
-
-[[package]]
-name = "hmac"
-version = "0.7.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5dcb5e64cda4c23119ab41ba960d1e170a774c8e4b9d9e6a9bc18aabf5e59695"
-dependencies = [
- "crypto-mac",
- "digest",
-]
-
-[[package]]
-name = "hmac-drbg"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c6e570451493f10f6581b48cdd530413b63ea9e780f544bfd3bdcaa0d89d1a7b"
-dependencies = [
- "digest",
- "generic-array",
- "hmac",
-]
-
-[[package]]
-name = "http"
-version = "0.1.21"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d6ccf5ede3a895d8856620237b2f02972c1bbc78d2965ad7fe8838d4a0ed41f0"
-dependencies = [
- "bytes 0.4.12",
- "fnv",
- "itoa",
-]
-
-[[package]]
-name = "httparse"
-version = "1.3.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9"
-
-[[package]]
-name = "idna"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9"
-dependencies = [
- "matches",
- "unicode-bidi",
- "unicode-normalization",
-]
-
-[[package]]
-name = "impl-codec"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1be51a921b067b0eaca2fad532d9400041561aa922221cc65f95a85641c6bf53"
-dependencies = [
- "parity-scale-codec",
-]
-
-[[package]]
-name = "impl-serde"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "58e3cae7e99c7ff5a995da2cf78dd0a5383740eda71d98cf7b1910c301ac69b8"
-dependencies = [
- "serde",
-]
-
-[[package]]
-name = "impl-serde"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5bbe9ea9b182f0fb1cabbd61f4ff9b7b7b9197955e95a7e4c27de5055eb29ff8"
-dependencies = [
- "serde",
-]
-
-[[package]]
-name = "impl-trait-for-tuples"
-version = "0.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ef5550a42e3740a0e71f909d4c861056a284060af885ae7aa6242820f920d9d"
-dependencies = [
- "proc-macro2 1.0.9",
- "quote 1.0.3",
- "syn 1.0.16",
-]
-
-[[package]]
-name = "integer-sqrt"
-version = "0.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f65877bf7d44897a473350b1046277941cee20b263397e90869c50b6e766088b"
-
-[[package]]
-name = "iovec"
-version = "0.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e"
-dependencies = [
- "libc",
-]
-
-[[package]]
-name = "ipnet"
-version = "2.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a859057dc563d1388c1e816f98a1892629075fc046ed06e845b883bb8b2916fb"
-
-[[package]]
-name = "itoa"
-version = "0.4.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e"
-
-[[package]]
-name = "joystream-node-runtime"
-version = "6.8.0"
-dependencies = [
- "parity-scale-codec",
- "safe-mix",
- "serde",
- "sr-io",
- "sr-primitives",
- "sr-staking-primitives",
- "sr-std",
- "sr-version",
- "srml-authority-discovery",
- "srml-authorship",
- "srml-babe",
- "srml-balances",
- "srml-executive",
- "srml-finality-tracker",
- "srml-grandpa",
- "srml-im-online",
- "srml-indices",
- "srml-offences",
- "srml-randomness-collective-flip",
- "srml-session",
- "srml-staking",
- "srml-staking-reward-curve",
- "srml-sudo",
- "srml-support",
- "srml-system",
- "srml-system-rpc-runtime-api",
- "srml-timestamp",
- "srml-transaction-payment",
- "substrate-authority-discovery-primitives",
- "substrate-client",
- "substrate-consensus-babe-primitives",
- "substrate-forum-module",
- "substrate-hiring-module",
- "substrate-offchain-primitives",
- "substrate-primitives",
- "substrate-recurring-reward-module",
- "substrate-session",
- "substrate-stake-module",
- "substrate-token-mint-module",
- "substrate-versioned-store",
- "substrate-versioned-store-permissions-module",
- "substrate-wasm-builder-runner",
-]
-
-[[package]]
-name = "js-sys"
-version = "0.3.36"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1cb931d43e71f560c81badb0191596562bafad2be06a3f9025b845c847c60df5"
-dependencies = [
- "wasm-bindgen",
-]
-
-[[package]]
-name = "keccak"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7"
-
-[[package]]
-name = "kernel32-sys"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
-dependencies = [
- "winapi 0.2.8",
- "winapi-build",
-]
-
-[[package]]
-name = "kvdb"
-version = "0.1.0"
-source = "git+https://github.com/paritytech/parity-common?rev=b0317f649ab2c665b7987b8475878fc4d2e1f81d#b0317f649ab2c665b7987b8475878fc4d2e1f81d"
-dependencies = [
- "elastic-array",
- "parity-bytes",
-]
-
-[[package]]
-name = "lazy_static"
-version = "1.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
-
-[[package]]
-name = "libc"
-version = "0.2.67"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eb147597cdf94ed43ab7a9038716637d2d1bf2bc571da995d0028dec06bd3018"
-
-[[package]]
-name = "libp2p"
-version = "0.13.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b4674c6738fdd8b1cf7104dd046abcef78dc932fe25f8eb40f3a8e71341717d"
-dependencies = [
- "bytes 0.4.12",
- "futures",
- "lazy_static",
- "libp2p-core",
- "libp2p-core-derive",
- "libp2p-deflate",
- "libp2p-dns",
- "libp2p-floodsub",
- "libp2p-identify",
- "libp2p-kad",
- "libp2p-mdns",
- "libp2p-mplex",
- "libp2p-noise",
- "libp2p-ping",
- "libp2p-plaintext",
- "libp2p-secio",
- "libp2p-swarm",
- "libp2p-tcp",
- "libp2p-uds",
- "libp2p-wasm-ext",
- "libp2p-websocket",
- "libp2p-yamux",
- "parity-multiaddr",
- "parity-multihash",
- "parking_lot 0.9.0",
- "smallvec 0.6.13",
- "tokio-codec",
- "tokio-executor",
- "tokio-io",
- "wasm-timer",
-]
-
-[[package]]
-name = "libp2p-core"
-version = "0.13.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01efc769c392d0d8863a7160d266f9b9f794968554f87490c8af4aa34ccaa94f"
-dependencies = [
- "asn1_der",
- "bs58",
- "bytes 0.4.12",
- "ed25519-dalek 1.0.0-pre.3",
- "failure",
- "fnv",
- "futures",
- "lazy_static",
- "libsecp256k1",
- "log",
- "multistream-select",
- "parity-multiaddr",
- "parity-multihash",
- "parking_lot 0.9.0",
- "protobuf",
- "quick-error",
- "rand 0.7.3",
- "ring",
- "rw-stream-sink",
- "sha2",
- "smallvec 0.6.13",
- "tokio-executor",
- "tokio-io",
- "unsigned-varint 0.2.3",
- "untrusted",
- "void",
- "wasm-timer",
- "zeroize 1.1.0",
-]
-
-[[package]]
-name = "libp2p-core-derive"
-version = "0.13.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1eeb2704ac14c60f31967e351ed928b848526a5fc6db4104520020665012826f"
-dependencies = [
- "quote 0.6.13",
- "syn 0.15.44",
-]
-
-[[package]]
-name = "libp2p-deflate"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef2b0bf5d37692ac90e2bffa436bec26c0b0def6c0cab7ea85ff67a353d58aaa"
-dependencies = [
- "flate2",
- "futures",
- "libp2p-core",
- "tokio-io",
-]
-
-[[package]]
-name = "libp2p-dns"
-version = "0.13.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3175fb0fc9016c95c8517a297bbdb5fb6bfbd5665bacd2eb23495d1cbdeb033"
-dependencies = [
- "futures",
- "libp2p-core",
- "log",
- "tokio-dns-unofficial",
-]
-
-[[package]]
-name = "libp2p-floodsub"
-version = "0.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b360bbaad2560d6b8a905bd63528273d933fe54475a44def47f31e23108b3683"
-dependencies = [
- "bs58",
- "bytes 0.4.12",
- "cuckoofilter",
- "fnv",
- "futures",
- "libp2p-core",
- "libp2p-swarm",
- "protobuf",
- "rand 0.6.5",
- "smallvec 0.6.13",
- "tokio-io",
-]
-
-[[package]]
-name = "libp2p-identify"
-version = "0.13.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c087bcd044a6f67a994573a92a109487a902a31555e4e63bcc4ae144c45594fe"
-dependencies = [
- "bytes 0.4.12",
- "futures",
- "libp2p-core",
- "libp2p-swarm",
- "log",
- "parity-multiaddr",
- "protobuf",
- "smallvec 0.6.13",
- "tokio-codec",
- "tokio-io",
- "unsigned-varint 0.2.3",
- "wasm-timer",
-]
-
-[[package]]
-name = "libp2p-kad"
-version = "0.13.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dcaf76a5b33b6c0203e85d450ae1855cae6860dc82eb0174ac1fee8bf68f7af5"
-dependencies = [
- "arrayvec 0.5.1",
- "bytes 0.4.12",
- "either",
- "fnv",
- "futures",
- "libp2p-core",
- "libp2p-swarm",
- "log",
- "parity-multiaddr",
- "parity-multihash",
- "protobuf",
- "rand 0.7.3",
- "sha2",
- "smallvec 0.6.13",
- "tokio-codec",
- "tokio-io",
- "uint",
- "unsigned-varint 0.2.3",
- "void",
- "wasm-timer",
-]
-
-[[package]]
-name = "libp2p-mdns"
-version = "0.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4c2e225a7dfc571c3ad77a0a5ecccc9537afe42d72289ac9f19768567cd677d"
-dependencies = [
- "data-encoding",
- "dns-parser",
- "futures",
- "libp2p-core",
- "libp2p-swarm",
- "log",
- "net2",
- "parity-multiaddr",
- "rand 0.6.5",
- "smallvec 0.6.13",
- "tokio-io",
- "tokio-reactor",
- "tokio-udp",
- "void",
- "wasm-timer",
-]
-
-[[package]]
-name = "libp2p-mplex"
-version = "0.13.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2fe584816d993dc0f893396521a3c93191d78a6f28a892b150baa714a12c3e5"
-dependencies = [
- "bytes 0.4.12",
- "fnv",
- "futures",
- "libp2p-core",
- "log",
- "parking_lot 0.8.0",
- "tokio-codec",
- "tokio-io",
- "unsigned-varint 0.2.3",
-]
-
-[[package]]
-name = "libp2p-noise"
-version = "0.11.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d50494fcba7cdab08390d72b3cb9d2c72fcf178e6a0c1043855ab259d818b972"
-dependencies = [
- "bytes 0.4.12",
- "curve25519-dalek 1.2.3",
- "futures",
- "lazy_static",
- "libp2p-core",
- "log",
- "protobuf",
- "rand 0.7.3",
- "ring",
- "snow",
- "tokio-io",
- "x25519-dalek",
- "zeroize 1.1.0",
-]
-
-[[package]]
-name = "libp2p-ping"
-version = "0.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b975ad345eb9bb29ddc64670664a50a8ab3e66e28357abb0f83cfc0a9ca2d78"
-dependencies = [
- "bytes 0.4.12",
- "futures",
- "libp2p-core",
- "libp2p-swarm",
- "log",
- "parity-multiaddr",
- "rand 0.7.3",
- "tokio-io",
- "void",
- "wasm-timer",
-]
-
-[[package]]
-name = "libp2p-plaintext"
-version = "0.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f07be6983e1c00e8f6a5676da54ed3a8cae7fb50f1fb6ea163414613ca656cc"
-dependencies = [
- "bytes 0.4.12",
- "futures",
- "libp2p-core",
- "log",
- "protobuf",
- "rw-stream-sink",
- "tokio-io",
- "void",
-]
-
-[[package]]
-name = "libp2p-secio"
-version = "0.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04aa6d67a5fb2b36241a1ba54037a13deb2594cf141e43b597ce379521d530a8"
-dependencies = [
- "aes-ctr",
- "bytes 0.4.12",
- "ctr",
- "futures",
- "hmac",
- "js-sys",
- "lazy_static",
- "libp2p-core",
- "log",
- "parity-send-wrapper",
- "protobuf",
- "rand 0.6.5",
- "ring",
- "rw-stream-sink",
- "sha2",
- "tokio-codec",
- "tokio-io",
- "twofish",
- "untrusted",
- "wasm-bindgen",
- "wasm-bindgen-futures",
- "web-sys",
-]
-
-[[package]]
-name = "libp2p-swarm"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd55bc9f5f9eac2bb1ff24ca3c8a655810a566ac38c7a6ee1f30aced5a62905b"
-dependencies = [
- "futures",
- "libp2p-core",
- "smallvec 0.6.13",
- "tokio-io",
- "void",
- "wasm-timer",
-]
-
-[[package]]
-name = "libp2p-tcp"
-version = "0.13.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "234a7093d05651ab5630db926a4a42ca8978a65bab8c27c2ce2b66b200c76989"
-dependencies = [
- "bytes 0.4.12",
- "futures",
- "get_if_addrs",
- "ipnet",
- "libp2p-core",
- "log",
- "tokio-io",
- "tokio-tcp",
- "tokio-timer",
-]
-
-[[package]]
-name = "libp2p-uds"
-version = "0.13.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e2fe0648967da3e56e4a55055c857c8c48326b66be0047d0e04c8ca60d34630"
-dependencies = [
- "futures",
- "libp2p-core",
- "log",
- "tokio-uds",
-]
-
-[[package]]
-name = "libp2p-wasm-ext"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f7b8f2bd81fb356e81352d4513856bc21215ecf91502aa1f55b6449642a9acf"
-dependencies = [
- "futures",
- "js-sys",
- "libp2p-core",
- "parity-send-wrapper",
- "tokio-io",
- "wasm-bindgen",
- "wasm-bindgen-futures",
-]
-
-[[package]]
-name = "libp2p-websocket"
-version = "0.13.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d74d4fc229ad7e8d1a973178786bdcd5dadbdd7b9822c4477c8687df6f82f66"
-dependencies = [
- "bytes 0.4.12",
- "futures",
- "libp2p-core",
- "log",
- "rw-stream-sink",
- "soketto",
- "tokio-codec",
- "tokio-io",
- "tokio-rustls",
- "url",
- "webpki-roots",
-]
-
-[[package]]
-name = "libp2p-yamux"
-version = "0.13.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1913eb7dd6eb5515957b6f1770296f6921968db87bc9b985f0e974b6657e1003"
-dependencies = [
- "futures",
- "libp2p-core",
- "log",
- "tokio-io",
- "yamux",
-]
-
-[[package]]
-name = "libsecp256k1"
-version = "0.3.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fc1e2c808481a63dc6da2074752fdd4336a3c8fcc68b83db6f1fd5224ae7962"
-dependencies = [
- "arrayref",
- "crunchy",
- "digest",
- "hmac-drbg",
- "rand 0.7.3",
- "sha2",
- "subtle 2.2.2",
- "typenum",
-]
-
-[[package]]
-name = "libz-sys"
-version = "1.0.25"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2eb5e43362e38e2bca2fd5f5134c4d4564a23a5c28e9b95411652021a8675ebe"
-dependencies = [
- "cc",
- "libc",
- "pkg-config",
- "vcpkg",
-]
-
-[[package]]
-name = "linked-hash-map"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae91b68aebc4ddb91978b11a1b02ddd8602a05ec19002801c5666000e05e0f83"
-
-[[package]]
-name = "lock_api"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62ebf1391f6acad60e5c8b43706dde4582df75c06698ab44511d15016bc2442c"
-dependencies = [
- "scopeguard 0.3.3",
-]
-
-[[package]]
-name = "lock_api"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed946d4529956a20f2d63ebe1b69996d5a2137c91913fe3ebbeff957f5bca7ff"
-dependencies = [
- "scopeguard 1.1.0",
-]
-
-[[package]]
-name = "lock_api"
-version = "0.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79b2de95ecb4691949fea4716ca53cdbcfccb2c612e19644a8bad05edcf9f47b"
-dependencies = [
- "scopeguard 1.1.0",
-]
-
-[[package]]
-name = "log"
-version = "0.4.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7"
-dependencies = [
- "cfg-if",
-]
-
-[[package]]
-name = "lru-cache"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
-dependencies = [
- "linked-hash-map",
-]
-
-[[package]]
-name = "malloc_size_of_derive"
-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",
- "synstructure",
-]
-
-[[package]]
-name = "matches"
-version = "0.1.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
-
-[[package]]
-name = "maybe-uninit"
-version = "2.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
-
-[[package]]
-name = "memchr"
-version = "2.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"
-
-[[package]]
-name = "memoffset"
-version = "0.5.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75189eb85871ea5c2e2c15abbdd541185f63b408415e5051f5cac122d8c774b9"
-dependencies = [
- "rustc_version",
-]
-
-[[package]]
-name = "memory-db"
-version = "0.15.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5dabfe0a8c69954ae3bcfc5fc14260a85fb80e1bf9f86a155f668d10a67e93dd"
-dependencies = [
- "ahash",
- "hash-db",
- "hashbrown 0.6.3",
- "parity-util-mem",
-]
-
-[[package]]
-name = "memory_units"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71d96e3f3c0b6325d8ccd83c33b28acb183edcb6c67938ba104ec546854b0882"
-
-[[package]]
-name = "merlin"
-version = "1.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b0942b357c1b4d0dc43ba724674ec89c3218e6ca2b3e8269e7cb53bcecd2f6e"
-dependencies = [
- "byteorder 1.3.4",
- "keccak",
- "rand_core 0.4.2",
- "zeroize 1.1.0",
-]
-
-[[package]]
-name = "miniz_oxide"
-version = "0.3.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aa679ff6578b1cddee93d7e82e263b94a575e0bfced07284eb0c037c1d2416a5"
-dependencies = [
- "adler32",
-]
-
-[[package]]
-name = "mio"
-version = "0.6.21"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "302dec22bcf6bae6dfb69c647187f4b4d0fb6f535521f7bc022430ce8e12008f"
-dependencies = [
- "cfg-if",
- "fuchsia-zircon",
- "fuchsia-zircon-sys",
- "iovec",
- "kernel32-sys",
- "libc",
- "log",
- "miow",
- "net2",
- "slab",
- "winapi 0.2.8",
-]
-
-[[package]]
-name = "mio-uds"
-version = "0.6.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "966257a94e196b11bb43aca423754d87429960a768de9414f3691d6957abf125"
-dependencies = [
- "iovec",
- "libc",
- "mio",
-]
-
-[[package]]
-name = "miow"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919"
-dependencies = [
- "kernel32-sys",
- "net2",
- "winapi 0.2.8",
- "ws2_32-sys",
-]
-
-[[package]]
-name = "multistream-select"
-version = "0.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc3ef54aab1b2e37e911bcb99e376dbe4c1e0710afcdb8428608e4f993b39c47"
-dependencies = [
- "bytes 0.4.12",
- "futures",
- "log",
- "smallvec 0.6.13",
- "tokio-io",
- "unsigned-varint 0.2.3",
-]
-
-[[package]]
-name = "net2"
-version = "0.2.33"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88"
-dependencies = [
- "cfg-if",
- "libc",
- "winapi 0.3.8",
-]
-
-[[package]]
-name = "nodrop"
-version = "0.1.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
-
-[[package]]
-name = "nohash-hasher"
-version = "0.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "721a2bf1c26159ebf17e0a980bc4ce61f4b2fec5ec3b42d42fddd7a84a9e538f"
-
-[[package]]
-name = "num-bigint"
-version = "0.2.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304"
-dependencies = [
- "autocfg 1.0.0",
- "num-integer",
- "num-traits",
-]
-
-[[package]]
-name = "num-integer"
-version = "0.1.42"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba"
-dependencies = [
- "autocfg 1.0.0",
- "num-traits",
-]
-
-[[package]]
-name = "num-rational"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da4dc79f9e6c81bef96148c8f6b8e72ad4541caa4a24373e900a36da07de03a3"
-dependencies = [
- "autocfg 1.0.0",
- "num-bigint",
- "num-integer",
- "num-traits",
-]
-
-[[package]]
-name = "num-traits"
-version = "0.2.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096"
-dependencies = [
- "autocfg 1.0.0",
-]
-
-[[package]]
-name = "num_cpus"
-version = "1.12.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46203554f085ff89c235cd12f7075f3233af9b11ed7c9e16dfe2560d03313ce6"
-dependencies = [
- "hermit-abi",
- "libc",
-]
-
-[[package]]
-name = "once_cell"
-version = "0.1.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "532c29a261168a45ce28948f9537ddd7a5dd272cc513b3017b1e82a88f962c37"
-dependencies = [
- "parking_lot 0.7.1",
-]
-
-[[package]]
-name = "once_cell"
-version = "0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d584f08c2d717d5c23a6414fc2822b71c651560713e54fa7eace675f758a355e"
-
-[[package]]
-name = "opaque-debug"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
-
-[[package]]
-name = "parity-bytes"
-version = "0.1.0"
-source = "git+https://github.com/paritytech/parity-common?rev=b0317f649ab2c665b7987b8475878fc4d2e1f81d#b0317f649ab2c665b7987b8475878fc4d2e1f81d"
-
-[[package]]
-name = "parity-multiaddr"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "82afcb7461eae5d122543d8be1c57d306ed89af2d6ff7f8b0f5a3cc8f7e511bc"
-dependencies = [
- "arrayref",
- "bs58",
- "byteorder 1.3.4",
- "bytes 0.4.12",
- "data-encoding",
- "parity-multihash",
- "percent-encoding",
- "serde",
- "unsigned-varint 0.2.3",
- "url",
-]
-
-[[package]]
-name = "parity-multihash"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a1cd2ba02391b81367bec529fb209019d718684fdc8ad6a712c2b536e46f775"
-dependencies = [
- "blake2",
- "bytes 0.5.4",
- "rand 0.7.3",
- "sha-1",
- "sha2",
- "sha3",
- "unsigned-varint 0.3.2",
-]
-
-[[package]]
-name = "parity-scale-codec"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f509c5e67ca0605ee17dcd3f91ef41cadd685c75a298fb6261b781a5acb3f910"
-dependencies = [
- "arrayvec 0.5.1",
- "bitvec",
- "byte-slice-cast",
- "parity-scale-codec-derive",
- "serde",
-]
-
-[[package]]
-name = "parity-scale-codec-derive"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a0ec292e92e8ec7c58e576adacc1e3f399c597c8f263c42f18420abe58e7245"
-dependencies = [
- "proc-macro-crate",
- "proc-macro2 1.0.9",
- "quote 1.0.3",
- "syn 1.0.16",
-]
-
-[[package]]
-name = "parity-send-wrapper"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aa9777aa91b8ad9dd5aaa04a9b6bcb02c7f1deb952fca5a66034d5e63afc5c6f"
-
-[[package]]
-name = "parity-util-mem"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "570093f39f786beea92dcc09e45d8aae7841516ac19a50431953ac82a0e8f85c"
-dependencies = [
- "cfg-if",
- "malloc_size_of_derive",
- "winapi 0.3.8",
-]
-
-[[package]]
-name = "parity-wasm"
-version = "0.40.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e39faaa292a687ea15120b1ac31899b13586446521df6c149e46f1584671e0f"
-
-[[package]]
-name = "parking_lot"
-version = "0.7.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab41b4aed082705d1056416ae4468b6ea99d52599ecf3169b00088d43113e337"
-dependencies = [
- "lock_api 0.1.5",
- "parking_lot_core 0.4.0",
-]
-
-[[package]]
-name = "parking_lot"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa7767817701cce701d5585b9c4db3cdd02086398322c1d7e8bf5094a96a2ce7"
-dependencies = [
- "lock_api 0.2.0",
- "parking_lot_core 0.5.0",
- "rustc_version",
-]
-
-[[package]]
-name = "parking_lot"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252"
-dependencies = [
- "lock_api 0.3.3",
- "parking_lot_core 0.6.2",
- "rustc_version",
-]
-
-[[package]]
-name = "parking_lot_core"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94c8c7923936b28d546dfd14d4472eaf34c99b14e1c973a32b3e6d4eb04298c9"
-dependencies = [
- "libc",
- "rand 0.6.5",
- "rustc_version",
- "smallvec 0.6.13",
- "winapi 0.3.8",
-]
-
-[[package]]
-name = "parking_lot_core"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb88cb1cb3790baa6776844f968fea3be44956cf184fa1be5a03341f5491278c"
-dependencies = [
- "cfg-if",
- "cloudabi",
- "libc",
- "rand 0.6.5",
- "redox_syscall",
- "rustc_version",
- "smallvec 0.6.13",
- "winapi 0.3.8",
-]
-
-[[package]]
-name = "parking_lot_core"
-version = "0.6.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b876b1b9e7ac6e1a74a6da34d25c42e17e8862aa409cbbbdcfc8d86c6f3bc62b"
-dependencies = [
- "cfg-if",
- "cloudabi",
- "libc",
- "redox_syscall",
- "rustc_version",
- "smallvec 0.6.13",
- "winapi 0.3.8",
-]
-
-[[package]]
-name = "paste"
-version = "0.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "63e1afe738d71b1ebab5f1207c055054015427dbfc7bbe9ee1266894156ec046"
-dependencies = [
- "paste-impl",
- "proc-macro-hack 0.5.11",
-]
-
-[[package]]
-name = "paste-impl"
-version = "0.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d4dc4a7f6f743211c5aab239640a65091535d97d43d92a52bca435a640892bb"
-dependencies = [
- "proc-macro-hack 0.5.11",
- "proc-macro2 1.0.9",
- "quote 1.0.3",
- "syn 1.0.16",
-]
-
-[[package]]
-name = "pbkdf2"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "006c038a43a45995a9670da19e67600114740e8511d4333bf97a56e66a7542d9"
-dependencies = [
- "byteorder 1.3.4",
- "crypto-mac",
-]
-
-[[package]]
-name = "percent-encoding"
-version = "2.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
-
-[[package]]
-name = "pin-utils"
-version = "0.1.0-alpha.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5894c618ce612a3fa23881b152b608bafb8c56cfc22f434a3ba3120b40f7b587"
-
-[[package]]
-name = "pkg-config"
-version = "0.3.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677"
-
-[[package]]
-name = "ppv-lite86"
-version = "0.2.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b"
-
-[[package]]
-name = "primitive-types"
-version = "0.6.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e4336f4f5d5524fa60bcbd6fe626f9223d8142a50e7053e979acdf0da41ab975"
-dependencies = [
- "fixed-hash",
- "impl-codec",
- "impl-serde 0.3.0",
- "uint",
-]
-
-[[package]]
-name = "proc-macro-crate"
-version = "0.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e10d4b51f154c8a7fb96fd6dad097cb74b863943ec010ac94b9fd1be8861fe1e"
-dependencies = [
- "toml",
-]
-
-[[package]]
-name = "proc-macro-hack"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "463bf29e7f11344e58c9e01f171470ab15c925c6822ad75028cc1c0e1d1eb63b"
-dependencies = [
- "proc-macro-hack-impl",
-]
-
-[[package]]
-name = "proc-macro-hack"
-version = "0.5.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ecd45702f76d6d3c75a80564378ae228a85f0b59d2f3ed43c91b4a69eb2ebfc5"
-dependencies = [
- "proc-macro2 1.0.9",
- "quote 1.0.3",
- "syn 1.0.16",
-]
-
-[[package]]
-name = "proc-macro-hack-impl"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38c47dcb1594802de8c02f3b899e2018c78291168a22c281be21ea0fb4796842"
-
-[[package]]
-name = "proc-macro2"
-version = "0.4.30"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759"
-dependencies = [
- "unicode-xid 0.1.0",
-]
-
-[[package]]
-name = "proc-macro2"
-version = "1.0.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c09721c6781493a2a492a96b5a5bf19b65917fe6728884e7c44dd0c60ca3435"
-dependencies = [
- "unicode-xid 0.2.0",
-]
-
-[[package]]
-name = "protobuf"
-version = "2.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "40361836defdd5871ff7e84096c6f6444af7fc157f8ef1789f54f147687caa20"
-
-[[package]]
-name = "quick-error"
-version = "1.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
-
-[[package]]
-name = "quote"
-version = "0.6.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1"
-dependencies = [
- "proc-macro2 0.4.30",
-]
-
-[[package]]
-name = "quote"
-version = "1.0.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2bdc6c187c65bca4260c9011c9e3132efe4909da44726bad24cf7572ae338d7f"
-dependencies = [
- "proc-macro2 1.0.9",
-]
-
-[[package]]
-name = "rand"
-version = "0.3.23"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c"
-dependencies = [
- "libc",
- "rand 0.4.6",
-]
-
-[[package]]
-name = "rand"
-version = "0.4.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
-dependencies = [
- "fuchsia-cprng",
- "libc",
- "rand_core 0.3.1",
- "rdrand",
- "winapi 0.3.8",
-]
-
-[[package]]
-name = "rand"
-version = "0.6.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca"
-dependencies = [
- "autocfg 0.1.7",
- "libc",
- "rand_chacha 0.1.1",
- "rand_core 0.4.2",
- "rand_hc 0.1.0",
- "rand_isaac",
- "rand_jitter",
- "rand_os",
- "rand_pcg",
- "rand_xorshift",
- "winapi 0.3.8",
-]
-
-[[package]]
-name = "rand"
-version = "0.7.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
-dependencies = [
- "getrandom",
- "libc",
- "rand_chacha 0.2.1",
- "rand_core 0.5.1",
- "rand_hc 0.2.0",
-]
-
-[[package]]
-name = "rand_chacha"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef"
-dependencies = [
- "autocfg 0.1.7",
- "rand_core 0.3.1",
-]
-
-[[package]]
-name = "rand_chacha"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03a2a90da8c7523f554344f921aa97283eadf6ac484a6d2a7d0212fa7f8d6853"
-dependencies = [
- "c2-chacha",
- "rand_core 0.5.1",
-]
-
-[[package]]
-name = "rand_core"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
-dependencies = [
- "rand_core 0.4.2",
-]
-
-[[package]]
-name = "rand_core"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
-
-[[package]]
-name = "rand_core"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
-dependencies = [
- "getrandom",
-]
-
-[[package]]
-name = "rand_hc"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4"
-dependencies = [
- "rand_core 0.3.1",
-]
-
-[[package]]
-name = "rand_hc"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
-dependencies = [
- "rand_core 0.5.1",
-]
-
-[[package]]
-name = "rand_isaac"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08"
-dependencies = [
- "rand_core 0.3.1",
-]
-
-[[package]]
-name = "rand_jitter"
-version = "0.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b"
-dependencies = [
- "libc",
- "rand_core 0.4.2",
- "winapi 0.3.8",
-]
-
-[[package]]
-name = "rand_os"
-version = "0.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071"
-dependencies = [
- "cloudabi",
- "fuchsia-cprng",
- "libc",
- "rand_core 0.4.2",
- "rdrand",
- "winapi 0.3.8",
-]
-
-[[package]]
-name = "rand_pcg"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44"
-dependencies = [
- "autocfg 0.1.7",
- "rand_core 0.4.2",
-]
-
-[[package]]
-name = "rand_xorshift"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c"
-dependencies = [
- "rand_core 0.3.1",
-]
-
-[[package]]
-name = "rdrand"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
-dependencies = [
- "rand_core 0.3.1",
-]
-
-[[package]]
-name = "redox_syscall"
-version = "0.1.56"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84"
-
-[[package]]
-name = "regex"
-version = "1.3.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "322cf97724bea3ee221b78fe25ac9c46114ebb51747ad5babd51a2fc6a8235a8"
-dependencies = [
- "aho-corasick",
- "memchr",
- "regex-syntax",
- "thread_local 1.0.1",
-]
-
-[[package]]
-name = "regex-syntax"
-version = "0.6.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1132f845907680735a84409c3bebc64d1364a5683ffbce899550cd09d5eaefc1"
-
-[[package]]
-name = "ring"
-version = "0.16.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "741ba1704ae21999c00942f9f5944f801e977f54302af346b596287599ad1862"
-dependencies = [
- "cc",
- "lazy_static",
- "libc",
- "spin",
- "untrusted",
- "web-sys",
- "winapi 0.3.8",
-]
-
-[[package]]
-name = "rustc-demangle"
-version = "0.1.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783"
-
-[[package]]
-name = "rustc-hex"
-version = "2.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6"
-
-[[package]]
-name = "rustc_version"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
-dependencies = [
- "semver",
-]
-
-[[package]]
-name = "rustls"
-version = "0.16.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b25a18b1bf7387f0145e7f8324e700805aade3842dd3db2e74e4cdeb4677c09e"
-dependencies = [
- "base64",
- "log",
- "ring",
- "sct",
- "webpki",
-]
-
-[[package]]
-name = "rw-stream-sink"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f9cbe61c20455d3015b2bb7be39e1872310283b8e5a52f5b242b0ac7581fe78"
-dependencies = [
- "bytes 0.4.12",
- "futures",
- "tokio-io",
-]
-
-[[package]]
-name = "ryu"
-version = "1.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8"
-
-[[package]]
-name = "safe-mix"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d3d055a2582e6b00ed7a31c1524040aa391092bf636328350813f3a0605215c"
-dependencies = [
- "rustc_version",
-]
-
-[[package]]
-name = "schnorrkel"
-version = "0.8.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eacd8381b3c37840c9c9f40472af529e49975bdcbc24f83c31059fd6539023d3"
-dependencies = [
- "curve25519-dalek 1.2.3",
- "failure",
- "merlin",
- "rand 0.6.5",
- "rand_core 0.4.2",
- "rand_os",
- "sha2",
- "subtle 2.2.2",
- "zeroize 0.9.3",
-]
-
-[[package]]
-name = "scopeguard"
-version = "0.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27"
-
-[[package]]
-name = "scopeguard"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
-
-[[package]]
-name = "sct"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c"
-dependencies = [
- "ring",
- "untrusted",
-]
-
-[[package]]
-name = "semver"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
-dependencies = [
- "semver-parser",
-]
-
-[[package]]
-name = "semver-parser"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
-
-[[package]]
-name = "send_wrapper"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a0eddf2e8f50ced781f288c19f18621fa72a3779e3cb58dbf23b07469b0abeb4"
-
-[[package]]
-name = "serde"
-version = "1.0.104"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "414115f25f818d7dfccec8ee535d76949ae78584fc4f79a6f45a904bf8ab4449"
-dependencies = [
- "serde_derive",
-]
-
-[[package]]
-name = "serde_derive"
-version = "1.0.104"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "128f9e303a5a29922045a830221b8f78ec74a5f544944f3d5984f8ec3895ef64"
-dependencies = [
- "proc-macro2 1.0.9",
- "quote 1.0.3",
- "syn 1.0.16",
-]
-
-[[package]]
-name = "serde_json"
-version = "1.0.48"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9371ade75d4c2d6cb154141b9752cf3781ec9c05e0e5cf35060e1e70ee7b9c25"
-dependencies = [
- "itoa",
- "ryu",
- "serde",
-]
-
-[[package]]
-name = "sha-1"
-version = "0.8.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df"
-dependencies = [
- "block-buffer",
- "digest",
- "fake-simd",
- "opaque-debug",
-]
-
-[[package]]
-name = "sha1"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d"
-
-[[package]]
-name = "sha2"
-version = "0.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "27044adfd2e1f077f649f59deb9490d3941d674002f7d062870a60ebe9bd47a0"
-dependencies = [
- "block-buffer",
- "digest",
- "fake-simd",
- "opaque-debug",
-]
-
-[[package]]
-name = "sha3"
-version = "0.8.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd26bc0e7a2e3a7c959bc494caf58b72ee0c71d67704e9520f736ca7e4853ecf"
-dependencies = [
- "block-buffer",
- "byte-tools",
- "digest",
- "keccak",
- "opaque-debug",
-]
-
-[[package]]
-name = "slab"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
-
-[[package]]
-name = "slog"
-version = "2.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1cc9c640a4adbfbcc11ffb95efe5aa7af7309e002adab54b185507dbf2377b99"
-dependencies = [
- "erased-serde",
-]
-
-[[package]]
-name = "slog-async"
-version = "2.3.0"
-source = "git+https://github.com/paritytech/slog-async#107848e7ded5e80dc43f6296c2b96039eb92c0a5"
-dependencies = [
- "crossbeam-channel",
- "slog",
- "take_mut",
- "thread_local 0.3.6",
-]
-
-[[package]]
-name = "slog-json"
-version = "2.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ddc0d2aff1f8f325ef660d9a0eb6e6dcd20b30b3f581a5897f58bf42d061c37a"
-dependencies = [
- "chrono",
- "erased-serde",
- "serde",
- "serde_json",
- "slog",
-]
-
-[[package]]
-name = "slog-scope"
-version = "4.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c44c89dd8b0ae4537d1ae318353eaf7840b4869c536e31c41e963d1ea523ee6"
-dependencies = [
- "arc-swap",
- "lazy_static",
- "slog",
-]
-
-[[package]]
-name = "smallvec"
-version = "0.6.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f7b0758c52e15a8b5e3691eae6cc559f08eee9406e548a4477ba4e67770a82b6"
-dependencies = [
- "maybe-uninit",
-]
-
-[[package]]
-name = "smallvec"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c2fb2ec9bcd216a5b0d0ccf31ab17b5ed1d627960edff65bbe95d3ce221cefc"
-
-[[package]]
-name = "snow"
-version = "0.6.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "afb767eee7d257ba202f0b9b08673bc13b22281632ef45267b19f13100accd2f"
-dependencies = [
- "arrayref",
- "rand_core 0.5.1",
- "ring",
- "rustc_version",
- "subtle 2.2.2",
-]
-
-[[package]]
-name = "soketto"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bceb1a3a15232d013d9a3b7cac9e5ce8e2313f348f01d4bc1097e5e53aa07095"
-dependencies = [
- "base64",
- "bytes 0.4.12",
- "flate2",
- "futures",
- "http",
- "httparse",
- "log",
- "rand 0.6.5",
- "sha1",
- "smallvec 0.6.13",
- "tokio-codec",
- "tokio-io",
-]
-
-[[package]]
-name = "spin"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
-
-[[package]]
-name = "sr-api-macros"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "blake2-rfc",
- "proc-macro-crate",
- "proc-macro2 1.0.9",
- "quote 1.0.3",
- "syn 1.0.16",
-]
-
-[[package]]
-name = "sr-arithmetic"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "integer-sqrt",
- "num-traits",
- "parity-scale-codec",
- "serde",
- "sr-std",
- "substrate-debug-derive",
-]
-
-[[package]]
-name = "sr-io"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "hash-db",
- "libsecp256k1",
- "log",
- "parity-scale-codec",
- "rustc_version",
- "sr-std",
- "substrate-externalities",
- "substrate-primitives",
- "substrate-state-machine",
- "substrate-trie",
- "tiny-keccak",
-]
-
-[[package]]
-name = "sr-primitives"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "impl-trait-for-tuples",
- "log",
- "parity-scale-codec",
- "paste",
- "rand 0.7.3",
- "serde",
- "sr-arithmetic",
- "sr-io",
- "sr-std",
- "substrate-application-crypto",
- "substrate-primitives",
-]
-
-[[package]]
-name = "sr-staking-primitives"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "parity-scale-codec",
- "sr-primitives",
- "sr-std",
-]
-
-[[package]]
-name = "sr-std"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "rustc_version",
-]
-
-[[package]]
-name = "sr-version"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "impl-serde 0.2.3",
- "parity-scale-codec",
- "serde",
- "sr-primitives",
- "sr-std",
-]
-
-[[package]]
-name = "srml-authority-discovery"
-version = "0.1.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "parity-scale-codec",
- "serde",
- "sr-io",
- "sr-primitives",
- "sr-std",
- "srml-session",
- "srml-support",
- "srml-system",
- "substrate-application-crypto",
- "substrate-primitives",
-]
-
-[[package]]
-name = "srml-authorship"
-version = "0.1.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "impl-trait-for-tuples",
- "parity-scale-codec",
- "sr-io",
- "sr-primitives",
- "sr-std",
- "srml-support",
- "srml-system",
- "substrate-inherents",
- "substrate-primitives",
-]
-
-[[package]]
-name = "srml-babe"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "hex-literal 0.2.1",
- "parity-scale-codec",
- "serde",
- "sr-io",
- "sr-primitives",
- "sr-staking-primitives",
- "sr-std",
- "srml-session",
- "srml-support",
- "srml-system",
- "srml-timestamp",
- "substrate-consensus-babe-primitives",
- "substrate-inherents",
-]
-
-[[package]]
-name = "srml-balances"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "parity-scale-codec",
- "safe-mix",
- "serde",
- "sr-primitives",
- "sr-std",
- "srml-support",
- "srml-system",
- "substrate-keyring",
-]
-
-[[package]]
-name = "srml-executive"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "parity-scale-codec",
- "serde",
- "sr-io",
- "sr-primitives",
- "sr-std",
- "srml-support",
- "srml-system",
-]
-
-[[package]]
-name = "srml-finality-tracker"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "impl-trait-for-tuples",
- "parity-scale-codec",
- "serde",
- "sr-primitives",
- "sr-std",
- "srml-support",
- "srml-system",
- "substrate-inherents",
-]
-
-[[package]]
-name = "srml-grandpa"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "parity-scale-codec",
- "serde",
- "sr-primitives",
- "sr-staking-primitives",
- "sr-std",
- "srml-finality-tracker",
- "srml-session",
- "srml-support",
- "srml-system",
- "substrate-finality-grandpa-primitives",
- "substrate-primitives",
-]
-
-[[package]]
-name = "srml-im-online"
-version = "0.1.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "parity-scale-codec",
- "serde",
- "sr-io",
- "sr-primitives",
- "sr-staking-primitives",
- "sr-std",
- "srml-authorship",
- "srml-session",
- "srml-support",
- "srml-system",
- "substrate-application-crypto",
- "substrate-primitives",
-]
-
-[[package]]
-name = "srml-indices"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "parity-scale-codec",
- "safe-mix",
- "serde",
- "sr-io",
- "sr-primitives",
- "sr-std",
- "srml-support",
- "srml-system",
- "substrate-keyring",
- "substrate-primitives",
-]
-
-[[package]]
-name = "srml-metadata"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "parity-scale-codec",
- "serde",
- "sr-std",
- "substrate-primitives",
-]
-
-[[package]]
-name = "srml-offences"
-version = "1.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "parity-scale-codec",
- "serde",
- "sr-primitives",
- "sr-staking-primitives",
- "sr-std",
- "srml-balances",
- "srml-support",
- "srml-system",
-]
-
-[[package]]
-name = "srml-randomness-collective-flip"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "parity-scale-codec",
- "safe-mix",
- "sr-primitives",
- "sr-std",
- "srml-support",
- "srml-system",
-]
-
-[[package]]
-name = "srml-session"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "impl-trait-for-tuples",
- "parity-scale-codec",
- "safe-mix",
- "serde",
- "sr-io",
- "sr-primitives",
- "sr-staking-primitives",
- "sr-std",
- "srml-support",
- "srml-system",
- "srml-timestamp",
- "substrate-trie",
-]
-
-[[package]]
-name = "srml-staking"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "parity-scale-codec",
- "safe-mix",
- "serde",
- "sr-io",
- "sr-primitives",
- "sr-staking-primitives",
- "sr-std",
- "srml-authorship",
- "srml-session",
- "srml-support",
- "srml-system",
- "substrate-keyring",
- "substrate-phragmen",
-]
-
-[[package]]
-name = "srml-staking-reward-curve"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "proc-macro-crate",
- "proc-macro2 1.0.9",
- "quote 1.0.3",
- "syn 1.0.16",
-]
-
-[[package]]
-name = "srml-sudo"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "parity-scale-codec",
- "serde",
- "sr-io",
- "sr-primitives",
- "sr-std",
- "srml-support",
- "srml-system",
-]
-
-[[package]]
-name = "srml-support"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "bitmask",
- "impl-trait-for-tuples",
- "log",
- "once_cell 0.2.4",
- "parity-scale-codec",
- "paste",
- "serde",
- "sr-io",
- "sr-primitives",
- "sr-std",
- "srml-metadata",
- "srml-support-procedural",
- "substrate-inherents",
- "substrate-primitives",
-]
-
-[[package]]
-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",
- "quote 1.0.3",
- "sr-api-macros",
- "srml-support-procedural-tools",
- "syn 1.0.16",
-]
-
-[[package]]
-name = "srml-support-procedural-tools"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "proc-macro-crate",
- "proc-macro2 1.0.9",
- "quote 1.0.3",
- "srml-support-procedural-tools-derive",
- "syn 1.0.16",
-]
-
-[[package]]
-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",
- "quote 1.0.3",
- "syn 1.0.16",
-]
-
-[[package]]
-name = "srml-system"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "impl-trait-for-tuples",
- "parity-scale-codec",
- "safe-mix",
- "serde",
- "sr-io",
- "sr-primitives",
- "sr-std",
- "sr-version",
- "srml-support",
- "substrate-primitives",
-]
-
-[[package]]
-name = "srml-system-rpc-runtime-api"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "parity-scale-codec",
- "substrate-client",
-]
-
-[[package]]
-name = "srml-timestamp"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "impl-trait-for-tuples",
- "parity-scale-codec",
- "serde",
- "sr-primitives",
- "sr-std",
- "srml-support",
- "srml-system",
- "substrate-inherents",
-]
-
-[[package]]
-name = "srml-transaction-payment"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "parity-scale-codec",
- "sr-primitives",
- "sr-std",
- "srml-support",
- "srml-system",
- "srml-transaction-payment-rpc-runtime-api",
-]
-
-[[package]]
-name = "srml-transaction-payment-rpc-runtime-api"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "parity-scale-codec",
- "serde",
- "sr-primitives",
- "sr-std",
- "substrate-client",
-]
-
-[[package]]
-name = "static_assertions"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
-
-[[package]]
-name = "stream-cipher"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8131256a5896cabcf5eb04f4d6dacbe1aefda854b0d9896e09cb58829ec5638c"
-dependencies = [
- "generic-array",
-]
-
-[[package]]
-name = "strum"
-version = "0.15.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e5d1c33039533f051704951680f1adfd468fd37ac46816ded0d9ee068e60f05f"
-
-[[package]]
-name = "strum_macros"
-version = "0.15.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "47cd23f5c7dee395a00fa20135e2ec0fffcdfa151c56182966d7a3261343432e"
-dependencies = [
- "heck",
- "proc-macro2 0.4.30",
- "quote 0.6.13",
- "syn 0.15.44",
-]
-
-[[package]]
-name = "substrate-application-crypto"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "parity-scale-codec",
- "serde",
- "sr-io",
- "sr-std",
- "substrate-primitives",
-]
-
-[[package]]
-name = "substrate-authority-discovery-primitives"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "parity-scale-codec",
- "sr-primitives",
- "sr-std",
- "substrate-client",
-]
-
-[[package]]
-name = "substrate-bip39"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3be511be555a3633e71739a79e4ddff6a6aaa6579fa6114182a51d72c3eb93c5"
-dependencies = [
- "hmac",
- "pbkdf2",
- "schnorrkel",
- "sha2",
-]
-
-[[package]]
-name = "substrate-client"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "derive_more",
- "fnv",
- "futures",
- "futures-preview",
- "hash-db",
- "hex-literal 0.2.1",
- "kvdb",
- "log",
- "parity-scale-codec",
- "parking_lot 0.9.0",
- "sr-api-macros",
- "sr-primitives",
- "sr-std",
- "sr-version",
- "substrate-consensus-common",
- "substrate-executor",
- "substrate-header-metadata",
- "substrate-inherents",
- "substrate-keyring",
- "substrate-primitives",
- "substrate-state-machine",
- "substrate-telemetry",
- "substrate-trie",
-]
-
-[[package]]
-name = "substrate-consensus-babe-primitives"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "parity-scale-codec",
- "schnorrkel",
- "sr-primitives",
- "sr-std",
- "substrate-application-crypto",
- "substrate-client",
- "substrate-consensus-slots",
-]
-
-[[package]]
-name = "substrate-consensus-common"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "derive_more",
- "futures-preview",
- "futures-timer",
- "libp2p",
- "log",
- "parity-scale-codec",
- "parking_lot 0.9.0",
- "sr-primitives",
- "sr-std",
- "sr-version",
- "substrate-inherents",
- "substrate-primitives",
-]
-
-[[package]]
-name = "substrate-consensus-slots"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "futures-preview",
- "futures-timer",
- "log",
- "parity-scale-codec",
- "parking_lot 0.9.0",
- "sr-primitives",
- "substrate-client",
- "substrate-consensus-common",
- "substrate-inherents",
- "substrate-primitives",
- "substrate-telemetry",
-]
-
-[[package]]
-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",
- "quote 1.0.3",
- "syn 1.0.16",
-]
-
-[[package]]
-name = "substrate-executor"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "derive_more",
- "lazy_static",
- "libsecp256k1",
- "log",
- "parity-scale-codec",
- "parity-wasm",
- "parking_lot 0.9.0",
- "sr-io",
- "sr-version",
- "substrate-externalities",
- "substrate-panic-handler",
- "substrate-primitives",
- "substrate-serializer",
- "substrate-trie",
- "substrate-wasm-interface",
- "tiny-keccak",
- "wasmi",
-]
-
-[[package]]
-name = "substrate-externalities"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "environmental",
- "primitive-types",
- "sr-std",
- "substrate-primitives-storage",
-]
-
-[[package]]
-name = "substrate-finality-grandpa-primitives"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "parity-scale-codec",
- "serde",
- "sr-primitives",
- "sr-std",
- "substrate-application-crypto",
- "substrate-client",
-]
-
-[[package]]
-name = "substrate-forum-module"
-version = "1.1.1"
-source = "git+https://github.com/joystream/substrate-forum-module?tag=v1.1.1#5918fc90d25faeac06311b0d6b05305cbe722a27"
-dependencies = [
- "hex-literal 0.1.4",
- "parity-scale-codec",
- "quote 0.6.13",
- "serde",
- "serde_derive",
- "sr-io",
- "sr-primitives",
- "sr-std",
- "srml-balances",
- "srml-support",
- "srml-support-procedural",
- "srml-system",
- "srml-timestamp",
-]
-
-[[package]]
-name = "substrate-header-metadata"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "lru-cache",
- "parking_lot 0.9.0",
- "sr-primitives",
-]
-
-[[package]]
-name = "substrate-hiring-module"
-version = "1.0.1"
-source = "git+https://github.com/Joystream/substrate-hiring-module?tag=v1.0.1#e387af033fb0e81c66d487bdec445153b0fe3cdf"
-dependencies = [
- "hex-literal 0.1.4",
- "parity-scale-codec",
- "quote 0.6.13",
- "serde",
- "serde_derive",
- "sr-io",
- "sr-primitives",
- "sr-std",
- "srml-balances",
- "srml-support",
- "srml-support-procedural",
- "srml-system",
- "srml-timestamp",
- "substrate-stake-module",
-]
-
-[[package]]
-name = "substrate-inherents"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "parity-scale-codec",
- "parking_lot 0.9.0",
- "sr-primitives",
- "sr-std",
-]
-
-[[package]]
-name = "substrate-keyring"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "lazy_static",
- "sr-primitives",
- "strum",
- "strum_macros",
- "substrate-primitives",
-]
-
-[[package]]
-name = "substrate-offchain-primitives"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "sr-primitives",
- "substrate-client",
-]
-
-[[package]]
-name = "substrate-panic-handler"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "backtrace",
- "log",
-]
-
-[[package]]
-name = "substrate-phragmen"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "serde",
- "sr-primitives",
- "sr-std",
-]
-
-[[package]]
-name = "substrate-primitives"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "base58",
- "blake2-rfc",
- "byteorder 1.3.4",
- "ed25519-dalek 0.9.1",
- "hash-db",
- "hash256-std-hasher",
- "hex",
- "impl-serde 0.2.3",
- "lazy_static",
- "libsecp256k1",
- "log",
- "num-traits",
- "parity-scale-codec",
- "parking_lot 0.9.0",
- "primitive-types",
- "rand 0.7.3",
- "regex",
- "rustc-hex",
- "schnorrkel",
- "serde",
- "sha2",
- "sr-std",
- "substrate-bip39",
- "substrate-debug-derive",
- "substrate-externalities",
- "substrate-primitives-storage",
- "tiny-bip39",
- "tiny-keccak",
- "twox-hash",
- "wasmi",
- "zeroize 0.10.1",
-]
-
-[[package]]
-name = "substrate-primitives-storage"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "impl-serde 0.2.3",
- "serde",
- "sr-std",
- "substrate-debug-derive",
-]
-
-[[package]]
-name = "substrate-recurring-reward-module"
-version = "1.0.1"
-source = "git+https://github.com/Joystream/substrate-recurring-reward-module?tag=v1.0.1#2c4bda1dea315629313643737c2f59979579fb50"
-dependencies = [
- "hex-literal 0.1.4",
- "parity-scale-codec",
- "quote 0.6.13",
- "serde",
- "serde_derive",
- "sr-io",
- "sr-primitives",
- "sr-std",
- "srml-balances",
- "srml-support",
- "srml-support-procedural",
- "srml-system",
- "srml-timestamp",
- "substrate-token-mint-module",
-]
-
-[[package]]
-name = "substrate-serializer"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "serde",
- "serde_json",
-]
-
-[[package]]
-name = "substrate-session"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "sr-primitives",
- "sr-std",
- "substrate-client",
- "substrate-primitives",
-]
-
-[[package]]
-name = "substrate-stake-module"
-version = "1.0.1"
-source = "git+https://github.com/Joystream/substrate-stake-module/?tag=v1.0.1#af5860c3cde5b11e37728df0b1dfbbdf9a9fa2f3"
-dependencies = [
- "hex-literal 0.1.4",
- "parity-scale-codec",
- "quote 0.6.13",
- "serde",
- "serde_derive",
- "sr-io",
- "sr-primitives",
- "sr-std",
- "srml-balances",
- "srml-support",
- "srml-support-procedural",
- "srml-system",
- "srml-timestamp",
-]
-
-[[package]]
-name = "substrate-state-machine"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "hash-db",
- "log",
- "num-traits",
- "parity-scale-codec",
- "parking_lot 0.9.0",
- "rand 0.7.3",
- "substrate-externalities",
- "substrate-panic-handler",
- "substrate-primitives",
- "substrate-trie",
- "trie-db",
- "trie-root",
-]
-
-[[package]]
-name = "substrate-telemetry"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "bytes 0.4.12",
- "futures",
- "futures-preview",
- "futures-timer",
- "libp2p",
- "log",
- "parking_lot 0.9.0",
- "rand 0.7.3",
- "serde",
- "slog",
- "slog-async",
- "slog-json",
- "slog-scope",
- "tokio-io",
- "void",
-]
-
-[[package]]
-name = "substrate-token-mint-module"
-version = "1.0.1"
-source = "git+https://github.com/joystream/substrate-token-minting-module/?tag=v1.0.1#7905ce50136cf8483a808a1946fbf123b9ca4bb8"
-dependencies = [
- "hex-literal 0.1.4",
- "parity-scale-codec",
- "quote 0.6.13",
- "serde",
- "serde_derive",
- "sr-io",
- "sr-primitives",
- "sr-std",
- "srml-balances",
- "srml-support",
- "srml-support-procedural",
- "srml-system",
- "srml-timestamp",
-]
-
-[[package]]
-name = "substrate-trie"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "hash-db",
- "memory-db",
- "parity-scale-codec",
- "sr-std",
- "substrate-primitives",
- "trie-db",
- "trie-root",
-]
-
-[[package]]
-name = "substrate-versioned-store"
-version = "1.0.1"
-source = "git+https://github.com/joystream/substrate-versioned-store-module?tag=v1.0.1#24bcd60e84c1ece74a8a3130beb740f6fa760145"
-dependencies = [
- "hex-literal 0.1.4",
- "parity-scale-codec",
- "quote 0.6.13",
- "serde",
- "serde_derive",
- "sr-io",
- "sr-primitives",
- "sr-std",
- "srml-balances",
- "srml-support",
- "srml-support-procedural",
- "srml-system",
- "srml-timestamp",
-]
-
-[[package]]
-name = "substrate-versioned-store-permissions-module"
-version = "1.0.1"
-source = "git+https://github.com/joystream/substrate-versioned-store-permissions-module?tag=v1.0.1#dd75f4bfe283673685c4ccf9de14384a546daa6e"
-dependencies = [
- "hex-literal 0.1.4",
- "parity-scale-codec",
- "quote 0.6.13",
- "serde",
- "serde_derive",
- "sr-io",
- "sr-primitives",
- "sr-std",
- "srml-support",
- "srml-support-procedural",
- "srml-system",
- "srml-timestamp",
- "substrate-versioned-store",
-]
-
-[[package]]
-name = "substrate-wasm-builder-runner"
-version = "1.0.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e30c70de7e7d5fd404fe26db1e7a4d6b553e2760b1ac490f249c04a960c483b8"
-
-[[package]]
-name = "substrate-wasm-interface"
-version = "2.0.0"
-source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
-dependencies = [
- "wasmi",
-]
-
-[[package]]
-name = "subtle"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee"
-
-[[package]]
-name = "subtle"
-version = "2.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c65d530b10ccaeac294f349038a597e435b18fb456aadd0840a623f83b9e941"
-
-[[package]]
-name = "syn"
-version = "0.15.44"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5"
-dependencies = [
- "proc-macro2 0.4.30",
- "quote 0.6.13",
- "unicode-xid 0.1.0",
-]
-
-[[package]]
-name = "syn"
-version = "1.0.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "123bd9499cfb380418d509322d7a6d52e5315f064fe4b3ad18a53d6b92c07859"
-dependencies = [
- "proc-macro2 1.0.9",
- "quote 1.0.3",
- "unicode-xid 0.2.0",
-]
-
-[[package]]
-name = "synstructure"
-version = "0.12.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545"
-dependencies = [
- "proc-macro2 1.0.9",
- "quote 1.0.3",
- "syn 1.0.16",
- "unicode-xid 0.2.0",
-]
-
-[[package]]
-name = "take_mut"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60"
-
-[[package]]
-name = "thread_local"
-version = "0.3.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b"
-dependencies = [
- "lazy_static",
-]
-
-[[package]]
-name = "thread_local"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14"
-dependencies = [
- "lazy_static",
-]
-
-[[package]]
-name = "time"
-version = "0.1.42"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f"
-dependencies = [
- "libc",
- "redox_syscall",
- "winapi 0.3.8",
-]
-
-[[package]]
-name = "tiny-bip39"
-version = "0.6.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1c5676413eaeb1ea35300a0224416f57abc3bd251657e0fafc12c47ff98c060"
-dependencies = [
- "failure",
- "hashbrown 0.1.8",
- "hmac",
- "once_cell 0.1.8",
- "pbkdf2",
- "rand 0.6.5",
- "sha2",
-]
-
-[[package]]
-name = "tiny-keccak"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d8a021c69bb74a44ccedb824a046447e2c84a01df9e5c20779750acb38e11b2"
-dependencies = [
- "crunchy",
-]
-
-[[package]]
-name = "tokio"
-version = "0.1.22"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a09c0b5bb588872ab2f09afa13ee6e9dac11e10a0ec9e8e3ba39a5a5d530af6"
-dependencies = [
- "bytes 0.4.12",
- "futures",
- "mio",
- "num_cpus",
- "tokio-codec",
- "tokio-current-thread",
- "tokio-executor",
- "tokio-fs",
- "tokio-io",
- "tokio-reactor",
- "tokio-sync",
- "tokio-tcp",
- "tokio-threadpool",
- "tokio-timer",
- "tokio-udp",
- "tokio-uds",
-]
-
-[[package]]
-name = "tokio-codec"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25b2998660ba0e70d18684de5d06b70b70a3a747469af9dea7618cc59e75976b"
-dependencies = [
- "bytes 0.4.12",
- "futures",
- "tokio-io",
-]
-
-[[package]]
-name = "tokio-current-thread"
-version = "0.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1de0e32a83f131e002238d7ccde18211c0a5397f60cbfffcb112868c2e0e20e"
-dependencies = [
- "futures",
- "tokio-executor",
-]
-
-[[package]]
-name = "tokio-dns-unofficial"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "82c65483db54eb91b4ef3a9389a3364558590faf30ce473141707c0e16fda975"
-dependencies = [
- "futures",
- "futures-cpupool",
- "lazy_static",
- "tokio",
-]
-
-[[package]]
-name = "tokio-executor"
-version = "0.1.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb2d1b8f4548dbf5e1f7818512e9c406860678f29c300cdf0ebac72d1a3a1671"
-dependencies = [
- "crossbeam-utils 0.7.2",
- "futures",
-]
-
-[[package]]
-name = "tokio-fs"
-version = "0.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "297a1206e0ca6302a0eed35b700d292b275256f596e2f3fea7729d5e629b6ff4"
-dependencies = [
- "futures",
- "tokio-io",
- "tokio-threadpool",
-]
-
-[[package]]
-name = "tokio-io"
-version = "0.1.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "57fc868aae093479e3131e3d165c93b1c7474109d13c90ec0dda2a1bbfff0674"
-dependencies = [
- "bytes 0.4.12",
- "futures",
- "log",
-]
-
-[[package]]
-name = "tokio-reactor"
-version = "0.1.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09bc590ec4ba8ba87652da2068d150dcada2cfa2e07faae270a5e0409aa51351"
-dependencies = [
- "crossbeam-utils 0.7.2",
- "futures",
- "lazy_static",
- "log",
- "mio",
- "num_cpus",
- "parking_lot 0.9.0",
- "slab",
- "tokio-executor",
- "tokio-io",
- "tokio-sync",
-]
-
-[[package]]
-name = "tokio-rustls"
-version = "0.10.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d7cf08f990090abd6c6a73cab46fed62f85e8aef8b99e4b918a9f4a637f0676"
-dependencies = [
- "bytes 0.4.12",
- "futures",
- "iovec",
- "rustls",
- "tokio-io",
- "webpki",
-]
-
-[[package]]
-name = "tokio-sync"
-version = "0.1.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "edfe50152bc8164fcc456dab7891fa9bf8beaf01c5ee7e1dd43a397c3cf87dee"
-dependencies = [
- "fnv",
- "futures",
-]
-
-[[package]]
-name = "tokio-tcp"
-version = "0.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "98df18ed66e3b72e742f185882a9e201892407957e45fbff8da17ae7a7c51f72"
-dependencies = [
- "bytes 0.4.12",
- "futures",
- "iovec",
- "mio",
- "tokio-io",
- "tokio-reactor",
-]
-
-[[package]]
-name = "tokio-threadpool"
-version = "0.1.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df720b6581784c118f0eb4310796b12b1d242a7eb95f716a8367855325c25f89"
-dependencies = [
- "crossbeam-deque",
- "crossbeam-queue",
- "crossbeam-utils 0.7.2",
- "futures",
- "lazy_static",
- "log",
- "num_cpus",
- "slab",
- "tokio-executor",
-]
-
-[[package]]
-name = "tokio-timer"
-version = "0.2.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93044f2d313c95ff1cb7809ce9a7a05735b012288a888b62d4434fd58c94f296"
-dependencies = [
- "crossbeam-utils 0.7.2",
- "futures",
- "slab",
- "tokio-executor",
-]
-
-[[package]]
-name = "tokio-udp"
-version = "0.1.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2a0b10e610b39c38b031a2fcab08e4b82f16ece36504988dcbd81dbba650d82"
-dependencies = [
- "bytes 0.4.12",
- "futures",
- "log",
- "mio",
- "tokio-codec",
- "tokio-io",
- "tokio-reactor",
-]
-
-[[package]]
-name = "tokio-uds"
-version = "0.2.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5076db410d6fdc6523df7595447629099a1fdc47b3d9f896220780fa48faf798"
-dependencies = [
- "bytes 0.4.12",
- "futures",
- "iovec",
- "libc",
- "log",
- "mio",
- "mio-uds",
- "tokio-codec",
- "tokio-io",
- "tokio-reactor",
-]
-
-[[package]]
-name = "toml"
-version = "0.5.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ffc92d160b1eef40665be3a05630d003936a3bc7da7421277846c2613e92c71a"
-dependencies = [
- "serde",
-]
-
-[[package]]
-name = "trie-db"
-version = "0.15.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d0b62d27e8aa1c07414549ac872480ac82380bab39e730242ab08d82d7cc098a"
-dependencies = [
- "elastic-array",
- "hash-db",
- "hashbrown 0.6.3",
- "log",
- "rand 0.6.5",
-]
-
-[[package]]
-name = "trie-root"
-version = "0.15.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b779f7c1c8fe9276365d9d5be5c4b5adeacf545117bb3f64c974305789c5c0b"
-dependencies = [
- "hash-db",
-]
-
-[[package]]
-name = "twofish"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "712d261e83e727c8e2dbb75dacac67c36e35db36a958ee504f2164fc052434e1"
-dependencies = [
- "block-cipher-trait",
- "byteorder 1.3.4",
- "opaque-debug",
-]
-
-[[package]]
-name = "twox-hash"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3bfd5b7557925ce778ff9b9ef90e3ade34c524b5ff10e239c69a42d546d2af56"
-dependencies = [
- "rand 0.7.3",
-]
-
-[[package]]
-name = "typenum"
-version = "1.11.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d2783fe2d6b8c1101136184eb41be8b1ad379e4657050b8aaff0c79ee7575f9"
-
-[[package]]
-name = "uint"
-version = "0.8.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e75a4cdd7b87b28840dba13c483b9a88ee6bbf16ba5c951ee1ecfcf723078e0d"
-dependencies = [
- "byteorder 1.3.4",
- "crunchy",
- "rustc-hex",
- "static_assertions",
-]
-
-[[package]]
-name = "unicode-bidi"
-version = "0.3.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5"
-dependencies = [
- "matches",
-]
-
-[[package]]
-name = "unicode-normalization"
-version = "0.1.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5479532badd04e128284890390c1e876ef7a993d0570b3597ae43dfa1d59afa4"
-dependencies = [
- "smallvec 1.2.0",
-]
-
-[[package]]
-name = "unicode-segmentation"
-version = "1.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0"
-
-[[package]]
-name = "unicode-xid"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
-
-[[package]]
-name = "unicode-xid"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
-
-[[package]]
-name = "unsigned-varint"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7f0023a96687fe169081e8adce3f65e3874426b7886e9234d490af2dc077959"
-dependencies = [
- "bytes 0.4.12",
- "tokio-codec",
-]
-
-[[package]]
-name = "unsigned-varint"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f38e01ad4b98f042e166c1bf9a13f9873a99d79eaa171ce7ca81e6dd0f895d8a"
-
-[[package]]
-name = "untrusted"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "60369ef7a31de49bcb3f6ca728d4ba7300d9a1658f94c727d4cab8c8d9f4aece"
-
-[[package]]
-name = "url"
-version = "2.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb"
-dependencies = [
- "idna",
- "matches",
- "percent-encoding",
-]
-
-[[package]]
-name = "vcpkg"
-version = "0.2.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fc439f2794e98976c88a2a2dafce96b930fe8010b0a256b3c2199a773933168"
-
-[[package]]
-name = "void"
-version = "1.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
-
-[[package]]
-name = "wasi"
-version = "0.9.0+wasi-snapshot-preview1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
-
-[[package]]
-name = "wasm-bindgen"
-version = "0.2.59"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3557c397ab5a8e347d434782bcd31fc1483d927a6826804cec05cc792ee2519d"
-dependencies = [
- "cfg-if",
- "wasm-bindgen-macro",
-]
-
-[[package]]
-name = "wasm-bindgen-backend"
-version = "0.2.59"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0da9c9a19850d3af6df1cb9574970b566d617ecfaf36eb0b706b6f3ef9bd2f8"
-dependencies = [
- "bumpalo",
- "lazy_static",
- "log",
- "proc-macro2 1.0.9",
- "quote 1.0.3",
- "syn 1.0.16",
- "wasm-bindgen-shared",
-]
-
-[[package]]
-name = "wasm-bindgen-futures"
-version = "0.3.27"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83420b37346c311b9ed822af41ec2e82839bfe99867ec6c54e2da43b7538771c"
-dependencies = [
- "cfg-if",
- "futures",
- "js-sys",
- "wasm-bindgen",
- "web-sys",
-]
-
-[[package]]
-name = "wasm-bindgen-macro"
-version = "0.2.59"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f6fde1d36e75a714b5fe0cffbb78978f222ea6baebb726af13c78869fdb4205"
-dependencies = [
- "quote 1.0.3",
- "wasm-bindgen-macro-support",
-]
-
-[[package]]
-name = "wasm-bindgen-macro-support"
-version = "0.2.59"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25bda4168030a6412ea8a047e27238cadf56f0e53516e1e83fec0a8b7c786f6d"
-dependencies = [
- "proc-macro2 1.0.9",
- "quote 1.0.3",
- "syn 1.0.16",
- "wasm-bindgen-backend",
- "wasm-bindgen-shared",
-]
-
-[[package]]
-name = "wasm-bindgen-shared"
-version = "0.2.59"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc9f36ad51f25b0219a3d4d13b90eb44cd075dff8b6280cca015775d7acaddd8"
-
-[[package]]
-name = "wasm-timer"
-version = "0.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aa3e01d234bb71760e685cfafa5e2c96f8ad877c161a721646356651069e26ac"
-dependencies = [
- "futures",
- "js-sys",
- "send_wrapper",
- "tokio-timer",
- "wasm-bindgen",
- "web-sys",
-]
-
-[[package]]
-name = "wasmi"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f31d26deb2d9a37e6cfed420edce3ed604eab49735ba89035e13c98f9a528313"
-dependencies = [
- "libc",
- "memory_units",
- "num-rational",
- "num-traits",
- "parity-wasm",
- "wasmi-validation",
-]
-
-[[package]]
-name = "wasmi-validation"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6bc0356e3df56e639fc7f7d8a99741915531e27ed735d911ed83d7e1339c8188"
-dependencies = [
- "parity-wasm",
-]
-
-[[package]]
-name = "web-sys"
-version = "0.3.36"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "721c6263e2c66fd44501cc5efbfa2b7dfa775d13e4ea38c46299646ed1f9c70a"
-dependencies = [
- "js-sys",
- "wasm-bindgen",
-]
-
-[[package]]
-name = "webpki"
-version = "0.21.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1f50e1972865d6b1adb54167d1c8ed48606004c2c9d0ea5f1eeb34d95e863ef"
-dependencies = [
- "ring",
- "untrusted",
-]
-
-[[package]]
-name = "webpki-roots"
-version = "0.18.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91cd5736df7f12a964a5067a12c62fa38e1bd8080aff1f80bc29be7c80d19ab4"
-dependencies = [
- "webpki",
-]
-
-[[package]]
-name = "winapi"
-version = "0.2.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
-
-[[package]]
-name = "winapi"
-version = "0.3.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6"
-dependencies = [
- "winapi-i686-pc-windows-gnu",
- "winapi-x86_64-pc-windows-gnu",
-]
-
-[[package]]
-name = "winapi-build"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
-
-[[package]]
-name = "winapi-i686-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
-
-[[package]]
-name = "winapi-x86_64-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
-
-[[package]]
-name = "ws2_32-sys"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
-dependencies = [
- "winapi 0.2.8",
- "winapi-build",
-]
-
-[[package]]
-name = "x25519-dalek"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ee1585dc1484373cbc1cee7aafda26634665cf449436fd6e24bfd1fad230538"
-dependencies = [
- "clear_on_drop",
- "curve25519-dalek 1.2.3",
- "rand_core 0.3.1",
-]
-
-[[package]]
-name = "yamux"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2758f29014c1cb7a6e74c1b1160ac8c8203be342d35b73462fc6a13cc6385423"
-dependencies = [
- "bytes 0.4.12",
- "futures",
- "log",
- "nohash-hasher",
- "parking_lot 0.9.0",
- "quick-error",
- "rand 0.7.3",
- "tokio-codec",
- "tokio-io",
-]
-
-[[package]]
-name = "zeroize"
-version = "0.9.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "45af6a010d13e4cf5b54c94ba5a2b2eba5596b9e46bf5875612d332a1f2b3f86"
-
-[[package]]
-name = "zeroize"
-version = "0.10.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4090487fa66630f7b166fba2bbb525e247a5449f41c468cc1d98f8ae6ac03120"
-
-[[package]]
-name = "zeroize"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3cbac2ed2ba24cc90f5e06485ac8c7c1e5449fe8911aef4d8877218af021a5b8"
-dependencies = [
- "zeroize_derive",
-]
-
-[[package]]
-name = "zeroize_derive"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "de251eec69fc7c1bc3923403d18ececb929380e016afe103da75f396704f8ca2"
-dependencies = [
- "proc-macro2 1.0.9",
- "quote 1.0.3",
- "syn 1.0.16",
- "synstructure",
-]

+ 18 - 303
Cargo.toml

@@ -1,305 +1,20 @@
-[package]
-authors = ['Joystream']
-edition = '2018'
-name = 'joystream-node-runtime'
-# Follow convention: https://github.com/Joystream/substrate-runtime-joystream/issues/1
-# {Authoring}.{Spec}.{Impl} of the RuntimeVersion
-version = '6.8.0'
-
-[features]
-default = ['std']
-no_std = []
-std = [
-    'authority-discovery-primitives/std',
-    'authority-discovery/std',
-    'authorship/std',
-    'finality-tracker/std',
-    'im-online/std',
-    'session/std',
-    'offences/std',
-    'sr-staking-primitives/std',
-    'staking/std',
-    'codec/std',
-    'substrate-client/std',
-    'rstd/std',
-    'runtime-io/std',
-    'srml-support/std',
-    'balances/std',
-    'babe/std',
-    'babe-primitives/std',
-    'executive/std',
-    'indices/std',
-    'grandpa/std',
-    'primitives/std',
-    'sr-primitives/std',
-    'system/std',
-    'timestamp/std',
-    'sudo/std',
-    'version/std',
-    'serde',
-    'safe-mix/std',
-    'offchain-primitives/std',
-    'substrate-session/std',
-    'transaction-payment/std',
-    'randomness-collective-flip/std',
-    'system-rpc-runtime-api/std',
-    'forum/std',
-    'minting/std',
-    'recurringrewards/std',
-    'stake/std',
-    'hiring/std',
-    'versioned_store/std',
-    'versioned_store_permissions/std'
+[workspace]
+members = [
+	"runtime",
+	"runtime-modules/common",
+	"runtime-modules/content-working-group",
+	"runtime-modules/forum",
+	"runtime-modules/governance",
+	"runtime-modules/hiring",
+	"runtime-modules/membership",
+	"runtime-modules/memo",
+	"runtime-modules/recurring-reward",
+	"runtime-modules/roles",
+	"runtime-modules/service-discovery",
+	"runtime-modules/stake",
+	"runtime-modules/storage",
+	"runtime-modules/token-minting",
+	"runtime-modules/versioned-store",
+	"runtime-modules/versioned-store-permissions",
 ]
 
-# [dependencies]
-# # https://users.rust-lang.org/t/failure-derive-compilation-error/39062
-# quote = '<=1.0.2'
-
-[dependencies.babe]
-default-features = false
-git = 'https://github.com/paritytech/substrate.git'
-package = 'srml-babe'
-rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
-
-[dependencies.babe-primitives]
-default-features = false
-git = 'https://github.com/paritytech/substrate.git'
-package = 'substrate-consensus-babe-primitives'
-rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
-
-[dependencies.balances]
-default_features = false
-git = 'https://github.com/paritytech/substrate.git'
-package = 'srml-balances'
-rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
-
-[dependencies.substrate-client]
-default_features = false
-git = 'https://github.com/paritytech/substrate.git'
-package = 'substrate-client'
-rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
-
-[dependencies.codec]
-default-features = false
-features = ['derive']
-package = 'parity-scale-codec'
-version = '1.0.0'
-
-[dependencies.executive]
-default_features = false
-git = 'https://github.com/paritytech/substrate.git'
-package = 'srml-executive'
-rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
-
-[dependencies.grandpa]
-default-features = false
-git = 'https://github.com/paritytech/substrate.git'
-package = 'srml-grandpa'
-rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
-
-[dependencies.indices]
-default_features = false
-git = 'https://github.com/paritytech/substrate.git'
-package = 'srml-indices'
-rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
-
-[dependencies.offchain-primitives]
-default-features = false
-git = 'https://github.com/paritytech/substrate.git'
-package = 'substrate-offchain-primitives'
-rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
-
-[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.runtime-io]
-default_features = false
-git = 'https://github.com/paritytech/substrate.git'
-package = 'sr-io'
-rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
-
-[dependencies.safe-mix]
-default-features = false
-version = '1.0'
-
-[dependencies.serde]
-features = ['derive']
-optional = true
-version = '1.0.101'
-
-[dependencies.sr-primitives]
-default_features = false
-git = 'https://github.com/paritytech/substrate.git'
-package = 'sr-primitives'
-rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
-
-[dependencies.substrate-session]
-default-features = false
-git = 'https://github.com/paritytech/substrate.git'
-rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
-
-[dependencies.sudo]
-default_features = false
-git = 'https://github.com/paritytech/substrate.git'
-package = 'srml-sudo'
-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.version]
-default_features = false
-git = 'https://github.com/paritytech/substrate.git'
-package = 'sr-version'
-rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
-
-[dependencies.authority-discovery-primitives]
-default_features = false
-git = 'https://github.com/paritytech/substrate.git'
-package = 'substrate-authority-discovery-primitives'
-rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
-
-[dependencies.authority-discovery]
-default_features = false
-git = 'https://github.com/paritytech/substrate.git'
-package = 'srml-authority-discovery'
-rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
-
-[dependencies.authorship]
-default_features = false
-git = 'https://github.com/paritytech/substrate.git'
-package = 'srml-authorship'
-rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
-
-[dependencies.finality-tracker]
-default_features = false
-git = 'https://github.com/paritytech/substrate.git'
-package = 'srml-finality-tracker'
-rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
-
-[dependencies.im-online]
-default_features = false
-git = 'https://github.com/paritytech/substrate.git'
-package = 'srml-im-online'
-rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
-
-[dependencies.session]
-default_features = false
-git = 'https://github.com/paritytech/substrate.git'
-package = 'srml-session'
-rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
-
-[dependencies.offences]
-default_features = false
-git = 'https://github.com/paritytech/substrate.git'
-package = 'srml-offences'
-rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
-
-[dependencies.sr-staking-primitives]
-default_features = false
-git = 'https://github.com/paritytech/substrate.git'
-package = 'sr-staking-primitives'
-rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
-
-[dependencies.staking]
-default_features = false
-git = 'https://github.com/paritytech/substrate.git'
-package = 'srml-staking'
-rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
-
-[dependencies.transaction-payment]
-package = "srml-transaction-payment"
-git = 'https://github.com/paritytech/substrate.git'
-default_features = false
-rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
-
-[dependencies.randomness-collective-flip]
-package = "srml-randomness-collective-flip"
-git = 'https://github.com/paritytech/substrate.git'
-default_features = false
-rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
-
-[dependencies.system-rpc-runtime-api]
-package = "srml-system-rpc-runtime-api"
-git = 'https://github.com/paritytech/substrate.git'
-default_features = false
-rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
-
-# don't rename the dependency it is causing some strange compiler error:
-# https://github.com/rust-lang/rust/issues/64450
-[dependencies.srml-staking-reward-curve]
-package = 'srml-staking-reward-curve'
-git = 'https://github.com/paritytech/substrate.git'
-default_features = false
-rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
-
-[build-dependencies.wasm-builder-runner]
-package = 'substrate-wasm-builder-runner'
-version = '1.0.4'
-
-[dependencies.forum]
-default_features = false
-package = 'substrate-forum-module'
-git = 'https://github.com/joystream/substrate-forum-module'
-tag = 'v1.1.1'
-
-[dependencies.minting]
-default_features = false
-package = 'substrate-token-mint-module'
-git = 'https://github.com/joystream/substrate-token-minting-module/'
-tag = 'v1.0.1'
-
-[dependencies.stake]
-default_features = false
-package = 'substrate-stake-module'
-git = 'https://github.com/Joystream/substrate-stake-module/'
-tag = 'v1.0.1'
-
-[dependencies.recurringrewards]
-default_features = false
-package = 'substrate-recurring-reward-module'
-git = 'https://github.com/Joystream/substrate-recurring-reward-module'
-tag = 'v1.0.1'
-
-[dependencies.hiring]
-default_features = false
-package = 'substrate-hiring-module'
-git = 'https://github.com/Joystream/substrate-hiring-module'
-tag = 'v1.0.1'
-
-[dependencies.versioned_store]
-default_features = false
-package ='substrate-versioned-store'
-git = 'https://github.com/joystream/substrate-versioned-store-module'
-tag = 'v1.0.1'
-
-[dependencies.versioned_store_permissions]
-default_features = false
-package = 'substrate-versioned-store-permissions-module'
-git = 'https://github.com/joystream/substrate-versioned-store-permissions-module'
-tag = 'v1.0.1'

+ 32 - 0
runtime-modules/common/Cargo.toml

@@ -0,0 +1,32 @@
+[package]
+name = 'substrate-common-module'
+version = '1.0.0'
+authors = ['Joystream contributors']
+edition = '2018'
+
+[features]
+default = ['std']
+std = [
+	'sr-primitives/std',
+	'srml-support/std',
+	'system/std',
+]
+
+
+[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'

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


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

@@ -0,0 +1,4 @@
+// Ensure we're `no_std` when compiling for Wasm.
+#![cfg_attr(not(feature = "std"), no_std)]
+
+pub mod currency;

+ 130 - 0
runtime-modules/content-working-group/Cargo.toml

@@ -0,0 +1,130 @@
+[package]
+name = 'substrate-content-working-group-module'
+version = '1.0.0'
+authors = ['Joystream contributors']
+edition = '2018'
+
+[features]
+default = ['std']
+std = [
+	'sr-primitives/std',
+	'srml-support/std',
+	'system/std',
+    'serde',
+    'codec/std',
+    'primitives/std',
+    'rstd/std',
+    'membership/std',
+    'forum/std',
+    'hiring/std',
+    'stake/std',
+    'minting/std',
+    'versioned_store/std',
+    'versioned_store_permissions/std',
+    'recurringrewards/std',
+]
+
+
+[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.rstd]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-std'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.serde]
+features = ['derive']
+optional = true
+version = '1.0.101'
+
+[dependencies.primitives]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'substrate-primitives'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.codec]
+default-features = false
+features = ['derive']
+package = 'parity-scale-codec'
+version = '1.0.0'
+
+[dependencies.forum]
+default_features = false
+package = 'substrate-forum-module'
+path = '../forum'
+
+[dependencies.minting]
+default_features = false
+package = 'substrate-token-mint-module'
+path = '../token-minting'
+
+[dependencies.stake]
+default_features = false
+package = 'substrate-stake-module'
+path = '../stake'
+
+[dependencies.recurringrewards]
+default_features = false
+package = 'substrate-recurring-reward-module'
+path = '../recurring-reward'
+
+[dependencies.hiring]
+default_features = false
+package = 'substrate-hiring-module'
+path = '../hiring'
+
+[dependencies.versioned_store]
+default_features = false
+package ='substrate-versioned-store'
+path = '../versioned-store'
+
+[dependencies.versioned_store_permissions]
+default_features = false
+package = 'substrate-versioned-store-permissions-module'
+path = '../versioned-store-permissions'
+
+[dependencies.membership]
+default_features = false
+package = 'substrate-membership-module'
+path = '../membership'
+
+[dev-dependencies.runtime-io]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-io'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dev-dependencies.balances]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-balances'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dev-dependencies.timestamp]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-timestamp'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dev-dependencies.common]
+default_features = false
+package = 'substrate-common-module'
+path = '../common'

+ 1 - 1
src/content_working_group/genesis.rs → runtime-modules/content-working-group/src/genesis.rs

@@ -1,6 +1,6 @@
 #![cfg(test)]
 
-use super::lib::{Trait, *};
+use crate::{Trait, *};
 pub use primitives::{map, Blake2Hasher, H256};
 use rstd::prelude::*;
 

+ 10 - 7
src/content_working_group/lib.rs → runtime-modules/content-working-group/src/lib.rs

@@ -1,15 +1,20 @@
 // Ensure we're `no_std` when compiling for Wasm.
 #![cfg_attr(not(feature = "std"), no_std)]
 
+#[cfg(test)]
+mod tests;
+
+#[cfg(test)]
+mod mock;
+
+pub mod genesis;
+
 #[cfg(feature = "std")]
 use serde::{Deserialize, Serialize};
 
 use codec::{Decode, Encode}; // Codec
                              //use rstd::collections::btree_map::BTreeMap;
-use crate::membership::{members, role_types};
-use hiring;
-use minting;
-use recurringrewards;
+use membership::{members, role_types};
 use rstd::collections::btree_map::BTreeMap;
 use rstd::collections::btree_set::BTreeSet;
 use rstd::convert::From;
@@ -23,9 +28,7 @@ use srml_support::{
     dispatch, // , StorageMap, , Parameter
     ensure,
 };
-use stake;
 use system::{self, ensure_root, ensure_signed};
-use versioned_store_permissions;
 
 /// DIRTY IMPORT BECAUSE
 /// InputValidationLengthConstraint has not been factored out yet!!!
@@ -792,7 +795,7 @@ struct WrappedBeginAcceptingApplicationsError { // can this be made generic, or
 }
 */
 
-pub(super) struct WrappedError<E> {
+pub struct WrappedError<E> {
     // can this be made generic, or does that undermine the whole orhpan rule spirit?
     pub error: E,
 }

+ 9 - 5
src/content_working_group/mock.rs → runtime-modules/content-working-group/src/mock.rs

@@ -1,6 +1,6 @@
 #![cfg(test)]
 
-pub use super::lib::{self, *}; // {self, Module, Trait, GenesisConfig};
+pub use crate::*; // {self, Module, Trait, GenesisConfig};
 pub use srml_support::traits::Currency;
 pub use system;
 
@@ -14,16 +14,16 @@ pub use sr_primitives::{
 
 use srml_support::{impl_outer_event, impl_outer_origin, parameter_types};
 
-pub use crate::currency::GovernanceCurrency;
-pub use crate::membership::members;
+pub use common::currency::GovernanceCurrency;
 pub use hiring;
+pub use membership::members;
 pub use minting;
 pub use recurringrewards;
 pub use stake;
 pub use versioned_store;
 pub use versioned_store_permissions;
 
-use super::genesis;
+use crate::genesis;
 
 parameter_types! {
     pub const BlockHashCount: u64 = 250;
@@ -48,6 +48,10 @@ impl_outer_origin! {
     pub enum Origin for Test {}
 }
 
+mod lib {
+    pub use crate::Event;
+}
+
 impl_outer_event! {
     pub enum TestEvent for Test {
         versioned_store<T>,
@@ -57,7 +61,7 @@ impl_outer_event! {
     }
 }
 
-pub type RawLibTestEvent = lib::RawEvent<
+pub type RawLibTestEvent = RawEvent<
     ChannelId<Test>,
     LeadId<Test>,
     CuratorOpeningId<Test>,

+ 0 - 0
src/content_working_group/mod.rs → runtime-modules/content-working-group/src/mod.rs


+ 57 - 56
src/content_working_group/tests.rs → runtime-modules/content-working-group/src/tests.rs

@@ -1,7 +1,6 @@
 #![cfg(test)]
 
 //use super::genesis;
-use super::lib;
 use super::mock::{self, *};
 //use crate::membership;
 use hiring;
@@ -295,9 +294,9 @@ impl UpdateChannelAsCurationActorFixture {
         assert_eq!(updated_channel, expected_updated_channel);
     }
 
-    fn get_event_deposited() -> lib::ChannelId<Test> {
+    fn get_event_deposited() -> crate::ChannelId<Test> {
         if let mock::TestEvent::lib(ref x) = System::events().last().unwrap().event {
-            if let lib::RawEvent::ChannelUpdatedByCurationActor(ref channel_id) = x {
+            if let crate::RawEvent::ChannelUpdatedByCurationActor(ref channel_id) = x {
                 return channel_id.clone();
             } else {
                 panic!("Event was not ChannelUpdatedByCurationActor.")
@@ -393,17 +392,17 @@ fn add_curator_opening_success() {
 
             assert_eq!(
                 get_last_event_or_panic(),
-                lib::RawEvent::CuratorOpeningAdded(expected_curator_opening_id)
+                crate::RawEvent::CuratorOpeningAdded(expected_curator_opening_id)
             );
 
             // Assert that given opening id has been added,
             // and has the right properties.
-            assert!(lib::CuratorOpeningById::<Test>::exists(
+            assert!(crate::CuratorOpeningById::<Test>::exists(
                 expected_curator_opening_id
             ));
 
             let created_curator_opening =
-                lib::CuratorOpeningById::<Test>::get(expected_curator_opening_id);
+                crate::CuratorOpeningById::<Test>::get(expected_curator_opening_id);
 
             let expected_curator_opening = CuratorOpening {
                 opening_id: expected_opening_id,
@@ -415,7 +414,7 @@ fn add_curator_opening_success() {
 
             // Assert that next id incremented.
             assert_eq!(
-                lib::NextCuratorOpeningId::<Test>::get(),
+                crate::NextCuratorOpeningId::<Test>::get(),
                 expected_opening_id + 1
             );
 
@@ -454,7 +453,7 @@ fn accept_curator_applications_success() {
 
             assert_eq!(
                 get_last_event_or_panic(),
-                lib::RawEvent::AcceptedCuratorApplications(curator_opening_id)
+                crate::RawEvent::AcceptedCuratorApplications(curator_opening_id)
             )
 
             /*
@@ -498,7 +497,7 @@ fn begin_curator_applicant_review_success() {
 
             assert_eq!(
                 get_last_event_or_panic(),
-                lib::RawEvent::BeganCuratorApplicationReview(
+                crate::RawEvent::BeganCuratorApplicationReview(
                     normal_opening_constructed.curator_opening_id
                 )
             );
@@ -632,7 +631,7 @@ fn withdraw_curator_application_success() {
             // Event was triggered
             assert_eq!(
                 get_last_event_or_panic(),
-                lib::RawEvent::CuratorApplicationWithdrawn(result.curator_application_id)
+                crate::RawEvent::CuratorApplicationWithdrawn(result.curator_application_id)
             );
 
             /*
@@ -676,7 +675,7 @@ fn terminate_curator_application_success() {
 
             assert_eq!(
                 get_last_event_or_panic(),
-                lib::RawEvent::CuratorApplicationTerminated(result.curator_application_id)
+                crate::RawEvent::CuratorApplicationTerminated(result.curator_application_id)
             );
 
             /*
@@ -752,7 +751,7 @@ fn apply_on_curator_opening_success() {
 
             assert_eq!(
                 get_last_event_or_panic(),
-                lib::RawEvent::AppliedOnCuratorOpening(
+                crate::RawEvent::AppliedOnCuratorOpening(
                     normal_opening_constructed.curator_opening_id,
                     new_curator_application_id
                 )
@@ -896,26 +895,27 @@ impl UpdateCuratorRoleAccountFixture {
         let updated_curator = CuratorById::<Test>::get(self.curator_id);
 
         assert_eq!(
-            lib::Curator {
+            crate::Curator {
                 role_account: self.new_role_account,
                 ..original_curator
             },
             updated_curator
         );
 
-        let (event_curator_id, event_new_role_account) = if let mock::TestEvent::lib(ref x) =
-            System::events().last().unwrap().event
-        {
-            if let lib::RawEvent::CuratorRoleAccountUpdated(ref curator_id, ref new_role_account) =
-                x
-            {
-                (curator_id.clone(), new_role_account.clone())
+        let (event_curator_id, event_new_role_account) =
+            if let mock::TestEvent::lib(ref x) = System::events().last().unwrap().event {
+                if let crate::RawEvent::CuratorRoleAccountUpdated(
+                    ref curator_id,
+                    ref new_role_account,
+                ) = x
+                {
+                    (curator_id.clone(), new_role_account.clone())
+                } else {
+                    panic!("Event was not CuratorRoleAccountUpdated.")
+                }
             } else {
-                panic!("Event was not CuratorRoleAccountUpdated.")
-            }
-        } else {
-            panic!("No event deposited.")
-        };
+                panic!("No event deposited.")
+            };
 
         assert_eq!(self.curator_id, event_curator_id);
 
@@ -977,19 +977,20 @@ impl UpdateCuratorRewardAccountFixture {
             Actually checking new reward account requires checking call to token mint module, but we cannot do that properly yet.
         */
 
-        let (event_curator_id, event_reward_account) = if let mock::TestEvent::lib(ref x) =
-            System::events().last().unwrap().event
-        {
-            if let lib::RawEvent::CuratorRewardAccountUpdated(ref curator_id, ref reward_account) =
-                x
-            {
-                (curator_id.clone(), reward_account.clone())
+        let (event_curator_id, event_reward_account) =
+            if let mock::TestEvent::lib(ref x) = System::events().last().unwrap().event {
+                if let crate::RawEvent::CuratorRewardAccountUpdated(
+                    ref curator_id,
+                    ref reward_account,
+                ) = x
+                {
+                    (curator_id.clone(), reward_account.clone())
+                } else {
+                    panic!("Event was not CuratorRewardAccountUpdated.")
+                }
             } else {
-                panic!("Event was not CuratorRewardAccountUpdated.")
-            }
-        } else {
-            panic!("No event deposited.")
-        };
+                panic!("No event deposited.")
+            };
 
         assert_eq!(self.curator_id, event_curator_id);
 
@@ -1058,7 +1059,7 @@ impl LeaveCuratorRoleFixture {
 
         assert_eq!(
             get_last_event_or_panic(),
-            lib::RawEvent::CuratorUnstaking(self.curator_id)
+            crate::RawEvent::CuratorUnstaking(self.curator_id)
         );
 
         // Tracking unstaking
@@ -1137,7 +1138,7 @@ impl TerminateCuratorRoleFixture {
 
         assert_eq!(
             get_last_event_or_panic(),
-            lib::RawEvent::CuratorUnstaking(self.curator_id)
+            crate::RawEvent::CuratorUnstaking(self.curator_id)
         );
 
         // Tracking unstaking
@@ -1220,7 +1221,7 @@ impl SetLeadFixture {
 
         assert_eq!(
             get_last_event_or_panic(),
-            lib::RawEvent::LeadSet(new_lead_id)
+            crate::RawEvent::LeadSet(new_lead_id)
         );
     }
 
@@ -1284,7 +1285,7 @@ impl UnsetLeadFixture {
 
         assert_eq!(
             get_last_event_or_panic(),
-            lib::RawEvent::LeadUnset(original_lead_id)
+            crate::RawEvent::LeadUnset(original_lead_id)
         );
     }
 
@@ -1353,7 +1354,7 @@ impl UnstakedFixture {
 
         assert_eq!(
             get_last_event_or_panic(),
-            lib::RawEvent::TerminatedCurator(curator_id)
+            crate::RawEvent::TerminatedCurator(curator_id)
         );
 
         // Unstaker gone
@@ -1547,7 +1548,7 @@ fn add_members_and_apply_on_opening(
 #[derive(Clone)]
 struct NewMemberAppliedResult {
     pub member_id: <Test as members::Trait>::MemberId,
-    pub curator_application_id: lib::CuratorApplicationId<Test>,
+    pub curator_application_id: crate::CuratorApplicationId<Test>,
 }
 
 fn add_member_and_apply_on_opening(
@@ -1611,7 +1612,7 @@ fn add_member_and_apply_on_opening(
 
     assert_eq!(
         get_last_event_or_panic(),
-        lib::RawEvent::AppliedOnCuratorOpening(curator_opening_id, new_curator_application_id)
+        crate::RawEvent::AppliedOnCuratorOpening(curator_opening_id, new_curator_application_id)
     );
 
     assert!(CuratorApplicationById::<Test>::exists(
@@ -1678,7 +1679,7 @@ fn setup_normal_opening() -> NormalOpeningConstructed {
 
     assert_eq!(
         get_last_event_or_panic(),
-        lib::RawEvent::CuratorOpeningAdded(expected_curator_opening_id)
+        crate::RawEvent::CuratorOpeningAdded(expected_curator_opening_id)
     );
 
     NormalOpeningConstructed {
@@ -1816,7 +1817,7 @@ fn setup_and_fill_opening(
 
     assert_eq!(
         get_last_event_or_panic(),
-        lib::RawEvent::CuratorOpeningFilled(
+        crate::RawEvent::CuratorOpeningFilled(
             setup_opening_in_review
                 .normal_opening_constructed
                 .curator_opening_id,
@@ -2049,16 +2050,16 @@ impl CreateChannelFixture {
         // keep channel id.
         assert_eq!(
             get_last_event_or_panic(),
-            lib::RawEvent::ChannelCreated(old_next_channel_id)
+            crate::RawEvent::ChannelCreated(old_next_channel_id)
         );
 
         let channel_id = old_next_channel_id;
 
         // Assert that given channel id has been added,
         // and has the right properties.
-        assert!(lib::ChannelById::<Test>::exists(channel_id));
+        assert!(crate::ChannelById::<Test>::exists(channel_id));
 
-        let created_channel = lib::ChannelById::<Test>::get(channel_id);
+        let created_channel = crate::ChannelById::<Test>::get(channel_id);
 
         let expected_channel = Channel {
             verified: false,
@@ -2081,20 +2082,20 @@ impl CreateChannelFixture {
         assert_eq!(created_channel, expected_channel);
 
         // Assert that next id incremented.
-        assert_eq!(lib::NextChannelId::<Test>::get(), channel_id + 1);
+        assert_eq!(crate::NextChannelId::<Test>::get(), channel_id + 1);
 
         // Assert that there is a mapping established for handle
         assert_eq!(
-            lib::ChannelIdByHandle::<Test>::get(self.channel_handle.clone()),
+            crate::ChannelIdByHandle::<Test>::get(self.channel_handle.clone()),
             channel_id
         );
 
         // Check that principal actually has been added
-        assert!(lib::PrincipalById::<Test>::exists(
+        assert!(crate::PrincipalById::<Test>::exists(
             created_channel.principal_id
         ));
 
-        let created_principal = lib::PrincipalById::<Test>::get(created_channel.principal_id);
+        let created_principal = crate::PrincipalById::<Test>::get(created_channel.principal_id);
 
         assert!(match created_principal {
             Principal::Lead => false,
@@ -2121,7 +2122,7 @@ fn add_member_and_set_as_lead() -> NewMemberAsLead {
 }
 
 pub fn set_channel_creation_enabled(enabled: bool) {
-    lib::Module::<Test>::set_channel_creation_enabled(Origin::signed(LEAD_ROLE_ACCOUNT), enabled)
+    crate::Module::<Test>::set_channel_creation_enabled(Origin::signed(LEAD_ROLE_ACCOUNT), enabled)
         .unwrap()
 }
 
@@ -2179,7 +2180,7 @@ pub fn set_lead(
 
     assert_eq!(
         get_last_event_or_panic(),
-        lib::RawEvent::LeadSet(expected_lead_id)
+        crate::RawEvent::LeadSet(expected_lead_id)
     );
 
     expected_lead_id
@@ -2206,7 +2207,7 @@ pub fn add_curator_opening() -> CuratorOpeningId<Test> {
 
     assert_eq!(
         get_last_event_or_panic(),
-        lib::RawEvent::CuratorOpeningAdded(expected_curator_opening_id)
+        crate::RawEvent::CuratorOpeningAdded(expected_curator_opening_id)
     );
 
     expected_curator_opening_id

+ 50 - 0
runtime-modules/forum/Cargo.toml

@@ -0,0 +1,50 @@
+[package]
+name = 'substrate-forum-module'
+version = '1.1.1'
+authors = ['Joystream contributors']
+edition = '2018'
+
+[dependencies]
+hex-literal = '0.1.0'
+serde = { version = '1.0.101', optional = true}
+serde_derive = { version = '1.0.101', optional = true }
+rstd = { package = 'sr-std', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+runtime-primitives = { package = 'sr-primitives', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+srml-support = { package = 'srml-support', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+srml-support-procedural = { package = 'srml-support-procedural', git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+system = { package = 'srml-system', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+balances = { package = 'srml-balances', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+codec = { package = 'parity-scale-codec', version = '1.0.0', default-features = false, features = ['derive'] }
+# https://users.rust-lang.org/t/failure-derive-compilation-error/39062
+quote = '<=1.0.2'
+
+[dependencies.timestamp]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-timestamp'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.runtime-io]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-io'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dev-dependencies]
+runtime-io = { package = 'sr-io', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+primitives = { package = 'substrate-primitives', git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+
+[features]
+default = ['std']
+std = [
+	'serde',
+	'serde_derive',
+	'codec/std',
+	'rstd/std',
+	'runtime-io/std',
+	'runtime-primitives/std',
+	'srml-support/std',
+	'system/std',
+  	'balances/std',
+	'timestamp/std',
+]

+ 1318 - 0
runtime-modules/forum/src/lib.rs

@@ -0,0 +1,1318 @@
+// 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.
+
+// Ensure we're `no_std` when compiling for Wasm.
+#![cfg_attr(not(feature = "std"), no_std)]
+
+#[cfg(feature = "std")]
+use serde_derive::{Deserialize, Serialize};
+
+use rstd::prelude::*;
+
+use codec::{Decode, Encode};
+use srml_support::{decl_event, decl_module, decl_storage, dispatch, ensure};
+
+mod mock;
+mod tests;
+
+/*
+ * MOVE ALL OF THESE OUT TO COMMON LATER
+ */
+
+/// Length constraint for input validation
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
+pub struct InputValidationLengthConstraint {
+    /// Minimum length
+    pub min: u16,
+
+    /// Difference between minimum length and max length.
+    /// While having max would have been more direct, this
+    /// way makes max < min unrepresentable semantically,
+    /// which is safer.
+    pub max_min_diff: u16,
+}
+
+impl InputValidationLengthConstraint {
+    /// Helper for computing max
+    pub fn max(&self) -> u16 {
+        self.min + self.max_min_diff
+    }
+
+    pub fn ensure_valid(
+        &self,
+        len: usize,
+        too_short_msg: &'static str,
+        too_long_msg: &'static str,
+    ) -> Result<(), &'static str> {
+        let length = len as u16;
+        if length < self.min {
+            Err(too_short_msg)
+        } else if length > self.max() {
+            Err(too_long_msg)
+        } else {
+            Ok(())
+        }
+    }
+}
+
+/// Constants
+/////////////////////////////////////////////////////////////////
+
+/// The greatest valid depth of a category.
+/// The depth of a root category is 0.
+const MAX_CATEGORY_DEPTH: u16 = 3;
+
+/// Error messages for dispatchables
+const ERROR_FORUM_SUDO_NOT_SET: &str = "Forum sudo not set.";
+const ERROR_ORIGIN_NOT_FORUM_SUDO: &str = "Origin not forum sudo.";
+const ERROR_CATEGORY_TITLE_TOO_SHORT: &str = "Category title too short.";
+const ERROR_CATEGORY_TITLE_TOO_LONG: &str = "Category title too long.";
+const ERROR_CATEGORY_DESCRIPTION_TOO_SHORT: &str = "Category description too long.";
+const ERROR_CATEGORY_DESCRIPTION_TOO_LONG: &str = "Category description too long.";
+const ERROR_ANCESTOR_CATEGORY_IMMUTABLE: &str =
+    "Ancestor category immutable, i.e. deleted or archived";
+const ERROR_MAX_VALID_CATEGORY_DEPTH_EXCEEDED: &str = "Maximum valid category depth exceeded.";
+const ERROR_CATEGORY_DOES_NOT_EXIST: &str = "Category does not exist.";
+const ERROR_NOT_FORUM_USER: &str = "Not forum user.";
+const ERROR_THREAD_TITLE_TOO_SHORT: &str = "Thread title too short.";
+const ERROR_THREAD_TITLE_TOO_LONG: &str = "Thread title too long.";
+const ERROR_POST_TEXT_TOO_SHORT: &str = "Post text too short.";
+const ERROR_POST_TEXT_TOO_LONG: &str = "Post too long.";
+const ERROR_THREAD_DOES_NOT_EXIST: &str = "Thread does not exist";
+const ERROR_THREAD_MODERATION_RATIONALE_TOO_SHORT: &str = "Thread moderation rationale too short.";
+const ERROR_THREAD_MODERATION_RATIONALE_TOO_LONG: &str = "Thread moderation rationale too long.";
+const ERROR_THREAD_ALREADY_MODERATED: &str = "Thread already moderated.";
+const ERROR_THREAD_MODERATED: &str = "Thread is moderated.";
+const ERROR_POST_DOES_NOT_EXIST: &str = "Post does not exist.";
+const ERROR_ACCOUNT_DOES_NOT_MATCH_POST_AUTHOR: &str = "Account does not match post author.";
+const ERROR_POST_MODERATED: &str = "Post is moderated.";
+const ERROR_POST_MODERATION_RATIONALE_TOO_SHORT: &str = "Post moderation rationale too short.";
+const ERROR_POST_MODERATION_RATIONALE_TOO_LONG: &str = "Post moderation rationale too long.";
+const ERROR_CATEGORY_NOT_BEING_UPDATED: &str = "Category not being updated.";
+const ERROR_CATEGORY_CANNOT_BE_UNARCHIVED_WHEN_DELETED: &str =
+    "Category cannot be unarchived when deleted.";
+
+//use srml_support::storage::*;
+
+//use sr_io::{StorageOverlay, ChildrenStorageOverlay};
+
+//#[cfg(feature = "std")]
+//use runtime_io::{StorageOverlay, ChildrenStorageOverlay};
+
+//#[cfg(any(feature = "std", test))]
+//use sr_primitives::{StorageOverlay, ChildrenStorageOverlay};
+
+use system;
+use system::{ensure_root, ensure_signed};
+
+/// Represents a user in this forum.
+#[derive(Debug, Copy, Clone)]
+pub struct ForumUser<AccountId> {
+    /// Identifier of user
+    pub id: AccountId, // In the future one could add things like
+                       // - updating post count of a user
+                       // - updating status (e.g. hero, new, etc.)
+                       //
+}
+
+/// Represents a regsitry of `ForumUser` instances.
+pub trait ForumUserRegistry<AccountId> {
+    fn get_forum_user(id: &AccountId) -> Option<ForumUser<AccountId>>;
+}
+
+/// Convenient composite time stamp
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
+pub struct BlockchainTimestamp<BlockNumber, Moment> {
+    block: BlockNumber,
+    time: Moment,
+}
+
+/// Represents a moderation outcome applied to a post or a thread.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
+pub struct ModerationAction<BlockNumber, Moment, AccountId> {
+    /// When action occured.
+    moderated_at: BlockchainTimestamp<BlockNumber, Moment>,
+
+    /// Account forum sudo which acted.
+    moderator_id: AccountId,
+
+    /// Moderation rationale
+    rationale: Vec<u8>,
+}
+
+/// Represents a revision of the text of a Post
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
+pub struct PostTextChange<BlockNumber, Moment> {
+    /// When this expiration occured
+    expired_at: BlockchainTimestamp<BlockNumber, Moment>,
+
+    /// Text that expired
+    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> {
+    /// Post identifier
+    id: PostId,
+
+    /// Id of thread to which this post corresponds.
+    thread_id: ThreadId,
+
+    /// The post number of this post in its thread, i.e. total number of posts added (including this)
+    /// to a thread when it was added.
+    /// Is needed to give light clients assurance about getting all posts in a given range,
+    // `created_at` is not sufficient.
+    /// Starts at 1 for first post in thread.
+    nr_in_thread: u32,
+
+    /// Current text of post
+    current_text: Vec<u8>,
+
+    /// Possible moderation of this post
+    moderation: Option<ModerationAction<BlockNumber, Moment, AccountId>>,
+
+    /// Edits of post ordered chronologically by edit time.
+    text_change_history: Vec<PostTextChange<BlockNumber, Moment>>,
+
+    /// When post was submitted.
+    created_at: BlockchainTimestamp<BlockNumber, Moment>,
+
+    /// Author of post.
+    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> {
+    /// Thread identifier
+    id: ThreadId,
+
+    /// Title
+    title: Vec<u8>,
+
+    /// Category in which this thread lives
+    category_id: CategoryId,
+
+    /// The thread number of this thread in its category, i.e. total number of thread added (including this)
+    /// to a category when it was added.
+    /// Is needed to give light clients assurance about getting all threads in a given range,
+    /// `created_at` is not sufficient.
+    /// Starts at 1 for first thread in category.
+    nr_in_category: u32,
+
+    /// Possible moderation of this thread
+    moderation: Option<ModerationAction<BlockNumber, Moment, AccountId>>,
+
+    /// Number of unmoderated and moderated posts in this thread.
+    /// The sum of these two only increases, and former is incremented
+    /// for each new post added to this thread. A new post is added
+    /// with a `nr_in_thread` equal to this sum
+    ///
+    /// When there is a moderation
+    /// of a post, the variables are incremented and decremented, respectively.
+    ///
+    /// These values are vital for light clients, in order to validate that they are
+    /// not being censored from posts in a thread.
+    num_unmoderated_posts: u32,
+    num_moderated_posts: u32,
+
+    /// When thread was established.
+    created_at: BlockchainTimestamp<BlockNumber, Moment>,
+
+    /// Author of post.
+    author_id: AccountId,
+}
+
+impl<BlockNumber, Moment, AccountId> Thread<BlockNumber, Moment, AccountId> {
+    fn num_posts_ever_created(&self) -> u32 {
+        self.num_unmoderated_posts + self.num_moderated_posts
+    }
+}
+
+/// Represents a category identifier
+pub type CategoryId = u64;
+
+/// Represents
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
+pub struct ChildPositionInParentCategory {
+    /// Id of parent category
+    parent_id: CategoryId,
+
+    /// Nr of the child in the parent
+    /// Starts at 1
+    child_nr_in_parent_category: u32,
+}
+
+/// Represents a category
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
+pub struct Category<BlockNumber, Moment, AccountId> {
+    /// Category identifier
+    id: CategoryId,
+
+    /// Title
+    title: Vec<u8>,
+
+    /// Description
+    description: Vec<u8>,
+
+    /// When category was established.
+    created_at: BlockchainTimestamp<BlockNumber, Moment>,
+
+    /// Whether category is deleted.
+    deleted: bool,
+
+    /// Whether category is archived.
+    archived: bool,
+
+    /// Number of subcategories (deleted, archived or neither),
+    /// unmoderated threads and moderated threads, _directly_ in this category.
+    ///
+    /// As noted, the first is unaffected by any change in state of direct subcategory.
+    ///
+    /// The sum of the latter two only increases, and former is incremented
+    /// for each new thread added to this category. A new thread is added
+    /// with a `nr_in_category` equal to this sum.
+    ///
+    /// When there is a moderation
+    /// of a thread, the variables are incremented and decremented, respectively.
+    ///
+    /// These values are vital for light clients, in order to validate that they are
+    /// not being censored from subcategories or threads in a category.
+    num_direct_subcategories: u32,
+    num_direct_unmoderated_threads: u32,
+    num_direct_moderated_threads: u32,
+
+    /// Position as child in parent, if present, otherwise this category is a root category
+    position_in_parent_category: Option<ChildPositionInParentCategory>,
+
+    /// Account of the moderator which created category.
+    moderator_id: AccountId,
+}
+
+impl<BlockNumber, Moment, AccountId> Category<BlockNumber, Moment, AccountId> {
+    fn num_threads_created(&self) -> u32 {
+        self.num_direct_unmoderated_threads + self.num_direct_moderated_threads
+    }
+}
+
+/// Represents a sequence of categories which have child-parent relatioonship
+/// where last element is final ancestor, or root, in the context of the category tree.
+type CategoryTreePath<BlockNumber, Moment, AccountId> =
+    Vec<Category<BlockNumber, Moment, AccountId>>;
+
+pub trait Trait: system::Trait + timestamp::Trait + Sized {
+    type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
+
+    type MembershipRegistry: ForumUserRegistry<Self::AccountId>;
+}
+
+decl_storage! {
+    trait Store for Module<T: Trait> as Forum {
+
+        /// Map category identifier to corresponding category.
+        pub CategoryById get(category_by_id) config(): map CategoryId => Category<T::BlockNumber, T::Moment, T::AccountId>;
+
+        /// Category identifier value to be used for the next Category created.
+        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>;
+
+        /// Thread identifier value to be used for next Thread in threadById.
+        pub NextThreadId get(next_thread_id) config(): ThreadId;
+
+        /// Map post identifier to corresponding post.
+        pub PostById get(post_by_id) config(): map PostId => Post<T::BlockNumber, T::Moment, T::AccountId>;
+
+        /// Post identifier value to be used for for next post created.
+        pub NextPostId get(next_post_id) config(): PostId;
+
+        /// Account of forum sudo.
+        pub ForumSudo get(forum_sudo) config(): Option<T::AccountId>;
+
+        /// Input constraints
+        /// These are all forward looking, that is they are enforced on all
+        /// future calls.
+        pub CategoryTitleConstraint get(category_title_constraint) config(): InputValidationLengthConstraint;
+        pub CategoryDescriptionConstraint get(category_description_constraint) config(): InputValidationLengthConstraint;
+        pub ThreadTitleConstraint get(thread_title_constraint) config(): InputValidationLengthConstraint;
+        pub PostTextConstraint get(post_text_constraint) config(): InputValidationLengthConstraint;
+        pub ThreadModerationRationaleConstraint get(thread_moderation_rationale_constraint) config(): InputValidationLengthConstraint;
+        pub PostModerationRationaleConstraint get(post_moderation_rationale_constraint) config(): InputValidationLengthConstraint;
+    }
+    /*
+    JUST GIVING UP ON ALL THIS FOR NOW BECAUSE ITS TAKING TOO LONG
+    Review : https://github.com/paritytech/polkadot/blob/620b8610431e7b5fdd71ce3e94c3ee0177406dcc/runtime/src/parachains.rs#L123-L141
+
+    add_extra_genesis {
+
+        // Explain why we need to put this here.
+        config(initial_forum_sudo) : Option<T::AccountId>;
+
+        build(|
+            storage: &mut generator::StorageOverlay,
+            _: &mut generator::ChildrenStorageOverlay,
+            config: &GenesisConfig<T>
+            | {
+
+
+            if let Some(account_id) = &config.initial_forum_sudo {
+                println!("{}: <ForumSudo<T>>::put(account_id)", account_id);
+                <ForumSudo<T> as generator::StorageValue<_>>::put(&account_id, storage);
+            }
+        })
+    }
+    */
+}
+
+decl_event!(
+    pub enum Event<T>
+    where
+        <T as system::Trait>::AccountId,
+    {
+        /// A category was introduced
+        CategoryCreated(CategoryId),
+
+        /// A category with given id was updated.
+        /// The second argument reflects the new archival status of the category, if changed.
+        /// The third argument reflects the new deletion status of the category, if changed.
+        CategoryUpdated(CategoryId, Option<bool>, Option<bool>),
+
+        /// A thread with given id was created.
+        ThreadCreated(ThreadId),
+
+        /// A thread with given id was moderated.
+        ThreadModerated(ThreadId),
+
+        /// Post with given id was created.
+        PostAdded(PostId),
+
+        /// Post with givne id was moderated.
+        PostModerated(PostId),
+
+        /// Post with given id had its text updated.
+        /// The second argument reflects the number of total edits when the text update occurs.
+        PostTextUpdated(PostId, u64),
+
+        /// Given account was set as forum sudo.
+        ForumSudoSet(Option<AccountId>, Option<AccountId>),
+    }
+);
+
+decl_module! {
+    pub struct Module<T: Trait> for enum Call where origin: T::Origin {
+
+        fn deposit_event() = default;
+
+        /// Set forum sudo.
+        fn set_forum_sudo(origin, new_forum_sudo: Option<T::AccountId>) -> dispatch::Result {
+            ensure_root(origin)?;
+
+            /*
+             * Question: when this routine is called by non sudo or with bad signature, what error is raised?
+             * Update ERror set in spec
+             */
+
+            // Hold on to old value
+            let old_forum_sudo = <ForumSudo<T>>::get().clone();
+
+            // Update forum sudo
+            match new_forum_sudo.clone() {
+                Some(account_id) => <ForumSudo<T>>::put(account_id),
+                None => <ForumSudo<T>>::kill()
+            };
+
+            // Generate event
+            Self::deposit_event(RawEvent::ForumSudoSet(old_forum_sudo, new_forum_sudo));
+
+            // All good.
+            Ok(())
+        }
+
+        /// Add a new category.
+        fn create_category(origin, parent: Option<CategoryId>, title: Vec<u8>, description: Vec<u8>) -> dispatch::Result {
+
+            // Check that its a valid signature
+            let who = ensure_signed(origin)?;
+
+            // Not signed by forum SUDO
+            Self::ensure_is_forum_sudo(&who)?;
+
+            // Validate title
+            Self::ensure_category_title_is_valid(&title)?;
+
+            // Validate description
+            Self::ensure_category_description_is_valid(&description)?;
+
+            // Position in parent field value for new category
+            let mut position_in_parent_category_field = None;
+
+            // If not root, then check that we can create in parent category
+            if let Some(parent_category_id) = parent {
+
+                let category_tree_path = Self::ensure_valid_category_and_build_category_tree_path(parent_category_id)?;
+
+                // Can we mutate in this category?
+                Self::ensure_can_add_subcategory_path_leaf(&category_tree_path)?;
+
+                /*
+                 * Here we are safe to mutate
+                 */
+
+                // Increment number of subcategories to reflect this new category being
+                // added as a child
+                <CategoryById<T>>::mutate(parent_category_id, |c| {
+                    c.num_direct_subcategories += 1;
+                });
+
+                // Set `position_in_parent_category_field`
+                let parent_category = category_tree_path.first().unwrap();
+
+                position_in_parent_category_field = Some(ChildPositionInParentCategory{
+                    parent_id: parent_category_id,
+                    child_nr_in_parent_category: parent_category.num_direct_subcategories
+                });
+
+            }
+
+            /*
+             * Here we are safe to mutate
+             */
+
+            let next_category_id = NextCategoryId::get();
+
+            // Create new category
+            let new_category = Category {
+                id : next_category_id,
+                title : title.clone(),
+                description: description.clone(),
+                created_at : Self::current_block_and_time(),
+                deleted: false,
+                archived: false,
+                num_direct_subcategories: 0,
+                num_direct_unmoderated_threads: 0,
+                num_direct_moderated_threads: 0,
+                position_in_parent_category: position_in_parent_category_field,
+                moderator_id: who
+            };
+
+            // Insert category in map
+            <CategoryById<T>>::insert(new_category.id, new_category);
+
+            // Update other things
+            NextCategoryId::put(next_category_id + 1);
+
+            // Generate event
+            Self::deposit_event(RawEvent::CategoryCreated(next_category_id));
+
+            Ok(())
+        }
+
+        /// Update category
+        fn update_category(origin, category_id: CategoryId, new_archival_status: Option<bool>, new_deletion_status: Option<bool>) -> dispatch::Result {
+
+            // Check that its a valid signature
+            let who = ensure_signed(origin)?;
+
+            // Not signed by forum SUDO
+            Self::ensure_is_forum_sudo(&who)?;
+
+            // Make sure something is actually being changed
+            ensure!(
+                new_archival_status.is_some() || new_deletion_status.is_some(),
+                ERROR_CATEGORY_NOT_BEING_UPDATED
+            );
+
+            // Get path from parent to root of category tree.
+            let category_tree_path = Self::ensure_valid_category_and_build_category_tree_path(category_id)?;
+
+            // When we are dealing with a non-root category, we
+            // must ensure mutability of our category by traversing to
+            // root.
+            if category_tree_path.len() > 1  {
+
+                // 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();
+                path_to_check.remove(0);
+
+                Self::ensure_can_mutate_in_path_leaf(&path_to_check)?;
+            }
+
+            // If the category itself is already deleted, then this
+            // update *must* simultaneously do an undelete, otherwise it is blocked,
+            // as we do not permit unarchiving a deleted category. Doing
+            // a simultanous undelete and unarchive is accepted.
+
+            let category = <CategoryById<T>>::get(category_id);
+
+            ensure!(
+                !category.deleted || (new_deletion_status == Some(false)),
+                ERROR_CATEGORY_CANNOT_BE_UNARCHIVED_WHEN_DELETED
+            );
+
+            // Mutate category, and set possible new change parameters
+
+            <CategoryById<T>>::mutate(category_id, |c| {
+
+                if let Some(archived) = new_archival_status {
+                    c.archived = archived;
+                }
+
+                if let Some(deleted) = new_deletion_status {
+                    c.deleted = deleted;
+                }
+            });
+
+            // Generate event
+            Self::deposit_event(RawEvent::CategoryUpdated(category_id, new_archival_status, new_deletion_status));
+
+            Ok(())
+        }
+
+        /// Create new thread in category
+        fn create_thread(origin, category_id: CategoryId, title: Vec<u8>, text: Vec<u8>) -> dispatch::Result {
+
+            /*
+             * Update SPEC with new errors,
+             * and mutation of Category class,
+             * as well as side effect to update Category::num_threads_created.
+             */
+
+            // Check that its a valid signature
+            let who = ensure_signed(origin)?;
+
+            // Check that account is forum member
+            Self::ensure_is_forum_member(&who)?;
+
+            // Get path from parent to root of category tree.
+            let category_tree_path = Self::ensure_valid_category_and_build_category_tree_path(category_id)?;
+
+            // No ancestor is blocking us doing mutation in this category
+            Self::ensure_can_mutate_in_path_leaf(&category_tree_path)?;
+
+            // Validate title
+            Self::ensure_thread_title_is_valid(&title)?;
+
+            // Validate post text
+            Self::ensure_post_text_is_valid(&text)?;
+
+            /*
+             * Here it is safe to mutate state.
+             */
+
+            // Add thread
+            let thread = Self::add_new_thread(category_id, &title, &who);
+
+            // Add inital post to thread
+            Self::add_new_post(thread.id, &text, &who);
+
+            // Generate event
+            Self::deposit_event(RawEvent::ThreadCreated(thread.id));
+
+            Ok(())
+        }
+
+        /// Moderate thread
+        fn moderate_thread(origin, thread_id: ThreadId, rationale: Vec<u8>) -> dispatch::Result {
+
+            // Check that its a valid signature
+            let who = ensure_signed(origin)?;
+
+            // Signed by forum SUDO
+            Self::ensure_is_forum_sudo(&who)?;
+
+            // Get thread
+            let mut thread = Self::ensure_thread_exists(&thread_id)?;
+
+            // Thread is not already moderated
+            ensure!(thread.moderation.is_none(), ERROR_THREAD_ALREADY_MODERATED);
+
+            // Rationale valid
+            Self::ensure_thread_moderation_rationale_is_valid(&rationale)?;
+
+            // Can mutate in corresponding category
+            let path = Self::build_category_tree_path(thread.category_id);
+
+            // Path must be non-empty, as category id is from thread in state
+            assert!(!path.is_empty());
+
+            Self::ensure_can_mutate_in_path_leaf(&path)?;
+
+            /*
+             * Here we are safe to mutate
+             */
+
+            // Add moderation to thread
+            thread.moderation = Some(ModerationAction {
+                moderated_at: Self::current_block_and_time(),
+                moderator_id: who,
+                rationale: rationale.clone()
+            });
+
+            <ThreadById<T>>::insert(thread_id, thread.clone());
+
+            // Update moderation/umoderation count of corresponding category
+            <CategoryById<T>>::mutate(thread.category_id, |category| {
+                category.num_direct_unmoderated_threads -= 1;
+                category.num_direct_moderated_threads += 1;
+            });
+
+            // Generate event
+            Self::deposit_event(RawEvent::ThreadModerated(thread_id));
+
+            Ok(())
+        }
+
+        /// Edit post text
+        fn add_post(origin, thread_id: ThreadId, text: Vec<u8>) -> dispatch::Result {
+
+            /*
+             * Update SPEC with new errors,
+             */
+
+            // Check that its a valid signature
+            let who = ensure_signed(origin)?;
+
+            // Check that account is forum member
+            Self::ensure_is_forum_member(&who)?;
+
+            // Validate post text
+            Self::ensure_post_text_is_valid(&text)?;
+
+            // Make sure thread exists and is mutable
+            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)?;
+
+            // No ancestor is blocking us doing mutation in this category
+            Self::ensure_can_mutate_in_path_leaf(&category_tree_path)?;
+
+            /*
+             * Here we are safe to mutate
+             */
+
+            let post = Self::add_new_post(thread_id, &text, &who);
+
+            // Generate event
+            Self::deposit_event(RawEvent::PostAdded(post.id));
+
+            Ok(())
+        }
+
+        /// Edit post text
+        fn edit_post_text(origin, post_id: PostId, new_text: Vec<u8>) -> dispatch::Result {
+
+            /* Edit spec.
+              - forum member guard missing
+              - check that both post and thread and category are mutable
+            */
+
+            // Check that its a valid signature
+            let who = ensure_signed(origin)?;
+
+            // Check that account is forum member
+            Self::ensure_is_forum_member(&who)?;
+
+            // Validate post text
+            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)?;
+
+            // Signer does not match creator of post with identifier postId
+            ensure!(post.author_id == who, ERROR_ACCOUNT_DOES_NOT_MATCH_POST_AUTHOR);
+
+            /*
+             * Here we are safe to mutate
+             */
+
+            <PostById<T>>::mutate(post_id, |p| {
+
+                let expired_post_text = PostTextChange {
+                    expired_at: Self::current_block_and_time(),
+                    text: post.current_text.clone()
+                };
+
+                // Set current text to new text
+                p.current_text = new_text;
+
+                // Copy current text to history of expired texts
+                p.text_change_history.push(expired_post_text);
+            });
+
+            // Generate event
+            Self::deposit_event(RawEvent::PostTextUpdated(post.id, post.text_change_history.len() as u64));
+
+            Ok(())
+        }
+
+        /// Moderate post
+        fn moderate_post(origin, post_id: PostId, rationale: Vec<u8>) -> dispatch::Result {
+
+            // Check that its a valid signature
+            let who = ensure_signed(origin)?;
+
+            // Signed by forum SUDO
+            Self::ensure_is_forum_sudo(&who)?;
+
+            // Make sure post exists and is mutable
+            let post = Self::ensure_post_is_mutable(&post_id)?;
+
+            Self::ensure_post_moderation_rationale_is_valid(&rationale)?;
+
+            /*
+             * Here we are safe to mutate
+             */
+
+            // Update moderation action on post
+            let moderation_action = ModerationAction{
+                moderated_at: Self::current_block_and_time(),
+                moderator_id: who,
+                rationale: rationale.clone()
+            };
+
+            <PostById<T>>::mutate(post_id, |p| {
+                p.moderation = Some(moderation_action);
+            });
+
+            // Update moderated and unmoderated post count of corresponding thread
+            <ThreadById<T>>::mutate(post.thread_id, |t| {
+                t.num_unmoderated_posts -= 1;
+                t.num_moderated_posts += 1;
+            });
+
+            // Generate event
+            Self::deposit_event(RawEvent::PostModerated(post.id));
+
+            Ok(())
+        }
+
+    }
+}
+
+impl<T: Trait> Module<T> {
+    fn ensure_category_title_is_valid(title: &Vec<u8>) -> dispatch::Result {
+        CategoryTitleConstraint::get().ensure_valid(
+            title.len(),
+            ERROR_CATEGORY_TITLE_TOO_SHORT,
+            ERROR_CATEGORY_TITLE_TOO_LONG,
+        )
+    }
+
+    fn ensure_category_description_is_valid(description: &Vec<u8>) -> dispatch::Result {
+        CategoryDescriptionConstraint::get().ensure_valid(
+            description.len(),
+            ERROR_CATEGORY_DESCRIPTION_TOO_SHORT,
+            ERROR_CATEGORY_DESCRIPTION_TOO_LONG,
+        )
+    }
+
+    fn ensure_thread_moderation_rationale_is_valid(rationale: &Vec<u8>) -> dispatch::Result {
+        ThreadModerationRationaleConstraint::get().ensure_valid(
+            rationale.len(),
+            ERROR_THREAD_MODERATION_RATIONALE_TOO_SHORT,
+            ERROR_THREAD_MODERATION_RATIONALE_TOO_LONG,
+        )
+    }
+
+    fn ensure_thread_title_is_valid(title: &Vec<u8>) -> dispatch::Result {
+        ThreadTitleConstraint::get().ensure_valid(
+            title.len(),
+            ERROR_THREAD_TITLE_TOO_SHORT,
+            ERROR_THREAD_TITLE_TOO_LONG,
+        )
+    }
+
+    fn ensure_post_text_is_valid(text: &Vec<u8>) -> dispatch::Result {
+        PostTextConstraint::get().ensure_valid(
+            text.len(),
+            ERROR_POST_TEXT_TOO_SHORT,
+            ERROR_POST_TEXT_TOO_LONG,
+        )
+    }
+
+    fn ensure_post_moderation_rationale_is_valid(rationale: &Vec<u8>) -> dispatch::Result {
+        PostModerationRationaleConstraint::get().ensure_valid(
+            rationale.len(),
+            ERROR_POST_MODERATION_RATIONALE_TOO_SHORT,
+            ERROR_POST_MODERATION_RATIONALE_TOO_LONG,
+        )
+    }
+
+    fn current_block_and_time() -> BlockchainTimestamp<T::BlockNumber, T::Moment> {
+        BlockchainTimestamp {
+            block: <system::Module<T>>::block_number(),
+            time: <timestamp::Module<T>>::now(),
+        }
+    }
+
+    fn ensure_post_is_mutable(
+        post_id: &PostId,
+    ) -> Result<Post<T::BlockNumber, T::Moment, T::AccountId>, &'static str> {
+        // Make sure post exists
+        let post = Self::ensure_post_exists(post_id)?;
+
+        // and is unmoderated
+        ensure!(post.moderation.is_none(), ERROR_POST_MODERATED);
+
+        // and make sure thread is mutable
+        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> {
+        if <PostById<T>>::exists(post_id) {
+            Ok(<PostById<T>>::get(post_id))
+        } else {
+            Err(ERROR_POST_DOES_NOT_EXIST)
+        }
+    }
+
+    fn ensure_thread_is_mutable(
+        thread_id: &ThreadId,
+    ) -> Result<Thread<T::BlockNumber, T::Moment, T::AccountId>, &'static str> {
+        // Make sure thread exists
+        let thread = Self::ensure_thread_exists(&thread_id)?;
+
+        // and is unmoderated
+        ensure!(thread.moderation.is_none(), ERROR_THREAD_MODERATED);
+
+        // and corresponding category is mutable
+        Self::ensure_catgory_is_mutable(thread.category_id)?;
+
+        Ok(thread)
+    }
+
+    fn ensure_thread_exists(
+        thread_id: &ThreadId,
+    ) -> Result<Thread<T::BlockNumber, T::Moment, T::AccountId>, &'static str> {
+        if <ThreadById<T>>::exists(thread_id) {
+            Ok(<ThreadById<T>>::get(thread_id))
+        } else {
+            Err(ERROR_THREAD_DOES_NOT_EXIST)
+        }
+    }
+
+    fn ensure_forum_sudo_set() -> Result<T::AccountId, &'static str> {
+        match <ForumSudo<T>>::get() {
+            Some(account_id) => Ok(account_id),
+            None => Err(ERROR_FORUM_SUDO_NOT_SET),
+        }
+    }
+
+    fn ensure_is_forum_sudo(account_id: &T::AccountId) -> dispatch::Result {
+        let forum_sudo_account = Self::ensure_forum_sudo_set()?;
+
+        ensure!(
+            *account_id == forum_sudo_account,
+            ERROR_ORIGIN_NOT_FORUM_SUDO
+        );
+        Ok(())
+    }
+
+    fn ensure_is_forum_member(
+        account_id: &T::AccountId,
+    ) -> Result<ForumUser<T::AccountId>, &'static str> {
+        let forum_user_query = T::MembershipRegistry::get_forum_user(account_id);
+
+        if let Some(forum_user) = forum_user_query {
+            Ok(forum_user)
+        } else {
+            Err(ERROR_NOT_FORUM_USER)
+        }
+    }
+
+    fn ensure_catgory_is_mutable(category_id: CategoryId) -> dispatch::Result {
+        let category_tree_path = Self::build_category_tree_path(category_id);
+
+        Self::ensure_can_mutate_in_path_leaf(&category_tree_path)
+    }
+
+    fn ensure_can_mutate_in_path_leaf(
+        category_tree_path: &CategoryTreePath<T::BlockNumber, T::Moment, T::AccountId>,
+    ) -> dispatch::Result {
+        // Is parent category directly or indirectly deleted or archived category
+        ensure!(
+            !category_tree_path.iter().any(
+                |c: &Category<T::BlockNumber, T::Moment, T::AccountId>| c.deleted || c.archived
+            ),
+            ERROR_ANCESTOR_CATEGORY_IMMUTABLE
+        );
+
+        Ok(())
+    }
+
+    fn ensure_can_add_subcategory_path_leaf(
+        category_tree_path: &CategoryTreePath<T::BlockNumber, T::Moment, T::AccountId>,
+    ) -> dispatch::Result {
+        Self::ensure_can_mutate_in_path_leaf(category_tree_path)?;
+
+        // Does adding a new category exceed maximum depth
+        let depth_of_new_category = 1 + 1 + category_tree_path.len();
+
+        ensure!(
+            depth_of_new_category <= MAX_CATEGORY_DEPTH as usize,
+            ERROR_MAX_VALID_CATEGORY_DEPTH_EXCEEDED
+        );
+
+        Ok(())
+    }
+
+    fn ensure_valid_category_and_build_category_tree_path(
+        category_id: CategoryId,
+    ) -> Result<CategoryTreePath<T::BlockNumber, T::Moment, T::AccountId>, &'static str> {
+        ensure!(
+            <CategoryById<T>>::exists(&category_id),
+            ERROR_CATEGORY_DOES_NOT_EXIST
+        );
+
+        // 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);
+
+        Ok(category_tree_path)
+    }
+
+    /// Builds path and populates in `path`.
+    /// Requires that `category_id` is valid
+    fn build_category_tree_path(
+        category_id: CategoryId,
+    ) -> CategoryTreePath<T::BlockNumber, T::Moment, T::AccountId> {
+        // Get path from parent to root of category tree.
+        let mut category_tree_path = vec![];
+
+        Self::_build_category_tree_path(category_id, &mut category_tree_path);
+
+        category_tree_path
+    }
+
+    /// Builds path and populates in `path`.
+    /// Requires that `category_id` is valid
+    fn _build_category_tree_path(
+        category_id: CategoryId,
+        path: &mut CategoryTreePath<T::BlockNumber, T::Moment, T::AccountId>,
+    ) {
+        // Grab category
+        let category = <CategoryById<T>>::get(category_id);
+
+        // Copy out position_in_parent_category
+        let position_in_parent_category_field = category.position_in_parent_category.clone();
+
+        // Add category to path container
+        path.push(category);
+
+        // Make recursive call on parent if we are not at root
+        if let Some(child_position_in_parent) = position_in_parent_category_field {
+            assert!(<CategoryById<T>>::exists(
+                &child_position_in_parent.parent_id
+            ));
+
+            Self::_build_category_tree_path(child_position_in_parent.parent_id, path);
+        }
+    }
+
+    fn add_new_thread(
+        category_id: CategoryId,
+        title: &Vec<u8>,
+        author_id: &T::AccountId,
+    ) -> Thread<T::BlockNumber, T::Moment, T::AccountId> {
+        // Get category
+        let category = <CategoryById<T>>::get(category_id);
+
+        // Create and add new thread
+        let new_thread_id = NextThreadId::get();
+
+        let new_thread = Thread {
+            id: new_thread_id,
+            title: title.clone(),
+            category_id: category_id,
+            nr_in_category: category.num_threads_created() + 1,
+            moderation: None,
+            num_unmoderated_posts: 0,
+            num_moderated_posts: 0,
+            created_at: Self::current_block_and_time(),
+            author_id: author_id.clone(),
+        };
+
+        // Store thread
+        <ThreadById<T>>::insert(new_thread_id, new_thread.clone());
+
+        // Update next thread id
+        NextThreadId::mutate(|n| {
+            *n += 1;
+        });
+
+        // Update unmoderated thread count in corresponding category
+        <CategoryById<T>>::mutate(category_id, |c| {
+            c.num_direct_unmoderated_threads += 1;
+        });
+
+        new_thread
+    }
+
+    /// 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>,
+        author_id: &T::AccountId,
+    ) -> Post<T::BlockNumber, T::Moment, T::AccountId> {
+        // Get thread
+        let thread = <ThreadById<T>>::get(thread_id);
+
+        // Make and add initial post
+        let new_post_id = NextPostId::get();
+
+        let new_post = Post {
+            id: new_post_id,
+            thread_id: thread_id,
+            nr_in_thread: thread.num_posts_ever_created() + 1,
+            current_text: text.clone(),
+            moderation: None,
+            text_change_history: vec![],
+            created_at: Self::current_block_and_time(),
+            author_id: author_id.clone(),
+        };
+
+        // Store post
+        <PostById<T>>::insert(new_post_id, new_post.clone());
+
+        // Update next post id
+        NextPostId::mutate(|n| {
+            *n += 1;
+        });
+
+        // Update unmoderated post count of thread
+        <ThreadById<T>>::mutate(thread_id, |t| {
+            t.num_unmoderated_posts += 1;
+        });
+
+        new_post
+    }
+}

+ 530 - 0
runtime-modules/forum/src/mock.rs

@@ -0,0 +1,530 @@
+#![cfg(test)]
+
+use crate::*;
+
+use primitives::H256;
+
+use crate::{GenesisConfig, Module, Trait};
+use runtime_primitives::{
+    testing::Header,
+    traits::{BlakeTwo256, IdentityLookup},
+    Perbill,
+};
+use srml_support::{impl_outer_origin, parameter_types};
+
+/// Module which has a full Substrate module for
+/// mocking behaviour of MembershipRegistry
+pub mod registry {
+
+    use super::*;
+    // use srml_support::*;
+
+    #[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
+    pub struct Member<AccountId> {
+        pub id: AccountId,
+    }
+
+    decl_storage! {
+        trait Store for Module<T: Trait> as MockForumUserRegistry {
+
+            pub ForumUserById get(forum_user_by_id) config(): map T::AccountId => Member<T::AccountId>;
+
+        }
+    }
+
+    decl_module! {
+        pub struct Module<T: Trait> for enum Call where origin: T::Origin {}
+    }
+
+    impl<T: Trait> Module<T> {
+        pub fn add_member(member: &Member<T::AccountId>) {
+            <ForumUserById<T>>::insert(member.id.clone(), member.clone());
+        }
+    }
+
+    impl<T: Trait> ForumUserRegistry<T::AccountId> for Module<T> {
+        fn get_forum_user(id: &T::AccountId) -> Option<ForumUser<T::AccountId>> {
+            if <ForumUserById<T>>::exists(id) {
+                let m = <ForumUserById<T>>::get(id);
+
+                Some(ForumUser { id: m.id })
+            } else {
+                None
+            }
+        }
+    }
+
+    pub type TestMembershipRegistryModule = Module<Runtime>;
+}
+
+impl_outer_origin! {
+    pub enum Origin for Runtime {}
+}
+
+// Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted.
+#[derive(Clone, PartialEq, Eq, Debug)]
+pub struct Runtime;
+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 Runtime {
+    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 WeightMultiplierUpdate = ();
+    type Event = ();
+    type BlockHashCount = BlockHashCount;
+    type MaximumBlockWeight = MaximumBlockWeight;
+    type MaximumBlockLength = MaximumBlockLength;
+    type AvailableBlockRatio = AvailableBlockRatio;
+    type Version = ();
+}
+
+impl timestamp::Trait for Runtime {
+    type Moment = u64;
+    type OnTimestampSet = ();
+    type MinimumPeriod = MinimumPeriod;
+}
+
+impl Trait for Runtime {
+    type Event = ();
+    type MembershipRegistry = registry::TestMembershipRegistryModule;
+}
+
+#[derive(Clone)]
+pub enum OriginType {
+    Signed(<Runtime as system::Trait>::AccountId),
+    //Inherent, <== did not find how to make such an origin yet
+    Root,
+}
+
+pub fn mock_origin(origin: OriginType) -> mock::Origin {
+    match origin {
+        OriginType::Signed(account_id) => Origin::signed(account_id),
+        //OriginType::Inherent => Origin::inherent,
+        OriginType::Root => system::RawOrigin::Root.into(), //Origin::root
+    }
+}
+
+pub const NOT_FORUM_SUDO_ORIGIN: OriginType = OriginType::Signed(111);
+
+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_POST_ID: ThreadId = 555;
+
+pub fn generate_text(len: usize) -> Vec<u8> {
+    vec![b'x'; len]
+}
+
+pub fn good_category_title() -> Vec<u8> {
+    b"Great new category".to_vec()
+}
+
+pub fn good_category_description() -> Vec<u8> {
+    b"This is a great new category for the forum".to_vec()
+}
+
+pub fn good_thread_title() -> Vec<u8> {
+    b"Great new thread".to_vec()
+}
+
+pub fn good_thread_text() -> Vec<u8> {
+    b"The first post in this thread".to_vec()
+}
+
+pub fn good_post_text() -> Vec<u8> {
+    b"A response in the thread".to_vec()
+}
+
+pub fn good_rationale() -> Vec<u8> {
+    b"This post violates our community rules".to_vec()
+}
+
+/*
+ * These test fixtures can be heavily refactored to avoid repotition, needs macros, and event
+ * assertions are also missing.
+ */
+
+pub struct CreateCategoryFixture {
+    pub origin: OriginType,
+    pub parent: Option<CategoryId>,
+    pub title: Vec<u8>,
+    pub description: Vec<u8>,
+    pub result: dispatch::Result,
+}
+
+impl CreateCategoryFixture {
+    pub fn call_and_assert(&self) {
+        assert_eq!(
+            TestForumModule::create_category(
+                mock_origin(self.origin.clone()),
+                self.parent,
+                self.title.clone(),
+                self.description.clone()
+            ),
+            self.result
+        )
+    }
+}
+
+pub struct UpdateCategoryFixture {
+    pub origin: OriginType,
+    pub category_id: CategoryId,
+    pub new_archival_status: Option<bool>,
+    pub new_deletion_status: Option<bool>,
+    pub result: dispatch::Result,
+}
+
+impl UpdateCategoryFixture {
+    pub fn call_and_assert(&self) {
+        assert_eq!(
+            TestForumModule::update_category(
+                mock_origin(self.origin.clone()),
+                self.category_id,
+                self.new_archival_status.clone(),
+                self.new_deletion_status.clone()
+            ),
+            self.result
+        )
+    }
+}
+
+pub struct CreateThreadFixture {
+    pub origin: OriginType,
+    pub category_id: CategoryId,
+    pub title: Vec<u8>,
+    pub text: Vec<u8>,
+    pub result: dispatch::Result,
+}
+
+impl CreateThreadFixture {
+    pub fn call_and_assert(&self) {
+        assert_eq!(
+            TestForumModule::create_thread(
+                mock_origin(self.origin.clone()),
+                self.category_id,
+                self.title.clone(),
+                self.text.clone()
+            ),
+            self.result
+        )
+    }
+}
+
+pub struct CreatePostFixture {
+    pub origin: OriginType,
+    pub thread_id: ThreadId,
+    pub text: Vec<u8>,
+    pub result: dispatch::Result,
+}
+
+impl CreatePostFixture {
+    pub fn call_and_assert(&self) {
+        assert_eq!(
+            TestForumModule::add_post(
+                mock_origin(self.origin.clone()),
+                self.thread_id,
+                self.text.clone()
+            ),
+            self.result
+        )
+    }
+}
+
+pub fn create_forum_member() -> OriginType {
+    let member_id = 123;
+    let new_member = registry::Member { id: member_id };
+    registry::TestMembershipRegistryModule::add_member(&new_member);
+    OriginType::Signed(member_id)
+}
+
+pub fn assert_create_category(
+    forum_sudo: OriginType,
+    parent_category_id: Option<CategoryId>,
+    expected_result: dispatch::Result,
+) {
+    CreateCategoryFixture {
+        origin: forum_sudo,
+        parent: parent_category_id,
+        title: good_category_title(),
+        description: good_category_description(),
+        result: expected_result,
+    }
+    .call_and_assert();
+}
+
+pub fn assert_create_thread(
+    forum_sudo: OriginType,
+    category_id: CategoryId,
+    expected_result: dispatch::Result,
+) {
+    CreateThreadFixture {
+        origin: forum_sudo,
+        category_id,
+        title: good_thread_title(),
+        text: good_thread_text(),
+        result: expected_result,
+    }
+    .call_and_assert();
+}
+
+pub fn assert_create_post(
+    forum_sudo: OriginType,
+    thread_id: ThreadId,
+    expected_result: dispatch::Result,
+) {
+    CreatePostFixture {
+        origin: forum_sudo,
+        thread_id,
+        text: good_thread_text(),
+        result: expected_result,
+    }
+    .call_and_assert();
+}
+
+pub fn create_category(
+    forum_sudo: OriginType,
+    parent_category_id: Option<CategoryId>,
+) -> CategoryId {
+    let category_id = TestForumModule::next_category_id();
+    assert_create_category(forum_sudo, parent_category_id, Ok(()));
+    category_id
+}
+
+pub fn create_root_category(forum_sudo: OriginType) -> CategoryId {
+    create_category(forum_sudo, None)
+}
+
+pub fn create_root_category_and_thread(
+    forum_sudo: OriginType,
+) -> (OriginType, CategoryId, ThreadId) {
+    let member_origin = create_forum_member();
+    let category_id = create_root_category(forum_sudo);
+    let thread_id = TestForumModule::next_thread_id();
+
+    CreateThreadFixture {
+        origin: member_origin.clone(),
+        category_id,
+        title: good_thread_title(),
+        text: good_thread_text(),
+        result: Ok(()),
+    }
+    .call_and_assert();
+
+    (member_origin, category_id, thread_id)
+}
+
+pub fn create_root_category_and_thread_and_post(
+    forum_sudo: OriginType,
+) -> (OriginType, CategoryId, ThreadId, PostId) {
+    let (member_origin, category_id, thread_id) = create_root_category_and_thread(forum_sudo);
+    let post_id = TestForumModule::next_post_id();
+
+    CreatePostFixture {
+        origin: member_origin.clone(),
+        thread_id: thread_id.clone(),
+        text: good_post_text(),
+        result: Ok(()),
+    }
+    .call_and_assert();
+
+    (member_origin, category_id, thread_id, post_id)
+}
+
+pub fn moderate_thread(
+    forum_sudo: OriginType,
+    thread_id: ThreadId,
+    rationale: Vec<u8>,
+) -> dispatch::Result {
+    TestForumModule::moderate_thread(mock_origin(forum_sudo), thread_id, rationale)
+}
+
+pub fn moderate_post(
+    forum_sudo: OriginType,
+    post_id: PostId,
+    rationale: Vec<u8>,
+) -> dispatch::Result {
+    TestForumModule::moderate_post(mock_origin(forum_sudo), post_id, rationale)
+}
+
+pub fn archive_category(forum_sudo: OriginType, category_id: CategoryId) -> dispatch::Result {
+    TestForumModule::update_category(mock_origin(forum_sudo), category_id, Some(true), None)
+}
+
+pub fn unarchive_category(forum_sudo: OriginType, category_id: CategoryId) -> dispatch::Result {
+    TestForumModule::update_category(mock_origin(forum_sudo), category_id, Some(false), None)
+}
+
+pub fn delete_category(forum_sudo: OriginType, category_id: CategoryId) -> dispatch::Result {
+    TestForumModule::update_category(mock_origin(forum_sudo), category_id, None, Some(true))
+}
+
+pub fn undelete_category(forum_sudo: OriginType, category_id: CategoryId) -> dispatch::Result {
+    TestForumModule::update_category(mock_origin(forum_sudo), category_id, None, Some(false))
+}
+
+pub fn assert_not_forum_sudo_cannot_update_category(
+    update_operation: fn(OriginType, CategoryId) -> dispatch::Result,
+) {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let category_id = create_root_category(origin.clone());
+        assert_eq!(
+            update_operation(NOT_FORUM_SUDO_ORIGIN, category_id),
+            Err(ERROR_ORIGIN_NOT_FORUM_SUDO)
+        );
+    });
+}
+
+// This function basically just builds a genesis storage key/value store according to
+// our desired mockup.
+
+// refactor
+/// - add each config as parameter, then
+///
+
+pub fn default_genesis_config() -> GenesisConfig<Runtime> {
+    GenesisConfig::<Runtime> {
+        category_by_id: vec![], // endowed_accounts.iter().cloned().map(|k|(k, 1 << 60)).collect(),
+        next_category_id: 1,
+        thread_by_id: vec![],
+        next_thread_id: 1,
+        post_by_id: vec![],
+        next_post_id: 1,
+
+        forum_sudo: 33,
+
+        category_title_constraint: InputValidationLengthConstraint {
+            min: 10,
+            max_min_diff: 140,
+        },
+
+        category_description_constraint: InputValidationLengthConstraint {
+            min: 10,
+            max_min_diff: 140,
+        },
+
+        thread_title_constraint: InputValidationLengthConstraint {
+            min: 3,
+            max_min_diff: 43,
+        },
+
+        post_text_constraint: InputValidationLengthConstraint {
+            min: 1,
+            max_min_diff: 1001,
+        },
+
+        thread_moderation_rationale_constraint: InputValidationLengthConstraint {
+            min: 10,
+            max_min_diff: 2000,
+        },
+
+        post_moderation_rationale_constraint: InputValidationLengthConstraint {
+            min: 10,
+            max_min_diff: 2000,
+        }, // JUST GIVING UP ON ALL THIS FOR NOW BECAUSE ITS TAKING TOO LONG
+
+           // Extra genesis fields
+           //initial_forum_sudo: Some(143)
+    }
+}
+
+pub type RuntimeMap<K, V> = std::vec::Vec<(K, V)>;
+pub type RuntimeCategory = Category<
+    <Runtime as system::Trait>::BlockNumber,
+    <Runtime as timestamp::Trait>::Moment,
+    <Runtime as system::Trait>::AccountId,
+>;
+pub type RuntimeThread = Thread<
+    <Runtime as system::Trait>::BlockNumber,
+    <Runtime as timestamp::Trait>::Moment,
+    <Runtime as system::Trait>::AccountId,
+>;
+pub type RuntimePost = Post<
+    <Runtime as system::Trait>::BlockNumber,
+    <Runtime as timestamp::Trait>::Moment,
+    <Runtime as system::Trait>::AccountId,
+>;
+pub type RuntimeBlockchainTimestamp = BlockchainTimestamp<
+    <Runtime as system::Trait>::BlockNumber,
+    <Runtime as timestamp::Trait>::Moment,
+>;
+
+pub fn genesis_config(
+    category_by_id: &RuntimeMap<CategoryId, RuntimeCategory>,
+    next_category_id: u64,
+    thread_by_id: &RuntimeMap<ThreadId, RuntimeThread>,
+    next_thread_id: u64,
+    post_by_id: &RuntimeMap<PostId, RuntimePost>,
+    next_post_id: u64,
+    forum_sudo: <Runtime as system::Trait>::AccountId,
+    category_title_constraint: &InputValidationLengthConstraint,
+    category_description_constraint: &InputValidationLengthConstraint,
+    thread_title_constraint: &InputValidationLengthConstraint,
+    post_text_constraint: &InputValidationLengthConstraint,
+    thread_moderation_rationale_constraint: &InputValidationLengthConstraint,
+    post_moderation_rationale_constraint: &InputValidationLengthConstraint,
+) -> GenesisConfig<Runtime> {
+    GenesisConfig::<Runtime> {
+        category_by_id: category_by_id.clone(),
+        next_category_id: next_category_id,
+        thread_by_id: thread_by_id.clone(),
+        next_thread_id: next_thread_id,
+        post_by_id: post_by_id.clone(),
+        next_post_id: next_post_id,
+        forum_sudo: forum_sudo,
+        category_title_constraint: category_title_constraint.clone(),
+        category_description_constraint: category_description_constraint.clone(),
+        thread_title_constraint: thread_title_constraint.clone(),
+        post_text_constraint: post_text_constraint.clone(),
+        thread_moderation_rationale_constraint: thread_moderation_rationale_constraint.clone(),
+        post_moderation_rationale_constraint: post_moderation_rationale_constraint.clone(),
+    }
+}
+
+// MockForumUserRegistry
+pub fn default_mock_forum_user_registry_genesis_config() -> registry::GenesisConfig<Runtime> {
+    registry::GenesisConfig::<Runtime> {
+        forum_user_by_id: vec![],
+    }
+}
+
+// NB!:
+// Wanted to have payload: a: &GenesisConfig<Test>
+// but borrow checker made my life miserabl, so giving up for now.
+pub fn build_test_externalities(config: GenesisConfig<Runtime>) -> runtime_io::TestExternalities {
+    let mut t = system::GenesisConfig::default()
+        .build_storage::<Runtime>()
+        .unwrap();
+
+    config.assimilate_storage(&mut t).unwrap();
+
+    // Add mock registry configuration
+    default_mock_forum_user_registry_genesis_config()
+        .assimilate_storage(&mut t)
+        .unwrap();
+
+    t.into()
+}
+
+// pub type System = system::Module<Runtime>;
+
+/// Export forum module on a test runtime
+pub type TestForumModule = Module<Runtime>;

+ 981 - 0
runtime-modules/forum/src/tests.rs

@@ -0,0 +1,981 @@
+#![cfg(test)]
+
+use super::*;
+use crate::mock::*;
+
+use srml_support::{assert_err, assert_ok};
+
+/*
+* NB!: No test checks for event emission!!!!
+*/
+
+/*
+ * set_forum_sudo
+ * ==============================================================================
+ *
+ * Missing cases
+ *
+ * set_forum_bad_origin
+ *
+ */
+
+#[test]
+fn set_forum_sudo_unset() {
+    let config = default_genesis_config();
+
+    build_test_externalities(config).execute_with(|| {
+        // Ensure that forum sudo is default
+        assert_eq!(TestForumModule::forum_sudo(), Some(33));
+
+        // Unset forum sudo
+        assert_ok!(TestForumModule::set_forum_sudo(
+            mock_origin(OriginType::Root),
+            None
+        ));
+
+        // Sudo no longer set
+        assert!(TestForumModule::forum_sudo().is_none());
+
+        // event emitted?!
+    });
+}
+
+#[test]
+fn set_forum_sudo_update() {
+    let config = default_genesis_config();
+
+    build_test_externalities(config).execute_with(|| {
+        // Ensure that forum sudo is default
+        assert_eq!(
+            TestForumModule::forum_sudo(),
+            Some(default_genesis_config().forum_sudo)
+        );
+
+        let new_forum_sudo_account_id = 780;
+
+        // Unset forum sudo
+        assert_ok!(TestForumModule::set_forum_sudo(
+            mock_origin(OriginType::Root),
+            Some(new_forum_sudo_account_id)
+        ));
+
+        // Sudo no longer set
+        assert_eq!(
+            TestForumModule::forum_sudo(),
+            Some(new_forum_sudo_account_id)
+        );
+    });
+}
+
+/*
+ * create_category
+ * ==============================================================================
+ *
+ * Missing cases
+ *
+ * create_category_bad_origin
+ * create_category_forum_sudo_not_set
+ */
+
+#[test]
+fn create_root_category_successfully() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        assert_create_category(origin, None, Ok(()));
+    });
+}
+
+#[test]
+fn create_subcategory_successfully() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let root_category_id = create_root_category(origin.clone());
+        assert_create_category(origin, Some(root_category_id), Ok(()));
+    });
+}
+
+#[test]
+fn create_category_title_too_short() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+    let min_len = config.category_title_constraint.min as usize;
+
+    build_test_externalities(config).execute_with(|| {
+        CreateCategoryFixture {
+            origin,
+            parent: None,
+            title: generate_text(min_len - 1),
+            description: good_category_description(),
+            result: Err(ERROR_CATEGORY_TITLE_TOO_SHORT),
+        }
+        .call_and_assert();
+    });
+}
+
+#[test]
+fn create_category_title_too_long() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+    let max_len = config.category_title_constraint.max() as usize;
+
+    build_test_externalities(config).execute_with(|| {
+        CreateCategoryFixture {
+            origin,
+            parent: None,
+            title: generate_text(max_len + 1),
+            description: good_category_description(),
+            result: Err(ERROR_CATEGORY_TITLE_TOO_LONG),
+        }
+        .call_and_assert();
+    });
+}
+
+#[test]
+fn create_category_description_too_short() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+    let min_len = config.category_description_constraint.min as usize;
+
+    build_test_externalities(config).execute_with(|| {
+        CreateCategoryFixture {
+            origin,
+            parent: None,
+            title: good_category_title(),
+            description: generate_text(min_len - 1),
+            result: Err(ERROR_CATEGORY_DESCRIPTION_TOO_SHORT),
+        }
+        .call_and_assert();
+    });
+}
+
+#[test]
+fn create_category_description_too_long() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+    let max_len = config.category_description_constraint.max() as usize;
+
+    build_test_externalities(config).execute_with(|| {
+        CreateCategoryFixture {
+            origin,
+            parent: None,
+            title: good_category_title(),
+            description: generate_text(max_len + 1),
+            result: Err(ERROR_CATEGORY_DESCRIPTION_TOO_LONG),
+        }
+        .call_and_assert();
+    });
+}
+
+/*
+ * update_category
+ * ==============================================================================
+ *
+ * Missing cases
+ *
+ * create_category_bad_origin
+ * create_category_forum_sudo_not_set
+ * create_category_origin_not_forum_sudo
+ * create_category_immutable_ancestor_category
+ */
+
+#[test]
+fn update_category_undelete_and_unarchive() {
+    /*
+     * Create an initial state with two levels of categories, where
+     * leaf category is deleted, and then try to undelete.
+     */
+
+    let forum_sudo = 32;
+
+    let created_at = RuntimeBlockchainTimestamp { block: 0, time: 0 };
+
+    let category_by_id = vec![
+        // A root category
+        (
+            1,
+            Category {
+                id: 1,
+                title: "New root".as_bytes().to_vec(),
+                description: "This is a new root category".as_bytes().to_vec(),
+                created_at: created_at.clone(),
+                deleted: false,
+                archived: false,
+                num_direct_subcategories: 1,
+                num_direct_unmoderated_threads: 0,
+                num_direct_moderated_threads: 0,
+                position_in_parent_category: None,
+                moderator_id: forum_sudo,
+            },
+        ),
+        // A subcategory of the one above
+        (
+            2,
+            Category {
+                id: 2,
+                title: "New subcategory".as_bytes().to_vec(),
+                description: "This is a new subcategory to root category"
+                    .as_bytes()
+                    .to_vec(),
+                created_at: created_at.clone(),
+                deleted: true,
+                archived: false,
+                num_direct_subcategories: 0,
+                num_direct_unmoderated_threads: 0,
+                num_direct_moderated_threads: 0,
+                position_in_parent_category: Some(ChildPositionInParentCategory {
+                    parent_id: 1,
+                    child_nr_in_parent_category: 1,
+                }),
+                moderator_id: forum_sudo,
+            },
+        ),
+    ];
+
+    // Set constraints to be sloppy, we don't care about enforcing them.
+    let sloppy_constraint = InputValidationLengthConstraint {
+        min: 0,
+        max_min_diff: 1000,
+    };
+
+    let config = genesis_config(
+        &category_by_id,             // category_by_id
+        category_by_id.len() as u64, // next_category_id
+        &vec![],                     // thread_by_id
+        1,                           // next_thread_id
+        &vec![],                     // post_by_id
+        1,                           // next_post_id
+        forum_sudo,
+        &sloppy_constraint,
+        &sloppy_constraint,
+        &sloppy_constraint,
+        &sloppy_constraint,
+        &sloppy_constraint,
+        &sloppy_constraint,
+    );
+
+    build_test_externalities(config).execute_with(|| {
+        UpdateCategoryFixture {
+            origin: OriginType::Signed(forum_sudo),
+            category_id: 2,
+            new_archival_status: None,        // same as before
+            new_deletion_status: Some(false), // undelete
+            result: Ok(()),
+        }
+        .call_and_assert();
+    });
+}
+
+/*
+ * create_thread
+ * ==============================================================================
+ *
+ * Missing cases
+ *
+ * create_thread_bad_origin
+ * create_thread_forum_sudo_not_set
+ * ...
+ */
+
+#[test]
+fn create_thread_successfully() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let category_id = create_root_category(origin);
+        let member_origin = create_forum_member();
+
+        CreateThreadFixture {
+            origin: member_origin,
+            category_id,
+            title: good_thread_title(),
+            text: good_thread_text(),
+            result: Ok(()),
+        }
+        .call_and_assert();
+    });
+}
+
+#[test]
+fn create_thread_title_too_short() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+    let min_len = config.thread_title_constraint.min as usize;
+
+    build_test_externalities(config).execute_with(|| {
+        let category_id = create_root_category(origin);
+        let member_origin = create_forum_member();
+
+        CreateThreadFixture {
+            origin: member_origin,
+            category_id,
+            title: generate_text(min_len - 1),
+            text: good_thread_text(),
+            result: Err(ERROR_THREAD_TITLE_TOO_SHORT),
+        }
+        .call_and_assert();
+    });
+}
+
+#[test]
+fn create_thread_title_too_long() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+    let max_len = config.thread_title_constraint.max() as usize;
+
+    build_test_externalities(config).execute_with(|| {
+        let category_id = create_root_category(origin);
+        let member_origin = create_forum_member();
+
+        CreateThreadFixture {
+            origin: member_origin,
+            category_id,
+            title: generate_text(max_len + 1),
+            text: good_thread_text(),
+            result: Err(ERROR_THREAD_TITLE_TOO_LONG),
+        }
+        .call_and_assert();
+    });
+}
+
+#[test]
+fn create_thread_text_too_short() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+    let min_len = config.post_text_constraint.min as usize;
+
+    build_test_externalities(config).execute_with(|| {
+        let category_id = create_root_category(origin);
+        let member_origin = create_forum_member();
+
+        CreateThreadFixture {
+            origin: member_origin,
+            category_id,
+            title: good_thread_title(),
+            text: generate_text(min_len - 1),
+            result: Err(ERROR_POST_TEXT_TOO_SHORT),
+        }
+        .call_and_assert();
+    });
+}
+
+#[test]
+fn create_thread_text_too_long() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+    let max_len = config.post_text_constraint.max() as usize;
+
+    build_test_externalities(config).execute_with(|| {
+        let category_id = create_root_category(origin);
+        let member_origin = create_forum_member();
+
+        CreateThreadFixture {
+            origin: member_origin,
+            category_id,
+            title: good_thread_title(),
+            text: generate_text(max_len + 1),
+            result: Err(ERROR_POST_TEXT_TOO_LONG),
+        }
+        .call_and_assert();
+    });
+}
+
+#[test]
+fn create_post_successfully() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let (_, _, _, _) = create_root_category_and_thread_and_post(origin);
+    });
+}
+
+#[test]
+fn create_post_text_too_short() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+    let min_len = config.post_text_constraint.min as usize;
+
+    build_test_externalities(config).execute_with(|| {
+        let (member_origin, _, thread_id) = create_root_category_and_thread(origin);
+
+        CreatePostFixture {
+            origin: member_origin,
+            thread_id,
+            text: generate_text(min_len - 1),
+            result: Err(ERROR_POST_TEXT_TOO_SHORT),
+        }
+        .call_and_assert();
+    });
+}
+
+#[test]
+fn create_post_text_too_long() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+    let max_len = config.post_text_constraint.max() as usize;
+
+    build_test_externalities(config).execute_with(|| {
+        let (member_origin, _, thread_id) = create_root_category_and_thread(origin);
+
+        CreatePostFixture {
+            origin: member_origin,
+            thread_id,
+            text: generate_text(max_len + 1),
+            result: Err(ERROR_POST_TEXT_TOO_LONG),
+        }
+        .call_and_assert();
+    });
+}
+
+// Test moderation:
+// -----------------------------------------------------------------------------
+
+#[test]
+fn moderate_thread_successfully() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let (_, _, thread_id) = create_root_category_and_thread(origin.clone());
+        assert_eq!(moderate_thread(origin, thread_id, good_rationale()), Ok(()));
+    });
+}
+
+#[test]
+fn cannot_moderate_already_moderated_thread() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let (_, _, thread_id) = create_root_category_and_thread(origin.clone());
+        assert_eq!(
+            moderate_thread(origin.clone(), thread_id.clone(), good_rationale()),
+            Ok(())
+        );
+        assert_eq!(
+            moderate_thread(origin, thread_id, good_rationale()),
+            Err(ERROR_THREAD_ALREADY_MODERATED)
+        );
+    });
+}
+
+#[test]
+fn moderate_thread_rationale_too_short() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+    let min_len = config.thread_moderation_rationale_constraint.min as usize;
+
+    build_test_externalities(config).execute_with(|| {
+        let (_, _, thread_id) = create_root_category_and_thread(origin.clone());
+        let bad_rationale = generate_text(min_len - 1);
+        assert_eq!(
+            moderate_thread(origin, thread_id, bad_rationale),
+            Err(ERROR_THREAD_MODERATION_RATIONALE_TOO_SHORT)
+        );
+    });
+}
+
+#[test]
+fn moderate_thread_rationale_too_long() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+    let max_len = config.thread_moderation_rationale_constraint.max() as usize;
+
+    build_test_externalities(config).execute_with(|| {
+        let (_, _, thread_id) = create_root_category_and_thread(origin.clone());
+        let bad_rationale = generate_text(max_len + 1);
+        assert_eq!(
+            moderate_thread(origin, thread_id, bad_rationale),
+            Err(ERROR_THREAD_MODERATION_RATIONALE_TOO_LONG)
+        );
+    });
+}
+
+#[test]
+fn moderate_post_successfully() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let (_, _, _, post_id) = create_root_category_and_thread_and_post(origin.clone());
+        assert_eq!(moderate_post(origin, post_id, good_rationale()), Ok(()));
+    });
+}
+
+#[test]
+fn moderate_post_rationale_too_short() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+    let min_len = config.post_moderation_rationale_constraint.min as usize;
+
+    build_test_externalities(config).execute_with(|| {
+        let (_, _, _, post_id) = create_root_category_and_thread_and_post(origin.clone());
+        let bad_rationale = generate_text(min_len - 1);
+        assert_eq!(
+            moderate_post(origin, post_id, bad_rationale),
+            Err(ERROR_POST_MODERATION_RATIONALE_TOO_SHORT)
+        );
+    });
+}
+
+#[test]
+fn moderate_post_rationale_too_long() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+    let max_len = config.post_moderation_rationale_constraint.max() as usize;
+
+    build_test_externalities(config).execute_with(|| {
+        let (_, _, _, post_id) = create_root_category_and_thread_and_post(origin.clone());
+        let bad_rationale = generate_text(max_len + 1);
+        assert_eq!(
+            moderate_post(origin, post_id, bad_rationale),
+            Err(ERROR_POST_MODERATION_RATIONALE_TOO_LONG)
+        );
+    });
+}
+
+#[test]
+fn cannot_moderate_already_moderated_post() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let (_, _, _, post_id) = create_root_category_and_thread_and_post(origin.clone());
+        assert_eq!(
+            moderate_post(origin.clone(), post_id.clone(), good_rationale()),
+            Ok(())
+        );
+        assert_eq!(
+            moderate_post(origin, post_id, good_rationale()),
+            Err(ERROR_POST_MODERATED)
+        );
+    });
+}
+
+// Not a forum sudo:
+// -----------------------------------------------------------------------------
+
+#[test]
+fn not_forum_sudo_cannot_create_root_category() {
+    let config = default_genesis_config();
+
+    build_test_externalities(config).execute_with(|| {
+        assert_create_category(
+            NOT_FORUM_SUDO_ORIGIN,
+            None,
+            Err(ERROR_ORIGIN_NOT_FORUM_SUDO),
+        );
+    });
+}
+
+#[test]
+fn not_forum_sudo_cannot_create_subcategory() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let root_category_id = create_root_category(origin);
+        assert_create_category(
+            NOT_FORUM_SUDO_ORIGIN,
+            Some(root_category_id),
+            Err(ERROR_ORIGIN_NOT_FORUM_SUDO),
+        );
+    });
+}
+
+#[test]
+fn not_forum_sudo_cannot_archive_category() {
+    assert_not_forum_sudo_cannot_update_category(archive_category);
+}
+
+#[test]
+fn not_forum_sudo_cannot_unarchive_category() {
+    assert_not_forum_sudo_cannot_update_category(unarchive_category);
+}
+
+#[test]
+fn not_forum_sudo_cannot_delete_category() {
+    assert_not_forum_sudo_cannot_update_category(delete_category);
+}
+
+#[test]
+fn not_forum_sudo_cannot_undelete_category() {
+    assert_not_forum_sudo_cannot_update_category(undelete_category);
+}
+
+#[test]
+fn not_forum_sudo_cannot_moderate_thread() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let (_, _, thread_id) = create_root_category_and_thread(origin.clone());
+        assert_eq!(
+            moderate_thread(NOT_FORUM_SUDO_ORIGIN, thread_id, good_rationale()),
+            Err(ERROR_ORIGIN_NOT_FORUM_SUDO)
+        );
+    });
+}
+
+#[test]
+fn not_forum_sudo_cannot_moderate_post() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let (_, _, _, post_id) = create_root_category_and_thread_and_post(origin.clone());
+        assert_eq!(
+            moderate_post(NOT_FORUM_SUDO_ORIGIN, post_id, good_rationale()),
+            Err(ERROR_ORIGIN_NOT_FORUM_SUDO)
+        );
+    });
+}
+
+// Not a member:
+// -----------------------------------------------------------------------------
+
+#[test]
+fn not_member_cannot_create_thread() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        CreateThreadFixture {
+            origin: NOT_MEMBER_ORIGIN,
+            category_id: create_root_category(origin),
+            title: good_thread_title(),
+            text: good_thread_text(),
+            result: Err(ERROR_NOT_FORUM_USER),
+        }
+        .call_and_assert();
+    });
+}
+
+#[test]
+fn not_member_cannot_create_post() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let (_, _, thread_id) = create_root_category_and_thread(origin);
+        CreatePostFixture {
+            origin: NOT_MEMBER_ORIGIN,
+            thread_id,
+            text: good_post_text(),
+            result: Err(ERROR_NOT_FORUM_USER),
+        }
+        .call_and_assert();
+    });
+}
+
+#[test]
+fn not_member_cannot_edit_post() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let (_, _, _, post_id) = create_root_category_and_thread_and_post(origin);
+        assert_err!(
+            TestForumModule::edit_post_text(
+                mock_origin(NOT_MEMBER_ORIGIN),
+                post_id,
+                good_rationale()
+            ),
+            ERROR_NOT_FORUM_USER
+        );
+    });
+}
+
+// Invalid id passed:
+// -----------------------------------------------------------------------------
+
+#[test]
+fn cannot_create_subcategory_with_invalid_parent_category_id() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        assert_create_category(
+            origin,
+            Some(INVLAID_CATEGORY_ID),
+            Err(ERROR_CATEGORY_DOES_NOT_EXIST),
+        );
+    });
+}
+
+#[test]
+fn cannot_create_thread_with_invalid_category_id() {
+    let config = default_genesis_config();
+
+    build_test_externalities(config).execute_with(|| {
+        CreateThreadFixture {
+            origin: create_forum_member(),
+            category_id: INVLAID_CATEGORY_ID,
+            title: good_thread_title(),
+            text: good_thread_text(),
+            result: Err(ERROR_CATEGORY_DOES_NOT_EXIST),
+        }
+        .call_and_assert();
+    });
+}
+
+#[test]
+fn cannot_create_post_with_invalid_thread_id() {
+    let config = default_genesis_config();
+
+    build_test_externalities(config).execute_with(|| {
+        CreatePostFixture {
+            origin: create_forum_member(),
+            thread_id: INVLAID_THREAD_ID,
+            text: good_post_text(),
+            result: Err(ERROR_THREAD_DOES_NOT_EXIST),
+        }
+        .call_and_assert();
+    });
+}
+
+#[test]
+fn cannot_moderate_thread_with_invalid_id() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        assert_err!(
+            moderate_thread(origin, INVLAID_THREAD_ID, good_rationale()),
+            ERROR_THREAD_DOES_NOT_EXIST
+        );
+    });
+}
+
+#[test]
+fn cannot_moderate_post_with_invalid_id() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        assert_err!(
+            moderate_post(origin, INVLAID_POST_ID, good_rationale()),
+            ERROR_POST_DOES_NOT_EXIST
+        );
+    });
+}
+
+// Successfull extrinsics
+// -----------------------------------------------------------------------------
+
+#[test]
+fn archive_then_unarchive_category_successfully() {
+    let config = default_genesis_config();
+    let forum_sudo = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let category_id = create_root_category(forum_sudo.clone());
+        assert_ok!(archive_category(forum_sudo.clone(), category_id.clone(),));
+        // TODO get category by id and assert archived == true.
+
+        assert_ok!(unarchive_category(forum_sudo, category_id,));
+        // TODO get category by id and assert archived == false.
+    });
+}
+
+#[test]
+fn delete_then_undelete_category_successfully() {
+    let config = default_genesis_config();
+    let forum_sudo = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let category_id = create_root_category(forum_sudo.clone());
+        assert_ok!(delete_category(forum_sudo.clone(), category_id.clone(),));
+        // TODO get category by id and assert deleted == true.
+
+        assert_ok!(undelete_category(forum_sudo.clone(), category_id.clone(),));
+        // TODO get category by id and assert deleted == false.
+    });
+}
+
+// TODO Consider to fix the logic of the forum module
+// #[test]
+// fn cannot_unarchive_not_archived_category() {
+//     let config = default_genesis_config();
+//     let forum_sudo = OriginType::Signed(config.forum_sudo);
+
+//     build_test_externalities(config).execute_with(|| {
+//         let category_id = create_root_category(forum_sudo.clone());
+
+//         // TODO bug in a logic! it should not be possible. !!!
+
+//         assert_err!(
+//             archive_category(
+//                 forum_sudo.clone(),
+//                 category_id.clone(),
+//             ),
+//             "... TODO expect error ..."
+//         );
+//     });
+// }
+
+// TODO Consider to fix the logic of the forum module
+// #[test]
+// fn cannot_undelete_not_deleted_category() {
+//     let config = default_genesis_config();
+//     let forum_sudo = OriginType::Signed(config.forum_sudo);
+
+//     build_test_externalities(config).execute_with(|| {
+//         let category_id = create_root_category(forum_sudo.clone());
+//         assert_err!(
+//             delete_category(
+//                 forum_sudo.clone(),
+//                 category_id.clone(),
+//             ),
+//             "... TODO expect error ..."
+//         );
+//     });
+// }
+
+// With archived / deleted category, moderated thread
+// -----------------------------------------------------------------------------
+
+#[test]
+fn cannot_create_subcategory_in_archived_category() {
+    let config = default_genesis_config();
+    let forum_sudo = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let category_id = create_root_category(forum_sudo.clone());
+        assert_ok!(archive_category(forum_sudo.clone(), category_id.clone(),));
+        assert_create_category(
+            forum_sudo,
+            Some(category_id),
+            Err(ERROR_ANCESTOR_CATEGORY_IMMUTABLE),
+        );
+    });
+}
+
+#[test]
+fn cannot_create_subcategory_in_deleted_category() {
+    let config = default_genesis_config();
+    let forum_sudo = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let category_id = create_root_category(forum_sudo.clone());
+        assert_ok!(delete_category(forum_sudo.clone(), category_id.clone(),));
+        assert_create_category(
+            forum_sudo,
+            Some(category_id),
+            Err(ERROR_ANCESTOR_CATEGORY_IMMUTABLE),
+        );
+    });
+}
+
+#[test]
+fn cannot_create_thread_in_archived_category() {
+    let config = default_genesis_config();
+    let forum_sudo = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let category_id = create_root_category(forum_sudo.clone());
+        assert_ok!(archive_category(forum_sudo.clone(), category_id.clone(),));
+        assert_create_thread(
+            create_forum_member(),
+            category_id,
+            Err(ERROR_ANCESTOR_CATEGORY_IMMUTABLE),
+        );
+    });
+}
+
+#[test]
+fn cannot_create_thread_in_deleted_category() {
+    let config = default_genesis_config();
+    let forum_sudo = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let category_id = create_root_category(forum_sudo.clone());
+        assert_ok!(delete_category(forum_sudo.clone(), category_id.clone(),));
+        assert_create_thread(
+            create_forum_member(),
+            category_id,
+            Err(ERROR_ANCESTOR_CATEGORY_IMMUTABLE),
+        );
+    });
+}
+
+#[test]
+fn cannot_create_post_in_thread_of_archived_category() {
+    let config = default_genesis_config();
+    let forum_sudo = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let category_id = create_root_category(forum_sudo.clone());
+        let thread_id = TestForumModule::next_thread_id();
+        assert_create_thread(create_forum_member(), category_id, Ok(()));
+        assert_ok!(archive_category(forum_sudo.clone(), category_id.clone(),));
+        assert_create_post(
+            create_forum_member(),
+            thread_id,
+            Err(ERROR_ANCESTOR_CATEGORY_IMMUTABLE),
+        );
+    });
+}
+
+#[test]
+fn cannot_create_post_in_thread_of_deleted_category() {
+    let config = default_genesis_config();
+    let forum_sudo = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let category_id = create_root_category(forum_sudo.clone());
+        let thread_id = TestForumModule::next_thread_id();
+        assert_create_thread(create_forum_member(), category_id, Ok(()));
+        assert_ok!(delete_category(forum_sudo.clone(), category_id.clone(),));
+        assert_create_post(
+            create_forum_member(),
+            thread_id,
+            Err(ERROR_ANCESTOR_CATEGORY_IMMUTABLE),
+        );
+    });
+}
+
+#[test]
+fn cannot_create_post_in_moderated_thread() {
+    let config = default_genesis_config();
+    let forum_sudo = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let (_, _, thread_id) = create_root_category_and_thread(forum_sudo.clone());
+        assert_ok!(moderate_thread(
+            forum_sudo,
+            thread_id.clone(),
+            good_rationale()
+        ));
+        assert_create_post(
+            create_forum_member(),
+            thread_id,
+            Err(ERROR_THREAD_MODERATED),
+        );
+    });
+}
+
+#[test]
+fn cannot_edit_post_in_moderated_thread() {
+    let config = default_genesis_config();
+    let forum_sudo = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let (member_origin, _, thread_id, post_id) =
+            create_root_category_and_thread_and_post(forum_sudo.clone());
+        assert_ok!(moderate_thread(forum_sudo, thread_id, good_rationale()));
+        assert_err!(
+            TestForumModule::edit_post_text(mock_origin(member_origin), post_id, good_rationale()),
+            ERROR_THREAD_MODERATED
+        );
+    });
+}
+
+// TODO impl
+// #[test]
+// fn cannot_edit_moderated_post() {}

+ 89 - 0
runtime-modules/governance/Cargo.toml

@@ -0,0 +1,89 @@
+[package]
+name = 'substrate-governance-module'
+version = '1.0.0'
+authors = ['Joystream contributors']
+edition = '2018'
+
+[features]
+default = ['std']
+std = [
+	'sr-primitives/std',
+	'srml-support/std',
+	'system/std',
+	'serde',
+	'codec/std',
+	'timestamp/std',
+	'primitives/std',
+	'rstd/std',
+	'common/std',
+	'membership/std',
+]
+
+[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.rstd]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-std'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.serde]
+features = ['derive']
+optional = true
+version = '1.0.101'
+
+[dependencies.timestamp]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-timestamp'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.primitives]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'substrate-primitives'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.codec]
+default-features = false
+features = ['derive']
+package = 'parity-scale-codec'
+version = '1.0.0'
+
+[dependencies.common]
+default_features = false
+package = 'substrate-common-module'
+path = '../common'
+
+[dependencies.membership]
+default_features = false
+package = 'substrate-membership-module'
+path = '../membership'
+
+[dev-dependencies.runtime-io]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-io'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dev-dependencies.balances]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-balances'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'

+ 2 - 2
src/governance/council.rs → runtime-modules/governance/src/council.rs

@@ -4,7 +4,7 @@ use srml_support::{decl_event, decl_module, decl_storage, ensure};
 use system::{self, ensure_root};
 
 pub use super::election::{self, CouncilElected, Seat, Seats};
-pub use crate::currency::{BalanceOf, GovernanceCurrency};
+pub use common::currency::{BalanceOf, GovernanceCurrency};
 
 // Hook For announcing that council term has ended
 pub trait CouncilTermEnded {
@@ -123,7 +123,7 @@ decl_module! {
 
 #[cfg(test)]
 mod tests {
-    use crate::governance::mock::*;
+    use crate::mock::*;
     use srml_support::*;
 
     fn add_council_member_as_root(account: <Test as system::Trait>::AccountId) -> dispatch::Result {

+ 2 - 4
src/governance/election.rs → runtime-modules/governance/src/election.rs

@@ -14,9 +14,7 @@ use super::sealed_vote::SealedVote;
 use super::stake::Stake;
 
 use super::council;
-pub use crate::currency::{BalanceOf, GovernanceCurrency};
-
-use crate::membership;
+pub use common::currency::{BalanceOf, GovernanceCurrency};
 
 pub trait Trait:
     system::Trait + council::Trait + GovernanceCurrency + membership::members::Trait
@@ -900,7 +898,7 @@ impl<T: Trait> council::CouncilTermEnded for Module<T> {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::governance::mock::*;
+    use crate::mock::*;
     use codec::Encode;
     use srml_support::*;
 

+ 3 - 0
src/governance/mod.rs → runtime-modules/governance/src/lib.rs

@@ -1,3 +1,6 @@
+// Ensure we're `no_std` when compiling for Wasm.
+#![cfg_attr(not(feature = "std"), no_std)]
+
 pub mod council;
 pub mod election;
 pub mod proposals;

+ 1 - 2
src/governance/mock.rs → runtime-modules/governance/src/mock.rs

@@ -1,8 +1,7 @@
 #![cfg(test)]
 
 pub use super::{council, election, proposals};
-pub use crate::currency::GovernanceCurrency;
-use crate::membership;
+pub use common::currency::GovernanceCurrency;
 pub use system;
 
 pub use primitives::{Blake2Hasher, H256};

+ 6 - 6
src/governance/proposals.rs → runtime-modules/governance/src/proposals.rs

@@ -15,8 +15,7 @@ use serde::{Deserialize, Serialize};
 use primitives::storage::well_known_keys;
 
 use super::council;
-pub use crate::currency::{BalanceOf, GovernanceCurrency};
-use crate::membership;
+pub use common::currency::{BalanceOf, GovernanceCurrency};
 
 const DEFAULT_APPROVAL_QUORUM: u32 = 60;
 const DEFAULT_MIN_STAKE: u32 = 100;
@@ -688,10 +687,11 @@ mod tests {
             .build_storage::<Test>()
             .unwrap();
 
-        // We use default for brevity, but you can configure as desired if needed.
-        balances::GenesisConfig::<Test>::default()
-            .assimilate_storage(&mut t)
-            .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()

+ 0 - 0
src/governance/sealed_vote.rs → runtime-modules/governance/src/sealed_vote.rs


+ 0 - 0
src/governance/stake.rs → runtime-modules/governance/src/stake.rs


+ 60 - 0
runtime-modules/hiring/Cargo.toml

@@ -0,0 +1,60 @@
+[package]
+name = 'substrate-hiring-module'
+version = '1.0.1'
+authors = ['Joystream contributors']
+edition = '2018'
+
+[dependencies]
+hex-literal = '0.1.0'
+serde = { version = '1.0', optional = true }
+serde_derive = { version = '1.0', optional = true }
+rstd = { package = 'sr-std', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+runtime-primitives = { package = 'sr-primitives', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+srml-support = { package = 'srml-support', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+srml-support-procedural = { package = 'srml-support-procedural', git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+system = { package = 'srml-system', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+balances = { package = 'srml-balances', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+codec = { package = 'parity-scale-codec', version = '1.0.0', default-features = false, features = ['derive'] }
+mockall = {version = "0.6.0", optional = true}
+# https://users.rust-lang.org/t/failure-derive-compilation-error/39062
+quote = '<=1.0.2'
+
+[dependencies.stake]
+default_features = false
+package = 'substrate-stake-module'
+path = '../stake'
+
+[dependencies.timestamp]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-timestamp'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.runtime-io]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-io'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dev-dependencies]
+runtime-io = { package = 'sr-io', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+primitives = { package = 'substrate-primitives', git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+mockall = "0.6.0"
+
+
+[features]
+default = ['std']
+test = ['mockall']
+std = [
+	'serde',
+	'serde_derive',
+	'codec/std',
+	'rstd/std',
+	'runtime-io/std',
+	'runtime-primitives/std',
+	'srml-support/std',
+	'system/std',
+  	'balances/std',
+	'timestamp/std',
+	'stake/std',
+]

+ 163 - 0
runtime-modules/hiring/src/hiring/application.rs

@@ -0,0 +1,163 @@
+use codec::{Decode, Encode};
+use rstd::clone::Clone;
+use rstd::vec::Vec;
+
+use crate::hiring::StakePurpose;
+
+/// An application for an actor to occupy an opening.
+#[derive(Encode, Decode, Default, Debug, Eq, PartialEq, Clone, PartialOrd, Ord)]
+pub struct Application<OpeningId, BlockNumber, StakeId> {
+    /// Identifier for opening for which this application is for.
+    pub opening_id: OpeningId,
+
+    /// Index of arrival across all applications for given opening,
+    /// which is needed for strictly ordering applications.
+    /// Starts at 0.
+    pub application_index_in_opening: u32,
+
+    /// Block at which this application was added.
+    pub add_to_opening_in_block: BlockNumber,
+
+    // NB: The given staking identifiers have a bloated purpose,
+    // and are mutable, fix this.
+    // https://github.com/Joystream/substrate-hiring-module/issues/11
+    /// Identifier for stake that may possibly be established for role.
+    /// Will be set iff the role staking policy of the corresponding opening
+    /// states so AND application is not inactive.
+    pub active_role_staking_id: Option<StakeId>,
+
+    /// Identifier for stake that may possibly be established for application
+    /// Will be set iff the application staking policy of the corresponding opening
+    /// states so.
+    pub active_application_staking_id: Option<StakeId>,
+
+    /// Status of this application
+    pub stage: ApplicationStage<BlockNumber>,
+
+    /// Application note
+    pub human_readable_text: Vec<u8>,
+}
+
+impl<OpeningId, BlockNumber, StakeId: PartialEq + Clone>
+    Application<OpeningId, BlockNumber, StakeId>
+{
+    /// Compares provided stake_id with internal stake defined by stake_purpose.
+    /// Returns None on equality, Some(stake_id) otherwise.
+    pub fn toggle_stake_id(
+        &self,
+        stake_id: StakeId,
+        stake_purpose: StakePurpose,
+    ) -> Option<StakeId> {
+        let active_staking_id = match stake_purpose {
+            StakePurpose::Application => self.active_application_staking_id.clone(),
+            StakePurpose::Role => self.active_role_staking_id.clone(),
+        };
+
+        match active_staking_id {
+            // If there is a match, toggle.
+            Some(id) => {
+                if id == stake_id {
+                    None
+                } else {
+                    Some(id)
+                }
+            }
+            _ => None,
+        }
+    }
+
+    /// Modifies an application and unstake provided stake_id.
+    /// If last stake unstaked - app stage becomes Inactive
+    pub(crate) fn unstake_application(
+        &mut self,
+        current_block_height: BlockNumber,
+        deactivation_initiated: BlockNumber,
+        cause: ApplicationDeactivationCause,
+        stake_id: StakeId,
+    ) -> bool {
+        // New values for application stakes
+        let new_active_role_staking_id = self.toggle_stake_id(stake_id.clone(), StakePurpose::Role);
+        let new_active_application_staking_id =
+            self.toggle_stake_id(stake_id, StakePurpose::Application);
+
+        // Are we now done unstaking?
+        // Is the case if thereare no application stakes set.
+        let is_now_done_unstaking =
+            new_active_role_staking_id.is_none() && new_active_application_staking_id.is_none();
+
+        self.active_role_staking_id = new_active_role_staking_id;
+        self.active_application_staking_id = new_active_application_staking_id;
+
+        // If we are done unstaking, then we go to the inactive stage
+        if is_now_done_unstaking {
+            self.stage = ApplicationStage::Inactive {
+                deactivation_initiated,
+                deactivated: current_block_height,
+                cause,
+            }
+        };
+
+        is_now_done_unstaking
+    }
+}
+
+/// Possible status of an application
+#[derive(Encode, Decode, Debug, Eq, PartialEq, Clone, PartialOrd, Ord)]
+pub enum ApplicationStage<BlockNumber> {
+    /// Normal active state
+    Active,
+
+    /// Waiting for one or more unstakings, with a non-zero unstaking period, to complete.
+    Unstaking {
+        /// When deactivation was initiated.
+        deactivation_initiated: BlockNumber,
+
+        /// The cause of the deactivation.
+        cause: ApplicationDeactivationCause,
+    },
+
+    ///  No longer active, can't do anything fun now.
+    Inactive {
+        /// When deactivation was initiated.
+        deactivation_initiated: BlockNumber,
+
+        /// When deactivation was completed, and the inactive state was established.
+        deactivated: BlockNumber,
+
+        /// The cause of the deactivation.
+        cause: ApplicationDeactivationCause,
+    },
+}
+
+/// Possible application deactivation causes
+#[derive(Encode, Decode, Debug, Eq, PartialEq, Clone, Copy, PartialOrd, Ord)]
+pub enum ApplicationDeactivationCause {
+    /// Deactivation initiated from outside
+    External, // Add ID here for simplicity?
+
+    /// Applicant was hired
+    Hired,
+
+    /// Applicant was not hired
+    NotHired,
+
+    /// Application was crowded out by another applicaiton
+    CrowdedOut,
+
+    /// Opening was cancelled
+    OpeningCancelled,
+
+    /// Review period expired
+    ReviewPeriodExpired,
+
+    /// Opening was filled
+    OpeningFilled,
+}
+
+/// OpeningStage must be default constructible because it indirectly is a value in a storage map.
+/// ***SHOULD NEVER ACTUALLY GET CALLED, IS REQUIRED TO DUE BAD STORAGE MODEL IN SUBSTRATE***
+impl<BlockNumber> Default for ApplicationStage<BlockNumber> {
+    fn default() -> Self {
+        ApplicationStage::Active
+    }
+}

+ 157 - 0
runtime-modules/hiring/src/hiring/mod.rs

@@ -0,0 +1,157 @@
+mod application;
+mod opening;
+mod staking_policy;
+
+pub use application::*;
+pub use opening::*;
+pub use staking_policy::*;
+
+/// Stake purpose enumeration.
+#[derive(Eq, PartialEq, Clone, Debug)]
+pub enum StakePurpose {
+    /// Stake for a role
+    Role,
+
+    /// Stake for an application
+    Application,
+}
+
+/// Informational result of the unstaked() method. Can be ignored.
+#[derive(Debug, Eq, PartialEq, Clone, Copy)]
+pub enum UnstakedResult {
+    /// Non-existentent stake id provided
+    StakeIdNonExistent,
+
+    /// Application is not in 'Unstaking' state
+    ApplicationIsNotUnstaking,
+
+    /// Fully unstaked
+    Unstaked,
+
+    /// Unstaking in progress
+    UnstakingInProgress,
+}
+
+/// Error of the add_application() API method.
+#[derive(Eq, PartialEq, Clone, Debug)]
+pub enum AddApplicationError {
+    /// Opening does not exist
+    OpeningDoesNotExist,
+
+    /// Provided redundant stake
+    StakeProvidedWhenRedundant(StakePurpose),
+
+    /// Required stake was not provided
+    StakeMissingWhenRequired(StakePurpose),
+
+    /// Provided stake amount is too low
+    StakeAmountTooLow(StakePurpose),
+
+    /// Opening is not in acception application active stage
+    OpeningNotInAcceptingApplicationsStage,
+
+    /// Newly created application was crowded out
+    NewApplicationWasCrowdedOut,
+}
+
+/// Successful result of the add_opening() API method.
+#[derive(Eq, PartialEq, Clone, Debug)]
+pub struct ApplicationAdded<ApplicationId> {
+    /// Added application id
+    pub application_id_added: ApplicationId,
+
+    /// Possible id of the crowded out application
+    pub application_id_crowded_out: Option<ApplicationId>,
+}
+
+/// Error of the deactivate_application() API method.
+#[derive(Eq, PartialEq, Clone, Debug)]
+pub enum DeactivateApplicationError {
+    /// Application does not exist
+    ApplicationDoesNotExist,
+
+    /// Application is not in active stage
+    ApplicationNotActive,
+
+    /// Opening is not in accepting application stage
+    OpeningNotAcceptingApplications,
+
+    /// Provided unstaking period is too short
+    UnstakingPeriodTooShort(StakePurpose),
+
+    /// Provided redundant unstaking period
+    RedundantUnstakingPeriodProvided(StakePurpose),
+}
+
+/// Error of the cancel_opening() API method.
+#[derive(Eq, PartialEq, Clone, Debug)]
+pub enum CancelOpeningError {
+    /// Provided unstaking period is too short
+    UnstakingPeriodTooShort(StakePurpose),
+
+    /// Provided redundant unstaking period
+    RedundantUnstakingPeriodProvided(StakePurpose),
+
+    /// Opening does not exist
+    OpeningDoesNotExist,
+
+    /// Opening cannot be cancelled
+    OpeningNotInCancellableStage,
+}
+
+/// NB:
+/// `OpeningCancelled` does not have the ideal form.
+/// https://github.com/Joystream/substrate-hiring-module/issues/10
+#[derive(Eq, PartialEq, Clone, Debug)]
+pub struct OpeningCancelled {
+    /// Number of unstaking application because of canceled opening
+    pub number_of_unstaking_applications: u32,
+
+    /// Number of deactivaed application because of canceled opening
+    pub number_of_deactivated_applications: u32,
+}
+
+/// Error of the begin_review() API method.
+#[derive(Eq, PartialEq, Clone, Debug)]
+pub enum BeginReviewError {
+    /// Opening does not exist
+    OpeningDoesNotExist,
+
+    /// Opening is not in acception application active stage
+    OpeningNotInAcceptingApplicationsStage,
+}
+
+/// Error of the begin_accepting_application() API method.
+#[derive(Eq, PartialEq, Clone, Debug)]
+pub enum BeginAcceptingApplicationsError {
+    /// Opening does not exist
+    OpeningDoesNotExist,
+
+    /// Opening is not in waiting to begin stage
+    OpeningIsNotInWaitingToBeginStage,
+}
+
+/// The possible outcome for an application in an opening which is being filled.
+#[derive(Eq, PartialEq, Clone, Debug)]
+pub enum ApplicationOutcomeInFilledOpening {
+    /// Application is successful
+    Success,
+
+    /// Application failed
+    Failure,
+}
+
+/// Error of the add_opening() API method.
+#[derive(Eq, PartialEq, Clone, Debug)]
+pub enum AddOpeningError {
+    /// Indicates that opening with provided parameters must be activated in the future
+    OpeningMustActivateInTheFuture,
+
+    /// It is not possible to stake less than the minimum balance defined in the
+    /// `Currency` module.
+    StakeAmountLessThanMinimumCurrencyBalance(StakePurpose),
+
+    /// It is not possible to provide application rationing policy with zero
+    /// 'max_active_applicants' parameter.
+    ApplicationRationingZeroMaxApplicants,
+}

+ 448 - 0
runtime-modules/hiring/src/hiring/opening.rs

@@ -0,0 +1,448 @@
+use rstd::clone::Clone;
+use rstd::collections::btree_set::BTreeSet;
+use rstd::prelude::*;
+use rstd::vec::Vec;
+
+use codec::{Decode, Encode};
+#[cfg(feature = "std")]
+use serde::{Deserialize, Serialize};
+use srml_support::ensure;
+
+use crate::hiring;
+use crate::hiring::*;
+
+/// An opening represents the process of hiring one or more new actors into some available role
+#[derive(Encode, Decode, Default, Debug, Eq, PartialEq, Clone)]
+pub struct Opening<Balance, BlockNumber, ApplicationId> {
+    /// Block at which opening was added
+    pub created: BlockNumber,
+
+    /// Current stage for this opening
+    pub stage: OpeningStage<BlockNumber, ApplicationId>,
+
+    /// Maximum length of the review stage.
+    pub max_review_period_length: BlockNumber,
+
+    /// Whether, and if so how, to limit the number of active applicants....
+    pub application_rationing_policy: Option<ApplicationRationingPolicy>,
+
+    /// Whether any staking is required just to apply, and if so, how that stake is managed.
+    pub application_staking_policy: Option<StakingPolicy<Balance, BlockNumber>>,
+
+    /// Whether any staking is required for the role, and if so, how that stake is managed.
+    pub role_staking_policy: Option<StakingPolicy<Balance, BlockNumber>>,
+
+    /// Description of opening
+    pub human_readable_text: Vec<u8>,
+}
+
+impl<Balance, BlockNumber, ApplicationId> Opening<Balance, BlockNumber, ApplicationId>
+where
+    Balance: PartialOrd + Clone,
+    BlockNumber: PartialOrd + Clone,
+    ApplicationId: Ord + Clone,
+{
+    ///Creates new instance of Opening
+    pub(crate) fn new(
+        current_block_height: BlockNumber,
+        activate_at: ActivateOpeningAt<BlockNumber>,
+        max_review_period_length: BlockNumber,
+        application_rationing_policy: Option<ApplicationRationingPolicy>,
+        application_staking_policy: Option<StakingPolicy<Balance, BlockNumber>>,
+        role_staking_policy: Option<StakingPolicy<Balance, BlockNumber>>,
+        human_readable_text: Vec<u8>,
+    ) -> Self {
+        // Construct new opening
+        let opening_stage = match activate_at {
+            ActivateOpeningAt::CurrentBlock => hiring::OpeningStage::Active {
+                // We immediately start accepting applications
+                stage: hiring::ActiveOpeningStage::AcceptingApplications {
+                    started_accepting_applicants_at_block: current_block_height.clone(),
+                },
+
+                // Empty set of applicants
+                applications_added: BTreeSet::new(),
+
+                // All counters set to 0
+                active_application_count: 0,
+                unstaking_application_count: 0,
+                deactivated_application_count: 0,
+            },
+
+            ActivateOpeningAt::ExactBlock(block_number) => hiring::OpeningStage::WaitingToBegin {
+                begins_at_block: block_number,
+            },
+        };
+
+        hiring::Opening {
+            created: current_block_height,
+            stage: opening_stage,
+            max_review_period_length,
+            application_rationing_policy,
+            application_staking_policy,
+            role_staking_policy,
+            human_readable_text,
+        }
+    }
+
+    pub(crate) fn clone_with_new_active_opening_stage(
+        self,
+        active_opening_stage: hiring::ActiveOpeningStage<BlockNumber>,
+    ) -> Self {
+        //TODO: hiring::OpeningStage::Active params should be changed to struct
+        //Copy parameters from previous active stage if any or set defaults
+        let (
+            applications_added,
+            active_application_count,
+            unstaking_application_count,
+            deactivated_application_count,
+        ) = if let hiring::OpeningStage::Active {
+            applications_added,
+            active_application_count,
+            unstaking_application_count,
+            deactivated_application_count,
+            ..
+        } = self.stage
+        {
+            //Active opening stage
+            (
+                applications_added,
+                active_application_count,
+                unstaking_application_count,
+                deactivated_application_count,
+            )
+        } else {
+            //Not active opening stage
+            (BTreeSet::new(), 0, 0, 0)
+        };
+
+        hiring::Opening {
+            stage: hiring::OpeningStage::Active {
+                stage: active_opening_stage,
+                applications_added,
+                active_application_count,
+                unstaking_application_count,
+                deactivated_application_count,
+            },
+            ..self
+        }
+    }
+
+    pub(crate) fn change_opening_stage_after_application_unstaked(&mut self) {
+        if let OpeningStage::Active {
+            ref stage,
+            ref applications_added,
+            active_application_count,
+            unstaking_application_count,
+            deactivated_application_count,
+        } = self.stage
+        {
+            self.stage = hiring::OpeningStage::Active {
+                stage: stage.clone(),
+                applications_added: applications_added.clone(),
+                active_application_count,
+                unstaking_application_count: unstaking_application_count - 1,
+                deactivated_application_count: deactivated_application_count + 1,
+            };
+        } else {
+            panic!("stage MUST be active")
+        }
+    }
+
+    /// Performs all necessary check before adding an opening
+    pub(crate) fn ensure_can_add_opening(
+        current_block_height: BlockNumber,
+        activate_at: ActivateOpeningAt<BlockNumber>,
+        runtime_minimum_balance: Balance,
+        application_rationing_policy: Option<ApplicationRationingPolicy>,
+        application_staking_policy: Option<StakingPolicy<Balance, BlockNumber>>,
+        role_staking_policy: Option<StakingPolicy<Balance, BlockNumber>>,
+    ) -> Result<(), AddOpeningError> {
+        // Check that exact activation is actually in the future
+        ensure!(
+            match activate_at {
+                ActivateOpeningAt::ExactBlock(block_number) => block_number > current_block_height,
+                _ => true,
+            },
+            AddOpeningError::OpeningMustActivateInTheFuture
+        );
+
+        if let Some(app_rationing_policy) = application_rationing_policy {
+            ensure!(
+                app_rationing_policy.max_active_applicants > 0,
+                AddOpeningError::ApplicationRationingZeroMaxApplicants
+            );
+        }
+
+        // Check that staking amounts clear minimum balance required.
+        StakingPolicy::ensure_amount_valid_in_opt_staking_policy(
+            application_staking_policy,
+            runtime_minimum_balance.clone(),
+            AddOpeningError::StakeAmountLessThanMinimumCurrencyBalance(StakePurpose::Application),
+        )?;
+
+        // Check that staking amounts clear minimum balance required.
+        StakingPolicy::ensure_amount_valid_in_opt_staking_policy(
+            role_staking_policy,
+            runtime_minimum_balance,
+            AddOpeningError::StakeAmountLessThanMinimumCurrencyBalance(StakePurpose::Role),
+        )?;
+
+        Ok(())
+    }
+}
+
+/// The stage at which an `Opening` may be at.
+#[derive(Encode, Decode, Debug, Eq, PartialEq, Clone)]
+pub enum OpeningStage<BlockNumber, ApplicationId> {
+    /// Opening is not active yet. Will be activated at 'begins_at_block' block number
+    WaitingToBegin {
+        /// Becomes active at block number
+        begins_at_block: BlockNumber,
+    },
+
+    /// Active opening stage
+    Active {
+        /// Active stage
+        stage: hiring::ActiveOpeningStage<BlockNumber>,
+
+        /// Set of identifiers for all applications which have been added, but not removed, for this opening.
+        /// Is required for timely on-chain lookup of all applications associated with an opening.
+        applications_added: BTreeSet<ApplicationId>, //BTreeMap<ApplicationId, ()>, //Vec<T::ApplicationId>,
+
+        // TODO: Drop these counters
+        // https://github.com/Joystream/substrate-hiring-module/issues/9
+        //
+        // Counters over all possible application states.
+        // Are needed to set `application_index_in_opening` in new applications
+        // Are very useful for light clients.
+        //
+        // NB: Remember that _all_ state transitions in applications will require updating these variables,
+        // its easy to forget!
+        //
+        // NB: The sum of
+        // - `active_application_count`
+        // - `unstaking_application_count`
+        // - `deactivated_application_count`
+        //
+        // equals the total number of applications ever added to the opening via `add_application`.
+        /// Active NOW
+        active_application_count: u32,
+
+        /// Unstaking NOW
+        unstaking_application_count: u32,
+
+        /// Deactivated at any time for any cause.
+        deactivated_application_count: u32,
+        // Removed at any time.
+        //removed_application_count: u32
+    },
+}
+
+impl<BlockNumber: Clone, ApplicationId: Ord + Clone> OpeningStage<BlockNumber, ApplicationId> {
+    /// The number of applications ever added to the opening via
+    /// `add_opening` extrinsic.
+    pub fn number_of_applications_ever_added(&self) -> u32 {
+        match self {
+            OpeningStage::WaitingToBegin { .. } => 0,
+
+            OpeningStage::Active {
+                active_application_count,
+                unstaking_application_count,
+                deactivated_application_count,
+                ..
+            } => {
+                active_application_count
+                    + unstaking_application_count
+                    + deactivated_application_count
+            }
+        }
+    }
+
+    /// Ensures that an opening is waiting to begin.
+    pub(crate) fn ensure_opening_stage_is_waiting_to_begin<Err>(
+        &self,
+        error: Err,
+    ) -> Result<BlockNumber, Err> {
+        if let OpeningStage::WaitingToBegin { begins_at_block } = self {
+            return Ok(begins_at_block.clone());
+        }
+
+        Err(error)
+    }
+
+    // Clones current stage. Panics if not Active.
+    // Adds application_id to applications_added collection.
+    // Increments 'active_application_count' counter.
+    pub(crate) fn clone_with_added_active_application(
+        self,
+        new_application_id: ApplicationId,
+    ) -> Self {
+        if let hiring::OpeningStage::Active {
+            stage,
+            active_application_count,
+            unstaking_application_count,
+            deactivated_application_count,
+            mut applications_added,
+        } = self
+        {
+            applications_added.insert(new_application_id);
+
+            hiring::OpeningStage::Active {
+                stage,
+                applications_added,
+                active_application_count: active_application_count + 1,
+                unstaking_application_count,
+                deactivated_application_count,
+            }
+        } else {
+            panic!("updated opening should be in active stage");
+        }
+    }
+}
+
+/// OpeningStage must be default constructible because it indirectly is a value in a storage map.
+/// ***SHOULD NEVER ACTUALLY GET CALLED, IS REQUIRED TO DUE BAD STORAGE MODEL IN SUBSTRATE***
+impl<BlockNumber: Default, ApplicationId> Default for OpeningStage<BlockNumber, ApplicationId> {
+    fn default() -> Self {
+        OpeningStage::WaitingToBegin {
+            begins_at_block: BlockNumber::default(),
+        }
+    }
+}
+
+/// Substages of an active opening stage
+#[derive(Encode, Decode, Debug, Eq, PartialEq, Clone)]
+pub enum ActiveOpeningStage<BlockNumber> {
+    /// Active opening accepts application
+    AcceptingApplications {
+        ///Start accepting applications at block number
+        started_accepting_applicants_at_block: BlockNumber,
+    },
+
+    /// Active opening is in review period
+    ReviewPeriod {
+        /// Start accepting applications at block number
+        started_accepting_applicants_at_block: BlockNumber,
+        /// Start review application at block number
+        started_review_period_at_block: BlockNumber,
+    },
+
+    /// Active opening was deactivated
+    Deactivated {
+        /// Deactivation cause
+        cause: OpeningDeactivationCause,
+
+        /// Deactivated at block number
+        deactivated_at_block: BlockNumber,
+
+        /// Start accepting applications at block number
+        started_accepting_applicants_at_block: BlockNumber,
+
+        /// Whether the review period had ever been started, and if so, at what block.
+        /// Deactivation can also occur directly from the AcceptingApplications stage.
+        started_review_period_at_block: Option<BlockNumber>,
+    },
+}
+
+impl<BlockNumber: Clone> ActiveOpeningStage<BlockNumber> {
+    // Ensures that active opening stage is accepting applications.
+    pub(crate) fn ensure_active_opening_is_accepting_applications<Err>(
+        &self,
+        error: Err,
+    ) -> Result<BlockNumber, Err> {
+        if let ActiveOpeningStage::AcceptingApplications {
+            started_accepting_applicants_at_block,
+        } = self
+        {
+            return Ok(started_accepting_applicants_at_block.clone());
+        }
+
+        Err(error)
+    }
+
+    // Ensures that active opening stage is in review period.
+    pub(crate) fn ensure_active_opening_is_in_review_period<Err>(
+        &self,
+        error: Err,
+    ) -> Result<(BlockNumber, BlockNumber), Err> {
+        match self {
+            ActiveOpeningStage::ReviewPeriod {
+                started_accepting_applicants_at_block,
+                started_review_period_at_block,
+            } => Ok((
+                started_accepting_applicants_at_block.clone(),
+                started_review_period_at_block.clone(),
+            )), // <= need proper type here in the future, not param
+            _ => Err(error),
+        }
+    }
+
+    // Creates new active opening stage on cancel opening
+    pub(crate) fn new_stage_on_cancelling(
+        self,
+        current_block_height: BlockNumber,
+    ) -> Result<ActiveOpeningStage<BlockNumber>, CancelOpeningError> {
+        match self {
+            ActiveOpeningStage::AcceptingApplications {
+                started_accepting_applicants_at_block,
+            } => Ok(ActiveOpeningStage::Deactivated {
+                cause: OpeningDeactivationCause::CancelledAcceptingApplications,
+                deactivated_at_block: current_block_height,
+                started_accepting_applicants_at_block,
+                started_review_period_at_block: None,
+            }),
+            ActiveOpeningStage::ReviewPeriod {
+                started_accepting_applicants_at_block,
+                started_review_period_at_block,
+            } => Ok(ActiveOpeningStage::Deactivated {
+                cause: OpeningDeactivationCause::CancelledInReviewPeriod,
+                deactivated_at_block: current_block_height,
+                started_accepting_applicants_at_block,
+                started_review_period_at_block: Some(started_review_period_at_block),
+            }),
+            ActiveOpeningStage::Deactivated { .. } => {
+                Err(CancelOpeningError::OpeningNotInCancellableStage)
+            }
+        }
+    }
+}
+
+/// Opening deactivation cause
+#[derive(Encode, Decode, Debug, Eq, PartialEq, Clone)]
+pub enum OpeningDeactivationCause {
+    /// Opening was cancelled before activation
+    CancelledBeforeActivation,
+
+    /// Opening was cancelled during accepting application stage
+    CancelledAcceptingApplications,
+
+    /// Opening was cancelled during accepting application stage
+    CancelledInReviewPeriod,
+
+    /// Opening was cancelled after review period exprired
+    ReviewPeriodExpired,
+
+    /// Opening was filled
+    Filled,
+}
+
+/// Safe and explict way of chosing
+#[derive(Encode, Decode, Eq, PartialEq, Clone, Debug)]
+pub enum ActivateOpeningAt<BlockNumber> {
+    /// Activate opening now (current block)
+    CurrentBlock,
+
+    /// Activate opening at block number
+    ExactBlock(BlockNumber),
+}
+
+/// How to limit the number of eligible applicants
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Debug, Eq, PartialEq, Clone)]
+pub struct ApplicationRationingPolicy {
+    /// The maximum number of applications that can be on the list at any time.
+    pub max_active_applicants: u32,
+    // How applicants will be ranked, in order to respect the maximum simultaneous application limit
+    //pub applicant_ranking: ApplicationRankingPolicy
+}

+ 79 - 0
runtime-modules/hiring/src/hiring/staking_policy.rs

@@ -0,0 +1,79 @@
+use codec::{Decode, Encode};
+
+#[cfg(feature = "std")]
+use serde::{Deserialize, Serialize};
+
+/// Policy for staking
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Debug, Eq, PartialEq, Clone)]
+pub struct StakingPolicy<Balance, BlockNumber> {
+    /// Staking amount
+    pub amount: Balance,
+
+    /// How to interpret the amount requirement
+    pub amount_mode: StakingAmountLimitMode,
+
+    /// The unstaking period length, if any, deactivation causes that are autonomous,
+    /// that is they are triggered internally to this module.
+    pub crowded_out_unstaking_period_length: Option<BlockNumber>,
+
+    /// The review period length
+    pub review_period_expired_unstaking_period_length: Option<BlockNumber>,
+}
+
+impl<Balance: PartialOrd + Clone, BlockNumber: Clone> StakingPolicy<Balance, BlockNumber> {
+    pub(crate) fn accepts_amount(&self, test_amount: &Balance) -> bool {
+        match self.amount_mode {
+            StakingAmountLimitMode::AtLeast => *test_amount >= self.amount,
+            StakingAmountLimitMode::Exact => *test_amount == self.amount,
+        }
+    }
+
+    pub(crate) fn opt_staking_policy_to_review_period_expired_unstaking_period(
+        opt_staking_policy: &Option<StakingPolicy<Balance, BlockNumber>>,
+    ) -> Option<BlockNumber> {
+        if let Some(ref staking_policy) = opt_staking_policy {
+            staking_policy
+                .review_period_expired_unstaking_period_length
+                .clone()
+        } else {
+            None
+        }
+    }
+
+    pub(crate) fn opt_staking_policy_to_crowded_out_unstaking_period(
+        opt_staking_policy: &Option<StakingPolicy<Balance, BlockNumber>>,
+    ) -> Option<BlockNumber> {
+        if let Some(ref staking_policy) = opt_staking_policy {
+            staking_policy.crowded_out_unstaking_period_length.clone()
+        } else {
+            None
+        }
+    }
+
+    /// Ensures that optional staking policy prescribes value that clears minimum balance requirement
+    pub(crate) fn ensure_amount_valid_in_opt_staking_policy<Err>(
+        opt_staking_policy: Option<StakingPolicy<Balance, BlockNumber>>,
+        runtime_minimum_balance: Balance,
+        error: Err,
+    ) -> Result<(), Err> {
+        if let Some(ref staking_policy) = opt_staking_policy {
+            if staking_policy.amount < runtime_minimum_balance {
+                return Err(error);
+            }
+        }
+
+        Ok(())
+    }
+}
+
+/// Constraints around staking amount
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Debug, Eq, PartialEq, Clone)]
+pub enum StakingAmountLimitMode {
+    /// Stake should be equal or greater than provided value
+    AtLeast,
+
+    /// Stake should be equal to provided value
+    Exact,
+}

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

@@ -0,0 +1,1526 @@
+//! Hiring substrate module for the Joystream platform
+//!
+//! Public APIs:
+//! - add_opening
+//! - ensure_can_add_application
+//! - add_application
+//! - deactivate_application
+//! - cancel_opening
+//! - fill_opening
+//! - begin_review
+//! - begin_acception_application
+//! - unstaked
+//!
+//! Dependency: Joystream stake module
+
+// 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)]
+
+// Test feature dependencies
+#[cfg(all(any(test, feature = "test"), not(target_arch = "wasm32")))]
+use mockall::predicate::*;
+#[cfg(all(any(test, feature = "test"), not(target_arch = "wasm32")))]
+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};
+
+use srml_support::traits::Currency;
+use srml_support::{decl_module, decl_storage, ensure, Parameter};
+
+use rstd::collections::btree_map::BTreeMap;
+use rstd::collections::btree_set::BTreeSet;
+use rstd::iter::Iterator;
+use rstd::prelude::*;
+
+use rstd::cell::RefCell;
+use rstd::rc::Rc;
+
+use crate::sr_api_hidden_includes_decl_storage::hidden_include::traits::Imbalance;
+
+mod hiring;
+#[macro_use]
+mod macroes;
+mod mock;
+mod test;
+
+pub use hiring::*;
+use stake;
+
+/// Main trait of hiring substrate module
+pub trait Trait: system::Trait + stake::Trait + Sized {
+    /// OpeningId type
+    type OpeningId: Parameter
+        + Member
+        + SimpleArithmetic
+        + Codec
+        + Default
+        + Copy
+        + MaybeSerialize
+        + PartialEq;
+
+    /// ApplicationId type
+    type ApplicationId: Parameter
+        + Member
+        + SimpleArithmetic
+        + Codec
+        + Default
+        + Copy
+        + MaybeSerialize
+        + PartialEq;
+
+    /// Type that will handle various staking events
+    type ApplicationDeactivatedHandler: ApplicationDeactivatedHandler<Self>;
+
+    /// Marker type for Stake module handler. Indicates that hiring module uses stake module mock.
+    type StakeHandlerProvider: StakeHandlerProvider<Self>;
+}
+
+decl_storage! {
+    trait Store for Module<T: Trait> as Hiring {
+
+        /// Openings.
+        pub OpeningById get(opening_by_id): linked_map T::OpeningId => Opening<BalanceOf<T>, T::BlockNumber, T::ApplicationId>;
+
+        /// Identifier for next opening to be added.
+        pub NextOpeningId get(next_opening_id): T::OpeningId;
+
+        /// Applications
+        pub ApplicationById get(application_by_id): linked_map T::ApplicationId => Application<T::OpeningId, T::BlockNumber, T::StakeId>;
+
+        /// Identifier for next application to be added.
+        pub NextApplicationId get(next_application_id): T::ApplicationId;
+
+        /// Internal purpose of given stake, i.e. fro what application, and whether for the role or for the application.
+        pub ApplicationIdByStakingId get(stake_purpose_by_staking_id): linked_map T::StakeId => T::ApplicationId;
+    }
+}
+
+decl_module! {
+    /// Main hiring module definition
+    pub struct Module<T: Trait> for enum Call where origin: T::Origin {
+
+        fn on_finalize(now: T::BlockNumber) {
+
+            //
+            // == MUTATION SAFE ==
+            //
+
+            // Change opening from WaitingToBegin stage to Active::AcceptingApplications stage
+            for (opening_id, opening) in Self::openings_waiting_to_begin_iterator(now) {
+                let opening_accepting_applications = opening.clone_with_new_active_opening_stage(
+                    hiring::ActiveOpeningStage::AcceptingApplications {
+                        started_accepting_applicants_at_block: now
+                });
+
+                <OpeningById<T>>::insert(opening_id, opening_accepting_applications);
+            }
+
+            // Deactivate opening
+            for (opening_id,
+                opening,
+                (
+                    applications_added,
+                    started_accepting_applicants_at_block,
+                    started_review_period_at_block
+                )) in Self::openings_expired_review_period_iterator(now) {
+
+                //
+                // Deactivate all applications that are part of this opening
+                //
+
+                // Get unstaking periods
+                let application_stake_unstaking_period = StakingPolicy::opt_staking_policy_to_review_period_expired_unstaking_period(&opening.application_staking_policy);
+                let role_stake_unstaking_period = StakingPolicy::opt_staking_policy_to_review_period_expired_unstaking_period(&opening.role_staking_policy);
+
+                // Get applications
+                let applications_map = Self::application_id_iter_to_map(applications_added.iter());
+
+                // Deactivate applications
+                Self::initiate_application_deactivations(
+                    &applications_map,
+                    application_stake_unstaking_period,
+                    role_stake_unstaking_period,
+                    hiring::ApplicationDeactivationCause::ReviewPeriodExpired
+                );
+
+                let deactivated_opening =
+                    opening.clone_with_new_active_opening_stage(
+                        hiring::ActiveOpeningStage::Deactivated {
+                            cause: hiring::OpeningDeactivationCause::ReviewPeriodExpired,
+                            deactivated_at_block: now,
+                            started_accepting_applicants_at_block: started_accepting_applicants_at_block,
+                            started_review_period_at_block: Some(started_review_period_at_block),
+                    });
+
+                <OpeningById<T>>::insert(opening_id, deactivated_opening);
+            }
+        }
+    }
+}
+
+/*
+ *  ======== Main API implementation ========
+ */
+
+// Public API implementation
+impl<T: Trait> Module<T> {
+    /// Add new opening based on given inputs policies.
+    /// The new Opening instance has stage WaitingToBegin, and is added to openingsById,
+    /// and has identifier equal to nextOpeningId.
+    /// The latter is incremented. The used identifier is returned.
+    pub fn add_opening(
+        activate_at: ActivateOpeningAt<T::BlockNumber>,
+        max_review_period_length: T::BlockNumber,
+        application_rationing_policy: Option<ApplicationRationingPolicy>,
+        application_staking_policy: Option<StakingPolicy<BalanceOf<T>, T::BlockNumber>>,
+        role_staking_policy: Option<StakingPolicy<BalanceOf<T>, T::BlockNumber>>,
+        human_readable_text: Vec<u8>,
+    ) -> Result<T::OpeningId, AddOpeningError> {
+        let current_block_height = <system::Module<T>>::block_number();
+
+        Opening::<BalanceOf<T>, T::BlockNumber, T::ApplicationId>::ensure_can_add_opening(
+            current_block_height,
+            activate_at.clone(),
+            T::Currency::minimum_balance(),
+            application_rationing_policy.clone(),
+            application_staking_policy.clone(),
+            role_staking_policy.clone(),
+        )?;
+
+        // == MUTATION SAFE ==
+
+        let new_opening = hiring::Opening::new(
+            current_block_height,
+            activate_at,
+            max_review_period_length,
+            application_rationing_policy,
+            application_staking_policy,
+            role_staking_policy,
+            human_readable_text,
+        );
+
+        // Get Id for new opening
+        let new_opening_id = <NextOpeningId<T>>::get();
+
+        // Insert opening in storage
+        <OpeningById<T>>::insert(new_opening_id, new_opening);
+
+        // Update NextOpeningId counter
+        <NextOpeningId<T>>::mutate(|id| *id += T::OpeningId::one());
+
+        // Return
+        Ok(new_opening_id)
+    }
+
+    /// Cancels opening with given identifier, using provided unstaking periods for
+    /// application and role, as necesary.
+    pub fn cancel_opening(
+        opening_id: T::OpeningId,
+        application_stake_unstaking_period: Option<T::BlockNumber>,
+        role_stake_unstaking_period: Option<T::BlockNumber>,
+    ) -> Result<OpeningCancelled, CancelOpeningError> {
+        // Ensure that the opening exists
+        let opening =
+            ensure_opening_exists!(T, opening_id, CancelOpeningError::OpeningDoesNotExist)?;
+
+        // Opening is in stage Active.{AcceptingApplications or ReviewPeriod}
+
+        let (
+            active_stage,
+            applications_added,
+            active_application_count,
+            unstaking_application_count,
+            deactivated_application_count,
+        ) = ensure_opening_is_active!(
+            opening.stage,
+            CancelOpeningError::OpeningNotInCancellableStage
+        )?;
+
+        //
+        let current_block_height = <system::Module<T>>::block_number(); // move later!
+        let new_active_stage = active_stage.new_stage_on_cancelling(current_block_height)?;
+
+        // Ensure unstaking periods are OK.
+        ensure_opt_unstaking_period_is_ok!(
+            application_stake_unstaking_period,
+            opening.application_staking_policy,
+            CancelOpeningError::UnstakingPeriodTooShort(StakePurpose::Application),
+            CancelOpeningError::RedundantUnstakingPeriodProvided(StakePurpose::Application)
+        )?;
+
+        ensure_opt_unstaking_period_is_ok!(
+            role_stake_unstaking_period,
+            opening.role_staking_policy,
+            CancelOpeningError::UnstakingPeriodTooShort(StakePurpose::Role),
+            CancelOpeningError::RedundantUnstakingPeriodProvided(StakePurpose::Role)
+        )?;
+
+        //
+        // == MUTATION SAFE ==
+        //
+
+        // Create and store new cancelled opening
+        let new_opening = Opening {
+            stage: hiring::OpeningStage::Active {
+                stage: new_active_stage,
+                applications_added: applications_added.clone(),
+                active_application_count,
+                unstaking_application_count,
+                deactivated_application_count,
+            },
+            ..opening
+        };
+
+        OpeningById::<T>::insert(opening_id, new_opening);
+
+        // Map with applications
+        let applications_map = Self::application_id_iter_to_map(applications_added.iter());
+
+        // Initiate deactivation of all active applications
+        let net_result = Self::initiate_application_deactivations(
+            &applications_map,
+            application_stake_unstaking_period,
+            role_stake_unstaking_period,
+            hiring::ApplicationDeactivationCause::OpeningCancelled,
+        );
+
+        // Return
+        Ok(OpeningCancelled {
+            number_of_unstaking_applications: net_result.number_of_unstaking_applications,
+            number_of_deactivated_applications: net_result.number_of_deactivated_applications,
+        })
+    }
+
+    /// Transit opening to the accepting application stage.
+    /// Applies when given opening is in WaitingToBegin stage.
+    /// The stage is updated to Active stage with AcceptingApplications substage
+    pub fn begin_accepting_applications(
+        opening_id: T::OpeningId,
+    ) -> Result<(), BeginAcceptingApplicationsError> {
+        // Ensure that the opening exists
+        let opening = ensure_opening_exists!(
+            T,
+            opening_id,
+            BeginAcceptingApplicationsError::OpeningDoesNotExist
+        )?;
+
+        // Ensure that it is the waiting to begin stage
+        opening.stage.ensure_opening_stage_is_waiting_to_begin(
+            BeginAcceptingApplicationsError::OpeningIsNotInWaitingToBeginStage,
+        )?;
+
+        //
+        // == MUTATION SAFE ==
+        //
+
+        let current_block_height = <system::Module<T>>::block_number();
+
+        // Update state of opening
+        let new_opening = opening.clone_with_new_active_opening_stage(
+            hiring::ActiveOpeningStage::AcceptingApplications {
+                started_accepting_applicants_at_block: current_block_height,
+            },
+        );
+
+        // Write back opening
+        <OpeningById<T>>::insert(opening_id, new_opening);
+
+        // DONE
+        Ok(())
+    }
+
+    /// Transit opening to the begin review period stage.
+    /// Applies when given opening is in Active stage and AcceptingApplications substage.
+    /// The stage is updated to Active stage and ReviewPeriod substage
+    pub fn begin_review(opening_id: T::OpeningId) -> Result<(), BeginReviewError> {
+        // Ensure that the opening exists
+        let opening = ensure_opening_exists!(T, opening_id, BeginReviewError::OpeningDoesNotExist)?;
+
+        // Opening is accepting applications
+        let (active_stage, _, _, _, _) = ensure_opening_is_active!(
+            opening.stage,
+            BeginReviewError::OpeningNotInAcceptingApplicationsStage
+        )?;
+
+        let started_accepting_applicants_at_block = ensure_active_opening_is_accepting_applications!(
+            active_stage,
+            BeginReviewError::OpeningNotInAcceptingApplicationsStage
+        )?;
+
+        //
+        // == MUTATION SAFE ==
+        //
+
+        let current_block_height = <system::Module<T>>::block_number();
+
+        let new_opening =
+            opening.clone_with_new_active_opening_stage(hiring::ActiveOpeningStage::ReviewPeriod {
+                started_accepting_applicants_at_block,
+                started_review_period_at_block: current_block_height,
+            });
+
+        // Update to new opening
+        <OpeningById<T>>::insert(opening_id, new_opening);
+
+        Ok(())
+    }
+
+    /// Fill an opening, identified with `opening_id`, currently in the review period.
+    /// Applies when given opening is in ReviewPeriod stage.
+    /// Given list of applications are deactivated to under the Hired,
+    /// all other active applicants are NotHired.
+    /// Separately for each group,
+    /// unstaking periods for any applicable application and/or role stake must be provided.
+    pub fn fill_opening(
+        opening_id: T::OpeningId,
+        successful_applications: BTreeSet<T::ApplicationId>,
+        opt_successful_applicant_application_stake_unstaking_period: Option<T::BlockNumber>,
+        opt_failed_applicant_application_stake_unstaking_period: Option<T::BlockNumber>,
+        /* this parameter does not make sense? opt_successful_applicant_role_stake_unstaking_period: Option<T::BlockNumber>, */
+        opt_failed_applicant_role_stake_unstaking_period: Option<T::BlockNumber>,
+    ) -> Result<(), FillOpeningError<T>> {
+        // Ensure that the opening exists
+        let opening = ensure_opening_exists!(T, opening_id, FillOpeningError::OpeningDoesNotExist)?;
+
+        let (active_stage, applications_added, _, _, _) = ensure_opening_is_active!(
+            opening.stage,
+            FillOpeningError::OpeningNotInReviewPeriodStage
+        )?;
+
+        // Ensure opening is in review period
+        let (started_accepting_applicants_at_block, started_review_period_at_block) = active_stage
+            .ensure_active_opening_is_in_review_period(
+                FillOpeningError::OpeningNotInReviewPeriodStage,
+            )?;
+
+        //
+        // Ensure that all unstaking periods are neither too short (0) nor redundant.
+        //
+
+        ensure_opt_unstaking_period_is_ok!(
+            opt_successful_applicant_application_stake_unstaking_period,
+            opening.application_staking_policy,
+            FillOpeningError::UnstakingPeriodTooShort(
+                StakePurpose::Application,
+                ApplicationOutcomeInFilledOpening::Success
+            ),
+            FillOpeningError::RedundantUnstakingPeriodProvided(
+                StakePurpose::Application,
+                ApplicationOutcomeInFilledOpening::Success
+            )
+        )?;
+
+        ensure_opt_unstaking_period_is_ok!(
+            opt_failed_applicant_application_stake_unstaking_period,
+            opening.application_staking_policy,
+            FillOpeningError::UnstakingPeriodTooShort(
+                StakePurpose::Application,
+                ApplicationOutcomeInFilledOpening::Failure
+            ),
+            FillOpeningError::RedundantUnstakingPeriodProvided(
+                StakePurpose::Application,
+                ApplicationOutcomeInFilledOpening::Failure
+            )
+        )?;
+
+        ensure_opt_unstaking_period_is_ok!(
+            opt_failed_applicant_role_stake_unstaking_period,
+            opening.role_staking_policy,
+            FillOpeningError::UnstakingPeriodTooShort(
+                StakePurpose::Role,
+                ApplicationOutcomeInFilledOpening::Failure
+            ),
+            FillOpeningError::RedundantUnstakingPeriodProvided(
+                StakePurpose::Role,
+                ApplicationOutcomeInFilledOpening::Failure
+            )
+        )?;
+
+        // Ensure that all successful applications actually exist
+        for application_id in &successful_applications {
+            ensure_application_exists!(
+                T,
+                *application_id,
+                FillOpeningError::ApplicationDoesNotExist(*application_id)
+            )?;
+        }
+
+        let successful_applications_map =
+            Self::application_id_iter_to_map(successful_applications.iter());
+
+        // Ensure that all successful applications are actually active and associated with the opening
+        for (application_id, application) in &successful_applications_map {
+            ensure_eq!(
+                application.stage,
+                hiring::ApplicationStage::Active,
+                FillOpeningError::ApplicationNotInActiveStage(*application_id,)
+            );
+
+            ensure_eq!(
+                application.opening_id,
+                opening_id,
+                FillOpeningError::ApplicationForWrongOpening(*application_id)
+            );
+        }
+
+        //
+        // == MUTATION SAFE ==
+        //
+
+        // Deactivate all successful applications, with cause being hired
+        Self::initiate_application_deactivations(
+            &successful_applications_map,
+            opt_successful_applicant_application_stake_unstaking_period,
+            None,
+            hiring::ApplicationDeactivationCause::Hired,
+        );
+
+        // Deactivate all unsuccessful applications, with cause being not being hired.
+
+        // First get all failed applications by their id.
+        let failed_applications_map = Self::application_id_iter_to_map(
+            applications_added.difference(&successful_applications),
+        );
+
+        // Deactivate all successful applications, with cause being not hired
+        Self::initiate_application_deactivations(
+            &failed_applications_map,
+            opt_failed_applicant_application_stake_unstaking_period,
+            opt_failed_applicant_role_stake_unstaking_period,
+            hiring::ApplicationDeactivationCause::NotHired,
+        );
+
+        // Grab current block height
+        let current_block_height = <system::Module<T>>::block_number();
+        // Get opening with updated counters
+        let opening_needed_for_data = <OpeningById<T>>::get(opening_id);
+
+        // Deactivate opening
+        let new_opening = opening_needed_for_data.clone_with_new_active_opening_stage(
+            hiring::ActiveOpeningStage::Deactivated {
+                cause: OpeningDeactivationCause::Filled,
+                deactivated_at_block: current_block_height,
+                started_accepting_applicants_at_block,
+                started_review_period_at_block: Some(started_review_period_at_block),
+            },
+        );
+
+        // Write back new opening
+        <OpeningById<T>>::insert(opening_id, new_opening);
+
+        // DONE
+        Ok(())
+    }
+
+    /// Adds a new application on the given opening, and begins staking for
+    /// the role, the application or both possibly.
+    pub fn ensure_can_add_application(
+        opening_id: T::OpeningId,
+        opt_role_stake_balance: Option<BalanceOf<T>>,
+        opt_application_stake_balance: Option<BalanceOf<T>>,
+    ) -> Result<DestructuredApplicationCanBeAddedEvaluation<T>, AddApplicationError> {
+        // Ensure that the opening exists
+        let opening =
+            ensure_opening_exists!(T, opening_id, AddApplicationError::OpeningDoesNotExist)?;
+
+        // Ensure that proposed stakes match the policy of the opening.
+        let opt_role_stake_balance = ensure_stake_balance_matches_staking_policy!(
+            &opt_role_stake_balance,
+            &opening.role_staking_policy,
+            AddApplicationError::StakeMissingWhenRequired(StakePurpose::Role),
+            AddApplicationError::StakeProvidedWhenRedundant(StakePurpose::Role),
+            AddApplicationError::StakeAmountTooLow(StakePurpose::Role)
+        )?;
+
+        let opt_application_stake_balance = ensure_stake_balance_matches_staking_policy!(
+            &opt_application_stake_balance,
+            &opening.application_staking_policy,
+            AddApplicationError::StakeMissingWhenRequired(StakePurpose::Application),
+            AddApplicationError::StakeProvidedWhenRedundant(StakePurpose::Application),
+            AddApplicationError::StakeAmountTooLow(StakePurpose::Application)
+        )?;
+
+        // Opening is accepting applications
+
+        let (
+            active_stage,
+            applications_added,
+            active_application_count,
+            unstaking_application_count,
+            deactivated_application_count,
+        ) = ensure_opening_is_active!(
+            opening.stage,
+            AddApplicationError::OpeningNotInAcceptingApplicationsStage
+        )?;
+
+        active_stage.ensure_active_opening_is_accepting_applications(
+            AddApplicationError::OpeningNotInAcceptingApplicationsStage,
+        )?;
+
+        // Ensure that the new application would actually make it
+        let would_get_added_success = ensure_application_would_get_added!(
+            &opening.application_rationing_policy,
+            &applications_added,
+            &opt_role_stake_balance,
+            &opt_application_stake_balance,
+            AddApplicationError::NewApplicationWasCrowdedOut
+        )?;
+
+        Ok(DestructuredApplicationCanBeAddedEvaluation {
+            opening,
+            active_stage,
+            applications_added,
+            active_application_count,
+            unstaking_application_count,
+            deactivated_application_count,
+            would_get_added_success,
+        })
+    }
+
+    /// Adds a new application on the given opening, and begins staking for
+    /// the role, the application or both possibly.
+    pub fn add_application(
+        opening_id: T::OpeningId,
+        opt_role_stake_imbalance: Option<NegativeImbalance<T>>,
+        opt_application_stake_imbalance: Option<NegativeImbalance<T>>,
+        human_readable_text: Vec<u8>,
+    ) -> Result<ApplicationAdded<T::ApplicationId>, AddApplicationError> {
+        let opt_role_stake_balance = Self::create_stake_balance(&opt_role_stake_imbalance);
+        let opt_application_stake_balance =
+            Self::create_stake_balance(&opt_application_stake_imbalance);
+
+        let can_be_added_destructured = Self::ensure_can_add_application(
+            opening_id,
+            opt_role_stake_balance,
+            opt_application_stake_balance,
+        )?;
+
+        //
+        // == MUTATION SAFE ==
+        //
+
+        // If required, deactive another application that was crowded out.
+        if let ApplicationAddedSuccess::CrowdsOutExistingApplication(
+            id_of_croweded_out_application,
+        ) = can_be_added_destructured.would_get_added_success
+        {
+            // Get relevant unstaking periods
+            let opt_application_stake_unstaking_period =
+                hiring::StakingPolicy::opt_staking_policy_to_crowded_out_unstaking_period(
+                    &can_be_added_destructured.opening.application_staking_policy,
+                );
+            let opt_role_stake_unstaking_period =
+                hiring::StakingPolicy::opt_staking_policy_to_crowded_out_unstaking_period(
+                    &can_be_added_destructured.opening.role_staking_policy,
+                );
+
+            // Fetch application
+            let crowded_out_application = <ApplicationById<T>>::get(id_of_croweded_out_application);
+
+            // Initiate actual deactivation
+            //
+            // MUST not have been ignored, is runtime invariant, false means code is broken.
+            // But should we do panic in runtime? Is there safer way?
+            let deactivation_result = Self::try_to_initiate_application_deactivation(
+                &crowded_out_application,
+                id_of_croweded_out_application,
+                opt_application_stake_unstaking_period,
+                opt_role_stake_unstaking_period,
+                hiring::ApplicationDeactivationCause::CrowdedOut,
+            );
+
+            assert_ne!(
+                deactivation_result,
+                ApplicationDeactivationInitiationResult::Ignored
+            );
+        }
+
+        // Get Id for this new application
+        let new_application_id = <NextApplicationId<T>>::get();
+
+        // Possibly initiate staking
+        let active_role_staking_id =
+            Self::infallible_opt_stake_initiation(opt_role_stake_imbalance, &new_application_id);
+        let active_application_staking_id = Self::infallible_opt_stake_initiation(
+            opt_application_stake_imbalance,
+            &new_application_id,
+        );
+
+        // Grab current block height
+        let current_block_height = <system::Module<T>>::block_number();
+
+        // Compute index for this new application
+        let application_index_in_opening =
+            can_be_added_destructured.calculate_total_application_count();
+
+        // Create a new application
+        let new_application = hiring::Application {
+            opening_id,
+            application_index_in_opening,
+            add_to_opening_in_block: current_block_height,
+            active_role_staking_id,
+            active_application_staking_id,
+            // Stage of new application
+            stage: hiring::ApplicationStage::Active,
+            human_readable_text,
+        };
+
+        // Insert into main application map
+        <ApplicationById<T>>::insert(new_application_id, new_application);
+
+        // Update next application id
+        <NextApplicationId<T>>::mutate(|id| *id += One::one());
+
+        // Update counter on opening
+        // Should reload after possible deactivation in try_to_initiate_application_deactivation
+        let opening_needed_for_data = <OpeningById<T>>::get(opening_id);
+        let new_active_stage = opening_needed_for_data
+            .stage
+            .clone_with_added_active_application(new_application_id);
+
+        <OpeningById<T>>::mutate(opening_id, |opening| {
+            opening.stage = new_active_stage;
+        });
+
+        let application_id_crowded_out = can_be_added_destructured
+            .would_get_added_success
+            .crowded_out_application_id();
+
+        // DONE
+        Ok(ApplicationAdded {
+            application_id_added: new_application_id,
+            application_id_crowded_out,
+        })
+    }
+
+    /// Deactive an active application.
+    /// Does currently not support slashing
+    pub fn deactive_application(
+        application_id: T::ApplicationId,
+        application_stake_unstaking_period: Option<T::BlockNumber>,
+        role_stake_unstaking_period: Option<T::BlockNumber>,
+    ) -> Result<(), DeactivateApplicationError> {
+        // Check that application id is valid, and if so,
+        // grab corresponding application and opening.
+        let (application, opening) = ensure_application_exists!(
+            T,
+            application_id,
+            DeactivateApplicationError::ApplicationDoesNotExist,
+            auto_fetch_opening
+        )?;
+
+        // Application is active
+        ensure_eq!(
+            application.stage,
+            hiring::ApplicationStage::Active,
+            DeactivateApplicationError::ApplicationNotActive
+        );
+
+        // Opening is accepting applications
+        let (active_stage, ..) = ensure_opening_is_active!(
+            opening.stage,
+            DeactivateApplicationError::OpeningNotAcceptingApplications
+        )?;
+
+        active_stage.ensure_active_opening_is_accepting_applications(
+            DeactivateApplicationError::OpeningNotAcceptingApplications,
+        )?;
+
+        // Ensure unstaking periods are OK.
+        ensure_opt_unstaking_period_is_ok!(
+            application_stake_unstaking_period,
+            opening.application_staking_policy,
+            DeactivateApplicationError::UnstakingPeriodTooShort(StakePurpose::Application),
+            DeactivateApplicationError::RedundantUnstakingPeriodProvided(StakePurpose::Application)
+        )?;
+
+        ensure_opt_unstaking_period_is_ok!(
+            role_stake_unstaking_period,
+            opening.role_staking_policy,
+            DeactivateApplicationError::UnstakingPeriodTooShort(StakePurpose::Role),
+            DeactivateApplicationError::RedundantUnstakingPeriodProvided(StakePurpose::Role)
+        )?;
+
+        //
+        // == MUTATION SAFE ==
+        //
+
+        // Deactive application
+        let result = Self::try_to_initiate_application_deactivation(
+            &application,
+            application_id,
+            application_stake_unstaking_period,
+            role_stake_unstaking_period,
+            hiring::ApplicationDeactivationCause::External,
+        );
+
+        assert_ne!(result, ApplicationDeactivationInitiationResult::Ignored);
+
+        // DONE
+        Ok(())
+    }
+
+    /// The stake, with the given id, was unstaked.
+    pub fn unstaked(stake_id: T::StakeId) -> UnstakedResult {
+        // Ignore unstaked
+        if !<ApplicationIdByStakingId<T>>::exists(stake_id) {
+            return UnstakedResult::StakeIdNonExistent;
+        }
+
+        // Get application
+        let application_id = <ApplicationIdByStakingId<T>>::get(stake_id);
+
+        assert!(<ApplicationById<T>>::exists(application_id));
+
+        let application = <ApplicationById<T>>::get(application_id);
+
+        // Make sure that we are actually unstaking, ignore otherwise.
+        let (deactivation_initiated, cause) = if let ApplicationStage::Unstaking {
+            deactivation_initiated,
+            cause,
+        } = application.stage
+        {
+            (deactivation_initiated, cause)
+        } else {
+            return UnstakedResult::ApplicationIsNotUnstaking;
+        };
+
+        //
+        // == MUTATION SAFE ==
+        //
+
+        // Drop stake from stake to application map
+        <ApplicationIdByStakingId<T>>::remove(stake_id);
+        let current_block_height = <system::Module<T>>::block_number();
+
+        // New application computed
+        let mut new_application = application.clone();
+        let is_now_done_unstaking = new_application.unstake_application(
+            current_block_height,
+            deactivation_initiated,
+            cause,
+            stake_id,
+        );
+
+        // Update to new application
+        <ApplicationById<T>>::insert(&application_id, new_application);
+
+        // If the application is now finished compeleting any pending unstaking process,
+        // then we need to update the opening counters, and make the deactivation callback.
+        if is_now_done_unstaking {
+            // Update Opening
+            // We know the stage MUST be active, hence mutate is certain.
+            <OpeningById<T>>::mutate(application.opening_id, |opening| {
+                opening.change_opening_stage_after_application_unstaked();
+            });
+
+            // Call handler
+            T::ApplicationDeactivatedHandler::deactivated(&application_id, cause);
+            return UnstakedResult::Unstaked;
+        }
+
+        UnstakedResult::UnstakingInProgress
+    }
+}
+
+/*
+ *  === Application Deactivated Handler  ======
+ */
+
+/// Handles application deactivation with a cause
+pub trait ApplicationDeactivatedHandler<T: Trait> {
+    /// An application, with the given id, was fully deactivated, with the
+    /// given cause, and was put in the inactive state.
+    fn deactivated(application_id: &T::ApplicationId, cause: hiring::ApplicationDeactivationCause);
+}
+
+/// Helper implementation so we can provide multiple handlers by grouping handlers in tuple pairs.
+/// For example for three handlers, A, B and C we can set the StakingEventHandler type on the trait to:
+/// type StakingEventHandler = ((A, B), C)
+impl<T: Trait> ApplicationDeactivatedHandler<T> for () {
+    fn deactivated(
+        _application_id: &T::ApplicationId,
+        _cause: hiring::ApplicationDeactivationCause,
+    ) {
+    }
+}
+
+/*
+ *  ======== API types bound to the Trait ========
+ */
+
+/// Error due to attempting to fill an opening.
+#[derive(Eq, PartialEq, Clone, Debug)]
+pub enum FillOpeningError<T: Trait> {
+    /// Opening does not exist
+    OpeningDoesNotExist,
+
+    /// Opening is not in review period
+    OpeningNotInReviewPeriodStage,
+
+    /// Provided unstaking period is too short
+    UnstakingPeriodTooShort(StakePurpose, ApplicationOutcomeInFilledOpening),
+
+    /// Provided redundant unstaking period
+    RedundantUnstakingPeriodProvided(StakePurpose, ApplicationOutcomeInFilledOpening),
+
+    /// Application does not exist
+    ApplicationDoesNotExist(T::ApplicationId),
+
+    /// Application is not in active stage
+    ApplicationNotInActiveStage(T::ApplicationId),
+
+    /// Application is not for the opening
+    ApplicationForWrongOpening(T::ApplicationId),
+}
+
+/// Product of ensure_can_add_application()
+#[derive(Eq, PartialEq, Clone, Debug)]
+pub struct DestructuredApplicationCanBeAddedEvaluation<T: Trait> {
+    /// Opening object
+    pub opening: Opening<BalanceOf<T>, T::BlockNumber, T::ApplicationId>,
+
+    /// Active opening stage
+    pub active_stage: ActiveOpeningStage<T::BlockNumber>,
+
+    /// Collection of added applicaiton ids
+    pub applications_added: BTreeSet<T::ApplicationId>,
+
+    /// Active applications counter
+    pub active_application_count: u32,
+
+    /// Unstaking applications counter
+    pub unstaking_application_count: u32,
+
+    /// Deactivated applications counter
+    pub deactivated_application_count: u32,
+
+    /// Prospects of application adding
+    pub would_get_added_success: ApplicationAddedSuccess<T>,
+}
+
+impl<T: Trait> DestructuredApplicationCanBeAddedEvaluation<T> {
+    pub(crate) fn calculate_total_application_count(&self) -> u32 {
+        // TODO: fix so that `number_of_appliations_ever_added` can be invoked.
+        // cant do this due to bad design of stage => opening.stage.number_of_appliations_ever_added();
+        self.active_application_count
+            + self.unstaking_application_count
+            + self.deactivated_application_count
+    }
+}
+
+/// Adding application result
+#[derive(Eq, PartialEq, Clone, Debug)]
+pub enum ApplicationAddedSuccess<T: Trait> {
+    /// Application was added without side-effects
+    Unconditionally,
+
+    /// Application has crowded out existing application
+    CrowdsOutExistingApplication(T::ApplicationId),
+}
+
+impl<T: Trait> ApplicationAddedSuccess<T> {
+    pub(crate) fn crowded_out_application_id(&self) -> Option<T::ApplicationId> {
+        if let ApplicationAddedSuccess::CrowdsOutExistingApplication(id) = self {
+            Some(*id)
+        } else {
+            None
+        }
+    }
+}
+
+/// Prospects of application. Whether it would be added to the opening.
+#[derive(Eq, PartialEq, Clone, Debug)]
+pub enum ApplicationWouldGetAddedEvaluation<T: Trait> {
+    /// Negative prospects
+    No,
+
+    /// Positive prospects
+    Yes(ApplicationAddedSuccess<T>),
+}
+
+/// 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;
+
+/*
+ *  ======== ======== ======== ======== =======
+ *  ======== PRIVATE TYPES AND METHODS ========
+ *  ======== ======== ======== ======== =======
+ */
+
+#[derive(PartialEq, Debug, Clone)]
+struct ApplicationsDeactivationsInitiationResult {
+    number_of_unstaking_applications: u32,
+    number_of_deactivated_applications: u32,
+}
+
+type ApplicationBTreeMap<T> = BTreeMap<
+    <T as Trait>::ApplicationId,
+    hiring::Application<
+        <T as Trait>::OpeningId,
+        <T as system::Trait>::BlockNumber,
+        <T as stake::Trait>::StakeId,
+    >,
+>;
+
+#[derive(PartialEq, Debug, Clone)]
+enum ApplicationDeactivationInitiationResult {
+    Ignored, // <= is there a case for kicking this out, making sure that initiation cannot happen when it may fail?
+    Unstaking,
+    Deactivated,
+}
+
+// Opening and application iterators
+impl<T: Trait> Module<T> {
+    // Iterate through ApplicationById map
+    fn application_id_iter_to_map<'a>(
+        application_id_iter: impl Iterator<Item = &'a T::ApplicationId>,
+    ) -> ApplicationBTreeMap<T> {
+        application_id_iter
+            .map(|application_id| {
+                let application = <ApplicationById<T>>::get(application_id);
+
+                (*application_id, application)
+            })
+            .collect::<BTreeMap<_, _>>()
+    }
+
+    // Compute iterator of openings waiting to begin
+    fn openings_waiting_to_begin_iterator(
+        now: T::BlockNumber,
+    ) -> impl Iterator<
+        Item = (
+            T::OpeningId,
+            Opening<BalanceOf<T>, T::BlockNumber, T::ApplicationId>,
+        ),
+    > {
+        <OpeningById<T>>::enumerate().filter_map(move |(opening_id, opening)| {
+            if let hiring::OpeningStage::WaitingToBegin { begins_at_block } = opening.stage {
+                if begins_at_block == now {
+                    Some((opening_id, opening))
+                } else {
+                    None
+                }
+            } else {
+                None
+            }
+        })
+    }
+
+    // Compute iterator of openings in expired review period
+    fn openings_expired_review_period_iterator(
+        now: T::BlockNumber,
+    ) -> impl Iterator<
+        Item = (
+            T::OpeningId,
+            Opening<BalanceOf<T>, T::BlockNumber, T::ApplicationId>,
+            (BTreeSet<T::ApplicationId>, T::BlockNumber, T::BlockNumber),
+        ),
+    > {
+        <OpeningById<T>>::enumerate().filter_map(move |(opening_id, opening)| {
+            if let hiring::OpeningStage::Active {
+                ref stage,
+                ref applications_added,
+                ..
+            } = opening.stage
+            {
+                if let hiring::ActiveOpeningStage::ReviewPeriod {
+                    ref started_accepting_applicants_at_block,
+                    ref started_review_period_at_block,
+                } = stage
+                {
+                    if now == opening.max_review_period_length + *started_review_period_at_block {
+                        Some((
+                            opening_id,
+                            opening.clone(),
+                            (
+                                applications_added.clone(),
+                                *started_accepting_applicants_at_block,
+                                *started_review_period_at_block,
+                            ),
+                        ))
+                    } else {
+                        None
+                    }
+                } else {
+                    None
+                }
+            } else {
+                None
+            }
+        })
+    }
+}
+
+// Application deactivation logic methods.
+impl<T: Trait> Module<T> {
+    fn initiate_application_deactivations(
+        applications: &ApplicationBTreeMap<T>,
+        application_stake_unstaking_period: Option<T::BlockNumber>,
+        role_stake_unstaking_period: Option<T::BlockNumber>,
+        cause: ApplicationDeactivationCause,
+    ) -> ApplicationsDeactivationsInitiationResult {
+        // Update stage on active applications, and collect result
+
+        applications
+            .iter()
+            .map(
+                |(application_id, application)| -> ApplicationDeactivationInitiationResult {
+                    // Initiate deactivations!
+                    Self::try_to_initiate_application_deactivation(
+                        application,
+                        *application_id,
+                        application_stake_unstaking_period,
+                        role_stake_unstaking_period,
+                        cause,
+                    )
+                },
+            )
+            .fold(
+                // Initiatial reducer value
+                ApplicationsDeactivationsInitiationResult {
+                    number_of_unstaking_applications: 0,
+                    number_of_deactivated_applications: 0,
+                },
+                |acc, deactivation_result| {
+                    // Update accumulator counters based on what actually happened
+                    match deactivation_result {
+                        ApplicationDeactivationInitiationResult::Ignored => acc,
+
+                        ApplicationDeactivationInitiationResult::Unstaking => {
+                            ApplicationsDeactivationsInitiationResult {
+                                number_of_unstaking_applications: 1 + acc
+                                    .number_of_unstaking_applications,
+                                number_of_deactivated_applications: acc
+                                    .number_of_deactivated_applications,
+                            }
+                        }
+
+                        ApplicationDeactivationInitiationResult::Deactivated => {
+                            ApplicationsDeactivationsInitiationResult {
+                                number_of_unstaking_applications: acc
+                                    .number_of_unstaking_applications,
+                                number_of_deactivated_applications: 1 + acc
+                                    .number_of_deactivated_applications,
+                            }
+                        }
+                    }
+                },
+            )
+    }
+
+    /// Initiates
+    fn try_to_initiate_application_deactivation(
+        application: &Application<T::OpeningId, T::BlockNumber, T::StakeId>,
+        application_id: T::ApplicationId,
+        application_stake_unstaking_period: Option<T::BlockNumber>,
+        role_stake_unstaking_period: Option<T::BlockNumber>,
+        cause: hiring::ApplicationDeactivationCause,
+    ) -> ApplicationDeactivationInitiationResult {
+        match application.stage {
+            ApplicationStage::Active => {
+                // Initiate unstaking of any active application stake
+                let application_was_unstaked = Self::opt_infallible_unstake(
+                    application.active_application_staking_id,
+                    application_stake_unstaking_period,
+                );
+
+                // Only unstake role stake for a non successful result ie. not Hired
+                let role_was_unstaked = cause != hiring::ApplicationDeactivationCause::Hired
+                    && Self::opt_infallible_unstake(
+                        application.active_role_staking_id,
+                        role_stake_unstaking_period,
+                    );
+
+                // Capture if any unstaking occured at all
+                let was_unstaked = application_was_unstaked || role_was_unstaked;
+
+                // Grab current block height
+                let current_block_height = <system::Module<T>>::block_number();
+
+                /*
+                 * TODO:
+                 * There should be a single transformation based on
+                 * was_unstaked which renders a new value for `application.stage`
+                 * and `opening.stage`, which guarantees to only produces new values
+                 * for given variant values, but the state machine types are currently
+                 * not well organised to support this.
+                 *
+                 * Likewise the construction of hiring::OpeningStage::Active below
+                 * is a wreck because of this.
+                 *
+                 * Issue: https://github.com/Joystream/joystream/issues/36#issuecomment-539567407
+                 */
+
+                // Figure out new stage for the application
+                let new_application_stage = if was_unstaked {
+                    ApplicationStage::Unstaking {
+                        deactivation_initiated: current_block_height,
+                        cause,
+                    }
+                } else {
+                    ApplicationStage::Inactive {
+                        deactivation_initiated: current_block_height,
+                        deactivated: current_block_height,
+                        cause,
+                    }
+                };
+
+                // Update the application stage
+                <ApplicationById<T>>::mutate(application_id, |application| {
+                    application.stage = new_application_stage;
+                });
+
+                // Update counters on opening
+                <OpeningById<T>>::mutate(application.opening_id, |opening| {
+                    // NB: This ugly byref destructuring is same issue as pointed out multiple times now.
+                    if let hiring::OpeningStage::Active {
+                        ref stage,
+                        ref applications_added,
+                        ref active_application_count,
+                        ref unstaking_application_count,
+                        ref deactivated_application_count,
+                    } = opening.stage
+                    {
+                        assert!(*active_application_count > 0);
+
+                        let new_active_application_count = active_application_count - 1;
+
+                        let new_unstaking_application_count =
+                            unstaking_application_count + if was_unstaked { 1 } else { 0 };
+
+                        let new_deactivated_application_count =
+                            deactivated_application_count + if was_unstaked { 0 } else { 1 };
+
+                        opening.stage = hiring::OpeningStage::Active {
+                            stage: stage.clone(),
+                            applications_added: applications_added.clone(),
+                            active_application_count: new_active_application_count,
+                            unstaking_application_count: new_unstaking_application_count,
+                            deactivated_application_count: new_deactivated_application_count,
+                        };
+                    } else {
+                        panic!("opening stage must be 'Active'");
+                    }
+                });
+
+                // Call handler(s)
+                if was_unstaked {
+                    T::ApplicationDeactivatedHandler::deactivated(&application_id, cause);
+                }
+
+                // Return conclusion
+                if was_unstaked {
+                    ApplicationDeactivationInitiationResult::Unstaking
+                } else {
+                    ApplicationDeactivationInitiationResult::Deactivated
+                }
+            }
+            _ => ApplicationDeactivationInitiationResult::Ignored,
+        }
+    }
+
+    /// Tries to unstake, based on a stake id which, if set, MUST
+    /// be ready to be unstaked, with an optional unstaking period.
+    ///
+    /// Returns whether unstaking was actually initiated.
+    fn opt_infallible_unstake(
+        opt_stake_id: Option<T::StakeId>,
+        opt_unstaking_period: Option<T::BlockNumber>,
+    ) -> bool {
+        if let Some(stake_id) = opt_stake_id {
+            // `initiate_unstaking` MUST hold, is runtime invariant, false means code is broken.
+            // But should we do panic in runtime? Is there safer way?
+
+            assert!(T::StakeHandlerProvider::staking()
+                .initiate_unstaking(&stake_id, opt_unstaking_period)
+                .is_ok());
+        }
+
+        opt_stake_id.is_some()
+    }
+}
+
+// Stake initiation
+impl<T: Trait> Module<T> {
+    fn infallible_opt_stake_initiation(
+        opt_imbalance: Option<NegativeImbalance<T>>,
+        application_id: &T::ApplicationId,
+    ) -> Option<T::StakeId> {
+        if let Some(imbalance) = opt_imbalance {
+            Some(Self::infallible_stake_initiation_on_application(
+                imbalance,
+                application_id,
+            ))
+        } else {
+            None
+        }
+    }
+
+    fn infallible_stake_initiation_on_application(
+        imbalance: NegativeImbalance<T>,
+        application_id: &T::ApplicationId,
+    ) -> T::StakeId {
+        // Create stake
+        let new_stake_id = T::StakeHandlerProvider::staking().create_stake();
+
+        // Keep track of this stake id to process unstaking callbacks that may
+        // be invoked later.
+        // NB: We purposefully update state to reflect mapping _before_ initiating staking below
+        // in order to be safe from race conditions arising out of third party code executing in callback of staking module.
+
+        // MUST never already be a key for new stake, false means code is broken.
+        // But should we do panic in runtime? Is there safer way?
+        assert!(!<ApplicationIdByStakingId<T>>::exists(new_stake_id));
+
+        <ApplicationIdByStakingId<T>>::insert(new_stake_id, application_id);
+
+        // Initiate staking
+        //
+        // MUST work, is runtime invariant, false means code is broken.
+        // But should we do panic in runtime? Is there safer way?
+        assert_eq!(
+            T::StakeHandlerProvider::staking().stake(&new_stake_id, imbalance),
+            Ok(())
+        );
+
+        new_stake_id
+    }
+}
+
+// Conditions for adding application
+impl<T: Trait> Module<T> {
+    /// Evaluates prospects for a new application
+    ///
+    pub(crate) fn would_application_get_added(
+        possible_opening_application_rationing_policy: &Option<ApplicationRationingPolicy>,
+        opening_applicants: &BTreeSet<T::ApplicationId>,
+        opt_role_stake_balance: &Option<BalanceOf<T>>,
+        opt_application_stake_balance: &Option<BalanceOf<T>>,
+    ) -> ApplicationWouldGetAddedEvaluation<T> {
+        // Check whether any rationing policy is set at all, if not
+        // then there is no rationing, and any application can get added.
+        let application_rationing_policy = if let Some(application_rationing_policy) =
+            possible_opening_application_rationing_policy
+        {
+            application_rationing_policy
+        } else {
+            return ApplicationWouldGetAddedEvaluation::Yes(
+                ApplicationAddedSuccess::Unconditionally,
+            );
+        };
+
+        // Map with applications
+        let applications_map = Self::application_id_iter_to_map(opening_applicants.iter());
+
+        let active_applications_with_stake_iter =
+            applications_map
+                .iter()
+                .filter_map(|(application_id, application)| {
+                    if application.stage == hiring::ApplicationStage::Active {
+                        let total_stake =
+                            Self::get_opt_stake_amount(application.active_role_staking_id)
+                                + Self::get_opt_stake_amount(
+                                    application.active_application_staking_id,
+                                );
+
+                        Some((application_id, application, total_stake))
+                    } else {
+                        None
+                    }
+                });
+
+        // Compute number of active applications
+        let number_of_active_applications = active_applications_with_stake_iter.clone().count();
+
+        // Check whether the current number of _active_ applicants is either at or above the maximum
+        // limit, if not, then we can add at least one additional application,
+        // otherwise we must evaluate whether this new application would specifically get added.
+        if (number_of_active_applications as u32)
+            < application_rationing_policy.max_active_applicants
+        {
+            return ApplicationWouldGetAddedEvaluation::Yes(
+                ApplicationAddedSuccess::Unconditionally,
+            );
+        }
+
+        // Here we try to figure out if the new application
+        // has sufficient stake to crowd out one of the already
+        // active applicants.
+
+        // The total stake of new application
+        let total_stake_of_new_application = opt_role_stake_balance.unwrap_or_default()
+            + opt_application_stake_balance.unwrap_or_default();
+
+        // The total stake of all current active applications
+        let opt_min_item = active_applications_with_stake_iter
+            .clone()
+            .min_by_key(|(_, _, total_stake)| *total_stake);
+
+        if let Some((application_id, _, lowest_active_total_stake)) = opt_min_item {
+            // Finally we compare the two and come up with a final evaluation
+            if total_stake_of_new_application <= lowest_active_total_stake {
+                ApplicationWouldGetAddedEvaluation::No // stake too low!
+            } else {
+                ApplicationWouldGetAddedEvaluation::Yes(
+                    ApplicationAddedSuccess::CrowdsOutExistingApplication(*application_id),
+                )
+            }
+        } else {
+            panic!("`number_of_active_applications` (length of `active_applications_iter`) == 0")
+        }
+    }
+
+    fn get_opt_stake_amount(stake_id: Option<T::StakeId>) -> BalanceOf<T> {
+        stake_id.map_or(<BalanceOf<T> as Zero>::zero(), |stake_id| {
+            // INVARIANT: stake MUST exist in the staking module
+            assert!(T::StakeHandlerProvider::staking().stake_exists(stake_id));
+
+            let stake = T::StakeHandlerProvider::staking().get_stake(stake_id);
+
+            match stake.staking_status {
+                // INVARIANT: stake MUST be in the staked state.
+                stake::StakingStatus::Staked(staked_state) => staked_state.staked_amount,
+                _ => panic!("stake MUST be in the staked state."),
+            }
+        })
+    }
+
+    pub(crate) fn create_stake_balance(
+        opt_stake_imbalance: &Option<NegativeImbalance<T>>,
+    ) -> Option<BalanceOf<T>> {
+        if let Some(ref imbalance) = opt_stake_imbalance {
+            Some(imbalance.peek())
+        } else {
+            None
+        }
+    }
+}
+
+/*
+ *  === Stake module wrappers  ======
+ */
+
+/// Defines stake module interface
+#[cfg_attr(
+    all(any(test, feature = "test"), not(target_arch = "wasm32")),
+    automock
+)]
+pub trait StakeHandler<T: StakeTrait> {
+    /// Adds a new Stake which is NotStaked, created at given block, into stakes map.
+    fn create_stake(&self) -> T::StakeId;
+
+    /// to the module's account, and the corresponding staked_balance is set to this amount in the new Staked state.
+    /// On error, as the negative imbalance is not returned to the caller, it is the caller's responsibility to return the funds
+    /// back to the source (by creating a new positive imbalance)
+    fn stake(
+        &self,
+        new_stake_id: &T::StakeId,
+        imbalance: NegativeImbalance<T>,
+    ) -> Result<(), StakeActionError<stake::StakingError>>;
+
+    /// Checks whether stake exists by its id
+    fn stake_exists(&self, stake_id: T::StakeId) -> bool;
+
+    /// Acquires stake by id
+    fn get_stake(&self, stake_id: T::StakeId) -> Stake<T::BlockNumber, BalanceOf<T>, T::SlashId>;
+
+    /// Initiate unstaking of a Staked stake.
+    fn initiate_unstaking(
+        &self,
+        stake_id: &T::StakeId,
+        unstaking_period: Option<T::BlockNumber>,
+    ) -> Result<(), StakeActionError<InitiateUnstakingError>>;
+}
+
+/// Allows to provide different StakeHandler implementation. Useful for mocks.
+pub trait StakeHandlerProvider<T: Trait> {
+    /// Returns StakeHandler. Mock entry point for stake module.
+    fn staking() -> Rc<RefCell<dyn StakeHandler<T>>>;
+}
+
+impl<T: Trait> StakeHandlerProvider<T> for Module<T> {
+    /// Returns StakeHandler. Mock entry point for stake module.
+    fn staking() -> Rc<RefCell<dyn StakeHandler<T>>> {
+        Rc::new(RefCell::new(HiringStakeHandler {}))
+    }
+}
+
+/// Default stake module logic implementation
+pub struct HiringStakeHandler;
+impl<T: Trait> StakeHandler<T> for HiringStakeHandler {
+    fn create_stake(&self) -> T::StakeId {
+        <stake::Module<T>>::create_stake()
+    }
+
+    fn stake(
+        &self,
+        new_stake_id: &T::StakeId,
+        imbalance: NegativeImbalance<T>,
+    ) -> Result<(), StakeActionError<StakingError>> {
+        <stake::Module<T>>::stake(new_stake_id, imbalance)
+    }
+
+    fn stake_exists(&self, stake_id: T::StakeId) -> bool {
+        <stake::Stakes<T>>::exists(stake_id)
+    }
+
+    fn get_stake(&self, stake_id: T::StakeId) -> Stake<T::BlockNumber, BalanceOf<T>, T::SlashId> {
+        <stake::Stakes<T>>::get(stake_id)
+    }
+
+    fn initiate_unstaking(
+        &self,
+        stake_id: &T::StakeId,
+        unstaking_period: Option<T::BlockNumber>,
+    ) -> Result<(), StakeActionError<InitiateUnstakingError>> {
+        <stake::Module<T>>::initiate_unstaking(&stake_id, unstaking_period)
+    }
+}
+
+// Proxy implementation of StakeHandler trait to simplify calls via staking() method
+// Allows to get rid of borrow() calls,
+// eg.: T::StakeHandlerProvider::staking().get_stake(stake_id);
+// instead of T::StakeHandlerProvider::staking().borrow().get_stake(stake_id);
+impl<T: Trait> StakeHandler<T> for Rc<RefCell<dyn StakeHandler<T>>> {
+    fn create_stake(&self) -> T::StakeId {
+        self.borrow().create_stake()
+    }
+
+    fn stake(
+        &self,
+        new_stake_id: &T::StakeId,
+        imbalance: NegativeImbalance<T>,
+    ) -> Result<(), StakeActionError<StakingError>> {
+        self.borrow().stake(new_stake_id, imbalance)
+    }
+
+    fn stake_exists(&self, stake_id: T::StakeId) -> bool {
+        self.borrow().stake_exists(stake_id)
+    }
+
+    fn get_stake(&self, stake_id: T::StakeId) -> Stake<T::BlockNumber, BalanceOf<T>, T::SlashId> {
+        self.borrow().get_stake(stake_id)
+    }
+
+    fn initiate_unstaking(
+        &self,
+        stake_id: &T::StakeId,
+        unstaking_period: Option<T::BlockNumber>,
+    ) -> Result<(), StakeActionError<InitiateUnstakingError>> {
+        self.borrow().initiate_unstaking(stake_id, unstaking_period)
+    }
+}

+ 188 - 0
runtime-modules/hiring/src/macroes.rs

@@ -0,0 +1,188 @@
+/// TODO: Move into Substrate-utility library
+/// Ensure that two expressions are equal.
+macro_rules! ensure_eq {
+    ($left:expr, $right:expr, $error:expr) => {
+        ensure!(($left) == ($right), $error)
+    };
+}
+
+/// TODO: Move into Substrate-utility library
+/// Ensure that a storage map, with a given name, has mapping for the given key value.
+macro_rules! ensure_map_key {
+    ($map_variable_name:ident , $runtime_trait:tt, $key:expr, $error:expr) => {{
+        if <$map_variable_name<$runtime_trait>>::exists($key) {
+            let value = <$map_variable_name<$runtime_trait>>::get($key);
+
+            Ok(value)
+        } else {
+            Err($error)
+        }
+    }};
+}
+
+/// Ensure that an opening exists in `OpeningnById`, and if so, return it.
+///
+/// Returns
+/// - `Ok(opening)` where `opening` is the opening, if it exists.
+/// - `Err($error)` otherwise
+macro_rules! ensure_opening_exists {
+    ($runtime_trait:tt, $opening_id:expr, $error:expr) => {{
+        ensure_map_key!(OpeningById, $runtime_trait, $opening_id, $error)
+    }};
+}
+
+/// Ensure that an applications exists in `ApplicationById` , and if so, return it along with the
+/// corresponding opening.
+macro_rules! ensure_application_exists {
+    ($runtime_trait:tt, $application_id:expr, $error:expr) => {{
+        ensure_map_key!(ApplicationById, $runtime_trait, $application_id, $error)
+    }};
+
+    ($runtime_trait:tt, $application_id:expr, $error:expr, auto_fetch_opening) => {{
+        ensure_application_exists!($runtime_trait, $application_id, $error).and_then(
+            |application| {
+                // Grab corresponding opening, which MUST exist.
+                let opening = <OpeningById<$runtime_trait>>::get(application.opening_id);
+
+                // Return both
+                Ok((application, opening))
+            },
+        )
+    }};
+}
+
+/// Ensures that an opening is active.
+macro_rules! ensure_opening_is_active {
+    ($stage:expr, $error:expr) => {{
+        match $stage {
+            hiring::OpeningStage::Active {
+                // <= need proper type here in the future, not param
+                ref stage,
+                ref applications_added,
+                ref active_application_count,
+                ref unstaking_application_count,
+                ref deactivated_application_count,
+            } => Ok((
+                stage.clone(),
+                applications_added.clone(),
+                active_application_count.clone(),
+                unstaking_application_count.clone(),
+                deactivated_application_count.clone(),
+            )),
+            _ => Err($error),
+        }
+    }};
+}
+
+/// Ensures that active opening stage is accepting applications.
+macro_rules! ensure_active_opening_is_accepting_applications {
+
+    ($stage:expr, $error:expr) => {{
+
+        match $stage {
+            hiring::ActiveOpeningStage::AcceptingApplications {
+                started_accepting_applicants_at_block
+            } => Ok(started_accepting_applicants_at_block), // <= need proper type here in the future, not param
+            _ => Err($error),
+        }
+    }}
+}
+
+/// Ensures that optional imbalance matches requirements of optional staking policy
+macro_rules! ensure_stake_balance_matches_staking_policy {
+    (
+        $opt_balance:expr,
+        $opt_policy: expr,
+        $stake_missing_when_required_error:expr,
+        $stake_provided_when_redundant_error:expr,
+        $stake_amount_too_low_error:expr
+
+    ) => {{
+        if let Some(ref balance) = $opt_balance {
+            if let Some(ref policy) = $opt_policy {
+                if !policy.accepts_amount(balance) {
+                    Err($stake_amount_too_low_error)
+                } else {
+                    Ok(Some(balance.clone()))
+                }
+            } else {
+                Err($stake_provided_when_redundant_error)
+            }
+        } else if $opt_policy.is_some() {
+            Err($stake_missing_when_required_error)
+        } else {
+            Ok(None)
+        }
+    }};
+}
+
+/// Ensures that an optional unstaking period is at least one block whens set.
+macro_rules! _ensure_opt_unstaking_period_not_zero {
+    ($opt_period:expr, $error:expr) => {{
+        if let Some(ref length) = $opt_period {
+            let lower_bound = One::one();
+
+            if *length < lower_bound {
+                Err($error)
+            } else {
+                Ok(())
+            }
+        } else {
+            Ok(())
+        }
+    }};
+}
+
+/// Ensures that provided unstaking period is not redundant
+macro_rules! _ensure_opt_unstaking_period_not_redundant {
+    (
+        $opt_policy:expr,
+        $opt_staking_period:expr,
+        $error:expr
+    ) => {{
+        if $opt_policy.is_some() || $opt_staking_period.is_none() {
+            Ok(())
+        } else {
+            Err($error)
+        }
+    }};
+}
+
+/// Ensures that provided unstaking period is valid
+macro_rules! ensure_opt_unstaking_period_is_ok {
+    (
+        $opt_staking_period:expr,
+        $opt_staking_policy:expr,
+        $period_zero_error:expr,
+        $period_redundant_error:expr
+    ) => {{
+        _ensure_opt_unstaking_period_not_zero!($opt_staking_period, $period_zero_error).and(
+            _ensure_opt_unstaking_period_not_redundant!(
+                $opt_staking_policy,
+                $opt_staking_period,
+                $period_redundant_error
+            ),
+        )
+    }};
+}
+
+/// Ensures that a new application would make it into a given opening
+macro_rules! ensure_application_would_get_added {
+    (
+        $opt_staking_policy:expr,
+        $applicants:expr,
+        $opt_role_stake_balance:expr,
+        $opt_application_stake_balance:expr,
+        $error:expr) => {{
+        match Self::would_application_get_added(
+            $opt_staking_policy,
+            $applicants,
+            $opt_role_stake_balance,
+            $opt_application_stake_balance,
+        ) {
+            // Would get added indeed!
+            ApplicationWouldGetAddedEvaluation::Yes(success) => Ok(success),
+            _ => Err($error),
+        }
+    }};
+}

+ 204 - 0
runtime-modules/hiring/src/mock.rs

@@ -0,0 +1,204 @@
+#![cfg(test)]
+
+use primitives::H256;
+use runtime_primitives::{
+    testing::Header,
+    traits::{BlakeTwo256, IdentityLookup},
+    Perbill,
+};
+use srml_support::{impl_outer_origin, parameter_types};
+
+use crate::hiring::ApplicationDeactivationCause;
+use crate::{Module, Trait};
+use balances;
+use stake;
+
+use std::cell::Cell;
+use std::cell::RefCell;
+use std::panic;
+use std::rc::Rc;
+
+impl_outer_origin! {
+    pub enum Origin for 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;
+}
+
+// Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted.
+#[derive(Clone, PartialEq, Eq, Debug)]
+pub struct Test;
+
+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 = ();
+}
+
+parameter_types! {
+    pub const ExistentialDeposit: u32 = 100;
+    pub const TransferFee: u32 = 0;
+    pub const CreationFee: u32 = 0;
+    pub const TransactionBaseFee: u32 = 1;
+    pub const TransactionByteFee: u32 = 0;
+    pub const InitialMembersBalance: u64 = 2000;
+    pub const StakePoolId: [u8; 8] = *b"joystake";
+}
+
+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 Trait for Test {
+    type OpeningId = u64;
+
+    type ApplicationId = u64;
+
+    type ApplicationDeactivatedHandler = TestApplicationDeactivatedHandler;
+
+    type StakeHandlerProvider = TestStakeHandlerProvider;
+}
+
+impl stake::Trait for Test {
+    type Currency = Balances;
+    type StakePoolId = StakePoolId;
+    type StakingEventsHandler = ();
+    type StakeId = u64;
+    type SlashId = u64;
+}
+
+pub type Balances = balances::Module<Test>;
+pub type System = system::Module<Test>;
+pub type Hiring = Module<Test>;
+
+// Intercepts panic method
+// Returns: whether panic occurred
+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()
+    }
+}
+
+pub struct TestStakeHandlerProvider;
+impl crate::StakeHandlerProvider<Test> for TestStakeHandlerProvider {
+    /// Returns StakeHandler. Mock entry point for stake module.
+    fn staking() -> Rc<RefCell<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
+// 3. Refcell - interior mutability to provide extra-mock capabilities (like mockall checkpoint).
+thread_local! {
+    pub static THREAD_LOCAL_STAKE_HANDLER:
+      RefCell<Rc<RefCell<dyn crate::StakeHandler<Test>>>> = RefCell::new(Rc::new(RefCell::new(crate::HiringStakeHandler {})));
+}
+
+// Sets stake handler implementation in hiring module. Mockall frameworks integration
+pub(crate) fn set_stake_handler_impl(mock: Rc<rstd::cell::RefCell<dyn crate::StakeHandler<Test>>>) {
+    // Hiring::staking.mock_safe(move || MockResult::Return(mock.clone()));
+    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(RefCell::new(crate::HiringStakeHandler {})));
+}
+
+pub fn build_test_externalities() -> runtime_io::TestExternalities {
+    let t = system::GenesisConfig::default()
+        .build_storage::<Test>()
+        .unwrap();
+
+    t.into()
+}
+
+// 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);
+}
+
+//
+// ******* ApplicationDeactivatedHandler mocks ********************
+//
+thread_local! {
+    pub static LAST_DEACTIVATED_APPLICATION:
+        Cell<Option<(<Test as Trait>::ApplicationId, ApplicationDeactivationCause)>> = Cell::new(None);
+}
+
+pub struct TestApplicationDeactivatedHandler;
+impl crate::ApplicationDeactivatedHandler<Test> for TestApplicationDeactivatedHandler {
+    fn deactivated(
+        application_id: &<Test as Trait>::ApplicationId,
+        cause: ApplicationDeactivationCause,
+    ) {
+        LAST_DEACTIVATED_APPLICATION.with(|f| {
+            f.replace(Some((*application_id, cause)));
+        });
+    }
+}
+
+impl TestApplicationDeactivatedHandler {
+    pub(crate) fn assert_deactivated_application(
+        expected_application_id: <Test as Trait>::ApplicationId,
+        expected_cause: ApplicationDeactivationCause,
+    ) {
+        let mut actual_deactivated_application = None;
+        LAST_DEACTIVATED_APPLICATION.with(|f| {
+            actual_deactivated_application = f.replace(None);
+        });
+
+        assert_eq!(
+            Some((expected_application_id, expected_cause)),
+            actual_deactivated_application
+        );
+    }
+}

+ 27 - 0
runtime-modules/hiring/src/test/mod.rs

@@ -0,0 +1,27 @@
+#![cfg(test)]
+
+mod public_api;
+mod smoke;
+mod staking_module;
+use public_api::*;
+
+use crate::mock::Test;
+use crate::*;
+
+use rstd::cell::RefCell;
+use rstd::rc::Rc;
+
+use std::panic;
+
+pub(crate) type OpeningId = <Test as Trait>::OpeningId;
+pub(crate) type ApplicationId = <Test as Trait>::ApplicationId;
+pub(crate) type BlockNumber = <Test as system::Trait>::BlockNumber;
+pub(crate) type StakeId = <Test as stake::Trait>::StakeId;
+pub(crate) type Balance =
+    <<Test as stake::Trait>::Currency as Currency<<Test as system::Trait>::AccountId>>::Balance;
+
+// Debug test object content. Recurring temporary usage - do not delete.
+#[allow(dead_code)]
+pub fn debug_print<T: rstd::fmt::Debug>(obj: T) {
+    println!("{:?}", obj);
+}

+ 437 - 0
runtime-modules/hiring/src/test/public_api/add_application.rs

@@ -0,0 +1,437 @@
+use crate::mock::*;
+use crate::test::*;
+use rstd::collections::btree_map::BTreeMap;
+use stake::NegativeImbalance;
+
+use crate::test::public_api::*;
+
+/*
+Most 'ensures' (add_application() fail reasons) covered in ensure_can_add_application_* tests.
+*/
+
+pub struct AddApplicationFixture {
+    pub opening_id: OpeningId,
+    pub opt_role_stake_imbalance: Option<NegativeImbalance<Test>>,
+    pub opt_application_stake_imbalance: Option<NegativeImbalance<Test>>,
+    pub human_readable_text: Vec<u8>,
+}
+
+impl AddApplicationFixture {
+    pub(crate) fn default_for_opening(opening_id: OpeningId) -> Self {
+        AddApplicationFixture {
+            opening_id,
+            opt_role_stake_imbalance: None,
+            opt_application_stake_imbalance: None,
+            human_readable_text: HUMAN_READABLE_TEXT.to_vec(),
+        }
+    }
+
+    pub(crate) fn add_application(
+        &self,
+    ) -> Result<ApplicationAdded<ApplicationId>, AddApplicationError> {
+        let mut opt_role_stake_imbalance = None;
+        if let Some(ref imbalance) = self.opt_role_stake_imbalance {
+            opt_role_stake_imbalance =
+                Some(stake::NegativeImbalance::<Test>::new(imbalance.peek()));
+        }
+
+        let mut opt_application_stake_imbalance = None;
+        if let Some(ref imbalance) = self.opt_application_stake_imbalance {
+            opt_application_stake_imbalance =
+                Some(stake::NegativeImbalance::<Test>::new(imbalance.peek()));
+        }
+
+        Hiring::add_application(
+            self.opening_id,
+            opt_role_stake_imbalance,
+            opt_application_stake_imbalance,
+            self.human_readable_text.clone(),
+        )
+    }
+
+    fn call_and_assert(
+        &self,
+        expected_result: Result<ApplicationAdded<ApplicationId>, AddApplicationError>,
+    ) {
+        let expected_application_id = Hiring::next_application_id();
+        // save opening state (can be invalid if invalid opening_id provided)
+        let old_opening_state = <OpeningById<Test>>::get(self.opening_id);
+
+        let add_application_result = self.add_application();
+
+        // Check expected result
+        assert_eq!(add_application_result, expected_result);
+
+        if add_application_result.is_ok() {
+            // Check next application id has been updated
+            assert_eq!(Hiring::next_application_id(), expected_application_id + 1);
+            // Check application exists
+            assert!(<ApplicationById<Test>>::exists(expected_application_id));
+        } else {
+            // Check next application id has not been updated
+            assert_eq!(Hiring::next_application_id(), expected_application_id);
+            // Check application does not exist
+            assert!(!<ApplicationById<Test>>::exists(expected_application_id));
+        };
+
+        //Check application content
+        self.assert_application_content(add_application_result.clone(), expected_application_id);
+
+        //Check opening state after add_application() call
+        self.assert_opening_content(
+            old_opening_state,
+            add_application_result,
+            expected_application_id,
+        );
+    }
+
+    fn assert_application_content(
+        &self,
+        add_application_result: Result<ApplicationAdded<ApplicationId>, AddApplicationError>,
+        expected_application_id: ApplicationId,
+    ) {
+        if add_application_result.is_ok() {
+            let opening = <OpeningById<Test>>::get(self.opening_id);
+            let total_applications_count;
+            if let OpeningStage::Active {
+                applications_added, ..
+            } = opening.stage
+            {
+                total_applications_count = applications_added.len();
+            } else {
+                panic!("Opening should be in active stage");
+            }
+
+            let found_application = <ApplicationById<Test>>::get(expected_application_id);
+            let expected_application_index_in_opening = total_applications_count as u32 - 1;
+
+            // Skip this check due external stake module dependency
+            let expected_active_role_staking_id = found_application.active_role_staking_id;
+
+            // Skip this check due external stake module dependency
+            let expected_active_application_staking_id =
+                found_application.active_application_staking_id;
+
+            let expected_application = Application {
+                opening_id: self.opening_id,
+                application_index_in_opening: expected_application_index_in_opening,
+                add_to_opening_in_block: 1,
+                active_role_staking_id: expected_active_role_staking_id,
+                active_application_staking_id: expected_active_application_staking_id,
+                stage: ApplicationStage::Active,
+                human_readable_text: HUMAN_READABLE_TEXT.to_vec(),
+            };
+
+            assert_eq!(found_application, expected_application);
+        }
+    }
+
+    fn assert_opening_content(
+        &self,
+        old_opening: Opening<Balance, BlockNumber, ApplicationId>,
+        add_application_result: Result<ApplicationAdded<ApplicationId>, AddApplicationError>,
+        expected_application_id: ApplicationId,
+    ) {
+        let new_opening_state = <OpeningById<Test>>::get(self.opening_id);
+
+        let mut expected_added_apps_in_opening;
+        let mut expected_active_application_count;
+        let mut expected_unstaking_application_count;
+        let expected_deactivated_application_count;
+        if let OpeningStage::Active {
+            applications_added,
+            active_application_count,
+            unstaking_application_count,
+            deactivated_application_count,
+            ..
+        } = old_opening.stage
+        {
+            expected_added_apps_in_opening = applications_added.clone();
+            expected_active_application_count = active_application_count;
+            expected_deactivated_application_count = deactivated_application_count;
+            expected_unstaking_application_count = unstaking_application_count;
+
+            if let Ok(add_app_data) = add_application_result {
+                expected_added_apps_in_opening.insert(expected_application_id);
+                if add_app_data.application_id_crowded_out.is_some() {
+                    expected_unstaking_application_count += 1;
+                } else {
+                    expected_active_application_count += 1;
+                }
+            }
+        } else {
+            panic!("Opening should be in active stage");
+        }
+
+        let expected_opening = Opening {
+            stage: OpeningStage::Active {
+                stage: ActiveOpeningStage::AcceptingApplications {
+                    started_accepting_applicants_at_block: 1,
+                },
+                applications_added: expected_added_apps_in_opening,
+                active_application_count: expected_active_application_count,
+                unstaking_application_count: expected_unstaking_application_count,
+                deactivated_application_count: expected_deactivated_application_count,
+            },
+            ..old_opening
+        };
+        assert_eq!(new_opening_state, expected_opening);
+    }
+}
+
+#[test]
+fn add_application_succeeds() {
+    build_test_externalities().execute_with(|| {
+        let opening_fixture = AddOpeningFixture::default();
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+        application_fixture.call_and_assert(Ok(ApplicationAdded {
+            application_id_added: 0,
+            application_id_crowded_out: None,
+        }));
+    });
+}
+
+#[test]
+fn add_application_succeeds_with_crowding_out() {
+    build_test_externalities().execute_with(|| {
+        let mut opening_fixture = AddOpeningFixture::default();
+        opening_fixture.application_rationing_policy = Some(hiring::ApplicationRationingPolicy {
+            max_active_applicants: 1,
+        });
+        opening_fixture.application_staking_policy = Some(StakingPolicy {
+            amount: 100,
+            amount_mode: StakingAmountLimitMode::AtLeast,
+            crowded_out_unstaking_period_length: None,
+            review_period_expired_unstaking_period_length: None,
+        });
+
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let mut application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+        application_fixture.opt_application_stake_imbalance =
+            Some(stake::NegativeImbalance::<Test>::new(100));
+
+        let add_appplication_result = application_fixture.add_application();
+        let application_id = add_appplication_result.unwrap().application_id_added;
+
+        application_fixture.opt_application_stake_imbalance =
+            Some(stake::NegativeImbalance::<Test>::new(101));
+
+        application_fixture.call_and_assert(Ok(ApplicationAdded {
+            application_id_added: 1,
+            application_id_crowded_out: Some(0),
+        }));
+
+        TestApplicationDeactivatedHandler::assert_deactivated_application(
+            application_id,
+            ApplicationDeactivationCause::CrowdedOut,
+        );
+    });
+}
+
+#[test]
+fn add_application_fails() {
+    build_test_externalities().execute_with(|| {
+        let mut opening_fixture = AddOpeningFixture::default();
+        opening_fixture.application_rationing_policy = Some(hiring::ApplicationRationingPolicy {
+            max_active_applicants: 1,
+        });
+        opening_fixture.application_staking_policy = Some(StakingPolicy {
+            amount: 100,
+            amount_mode: StakingAmountLimitMode::AtLeast,
+            crowded_out_unstaking_period_length: None,
+            review_period_expired_unstaking_period_length: None,
+        });
+
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let mut application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+        application_fixture.opt_application_stake_imbalance =
+            Some(stake::NegativeImbalance::<Test>::new(50));
+
+        application_fixture.call_and_assert(Err(AddApplicationError::StakeAmountTooLow(
+            StakePurpose::Application,
+        )));
+    });
+}
+
+#[test]
+fn add_application_succeeds_with_created_application_stake() {
+    build_test_externalities().execute_with(|| {
+        let mut opening_fixture = AddOpeningFixture::default();
+        opening_fixture.application_rationing_policy = Some(hiring::ApplicationRationingPolicy {
+            max_active_applicants: 1,
+        });
+        opening_fixture.application_staking_policy = Some(StakingPolicy {
+            amount: 100,
+            amount_mode: StakingAmountLimitMode::AtLeast,
+            crowded_out_unstaking_period_length: None,
+            review_period_expired_unstaking_period_length: None,
+        });
+
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let mut application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+        application_fixture.opt_application_stake_imbalance =
+            Some(stake::NegativeImbalance::<Test>::new(100));
+
+        let app_application_result = application_fixture.add_application();
+        let application_id = app_application_result.unwrap().application_id_added;
+
+        let application = <ApplicationById<Test>>::get(application_id);
+        let application_stake_id = application.active_application_staking_id.unwrap();
+
+        let stake = Hiring::staking().get_stake(application_stake_id);
+        let expected_stake = stake::Stake {
+            created: 1,
+            staking_status: stake::StakingStatus::Staked(stake::StakedState {
+                staked_amount: 100,
+                staked_status: stake::StakedStatus::Normal,
+                next_slash_id: 0,
+                ongoing_slashes: BTreeMap::new(),
+            }),
+        };
+
+        assert_eq!(stake, expected_stake);
+    });
+}
+
+#[test]
+fn add_application_succeeds_with_crowding_out_with_staking_mocks() {
+    handle_mock(|| {
+        build_test_externalities().execute_with(|| {
+            let mock = default_mock_for_creating_stake();
+            set_stake_handler_impl(mock.clone());
+
+            let mut opening_fixture = AddOpeningFixture::default();
+            opening_fixture.application_rationing_policy =
+                Some(hiring::ApplicationRationingPolicy {
+                    max_active_applicants: 1,
+                });
+            opening_fixture.application_staking_policy = Some(StakingPolicy {
+                amount: 100,
+                amount_mode: StakingAmountLimitMode::AtLeast,
+                crowded_out_unstaking_period_length: None,
+                review_period_expired_unstaking_period_length: None,
+            });
+
+            let add_opening_result = opening_fixture.add_opening();
+            let opening_id = add_opening_result.unwrap();
+
+            let mut application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+            application_fixture.opt_application_stake_imbalance =
+                Some(stake::NegativeImbalance::<Test>::new(100));
+
+            assert!(application_fixture.add_application().is_ok());
+            mock.borrow_mut().checkpoint();
+
+            application_fixture.opt_application_stake_imbalance =
+                Some(stake::NegativeImbalance::<Test>::new(101));
+
+            let mock2 = {
+                let mut mock = crate::MockStakeHandler::<Test>::new();
+                mock.expect_stake_exists().returning(|_| true);
+
+                mock.expect_stake().times(1).returning(|_, _| Ok(()));
+
+                mock.expect_initiate_unstaking()
+                    .times(1)
+                    .returning(|_, _| Ok(()));
+
+                mock.expect_create_stake().times(1).returning(|| 1);
+
+                mock.expect_get_stake().returning(|_| stake::Stake {
+                    created: 1,
+                    staking_status: stake::StakingStatus::Staked(stake::StakedState {
+                        staked_amount: 100,
+                        staked_status: stake::StakedStatus::Normal,
+                        next_slash_id: 0,
+                        ongoing_slashes: BTreeMap::new(),
+                    }),
+                });
+
+                Rc::new(RefCell::new(mock))
+            };
+
+            set_stake_handler_impl(mock2.clone());
+
+            assert!(application_fixture.add_application().is_ok());
+        });
+    });
+}
+
+#[test]
+fn add_application_succeeds_with_crowding_out_with_role_staking_mocks() {
+    handle_mock(|| {
+        build_test_externalities().execute_with(|| {
+            let mock = default_mock_for_creating_stake();
+            set_stake_handler_impl(mock.clone());
+
+            let mut opening_fixture = AddOpeningFixture::default();
+            opening_fixture.application_rationing_policy =
+                Some(hiring::ApplicationRationingPolicy {
+                    max_active_applicants: 1,
+                });
+            opening_fixture.role_staking_policy = Some(StakingPolicy {
+                amount: 100,
+                amount_mode: StakingAmountLimitMode::AtLeast,
+                crowded_out_unstaking_period_length: None,
+                review_period_expired_unstaking_period_length: None,
+            });
+
+            let add_opening_result = opening_fixture.add_opening();
+            let opening_id = add_opening_result.unwrap();
+
+            let mut application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+            application_fixture.opt_role_stake_imbalance =
+                Some(stake::NegativeImbalance::<Test>::new(100));
+
+            let add_appplication_result = application_fixture.add_application();
+            let application_id = add_appplication_result.unwrap().application_id_added;
+            mock.borrow_mut().checkpoint();
+
+            application_fixture.opt_role_stake_imbalance =
+                Some(stake::NegativeImbalance::<Test>::new(101));
+
+            let mock2 = {
+                let mut mock = crate::MockStakeHandler::<Test>::new();
+                mock.expect_stake_exists().returning(|_| true);
+
+                mock.expect_stake().times(1).returning(|_, _| Ok(()));
+
+                mock.expect_initiate_unstaking()
+                    .times(1)
+                    .returning(|_, _| Ok(()));
+
+                mock.expect_create_stake().times(1).returning(|| 1);
+
+                mock.expect_get_stake().returning(|_| stake::Stake {
+                    created: 1,
+                    staking_status: stake::StakingStatus::Staked(stake::StakedState {
+                        staked_amount: 100,
+                        staked_status: stake::StakedStatus::Normal,
+                        next_slash_id: 0,
+                        ongoing_slashes: BTreeMap::new(),
+                    }),
+                });
+
+                Rc::new(RefCell::new(mock))
+            };
+
+            set_stake_handler_impl(mock2.clone());
+
+            assert!(application_fixture.add_application().is_ok());
+
+            TestApplicationDeactivatedHandler::assert_deactivated_application(
+                application_id,
+                ApplicationDeactivationCause::CrowdedOut,
+            );
+        });
+    });
+}

+ 198 - 0
runtime-modules/hiring/src/test/public_api/add_opening.rs

@@ -0,0 +1,198 @@
+use crate::mock::*;
+use crate::test::*;
+use crate::StakingAmountLimitMode::Exact;
+use rstd::collections::btree_set::BTreeSet;
+
+static FIRST_BLOCK_HEIGHT: <Test as system::Trait>::BlockNumber = 1;
+pub static HUMAN_READABLE_TEXT: &[u8] = b"HUMAN_READABLE_TEXT!!!!";
+
+pub struct AddOpeningFixture<Balance> {
+    pub activate_at: ActivateOpeningAt<BlockNumber>,
+    pub max_review_period_length: BlockNumber,
+    pub application_rationing_policy: Option<ApplicationRationingPolicy>,
+    pub application_staking_policy: Option<StakingPolicy<Balance, BlockNumber>>,
+    pub role_staking_policy: Option<StakingPolicy<Balance, BlockNumber>>,
+    pub human_readable_text: Vec<u8>,
+}
+
+impl<Balance> Default for AddOpeningFixture<Balance> {
+    fn default() -> Self {
+        AddOpeningFixture {
+            activate_at: ActivateOpeningAt::CurrentBlock,
+            max_review_period_length: 672,
+            application_rationing_policy: None,
+            application_staking_policy: None,
+            role_staking_policy: None,
+            human_readable_text: HUMAN_READABLE_TEXT.to_vec(),
+        }
+    }
+}
+
+impl AddOpeningFixture<OpeningId> {
+    fn call_and_assert(&self, expected_result: Result<OpeningId, AddOpeningError>) {
+        let expected_opening_id = Hiring::next_opening_id();
+
+        let add_opening_result = self.add_opening();
+        assert_eq!(add_opening_result, expected_result);
+
+        if add_opening_result.is_ok() {
+            // Check next opening id has been updated
+            assert_eq!(Hiring::next_opening_id(), expected_opening_id + 1);
+            // Check opening exists
+            assert!(<OpeningById<Test>>::exists(expected_opening_id));
+        } else {
+            // Check next opening id has not been updated
+            assert_eq!(Hiring::next_opening_id(), expected_opening_id);
+            // Check opening does not exist
+            assert!(!<OpeningById<Test>>::exists(expected_opening_id));
+        };
+
+        //Check opening content
+        if add_opening_result.is_ok() {
+            self.assert_opening_content(expected_opening_id);
+        }
+    }
+
+    fn assert_opening_content(&self, expected_opening_id: OpeningId) {
+        let expected_opening_stage = match self.activate_at {
+            ActivateOpeningAt::CurrentBlock => OpeningStage::Active {
+                stage: ActiveOpeningStage::AcceptingApplications {
+                    started_accepting_applicants_at_block: FIRST_BLOCK_HEIGHT,
+                },
+                applications_added: BTreeSet::new(),
+                active_application_count: 0,
+                unstaking_application_count: 0,
+                deactivated_application_count: 0,
+            },
+            ActivateOpeningAt::ExactBlock(block_number) => OpeningStage::WaitingToBegin {
+                begins_at_block: block_number,
+            },
+        };
+
+        let expected_opening = Opening {
+            created: FIRST_BLOCK_HEIGHT,
+            stage: expected_opening_stage,
+            max_review_period_length: self.max_review_period_length,
+            application_rationing_policy: self.application_rationing_policy.clone(),
+            application_staking_policy: self.application_staking_policy.clone(),
+            role_staking_policy: self.role_staking_policy.clone(),
+            human_readable_text: HUMAN_READABLE_TEXT.to_vec(),
+        };
+
+        let found_opening = Hiring::opening_by_id(expected_opening_id);
+        assert_eq!(found_opening, expected_opening);
+    }
+
+    pub(crate) fn add_opening(&self) -> Result<OpeningId, AddOpeningError> {
+        Hiring::add_opening(
+            self.activate_at.clone(),
+            self.max_review_period_length,
+            self.application_rationing_policy.clone(),
+            self.application_staking_policy.clone(),
+            self.role_staking_policy.clone(),
+            self.human_readable_text.clone(),
+        )
+    }
+}
+
+#[test]
+fn add_opening_succeeds_with_exact_block() {
+    build_test_externalities().execute_with(|| {
+        let opening_data = AddOpeningFixture::default();
+
+        let expected_opening_id = 0;
+
+        // Add an opening, check that the returned value is Zero
+        opening_data.call_and_assert(Ok(expected_opening_id));
+    });
+}
+
+#[test]
+fn add_opening_succeeds_with_waiting_to_begin() {
+    build_test_externalities().execute_with(|| {
+        let mut opening_data = AddOpeningFixture::default();
+        opening_data.activate_at = ActivateOpeningAt::ExactBlock(22);
+        let expected_opening_id = 0;
+
+        // Add an opening, check that the returned value is Zero
+        opening_data.call_and_assert(Ok(expected_opening_id));
+    });
+}
+
+#[test]
+fn add_opening_fails_due_to_activation_in_the_past() {
+    build_test_externalities().execute_with(|| {
+        let mut opening_data = AddOpeningFixture::default();
+        opening_data.activate_at = ActivateOpeningAt::ExactBlock(0);
+
+        opening_data.call_and_assert(Err(AddOpeningError::OpeningMustActivateInTheFuture));
+    });
+}
+
+#[test]
+fn add_opening_succeeds_or_fails_due_to_application_staking_policy() {
+    build_test_externalities().execute_with(|| {
+        let mut opening_data = AddOpeningFixture::default();
+        //Valid stake amount
+        opening_data.application_staking_policy = Some(StakingPolicy {
+            amount: 300,
+            amount_mode: Exact,
+            crowded_out_unstaking_period_length: None,
+            review_period_expired_unstaking_period_length: None,
+        });
+
+        opening_data.call_and_assert(Ok(0));
+
+        //Invalid stake amount
+        opening_data.application_staking_policy = Some(StakingPolicy {
+            amount: 1,
+            amount_mode: Exact,
+            crowded_out_unstaking_period_length: None,
+            review_period_expired_unstaking_period_length: None,
+        });
+
+        opening_data.call_and_assert(Err(
+            AddOpeningError::StakeAmountLessThanMinimumCurrencyBalance(StakePurpose::Application),
+        ));
+    });
+}
+
+#[test]
+fn add_opening_succeeds_or_fails_due_to_role_staking_policy() {
+    build_test_externalities().execute_with(|| {
+        let mut opening_data = AddOpeningFixture::default();
+        //Valid stake amount
+        opening_data.role_staking_policy = Some(StakingPolicy {
+            amount: 100,
+            amount_mode: Exact,
+            crowded_out_unstaking_period_length: None,
+            review_period_expired_unstaking_period_length: None,
+        });
+
+        opening_data.call_and_assert(Ok(0));
+
+        //Invalid stake amount
+        opening_data.role_staking_policy = Some(StakingPolicy {
+            amount: 1,
+            amount_mode: Exact,
+            crowded_out_unstaking_period_length: None,
+            review_period_expired_unstaking_period_length: None,
+        });
+
+        opening_data.call_and_assert(Err(
+            AddOpeningError::StakeAmountLessThanMinimumCurrencyBalance(StakePurpose::Role),
+        ));
+    });
+}
+
+#[test]
+fn add_opening_succeeds_or_fails_due_to_invalid_application_rationing_policy() {
+    build_test_externalities().execute_with(|| {
+        let mut opening_data = AddOpeningFixture::default();
+        opening_data.application_rationing_policy = Some(ApplicationRationingPolicy {
+            max_active_applicants: 0,
+        });
+
+        opening_data.call_and_assert(Err(AddOpeningError::ApplicationRationingZeroMaxApplicants));
+    });
+}

+ 64 - 0
runtime-modules/hiring/src/test/public_api/begin_accepting_applications.rs

@@ -0,0 +1,64 @@
+use crate::mock::*;
+use crate::test::*;
+use rstd::collections::btree_set::BTreeSet;
+
+#[test]
+fn begin_accepting_applications_fails_with_no_opening() {
+    build_test_externalities().execute_with(|| {
+        assert_eq!(
+            Hiring::begin_accepting_applications(2),
+            Err(BeginAcceptingApplicationsError::OpeningDoesNotExist)
+        );
+    });
+}
+
+#[test]
+fn begin_accepting_applications_fails_with_not_in_waiting_for_begin() {
+    build_test_externalities().execute_with(|| {
+        let opening_fixture = AddOpeningFixture::default();
+
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        assert_eq!(
+            Hiring::begin_accepting_applications(opening_id),
+            Err(BeginAcceptingApplicationsError::OpeningIsNotInWaitingToBeginStage)
+        );
+    });
+}
+
+#[test]
+fn begin_accepting_applications_succeeds() {
+    build_test_externalities().execute_with(|| {
+        let mut opening_fixture = AddOpeningFixture::default();
+        // set future activation
+        opening_fixture.activate_at = ActivateOpeningAt::ExactBlock(22);
+
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        assert_eq!(Hiring::begin_accepting_applications(opening_id), Ok(()));
+
+        let updated_opening = <OpeningById<Test>>::get(opening_id);
+
+        let expected_opening_state = Opening {
+            created: 1,
+            stage: OpeningStage::Active {
+                stage: ActiveOpeningStage::AcceptingApplications {
+                    started_accepting_applicants_at_block: 1,
+                },
+                applications_added: BTreeSet::new(),
+                active_application_count: 0,
+                unstaking_application_count: 0,
+                deactivated_application_count: 0,
+            },
+            max_review_period_length: 672,
+            application_rationing_policy: None,
+            application_staking_policy: None,
+            role_staking_policy: None,
+            human_readable_text: HUMAN_READABLE_TEXT.to_vec(),
+        };
+
+        assert_eq!(updated_opening, expected_opening_state);
+    });
+}

+ 55 - 0
runtime-modules/hiring/src/test/public_api/begin_review.rs

@@ -0,0 +1,55 @@
+use crate::mock::*;
+use crate::test::*;
+
+#[test]
+fn begin_review_fails_with_no_opening() {
+    build_test_externalities().execute_with(|| {
+        assert_eq!(
+            Hiring::begin_review(2),
+            Err(BeginReviewError::OpeningDoesNotExist)
+        );
+    });
+}
+
+#[test]
+fn begin_review_fails_with_not_in_accepting_application_stage() {
+    build_test_externalities().execute_with(|| {
+        let mut opening_fixture = AddOpeningFixture::default();
+        // set future activation
+        opening_fixture.activate_at = ActivateOpeningAt::ExactBlock(22);
+
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        assert_eq!(
+            Hiring::begin_review(opening_id),
+            Err(BeginReviewError::OpeningNotInAcceptingApplicationsStage)
+        );
+    });
+}
+
+#[test]
+fn begin_review_succeeds() {
+    build_test_externalities().execute_with(|| {
+        let opening_fixture = AddOpeningFixture::default();
+
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+        let application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+        assert!(application_fixture.add_application().is_ok());
+
+        let old_opening = <OpeningById<Test>>::get(opening_id);
+
+        assert_eq!(Hiring::begin_review(opening_id), Ok(()));
+
+        let updated_opening = <OpeningById<Test>>::get(opening_id);
+
+        let expected_opening_state =
+            old_opening.clone_with_new_active_opening_stage(ActiveOpeningStage::ReviewPeriod {
+                started_accepting_applicants_at_block: 1,
+                started_review_period_at_block: 1,
+            });
+
+        assert_eq!(updated_opening, expected_opening_state);
+    });
+}

+ 431 - 0
runtime-modules/hiring/src/test/public_api/cancel_opening.rs

@@ -0,0 +1,431 @@
+use crate::mock::*;
+use crate::test::*;
+
+use crate::test::public_api::*;
+use rstd::collections::btree_map::BTreeMap;
+
+/*
+Not covered:
+- Application content check
+*/
+
+pub struct CancelOpeningFixture {
+    pub opening_id: OpeningId,
+    pub application_stake_unstaking_period: Option<BlockNumber>,
+    pub role_stake_unstaking_period: Option<BlockNumber>,
+}
+
+impl CancelOpeningFixture {
+    pub(crate) fn default_for_opening(opening_id: OpeningId) -> Self {
+        CancelOpeningFixture {
+            opening_id,
+            application_stake_unstaking_period: None,
+            role_stake_unstaking_period: None,
+        }
+    }
+
+    fn call_and_assert(&self, expected_result: Result<OpeningCancelled, CancelOpeningError>) {
+        let old_opening = <OpeningById<Test>>::get(self.opening_id);
+        let old_applications = self.extract_applications();
+
+        let cancel_opening_result = self.cancel_opening();
+
+        assert_eq!(cancel_opening_result, expected_result);
+
+        self.assert_opening_content(old_opening, cancel_opening_result.clone());
+
+        if !cancel_opening_result.is_ok() {
+            self.assert_same_applications(old_applications);
+        }
+    }
+
+    fn assert_same_applications(
+        &self,
+        old_applications: BTreeMap<ApplicationId, Application<OpeningId, BlockNumber, StakeId>>,
+    ) {
+        for (app_id, application) in old_applications {
+            let test_application = <ApplicationById<Test>>::get(app_id);
+            assert_eq!(application, test_application)
+        }
+    }
+
+    fn extract_applications(
+        &self,
+    ) -> BTreeMap<ApplicationId, Application<OpeningId, BlockNumber, StakeId>> {
+        let opening = <OpeningById<Test>>::get(self.opening_id);
+
+        if let OpeningStage::Active {
+            applications_added, ..
+        } = opening.stage
+        {
+            applications_added
+                .iter()
+                .map(|app_id| (*app_id, <ApplicationById<Test>>::get(app_id)))
+                .collect::<BTreeMap<ApplicationId, Application<OpeningId, BlockNumber, StakeId>>>()
+        } else {
+            BTreeMap::new()
+        }
+    }
+
+    pub(crate) fn cancel_opening(&self) -> Result<OpeningCancelled, CancelOpeningError> {
+        Hiring::cancel_opening(
+            self.opening_id,
+            self.application_stake_unstaking_period,
+            self.role_stake_unstaking_period,
+        )
+    }
+
+    fn assert_opening_content(
+        &self,
+        old_opening: Opening<Balance, BlockNumber, ApplicationId>,
+        cancel_opening_result: Result<OpeningCancelled, CancelOpeningError>,
+    ) {
+        let new_opening = <OpeningById<Test>>::get(self.opening_id);
+        let mut expected_opening = old_opening.clone();
+
+        if cancel_opening_result.is_ok() {
+            if let hiring::OpeningStage::Active {
+                stage,
+                applications_added,
+                ..
+            } = old_opening.stage
+            {
+                // compose expected stage
+                let expected_active_stage = match stage {
+                    ActiveOpeningStage::AcceptingApplications {
+                        started_accepting_applicants_at_block,
+                    } => ActiveOpeningStage::Deactivated {
+                        cause: OpeningDeactivationCause::CancelledAcceptingApplications,
+                        deactivated_at_block: 1,
+                        started_accepting_applicants_at_block,
+                        started_review_period_at_block: None,
+                    },
+                    ActiveOpeningStage::ReviewPeriod {
+                        started_accepting_applicants_at_block,
+                        started_review_period_at_block,
+                    } => ActiveOpeningStage::Deactivated {
+                        cause: OpeningDeactivationCause::CancelledInReviewPeriod,
+                        deactivated_at_block: 1,
+                        started_accepting_applicants_at_block,
+                        started_review_period_at_block: Some(started_review_period_at_block),
+                    },
+                    ActiveOpeningStage::Deactivated { .. } => {
+                        panic!("OpeningNotInCancellableStage")
+                    }
+                };
+
+                // calculate application counters
+                let mut deactivated_app_count = 0;
+                let mut unstaking_app_count = 0;
+                for app_id in applications_added.clone() {
+                    let application = <ApplicationById<Test>>::get(app_id);
+
+                    match application.stage {
+                        ApplicationStage::Active => panic!("Cannot be in active stage"),
+                        ApplicationStage::Inactive { .. } => {
+                            deactivated_app_count += 1;
+                        }
+                        ApplicationStage::Unstaking { .. } => {
+                            unstaking_app_count += 1;
+                        }
+                    }
+                }
+
+                expected_opening.stage = hiring::OpeningStage::Active {
+                    stage: expected_active_stage,
+                    applications_added,
+                    active_application_count: 0,
+                    unstaking_application_count: unstaking_app_count,
+                    deactivated_application_count: deactivated_app_count,
+                };
+            } else {
+                panic!("old opening stage MUST be active")
+            }
+        };
+
+        assert_eq!(expected_opening, new_opening);
+    }
+}
+
+#[test]
+fn cancel_opening_fails_due_to_opening_not_existing() {
+    build_test_externalities().execute_with(|| {
+        let cancel_opening_fixture = CancelOpeningFixture::default_for_opening(0);
+        cancel_opening_fixture.call_and_assert(Err(CancelOpeningError::OpeningDoesNotExist));
+    });
+}
+
+#[test]
+fn cancel_opening_succeeds_with_single_crowded_out_application() {
+    build_test_externalities().execute_with(|| {
+        let mut opening_fixture = AddOpeningFixture::default();
+        opening_fixture.application_rationing_policy = Some(hiring::ApplicationRationingPolicy {
+            max_active_applicants: 1,
+        });
+        opening_fixture.application_staking_policy = Some(StakingPolicy {
+            amount: 100,
+            amount_mode: StakingAmountLimitMode::AtLeast,
+            crowded_out_unstaking_period_length: None,
+            review_period_expired_unstaking_period_length: None,
+        });
+        opening_fixture.role_staking_policy = Some(StakingPolicy {
+            amount: 100,
+            amount_mode: StakingAmountLimitMode::AtLeast,
+            crowded_out_unstaking_period_length: None,
+            review_period_expired_unstaking_period_length: None,
+        });
+
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let mut application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+        application_fixture.opt_application_stake_imbalance =
+            Some(stake::NegativeImbalance::<Test>::new(100));
+        application_fixture.opt_role_stake_imbalance =
+            Some(stake::NegativeImbalance::<Test>::new(100));
+
+        assert!(application_fixture.add_application().is_ok());
+
+        application_fixture.opt_application_stake_imbalance =
+            Some(stake::NegativeImbalance::<Test>::new(101));
+        application_fixture.opt_role_stake_imbalance =
+            Some(stake::NegativeImbalance::<Test>::new(101));
+
+        assert!(application_fixture.add_application().is_ok());
+
+        let cancel_opening_fixture = CancelOpeningFixture::default_for_opening(opening_id);
+        cancel_opening_fixture.call_and_assert(Ok(OpeningCancelled {
+            number_of_unstaking_applications: 1,
+            number_of_deactivated_applications: 0,
+        }));
+    });
+}
+
+#[test]
+fn cancel_opening_succeeds_with_single_unstaking_application() {
+    build_test_externalities().execute_with(|| {
+        let mut opening_fixture = AddOpeningFixture::default();
+        opening_fixture.application_staking_policy = Some(StakingPolicy {
+            amount: 100,
+            amount_mode: StakingAmountLimitMode::AtLeast,
+            crowded_out_unstaking_period_length: None,
+            review_period_expired_unstaking_period_length: None,
+        });
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let mut application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+        application_fixture.opt_application_stake_imbalance =
+            Some(stake::NegativeImbalance::<Test>::new(100));
+
+        let app_application_result = application_fixture.add_application();
+        assert!(app_application_result.is_ok());
+
+        let cancel_opening_fixture = CancelOpeningFixture::default_for_opening(opening_id);
+        cancel_opening_fixture.call_and_assert(Ok(OpeningCancelled {
+            number_of_unstaking_applications: 1,
+            number_of_deactivated_applications: 0,
+        }));
+    });
+}
+
+#[test]
+fn cancel_opening_succeeds_with_single_deactivated_application() {
+    build_test_externalities().execute_with(|| {
+        let opening_fixture = AddOpeningFixture::default();
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+        let app_application_result = application_fixture.add_application();
+        assert!(app_application_result.is_ok());
+
+        let cancel_opening_fixture = CancelOpeningFixture::default_for_opening(opening_id);
+        cancel_opening_fixture.call_and_assert(Ok(OpeningCancelled {
+            number_of_unstaking_applications: 0,
+            number_of_deactivated_applications: 1,
+        }));
+    });
+}
+
+#[test]
+fn cancel_opening_fails_due_to_opening_is_not_active() {
+    build_test_externalities().execute_with(|| {
+        let mut opening_fixture = AddOpeningFixture::default();
+        opening_fixture.activate_at = ActivateOpeningAt::ExactBlock(5);
+
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let cancel_opening_fixture = CancelOpeningFixture::default_for_opening(opening_id);
+        cancel_opening_fixture
+            .call_and_assert(Err(CancelOpeningError::OpeningNotInCancellableStage));
+    });
+}
+
+#[test]
+fn cancel_opening_fails_due_to_opening_not_in_cancellable_stage() {
+    build_test_externalities().execute_with(|| {
+        let opening_fixture = AddOpeningFixture::default();
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+        let app_application_result = application_fixture.add_application();
+        assert!(app_application_result.is_ok());
+
+        let cancel_opening_fixture = CancelOpeningFixture::default_for_opening(opening_id);
+        assert!(cancel_opening_fixture.cancel_opening().is_ok());
+        cancel_opening_fixture
+            .call_and_assert(Err(CancelOpeningError::OpeningNotInCancellableStage));
+    });
+}
+
+#[test]
+fn cancel_opening_fails_due_to_redundant_application_unstaking_period() {
+    build_test_externalities().execute_with(|| {
+        let opening_fixture = AddOpeningFixture::default();
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let mut cancel_opening_fixture = CancelOpeningFixture::default_for_opening(opening_id);
+        cancel_opening_fixture.application_stake_unstaking_period = Some(0);
+        cancel_opening_fixture.call_and_assert(Err(CancelOpeningError::UnstakingPeriodTooShort(
+            StakePurpose::Application,
+        )));
+    });
+}
+
+#[test]
+fn cancel_opening_fails_due_to_redundant_role_unstaking_period() {
+    build_test_externalities().execute_with(|| {
+        let opening_fixture = AddOpeningFixture::default();
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let mut cancel_opening_fixture = CancelOpeningFixture::default_for_opening(opening_id);
+        cancel_opening_fixture.role_stake_unstaking_period = Some(50);
+        cancel_opening_fixture.call_and_assert(Err(
+            CancelOpeningError::RedundantUnstakingPeriodProvided(StakePurpose::Role),
+        ));
+    });
+}
+
+#[test]
+fn cancel_opening_fails_due_to_too_short_application_unstaking_period() {
+    build_test_externalities().execute_with(|| {
+        let mut opening_fixture = AddOpeningFixture::default();
+        opening_fixture.application_staking_policy = Some(StakingPolicy {
+            amount: 100,
+            amount_mode: StakingAmountLimitMode::AtLeast,
+            crowded_out_unstaking_period_length: None,
+            review_period_expired_unstaking_period_length: None,
+        });
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let mut cancel_opening_fixture = CancelOpeningFixture::default_for_opening(opening_id);
+        cancel_opening_fixture.application_stake_unstaking_period = Some(0);
+        cancel_opening_fixture.call_and_assert(Err(CancelOpeningError::UnstakingPeriodTooShort(
+            StakePurpose::Application,
+        )));
+    });
+}
+
+#[test]
+fn cancel_opening_fails_due_to_too_short_role_unstaking_period() {
+    build_test_externalities().execute_with(|| {
+        let mut opening_fixture = AddOpeningFixture::default();
+        opening_fixture.role_staking_policy = Some(StakingPolicy {
+            amount: 100,
+            amount_mode: StakingAmountLimitMode::AtLeast,
+            crowded_out_unstaking_period_length: None,
+            review_period_expired_unstaking_period_length: None,
+        });
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let mut cancel_opening_fixture = CancelOpeningFixture::default_for_opening(opening_id);
+        cancel_opening_fixture.role_stake_unstaking_period = Some(0);
+        cancel_opening_fixture.call_and_assert(Err(CancelOpeningError::UnstakingPeriodTooShort(
+            StakePurpose::Role,
+        )));
+    });
+}
+
+#[test]
+fn cancel_opening_succeeds_with_single_unstaking_application_with_application_stake_checks() {
+    handle_mock(|| {
+        build_test_externalities().execute_with(|| {
+            let mock = default_mock_for_creating_stake();
+            set_stake_handler_impl(mock.clone());
+
+            let mut opening_fixture = AddOpeningFixture::default();
+            opening_fixture.application_staking_policy = Some(StakingPolicy {
+                amount: 100,
+                amount_mode: StakingAmountLimitMode::AtLeast,
+                crowded_out_unstaking_period_length: None,
+                review_period_expired_unstaking_period_length: None,
+            });
+            let add_opening_result = opening_fixture.add_opening();
+            let opening_id = add_opening_result.unwrap();
+
+            let mut application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+            application_fixture.opt_application_stake_imbalance =
+                Some(stake::NegativeImbalance::<Test>::new(100));
+
+            let app_application_result = application_fixture.add_application();
+            let application_id = app_application_result.unwrap().application_id_added;
+
+            let mock2 = default_mock_for_unstaking();
+            set_stake_handler_impl(mock2.clone());
+
+            let cancel_opening_fixture = CancelOpeningFixture::default_for_opening(opening_id);
+            cancel_opening_fixture.call_and_assert(Ok(OpeningCancelled {
+                number_of_unstaking_applications: 1,
+                number_of_deactivated_applications: 0,
+            }));
+
+            TestApplicationDeactivatedHandler::assert_deactivated_application(
+                application_id,
+                ApplicationDeactivationCause::OpeningCancelled,
+            );
+        })
+    });
+}
+
+#[test]
+fn cancel_opening_succeeds_with_single_unstaking_application_with_role_stake_checks() {
+    handle_mock(|| {
+        build_test_externalities().execute_with(|| {
+            let mock = default_mock_for_creating_stake();
+            set_stake_handler_impl(mock.clone());
+
+            let mut opening_fixture = AddOpeningFixture::default();
+            opening_fixture.role_staking_policy = Some(StakingPolicy {
+                amount: 100,
+                amount_mode: StakingAmountLimitMode::AtLeast,
+                crowded_out_unstaking_period_length: None,
+                review_period_expired_unstaking_period_length: None,
+            });
+            let add_opening_result = opening_fixture.add_opening();
+            let opening_id = add_opening_result.unwrap();
+
+            let mut application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+            application_fixture.opt_role_stake_imbalance =
+                Some(stake::NegativeImbalance::<Test>::new(100));
+
+            let app_application_result = application_fixture.add_application();
+            assert!(app_application_result.is_ok());
+
+            let mock2 = default_mock_for_unstaking();
+            set_stake_handler_impl(mock2.clone());
+
+            let cancel_opening_fixture = CancelOpeningFixture::default_for_opening(opening_id);
+            cancel_opening_fixture.call_and_assert(Ok(OpeningCancelled {
+                number_of_unstaking_applications: 1,
+                number_of_deactivated_applications: 0,
+            }));
+        })
+    });
+}

+ 382 - 0
runtime-modules/hiring/src/test/public_api/deactivate_application.rs

@@ -0,0 +1,382 @@
+use crate::mock::*;
+use crate::test::public_api::*;
+use crate::test::*;
+use crate::ApplicationDeactivationCause;
+
+/*
+Not covered:
+- application content checks: deactivation in future blocks
+*/
+
+pub struct DeactivateApplicationFixture {
+    pub application_id: ApplicationId,
+    pub application_stake_unstaking_period: Option<BlockNumber>,
+    pub role_stake_unstaking_period: Option<BlockNumber>,
+}
+
+impl DeactivateApplicationFixture {
+    pub(crate) fn default_for_application_id(application_id: ApplicationId) -> Self {
+        DeactivateApplicationFixture {
+            application_id,
+            application_stake_unstaking_period: None,
+            role_stake_unstaking_period: None,
+        }
+    }
+
+    pub(crate) fn deactivate_application(&self) -> Result<(), DeactivateApplicationError> {
+        Hiring::deactive_application(
+            self.application_id,
+            self.application_stake_unstaking_period,
+            self.role_stake_unstaking_period,
+        )
+    }
+
+    fn call_and_assert(&self, expected_result: Result<(), DeactivateApplicationError>) {
+        // save opening state (can be invalid if invalid opening_id provided)
+        let old_application = <ApplicationById<Test>>::get(self.application_id);
+        let old_opening_state = <OpeningById<Test>>::get(old_application.opening_id);
+
+        let deactivate_application_result = self.deactivate_application();
+
+        assert_eq!(deactivate_application_result, expected_result);
+
+        self.assert_application_content(
+            old_application.clone(),
+            deactivate_application_result.clone(),
+        );
+
+        self.assert_opening_content(
+            old_application.opening_id,
+            old_opening_state,
+            deactivate_application_result,
+        );
+    }
+
+    fn assert_application_content(
+        &self,
+        old_application_state: Application<OpeningId, BlockNumber, StakeId>,
+        deactivate_application_result: Result<(), DeactivateApplicationError>,
+    ) {
+        let actual_application_state = <ApplicationById<Test>>::get(self.application_id);
+
+        let expected_application_state = if deactivate_application_result.is_ok() {
+            if old_application_state
+                .active_application_staking_id
+                .is_some()
+                || old_application_state.active_role_staking_id.is_some()
+            {
+                Application {
+                    stage: ApplicationStage::Unstaking {
+                        deactivation_initiated: 1,
+                        cause: ApplicationDeactivationCause::External,
+                    },
+                    ..old_application_state
+                }
+            } else {
+                Application {
+                    stage: ApplicationStage::Inactive {
+                        deactivation_initiated: 1,
+                        deactivated: 1,
+                        cause: ApplicationDeactivationCause::External,
+                    },
+                    ..old_application_state
+                }
+            }
+        } else {
+            old_application_state
+        };
+
+        assert_eq!(expected_application_state, actual_application_state);
+    }
+
+    fn assert_opening_content(
+        &self,
+        opening_id: OpeningId,
+        old_opening: Opening<Balance, BlockNumber, ApplicationId>,
+        actual_result: Result<(), DeactivateApplicationError>,
+    ) {
+        // invalid opening stages are not supported
+
+        // check for opening existence
+        if !<OpeningById<Test>>::exists(opening_id) {
+            return;
+        }
+
+        // check only for active opening
+        if let OpeningStage::Active {
+            stage,
+            applications_added,
+            active_application_count,
+            unstaking_application_count,
+            deactivated_application_count,
+        } = old_opening.stage
+        {
+            // check only for accepting application stage
+            if let ActiveOpeningStage::AcceptingApplications { .. } = stage {
+                let mut expected_active_application_count = active_application_count;
+                let mut expected_deactivated_application_count = deactivated_application_count;
+                let mut expected_unstaking_application_count = unstaking_application_count;
+
+                if actual_result.is_ok() {
+                    expected_active_application_count -= 1;
+
+                    if old_opening.application_staking_policy.is_some()
+                        || old_opening.role_staking_policy.is_some()
+                    {
+                        expected_unstaking_application_count += 1;
+                    } else {
+                        expected_deactivated_application_count += 1;
+                    }
+                }
+                let expected_opening = Opening {
+                    stage: OpeningStage::Active {
+                        stage: ActiveOpeningStage::AcceptingApplications {
+                            started_accepting_applicants_at_block: 1,
+                        },
+                        applications_added,
+                        active_application_count: expected_active_application_count,
+                        unstaking_application_count: expected_unstaking_application_count,
+                        deactivated_application_count: expected_deactivated_application_count,
+                    },
+                    ..old_opening
+                };
+
+                let new_opening_state = <OpeningById<Test>>::get(opening_id);
+                assert_eq!(new_opening_state, expected_opening);
+            }
+        }
+    }
+}
+
+#[test]
+fn deactivate_application_succeeds() {
+    build_test_externalities().execute_with(|| {
+        let opening_fixture = AddOpeningFixture::default();
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+        let app_application_result = application_fixture.add_application();
+        let application_id = app_application_result.unwrap().application_id_added;
+
+        let deactivate_application_fixture =
+            DeactivateApplicationFixture::default_for_application_id(application_id);
+
+        deactivate_application_fixture.call_and_assert(Ok(()));
+    });
+}
+
+#[test]
+fn deactivate_application_fails_with_no_application() {
+    build_test_externalities().execute_with(|| {
+        let deactivate_application_fixture =
+            DeactivateApplicationFixture::default_for_application_id(0);
+
+        deactivate_application_fixture
+            .call_and_assert(Err(DeactivateApplicationError::ApplicationDoesNotExist));
+    });
+}
+
+#[test]
+fn deactivate_application_fails_with_redundant_application_staking_period_provided() {
+    build_test_externalities().execute_with(|| {
+        let opening_fixture = AddOpeningFixture::default();
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+        let app_application_result = application_fixture.add_application();
+        let application_id = app_application_result.unwrap().application_id_added;
+
+        let mut deactivate_application_fixture =
+            DeactivateApplicationFixture::default_for_application_id(application_id);
+        deactivate_application_fixture.application_stake_unstaking_period = Some(3);
+
+        deactivate_application_fixture.call_and_assert(Err(
+            DeactivateApplicationError::RedundantUnstakingPeriodProvided(StakePurpose::Application),
+        ))
+    });
+}
+
+#[test]
+fn deactivate_application_fails_with_redundant_role_staking_period_provided() {
+    build_test_externalities().execute_with(|| {
+        let opening_fixture = AddOpeningFixture::default();
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+        let app_application_result = application_fixture.add_application();
+        let application_id = app_application_result.unwrap().application_id_added;
+
+        let mut deactivate_application_fixture =
+            DeactivateApplicationFixture::default_for_application_id(application_id);
+        deactivate_application_fixture.role_stake_unstaking_period = Some(3);
+
+        deactivate_application_fixture.call_and_assert(Err(
+            DeactivateApplicationError::RedundantUnstakingPeriodProvided(StakePurpose::Role),
+        ))
+    });
+}
+
+#[test]
+fn deactivate_application_fails_for_already_deactivated_application() {
+    build_test_externalities().execute_with(|| {
+        let opening_fixture = AddOpeningFixture::default();
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+        let app_application_result = application_fixture.add_application();
+        let application_id = app_application_result.unwrap().application_id_added;
+
+        let deactivate_application_fixture =
+            DeactivateApplicationFixture::default_for_application_id(application_id);
+
+        // deactivate first
+        assert!(deactivate_application_fixture
+            .deactivate_application()
+            .is_ok());
+
+        // try again to deactivate
+        deactivate_application_fixture
+            .call_and_assert(Err(DeactivateApplicationError::ApplicationNotActive))
+    });
+}
+
+#[test]
+fn deactivate_application_for_opening_not_accepting_applications() {
+    build_test_externalities().execute_with(|| {
+        let opening_fixture = AddOpeningFixture::default();
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+        let app_application_result = application_fixture.add_application();
+        let application_id = app_application_result.unwrap().application_id_added;
+
+        assert!(Hiring::begin_review(opening_id).is_ok());
+
+        let deactivate_application_fixture =
+            DeactivateApplicationFixture::default_for_application_id(application_id);
+
+        deactivate_application_fixture.call_and_assert(Err(
+            DeactivateApplicationError::OpeningNotAcceptingApplications,
+        ))
+    });
+}
+
+#[test]
+fn deactivate_application_fails_with_too_short_application_unstaking_period_provided() {
+    build_test_externalities().execute_with(|| {
+        let opening_fixture = AddOpeningFixture::default();
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+        let app_application_result = application_fixture.add_application();
+        let application_id = app_application_result.unwrap().application_id_added;
+
+        let mut deactivate_application_fixture =
+            DeactivateApplicationFixture::default_for_application_id(application_id);
+        deactivate_application_fixture.application_stake_unstaking_period = Some(0);
+
+        deactivate_application_fixture.call_and_assert(Err(
+            DeactivateApplicationError::UnstakingPeriodTooShort(StakePurpose::Application),
+        ))
+    });
+}
+
+#[test]
+fn deactivate_application_fails_with_too_short_role_unstaking_period_provided() {
+    build_test_externalities().execute_with(|| {
+        let opening_fixture = AddOpeningFixture::default();
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+        let app_application_result = application_fixture.add_application();
+        let application_id = app_application_result.unwrap().application_id_added;
+
+        let mut deactivate_application_fixture =
+            DeactivateApplicationFixture::default_for_application_id(application_id);
+        deactivate_application_fixture.role_stake_unstaking_period = Some(0);
+
+        deactivate_application_fixture.call_and_assert(Err(
+            DeactivateApplicationError::UnstakingPeriodTooShort(StakePurpose::Role),
+        ))
+    });
+}
+
+#[test]
+fn deactivate_application_succeeds_with_application_stake_checks() {
+    handle_mock(|| {
+        build_test_externalities().execute_with(|| {
+            let mock = default_mock_for_creating_stake();
+            set_stake_handler_impl(mock.clone());
+
+            let mut opening_fixture = AddOpeningFixture::default();
+            opening_fixture.application_staking_policy = Some(StakingPolicy {
+                amount: 100,
+                amount_mode: StakingAmountLimitMode::AtLeast,
+                crowded_out_unstaking_period_length: None,
+                review_period_expired_unstaking_period_length: None,
+            });
+            let add_opening_result = opening_fixture.add_opening();
+            let opening_id = add_opening_result.unwrap();
+
+            let mut application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+            application_fixture.opt_application_stake_imbalance =
+                Some(stake::NegativeImbalance::<Test>::new(100));
+            let app_application_result = application_fixture.add_application();
+            let application_id = app_application_result.unwrap().application_id_added;
+
+            let deactivate_application_fixture =
+                DeactivateApplicationFixture::default_for_application_id(application_id);
+
+            let mock2 = default_mock_for_unstaking();
+            set_stake_handler_impl(mock2.clone());
+
+            deactivate_application_fixture.call_and_assert(Ok(()));
+
+            TestApplicationDeactivatedHandler::assert_deactivated_application(
+                application_id,
+                ApplicationDeactivationCause::External,
+            );
+        });
+    });
+}
+
+#[test]
+fn deactivate_application_succeeds_with_role_stake_checks() {
+    handle_mock(|| {
+        build_test_externalities().execute_with(|| {
+            let mock = default_mock_for_creating_stake();
+            set_stake_handler_impl(mock.clone());
+
+            let mut opening_fixture = AddOpeningFixture::default();
+            opening_fixture.role_staking_policy = Some(StakingPolicy {
+                amount: 100,
+                amount_mode: StakingAmountLimitMode::AtLeast,
+                crowded_out_unstaking_period_length: None,
+                review_period_expired_unstaking_period_length: None,
+            });
+            let add_opening_result = opening_fixture.add_opening();
+            let opening_id = add_opening_result.unwrap();
+
+            let mut application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+            application_fixture.opt_role_stake_imbalance =
+                Some(stake::NegativeImbalance::<Test>::new(100));
+            let app_application_result = application_fixture.add_application();
+            let application_id = app_application_result.unwrap().application_id_added;
+
+            let deactivate_application_fixture =
+                DeactivateApplicationFixture::default_for_application_id(application_id);
+
+            let mock2 = default_mock_for_unstaking();
+            set_stake_handler_impl(mock2.clone());
+
+            deactivate_application_fixture.call_and_assert(Ok(()))
+        });
+    });
+}

+ 365 - 0
runtime-modules/hiring/src/test/public_api/ensure_can_add_application.rs

@@ -0,0 +1,365 @@
+use crate::mock::*;
+use crate::test::*;
+
+use crate::hiring::*;
+use rstd::collections::btree_set::BTreeSet;
+
+#[test]
+fn ensure_can_add_application_fails_with_no_opening() {
+    build_test_externalities().execute_with(|| {
+        assert_eq!(
+            Hiring::ensure_can_add_application(2, None, None),
+            Err(AddApplicationError::OpeningDoesNotExist)
+        );
+    });
+}
+
+#[test]
+fn ensure_can_add_application_fails_with_redundant_role_stake() {
+    build_test_externalities().execute_with(|| {
+        let opening_fixture = AddOpeningFixture::default();
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        assert_eq!(
+            Hiring::ensure_can_add_application(opening_id, Some(200), None),
+            Err(AddApplicationError::StakeProvidedWhenRedundant(
+                StakePurpose::Role
+            ))
+        );
+    });
+}
+
+#[test]
+fn ensure_can_add_application_fails_with_too_low_role_stake_amout() {
+    build_test_externalities().execute_with(|| {
+        let mut opening_fixture = AddOpeningFixture::default();
+        opening_fixture.role_staking_policy = Some(StakingPolicy {
+            amount: 100,
+            amount_mode: StakingAmountLimitMode::Exact,
+            crowded_out_unstaking_period_length: None,
+            review_period_expired_unstaking_period_length: None,
+        });
+
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        assert_eq!(
+            Hiring::ensure_can_add_application(opening_id, Some(200), None),
+            Err(AddApplicationError::StakeAmountTooLow(StakePurpose::Role))
+        );
+    });
+}
+
+#[test]
+fn ensure_can_add_application_fails_with_missing_role_stake_when_required() {
+    build_test_externalities().execute_with(|| {
+        //**** stake provided when redundant
+        let mut opening_fixture = AddOpeningFixture::default();
+        opening_fixture.role_staking_policy = Some(StakingPolicy {
+            amount: 100,
+            amount_mode: StakingAmountLimitMode::Exact,
+            crowded_out_unstaking_period_length: None,
+            review_period_expired_unstaking_period_length: None,
+        });
+
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        assert_eq!(
+            Hiring::ensure_can_add_application(opening_id, None, None),
+            Err(AddApplicationError::StakeMissingWhenRequired(
+                StakePurpose::Role
+            ))
+        );
+    });
+}
+
+#[test]
+fn ensure_can_add_application_fails_with_redundant_application_stake() {
+    build_test_externalities().execute_with(|| {
+        let opening_fixture = AddOpeningFixture::default();
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        assert_eq!(
+            Hiring::ensure_can_add_application(opening_id, None, Some(200),),
+            Err(AddApplicationError::StakeProvidedWhenRedundant(
+                StakePurpose::Application
+            ))
+        );
+    });
+}
+
+#[test]
+fn ensure_can_add_application_fails_with_too_low_application_stake_amout() {
+    build_test_externalities().execute_with(|| {
+        let mut opening_fixture = AddOpeningFixture::default();
+        opening_fixture.application_staking_policy = Some(StakingPolicy {
+            amount: 100,
+            amount_mode: StakingAmountLimitMode::Exact,
+            crowded_out_unstaking_period_length: None,
+            review_period_expired_unstaking_period_length: None,
+        });
+
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        assert_eq!(
+            Hiring::ensure_can_add_application(opening_id, None, Some(200),),
+            Err(AddApplicationError::StakeAmountTooLow(
+                StakePurpose::Application
+            ))
+        );
+    });
+}
+
+#[test]
+fn ensure_can_add_application_fails_with_missing_application_stake_when_required() {
+    build_test_externalities().execute_with(|| {
+        //**** stake provided when redundant
+        let mut opening_fixture = AddOpeningFixture::default();
+        opening_fixture.application_staking_policy = Some(StakingPolicy {
+            amount: 100,
+            amount_mode: StakingAmountLimitMode::Exact,
+            crowded_out_unstaking_period_length: None,
+            review_period_expired_unstaking_period_length: None,
+        });
+
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        assert_eq!(
+            Hiring::ensure_can_add_application(opening_id, None, None),
+            Err(AddApplicationError::StakeMissingWhenRequired(
+                StakePurpose::Application
+            ))
+        );
+    });
+}
+
+#[test]
+fn ensure_can_add_application_fails_with_non_active_opening() {
+    build_test_externalities().execute_with(|| {
+        let mut opening_fixture = AddOpeningFixture::default();
+        opening_fixture.activate_at = ActivateOpeningAt::ExactBlock(22);
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        assert_eq!(
+            Hiring::ensure_can_add_application(opening_id, None, None),
+            Err(AddApplicationError::OpeningNotInAcceptingApplicationsStage)
+        );
+    });
+}
+
+#[test]
+fn ensure_can_add_application_fails_with_non_accepting_application_stage() {
+    build_test_externalities().execute_with(|| {
+        let opening_fixture = AddOpeningFixture::default();
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        assert_eq!(Hiring::begin_review(opening_id), Ok(()));
+
+        assert_eq!(
+            Hiring::ensure_can_add_application(opening_id, None, None),
+            Err(AddApplicationError::OpeningNotInAcceptingApplicationsStage)
+        );
+    });
+}
+
+#[test]
+fn ensure_can_add_application_succeeds_with_application_rationing_policy() {
+    build_test_externalities().execute_with(|| {
+        let mut opening_fixture = AddOpeningFixture::default();
+        opening_fixture.application_rationing_policy = Some(hiring::ApplicationRationingPolicy {
+            max_active_applicants: 1,
+        });
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        assert!(Hiring::ensure_can_add_application(opening_id, None, None).is_ok(),);
+    });
+}
+
+#[test]
+fn ensure_can_add_application_fails_with_application_rationing_policy() {
+    build_test_externalities().execute_with(|| {
+        let mut opening_fixture = AddOpeningFixture::default();
+        opening_fixture.application_rationing_policy = Some(hiring::ApplicationRationingPolicy {
+            max_active_applicants: 1,
+        });
+        opening_fixture.application_staking_policy = Some(StakingPolicy {
+            amount: 100,
+            amount_mode: StakingAmountLimitMode::AtLeast,
+            crowded_out_unstaking_period_length: None,
+            review_period_expired_unstaking_period_length: None,
+        });
+
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let mut application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+        application_fixture.opt_application_stake_imbalance =
+            Some(stake::NegativeImbalance::<Test>::new(100));
+
+        assert!(application_fixture.add_application().is_ok());
+
+        assert_eq!(
+            Hiring::ensure_can_add_application(opening_id, None, Some(100)),
+            Err(AddApplicationError::NewApplicationWasCrowdedOut)
+        );
+    });
+}
+
+#[test]
+fn ensure_can_add_application_succeeds() {
+    build_test_externalities().execute_with(|| {
+        let mut opening_fixture = AddOpeningFixture::default();
+        opening_fixture.application_staking_policy = Some(StakingPolicy {
+            amount: 100,
+            amount_mode: StakingAmountLimitMode::Exact,
+            crowded_out_unstaking_period_length: None,
+            review_period_expired_unstaking_period_length: None,
+        });
+        opening_fixture.role_staking_policy = Some(StakingPolicy {
+            amount: 100,
+            amount_mode: StakingAmountLimitMode::Exact,
+            crowded_out_unstaking_period_length: None,
+            review_period_expired_unstaking_period_length: None,
+        });
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let ensure_can_add_application_result =
+            Hiring::ensure_can_add_application(opening_id, Some(100), Some(100));
+
+        assert_eq!(
+            ensure_can_add_application_result,
+            Ok(DestructuredApplicationCanBeAddedEvaluation {
+                opening: Opening {
+                    created: 1,
+                    stage: hiring::OpeningStage::Active {
+                        stage: hiring::ActiveOpeningStage::AcceptingApplications {
+                            started_accepting_applicants_at_block: 1
+                        },
+                        applications_added: BTreeSet::new(),
+                        active_application_count: 0,
+                        unstaking_application_count: 0,
+                        deactivated_application_count: 0
+                    },
+                    max_review_period_length: 672,
+                    application_rationing_policy: None,
+                    application_staking_policy: Some(StakingPolicy {
+                        amount: 100,
+                        amount_mode: StakingAmountLimitMode::Exact,
+                        crowded_out_unstaking_period_length: None,
+                        review_period_expired_unstaking_period_length: None
+                    }),
+                    role_staking_policy: Some(StakingPolicy {
+                        amount: 100,
+                        amount_mode: StakingAmountLimitMode::Exact,
+                        crowded_out_unstaking_period_length: None,
+                        review_period_expired_unstaking_period_length: None
+                    }),
+                    human_readable_text: HUMAN_READABLE_TEXT.to_vec()
+                },
+                active_stage: hiring::ActiveOpeningStage::AcceptingApplications {
+                    started_accepting_applicants_at_block: 1
+                },
+                applications_added: BTreeSet::new(),
+                active_application_count: 0,
+                unstaking_application_count: 0,
+                deactivated_application_count: 0,
+                would_get_added_success: ApplicationAddedSuccess::Unconditionally
+            })
+        );
+    });
+}
+
+#[test]
+fn ensure_can_add_application_new_application_should_be_crowded_out_with_exact_stake() {
+    ensure_can_add_application_new_application_should_be_crowded_out_with_staking_policy(
+        StakingPolicy {
+            amount: 100,
+            amount_mode: StakingAmountLimitMode::Exact,
+            crowded_out_unstaking_period_length: None,
+            review_period_expired_unstaking_period_length: None,
+        },
+    );
+}
+
+#[test]
+fn ensure_can_add_application_new_application_should_be_crowded_out_with_atleast_stake() {
+    ensure_can_add_application_new_application_should_be_crowded_out_with_staking_policy(
+        StakingPolicy {
+            amount: 100,
+            amount_mode: StakingAmountLimitMode::AtLeast,
+            crowded_out_unstaking_period_length: None,
+            review_period_expired_unstaking_period_length: None,
+        },
+    );
+}
+
+fn ensure_can_add_application_new_application_should_be_crowded_out_with_staking_policy(
+    staking_policy: StakingPolicy<Balance, BlockNumber>,
+) {
+    build_test_externalities().execute_with(|| {
+        let mut opening_fixture = AddOpeningFixture::default();
+        opening_fixture.application_rationing_policy = Some(hiring::ApplicationRationingPolicy {
+            max_active_applicants: 1,
+        });
+        opening_fixture.application_staking_policy = Some(staking_policy);
+
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let mut application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+        application_fixture.opt_application_stake_imbalance =
+            Some(stake::NegativeImbalance::<Test>::new(100));
+
+        assert!(application_fixture.add_application().is_ok());
+
+        assert_eq!(
+            Hiring::ensure_can_add_application(opening_id, None, Some(100)),
+            Err(AddApplicationError::NewApplicationWasCrowdedOut)
+        );
+    });
+}
+
+#[test]
+fn ensure_can_add_application_should_crowd_out_application() {
+    build_test_externalities().execute_with(|| {
+        let mut opening_fixture = AddOpeningFixture::default();
+        opening_fixture.application_rationing_policy = Some(hiring::ApplicationRationingPolicy {
+            max_active_applicants: 1,
+        });
+        opening_fixture.application_staking_policy = Some(StakingPolicy {
+            amount: 100,
+            amount_mode: StakingAmountLimitMode::AtLeast,
+            crowded_out_unstaking_period_length: None,
+            review_period_expired_unstaking_period_length: None,
+        });
+
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let mut application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+        application_fixture.opt_application_stake_imbalance =
+            Some(stake::NegativeImbalance::<Test>::new(100));
+
+        assert!(application_fixture.add_application().is_ok());
+
+        let destructered_app_result = Hiring::ensure_can_add_application(opening_id, None, Some(101));
+        assert!(destructered_app_result.is_ok());
+
+        let destructered_app = destructered_app_result.unwrap();
+
+        if let ApplicationAddedSuccess::CrowdsOutExistingApplication(application_id) = destructered_app.would_get_added_success {
+            assert_eq!(0, application_id);
+        } else {
+            panic!("Expected ApplicationAddedSuccess::CrowdsOutExistingApplication(application_id == 0)")
+        }
+    });
+}

+ 474 - 0
runtime-modules/hiring/src/test/public_api/fill_opening.rs

@@ -0,0 +1,474 @@
+use crate::mock::*;
+use crate::test::*;
+
+use crate::test::public_api::*;
+use rstd::collections::btree_set::BTreeSet;
+use rstd::result::Result;
+
+/*
+Not covered:
+- Application content check
+*/
+
+pub struct FillOpeningFixture {
+    pub opening_id: OpeningId,
+    pub successful_applications: BTreeSet<<mock::Test as Trait>::ApplicationId>,
+    pub opt_successful_applicant_application_stake_unstaking_period: Option<BlockNumber>,
+    pub opt_failed_applicant_application_stake_unstaking_period: Option<BlockNumber>,
+    pub opt_failed_applicant_role_stake_unstaking_period: Option<BlockNumber>,
+}
+
+impl FillOpeningFixture {
+    pub(crate) fn default_for_opening(opening_id: OpeningId) -> Self {
+        FillOpeningFixture {
+            opening_id,
+            successful_applications: BTreeSet::new(),
+            opt_successful_applicant_application_stake_unstaking_period: None,
+            opt_failed_applicant_application_stake_unstaking_period: None,
+            opt_failed_applicant_role_stake_unstaking_period: None,
+        }
+    }
+
+    fn call_and_assert(&self, expected_result: Result<(), FillOpeningError<mock::Test>>) {
+        let old_opening = <OpeningById<Test>>::get(self.opening_id);
+        let old_applications = self.extract_successful_applications();
+
+        let fill_opening_result = self.fill_opening();
+
+        assert_eq!(fill_opening_result, expected_result);
+
+        self.assert_opening_content(old_opening, fill_opening_result.clone());
+
+        if !fill_opening_result.is_ok() {
+            self.assert_same_applications(old_applications);
+        }
+    }
+
+    fn assert_same_applications(
+        &self,
+        old_applications: BTreeMap<ApplicationId, Application<OpeningId, BlockNumber, StakeId>>,
+    ) {
+        for (app_id, application) in old_applications {
+            let test_application = <ApplicationById<Test>>::get(app_id);
+            assert_eq!(application, test_application)
+        }
+    }
+
+    fn extract_successful_applications(
+        &self,
+    ) -> BTreeMap<ApplicationId, Application<OpeningId, BlockNumber, StakeId>> {
+        self.successful_applications
+            .iter()
+            .map(|app_id| (*app_id, <ApplicationById<Test>>::get(app_id)))
+            .collect::<BTreeMap<ApplicationId, Application<OpeningId, BlockNumber, StakeId>>>()
+    }
+
+    pub(crate) fn fill_opening(&self) -> Result<(), FillOpeningError<mock::Test>> {
+        Hiring::fill_opening(
+            self.opening_id,
+            self.successful_applications.clone(),
+            self.opt_successful_applicant_application_stake_unstaking_period,
+            self.opt_failed_applicant_application_stake_unstaking_period,
+            self.opt_failed_applicant_role_stake_unstaking_period,
+        )
+    }
+
+    fn assert_opening_content(
+        &self,
+        old_opening: Opening<Balance, BlockNumber, ApplicationId>,
+        fill_opening_result: Result<(), FillOpeningError<mock::Test>>,
+    ) {
+        let new_opening = <OpeningById<Test>>::get(self.opening_id);
+        let mut expected_opening = old_opening.clone();
+
+        if fill_opening_result.is_ok() {
+            if let hiring::OpeningStage::Active {
+                applications_added, ..
+            } = old_opening.stage
+            {
+                // compose expected stage
+                let expected_active_stage = ActiveOpeningStage::Deactivated {
+                    cause: OpeningDeactivationCause::Filled,
+                    deactivated_at_block: 1,
+                    started_accepting_applicants_at_block: 1,
+                    started_review_period_at_block: Some(1),
+                };
+
+                // calculate application counters
+                let mut deactivated_app_count = 0;
+                let mut unstaking_app_count = 0;
+                for app_id in applications_added.clone() {
+                    let application = <ApplicationById<Test>>::get(app_id);
+
+                    match application.stage {
+                        ApplicationStage::Active => panic!("Cannot be in active stage"),
+                        ApplicationStage::Inactive { .. } => {
+                            deactivated_app_count += 1;
+                        }
+                        ApplicationStage::Unstaking { .. } => {
+                            unstaking_app_count += 1;
+                        }
+                    }
+                }
+
+                expected_opening.stage = hiring::OpeningStage::Active {
+                    stage: expected_active_stage,
+                    applications_added,
+                    active_application_count: 0,
+                    unstaking_application_count: unstaking_app_count,
+                    deactivated_application_count: deactivated_app_count,
+                };
+            } else {
+                panic!("old opening stage MUST be active")
+            }
+        };
+        assert_eq!(expected_opening, new_opening);
+    }
+}
+
+#[test]
+fn fill_opening_fails_due_to_opening_not_existing() {
+    build_test_externalities().execute_with(|| {
+        let fill_opening_fixture = FillOpeningFixture::default_for_opening(0);
+        fill_opening_fixture.call_and_assert(Err(FillOpeningError::OpeningDoesNotExist));
+    });
+}
+
+#[test]
+fn fill_opening_fails_due_to_opening_is_not_active() {
+    build_test_externalities().execute_with(|| {
+        let mut opening_fixture = AddOpeningFixture::default();
+        opening_fixture.activate_at = ActivateOpeningAt::ExactBlock(5);
+
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let fill_opening_fixture = FillOpeningFixture::default_for_opening(opening_id);
+        fill_opening_fixture.call_and_assert(Err(FillOpeningError::OpeningNotInReviewPeriodStage));
+    });
+}
+
+#[test]
+fn fill_opening_fails_due_to_redundant_successful_application_unstaking_period() {
+    build_test_externalities().execute_with(|| {
+        let opening_fixture = AddOpeningFixture::default();
+
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+        assert!(Hiring::begin_review(opening_id).is_ok());
+
+        let mut fill_opening_fixture = FillOpeningFixture::default_for_opening(opening_id);
+        fill_opening_fixture.opt_successful_applicant_application_stake_unstaking_period = Some(50);
+        fill_opening_fixture.call_and_assert(Err(
+            FillOpeningError::RedundantUnstakingPeriodProvided(
+                StakePurpose::Application,
+                ApplicationOutcomeInFilledOpening::Success,
+            ),
+        ));
+    });
+}
+
+#[test]
+fn fill_opening_fails_due_too_short_successful_application_unstaking_period() {
+    build_test_externalities().execute_with(|| {
+        let opening_fixture = AddOpeningFixture::default();
+
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+        assert!(Hiring::begin_review(opening_id).is_ok());
+
+        let mut fill_opening_fixture = FillOpeningFixture::default_for_opening(opening_id);
+        fill_opening_fixture.opt_successful_applicant_application_stake_unstaking_period = Some(0);
+        fill_opening_fixture.call_and_assert(Err(FillOpeningError::UnstakingPeriodTooShort(
+            StakePurpose::Application,
+            ApplicationOutcomeInFilledOpening::Success,
+        )));
+    });
+}
+
+#[test]
+fn fill_opening_fails_due_to_redundant_failed_application_unstaking_period() {
+    build_test_externalities().execute_with(|| {
+        let opening_fixture = AddOpeningFixture::default();
+
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+        assert!(Hiring::begin_review(opening_id).is_ok());
+
+        let mut fill_opening_fixture = FillOpeningFixture::default_for_opening(opening_id);
+        fill_opening_fixture.opt_failed_applicant_application_stake_unstaking_period = Some(50);
+        fill_opening_fixture.call_and_assert(Err(
+            FillOpeningError::RedundantUnstakingPeriodProvided(
+                StakePurpose::Application,
+                ApplicationOutcomeInFilledOpening::Failure,
+            ),
+        ));
+    });
+}
+
+#[test]
+fn fill_opening_fails_due_too_short_failed_application_unstaking_period() {
+    build_test_externalities().execute_with(|| {
+        let opening_fixture = AddOpeningFixture::default();
+
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+        assert!(Hiring::begin_review(opening_id).is_ok());
+
+        let mut fill_opening_fixture = FillOpeningFixture::default_for_opening(opening_id);
+        fill_opening_fixture.opt_failed_applicant_application_stake_unstaking_period = Some(0);
+        fill_opening_fixture.call_and_assert(Err(FillOpeningError::UnstakingPeriodTooShort(
+            StakePurpose::Application,
+            ApplicationOutcomeInFilledOpening::Failure,
+        )));
+    });
+}
+
+#[test]
+fn fill_opening_fails_due_to_redundant_failed_role_unstaking_period() {
+    build_test_externalities().execute_with(|| {
+        let opening_fixture = AddOpeningFixture::default();
+
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+        assert!(Hiring::begin_review(opening_id).is_ok());
+
+        let mut fill_opening_fixture = FillOpeningFixture::default_for_opening(opening_id);
+        fill_opening_fixture.opt_failed_applicant_role_stake_unstaking_period = Some(50);
+        fill_opening_fixture.call_and_assert(Err(
+            FillOpeningError::RedundantUnstakingPeriodProvided(
+                StakePurpose::Role,
+                ApplicationOutcomeInFilledOpening::Failure,
+            ),
+        ));
+    });
+}
+
+#[test]
+fn fill_opening_fails_due_too_short_failed_role_unstaking_period() {
+    build_test_externalities().execute_with(|| {
+        let opening_fixture = AddOpeningFixture::default();
+
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+        assert!(Hiring::begin_review(opening_id).is_ok());
+
+        let mut fill_opening_fixture = FillOpeningFixture::default_for_opening(opening_id);
+        fill_opening_fixture.opt_failed_applicant_role_stake_unstaking_period = Some(0);
+        fill_opening_fixture.call_and_assert(Err(FillOpeningError::UnstakingPeriodTooShort(
+            StakePurpose::Role,
+            ApplicationOutcomeInFilledOpening::Failure,
+        )));
+    });
+}
+
+#[test]
+fn fill_opening_fails_due_not_existing_application() {
+    build_test_externalities().execute_with(|| {
+        let opening_fixture = AddOpeningFixture::default();
+
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+        assert!(Hiring::begin_review(opening_id).is_ok());
+
+        let mut fill_opening_fixture = FillOpeningFixture::default_for_opening(opening_id);
+        let mut apps = BTreeSet::new();
+        let invalid_app_id = 10;
+        apps.insert(invalid_app_id);
+        fill_opening_fixture.successful_applications = apps;
+        fill_opening_fixture.call_and_assert(Err(FillOpeningError::ApplicationDoesNotExist(
+            invalid_app_id,
+        )));
+    });
+}
+
+#[test]
+fn fill_opening_fails_due_not_active_application() {
+    build_test_externalities().execute_with(|| {
+        let opening_fixture = AddOpeningFixture::default();
+
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+        let app_application_result = application_fixture.add_application();
+        let application_id = app_application_result.unwrap().application_id_added;
+
+        let deactivate_application_fixture =
+            DeactivateApplicationFixture::default_for_application_id(application_id);
+
+        assert!(deactivate_application_fixture
+            .deactivate_application()
+            .is_ok());
+        assert!(Hiring::begin_review(opening_id).is_ok());
+
+        let mut fill_opening_fixture = FillOpeningFixture::default_for_opening(opening_id);
+        let mut apps = BTreeSet::new();
+        apps.insert(application_id);
+
+        fill_opening_fixture.successful_applications = apps;
+        fill_opening_fixture.call_and_assert(Err(FillOpeningError::ApplicationNotInActiveStage(
+            application_id,
+        )));
+    });
+}
+
+#[test]
+fn fill_opening_succeeds() {
+    build_test_externalities().execute_with(|| {
+        let opening_fixture = AddOpeningFixture::default();
+
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+        let to_deactivate_app_result = application_fixture.add_application();
+        let to_deactivate_app_id = to_deactivate_app_result.unwrap().application_id_added;
+
+        assert!(Hiring::deactive_application(to_deactivate_app_id, None, None).is_ok());
+
+        let app_application_result = application_fixture.add_application();
+        let application_id = app_application_result.unwrap().application_id_added;
+
+        assert!(Hiring::begin_review(opening_id).is_ok());
+
+        let mut fill_opening_fixture = FillOpeningFixture::default_for_opening(opening_id);
+        let mut apps = BTreeSet::new();
+        apps.insert(application_id);
+
+        fill_opening_fixture.successful_applications = apps;
+        fill_opening_fixture.call_and_assert(Ok(()));
+    });
+}
+
+#[test]
+fn fill_opening_succeeds_with_application_stake_checks() {
+    handle_mock(|| {
+        build_test_externalities().execute_with(|| {
+            let mock = default_mock_for_creating_stake();
+            set_stake_handler_impl(mock.clone());
+
+            let mut opening_fixture = AddOpeningFixture::default();
+            opening_fixture.application_staking_policy = Some(StakingPolicy {
+                amount: 100,
+                amount_mode: StakingAmountLimitMode::AtLeast,
+                crowded_out_unstaking_period_length: None,
+                review_period_expired_unstaking_period_length: None,
+            });
+
+            let add_opening_result = opening_fixture.add_opening();
+            let opening_id = add_opening_result.unwrap();
+
+            let mut application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+            application_fixture.opt_application_stake_imbalance =
+                Some(stake::NegativeImbalance::<Test>::new(100));
+            let app_result = application_fixture.add_application();
+            let application_id = app_result.unwrap().application_id_added;
+
+            assert!(Hiring::begin_review(opening_id).is_ok());
+
+            let mut fill_opening_fixture = FillOpeningFixture::default_for_opening(opening_id);
+            let mut apps = BTreeSet::new();
+            apps.insert(application_id);
+
+            fill_opening_fixture.successful_applications = apps;
+
+            let mock2 = default_mock_for_unstaking();
+            set_stake_handler_impl(mock2.clone());
+
+            fill_opening_fixture.call_and_assert(Ok(()));
+
+            TestApplicationDeactivatedHandler::assert_deactivated_application(
+                application_id,
+                ApplicationDeactivationCause::Hired,
+            );
+        });
+    });
+}
+
+#[test]
+fn fill_opening_succeeds_with_not_role_stake_unstaked() {
+    handle_mock(|| {
+        build_test_externalities().execute_with(|| {
+            let mock = default_mock_for_creating_stake();
+            set_stake_handler_impl(mock.clone());
+
+            let mut opening_fixture = AddOpeningFixture::default();
+            opening_fixture.role_staking_policy = Some(StakingPolicy {
+                amount: 100,
+                amount_mode: StakingAmountLimitMode::AtLeast,
+                crowded_out_unstaking_period_length: None,
+                review_period_expired_unstaking_period_length: None,
+            });
+
+            let add_opening_result = opening_fixture.add_opening();
+            let opening_id = add_opening_result.unwrap();
+
+            let mut application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+            application_fixture.opt_role_stake_imbalance =
+                Some(stake::NegativeImbalance::<Test>::new(100));
+            let app_result = application_fixture.add_application();
+            let application_id = app_result.unwrap().application_id_added;
+
+            assert!(Hiring::begin_review(opening_id).is_ok());
+
+            let mut fill_opening_fixture = FillOpeningFixture::default_for_opening(opening_id);
+            let mut apps = BTreeSet::new();
+            apps.insert(application_id);
+
+            fill_opening_fixture.successful_applications = apps;
+
+            let mock2 = {
+                let mut mock = crate::MockStakeHandler::<Test>::new();
+                mock.expect_stake_exists().returning(|_| true);
+
+                mock.expect_get_stake().returning(|_| stake::Stake {
+                    created: 1,
+                    staking_status: stake::StakingStatus::Staked(stake::StakedState {
+                        staked_amount: 100,
+                        staked_status: stake::StakedStatus::Normal,
+                        next_slash_id: 0,
+                        ongoing_slashes: BTreeMap::new(),
+                    }),
+                });
+
+                Rc::new(RefCell::new(mock))
+            };
+            set_stake_handler_impl(mock2.clone());
+
+            fill_opening_fixture.call_and_assert(Ok(()));
+        });
+    });
+}
+
+#[test]
+fn fill_opening_fails_with_application_to_wrong_opening() {
+    build_test_externalities().execute_with(|| {
+        // opening 1
+        let opening_fixture = AddOpeningFixture::default();
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+        // opening 2
+        let opening_fixture2 = AddOpeningFixture::default();
+        let add_opening_result2 = opening_fixture2.add_opening();
+        let opening_id2 = add_opening_result2.unwrap();
+
+        // application to opening 2
+        let application_fixture = AddApplicationFixture::default_for_opening(opening_id2);
+        let app_application_result = application_fixture.add_application();
+        let application_id = app_application_result.unwrap().application_id_added;
+
+        // opening 1 being review
+        assert!(Hiring::begin_review(opening_id).is_ok());
+
+        // fill opening 1 with application made to opening 2 (which should fail)
+        let mut fill_opening_fixture = FillOpeningFixture::default_for_opening(opening_id);
+        let mut apps = BTreeSet::new();
+        apps.insert(application_id);
+
+        fill_opening_fixture.successful_applications = apps;
+        fill_opening_fixture.call_and_assert(Err(FillOpeningError::ApplicationForWrongOpening(
+            application_id,
+        )));
+    });
+}

+ 50 - 0
runtime-modules/hiring/src/test/public_api/mod.rs

@@ -0,0 +1,50 @@
+mod add_application;
+mod add_opening;
+mod deactivate_application;
+
+mod begin_accepting_applications;
+mod begin_review;
+mod cancel_opening;
+mod ensure_can_add_application;
+mod fill_opening;
+mod on_finalize;
+mod unstaked;
+
+pub use add_application::AddApplicationFixture;
+pub use add_opening::{AddOpeningFixture, HUMAN_READABLE_TEXT};
+pub use deactivate_application::DeactivateApplicationFixture;
+
+use crate::mock::Test;
+use rstd::cell::RefCell;
+use rstd::collections::btree_map::BTreeMap;
+use rstd::rc::Rc;
+
+fn default_mock_for_creating_stake() -> Rc<RefCell<crate::MockStakeHandler<Test>>> {
+    let mut mock = crate::MockStakeHandler::<Test>::new();
+
+    mock.expect_stake().times(1).returning(|_, _| Ok(()));
+    mock.expect_create_stake().times(1).returning(|| 0);
+
+    Rc::new(rstd::cell::RefCell::new(mock))
+}
+
+fn default_mock_for_unstaking() -> Rc<RefCell<crate::MockStakeHandler<Test>>> {
+    let mut mock = crate::MockStakeHandler::<Test>::new();
+    mock.expect_stake_exists().returning(|_| true);
+
+    mock.expect_initiate_unstaking()
+        .times(1)
+        .returning(|_, _| Ok(()));
+
+    mock.expect_get_stake().returning(|_| stake::Stake {
+        created: 1,
+        staking_status: stake::StakingStatus::Staked(stake::StakedState {
+            staked_amount: 100,
+            staked_status: stake::StakedStatus::Normal,
+            next_slash_id: 0,
+            ongoing_slashes: BTreeMap::new(),
+        }),
+    });
+
+    Rc::new(RefCell::new(mock))
+}

+ 177 - 0
runtime-modules/hiring/src/test/public_api/on_finalize.rs

@@ -0,0 +1,177 @@
+use crate::mock::*;
+use crate::test::*;
+
+use runtime_primitives::traits::{OnFinalize, OnInitialize};
+
+// 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());
+        <Hiring 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());
+        <Hiring as OnInitialize<u64>>::on_initialize(System::block_number());
+    }
+}
+
+fn run_to_block_and_finalize(n: u64) {
+    run_to_block(n);
+    <Hiring as OnFinalize<u64>>::on_finalize(n);
+}
+
+#[test]
+fn on_finalize_should_work_on_empty_data() {
+    build_test_externalities().execute_with(|| {
+        run_to_block_and_finalize(42);
+    });
+}
+
+#[test]
+fn on_finalize_should_not_change_opening_prematurely() {
+    build_test_externalities().execute_with(|| {
+        let opening_activation_block = 2;
+        let mut opening_fixture = AddOpeningFixture::default();
+        opening_fixture.activate_at = ActivateOpeningAt::ExactBlock(opening_activation_block);
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let old_opening = <OpeningById<Test>>::get(opening_id);
+
+        let block_to_finalize = opening_activation_block - 1;
+        run_to_block_and_finalize(block_to_finalize);
+
+        let new_opening = <OpeningById<Test>>::get(opening_id);
+
+        assert_eq!(old_opening, new_opening);
+    });
+}
+
+#[test]
+fn on_finalize_should_change_waiting_to_begin_opening_stage_to_active_and_acception_application() {
+    build_test_externalities().execute_with(|| {
+        let opening_activation_block = 2;
+        let mut opening_fixture = AddOpeningFixture::default();
+        opening_fixture.activate_at = ActivateOpeningAt::ExactBlock(opening_activation_block);
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let old_opening = <OpeningById<Test>>::get(opening_id);
+        // ensure preparation are valid
+        if let OpeningStage::WaitingToBegin { .. } = old_opening.stage {
+            // expected
+        } else {
+            panic!("planned to be WaitingToBegin")
+        }
+
+        let block_to_finalize = opening_activation_block;
+        run_to_block_and_finalize(block_to_finalize);
+
+        // ensure on_finalize worked
+        let new_opening = <OpeningById<Test>>::get(opening_id);
+        if let OpeningStage::Active { stage, .. } = new_opening.stage {
+            if let ActiveOpeningStage::AcceptingApplications { .. } = stage {
+                // expected
+            } else {
+                panic!("should be AcceptingApplications")
+            }
+        } else {
+            panic!("should be Active")
+        }
+    });
+}
+
+#[test]
+fn on_finalize_should_not_change_opening_to_begin_review_stage_prematurely() {
+    build_test_externalities().execute_with(|| {
+        let mut opening_fixture = AddOpeningFixture::default();
+        opening_fixture.max_review_period_length = 2;
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        assert!(Hiring::begin_review(opening_id).is_ok());
+
+        let old_opening = <OpeningById<Test>>::get(opening_id);
+
+        let block_to_finalize = opening_fixture.max_review_period_length - 1;
+        run_to_block_and_finalize(block_to_finalize);
+
+        let new_opening = <OpeningById<Test>>::get(opening_id);
+
+        assert_eq!(old_opening, new_opening);
+    });
+}
+
+#[test]
+fn on_finalize_should_deactivate_opening_on_begin_review_stage() {
+    build_test_externalities().execute_with(|| {
+        let mut opening_fixture = AddOpeningFixture::default();
+        opening_fixture.max_review_period_length = 2;
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        assert!(Hiring::begin_review(opening_id).is_ok());
+
+        let old_opening = <OpeningById<Test>>::get(opening_id);
+        if let OpeningStage::Active { stage, .. } = old_opening.stage {
+            if let ActiveOpeningStage::ReviewPeriod { .. } = stage {
+                // expected
+            } else {
+                panic!("should be BeginReview")
+            }
+        } else {
+            panic!("should be Active")
+        }
+
+        let block_to_finalize = opening_fixture.max_review_period_length + 1;
+        run_to_block_and_finalize(block_to_finalize);
+
+        let new_opening = <OpeningById<Test>>::get(opening_id);
+        if let OpeningStage::Active { stage, .. } = new_opening.stage {
+            if let ActiveOpeningStage::Deactivated { .. } = stage {
+                // expected
+            } else {
+                panic!("should be Deactivated")
+            }
+        } else {
+            panic!("should be Active")
+        }
+    });
+}
+
+#[test]
+fn on_finalize_should_deactivate_application_with_review_period_expired_cause() {
+    build_test_externalities().execute_with(|| {
+        let mut opening_fixture = AddOpeningFixture::default();
+        opening_fixture.max_review_period_length = 2;
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+        let app_application_result = application_fixture.add_application();
+        let application_id = app_application_result.unwrap().application_id_added;
+
+        assert!(Hiring::begin_review(opening_id).is_ok());
+
+        let old_application = <ApplicationById<Test>>::get(application_id);
+        if let ApplicationStage::Active = old_application.stage {
+            // expected
+        } else {
+            panic!("should be Active")
+        }
+
+        let block_to_finalize = opening_fixture.max_review_period_length + 1;
+        run_to_block_and_finalize(block_to_finalize);
+
+        let new_application = <ApplicationById<Test>>::get(application_id);
+        if let ApplicationStage::Inactive { cause, .. } = new_application.stage {
+            if let ApplicationDeactivationCause::ReviewPeriodExpired = cause {
+                // expected
+            } else {
+                panic!("should be ReviewPeriodExpired")
+            }
+        } else {
+            panic!("should be Active")
+        }
+    });
+}

+ 302 - 0
runtime-modules/hiring/src/test/public_api/unstaked.rs

@@ -0,0 +1,302 @@
+use crate::mock::*;
+use crate::test::public_api::*;
+use crate::test::*;
+
+pub struct UnstakedFixture {
+    pub opening_id: OpeningId,
+    pub application_id: ApplicationId,
+    pub stake_id: StakeId,
+}
+
+impl UnstakedFixture {
+    fn call_and_assert(&self, expected_result: UnstakedResult) {
+        let old_opening = <OpeningById<Test>>::get(self.opening_id);
+        let old_application = <ApplicationById<Test>>::get(self.application_id);
+
+        let unstaked_result = self.unstaked();
+
+        assert_eq!(unstaked_result, expected_result);
+
+        self.assert_application_content(old_application.clone(), unstaked_result);
+
+        self.assert_opening_content(old_opening, unstaked_result);
+    }
+
+    pub(crate) fn unstaked(&self) -> UnstakedResult {
+        Hiring::unstaked(self.stake_id)
+    }
+
+    fn assert_application_content(
+        &self,
+        old_application: Application<OpeningId, BlockNumber, StakeId>,
+        unstaked_result: UnstakedResult,
+    ) {
+        let new_application = <ApplicationById<Test>>::get(self.application_id);
+
+        match unstaked_result {
+            UnstakedResult::StakeIdNonExistent | UnstakedResult::ApplicationIsNotUnstaking => {
+                assert_eq!(old_application, new_application);
+            }
+            UnstakedResult::UnstakingInProgress => {
+                let expected_active_role_staking_id =
+                    old_application.toggle_stake_id(self.stake_id, StakePurpose::Role);
+                let expected_active_application_staking_id =
+                    old_application.toggle_stake_id(self.stake_id, StakePurpose::Application);
+                let expected_application = hiring::Application {
+                    active_role_staking_id: expected_active_role_staking_id,
+                    active_application_staking_id: expected_active_application_staking_id,
+                    ..old_application
+                };
+                assert_eq!(expected_application, new_application);
+            }
+            UnstakedResult::Unstaked => {
+                let expected_active_role_staking_id =
+                    old_application.toggle_stake_id(self.stake_id, StakePurpose::Role);
+                let expected_active_application_staking_id =
+                    old_application.toggle_stake_id(self.stake_id, StakePurpose::Application);
+
+                if let ApplicationStage::Unstaking {
+                    deactivation_initiated,
+                    cause,
+                } = old_application.stage
+                {
+                    let expected_application_stage = ApplicationStage::Inactive {
+                        deactivation_initiated,
+                        deactivated: 1,
+                        cause,
+                    };
+
+                    let expected_application = hiring::Application {
+                        active_role_staking_id: expected_active_role_staking_id,
+                        active_application_staking_id: expected_active_application_staking_id,
+                        stage: expected_application_stage,
+                        ..old_application
+                    };
+                    assert_eq!(expected_application, new_application);
+                } else {
+                    panic!("old application should be in Unstaking stage")
+                }
+            }
+        };
+    }
+
+    fn assert_opening_content(
+        &self,
+        old_opening: Opening<Balance, BlockNumber, ApplicationId>,
+        unstaked_result: UnstakedResult,
+    ) {
+        let new_opening = <OpeningById<Test>>::get(self.opening_id);
+        let mut expected_opening = old_opening.clone();
+
+        if let UnstakedResult::Unstaked = unstaked_result {
+            if let hiring::OpeningStage::Active {
+                stage,
+                applications_added,
+                active_application_count,
+                unstaking_application_count,
+                deactivated_application_count,
+            } = old_opening.stage
+            {
+                expected_opening.stage = hiring::OpeningStage::Active {
+                    stage,
+                    applications_added,
+                    active_application_count,
+                    unstaking_application_count: unstaking_application_count - 1,
+                    deactivated_application_count: deactivated_application_count + 1,
+                };
+            } else {
+                panic!("old opening stage MUST be active")
+            }
+        };
+
+        assert_eq!(expected_opening, new_opening);
+    }
+}
+
+#[test]
+fn unstaked_returns_non_existent_stake_id() {
+    build_test_externalities().execute_with(|| {
+        let unstaked_fixture = UnstakedFixture {
+            opening_id: 0,
+            application_id: 0,
+            stake_id: 0,
+        };
+
+        unstaked_fixture.call_and_assert(UnstakedResult::StakeIdNonExistent);
+    });
+}
+
+#[test]
+fn unstaked_returns_application_is_not_unstaking() {
+    build_test_externalities().execute_with(|| {
+        let mut opening_fixture = AddOpeningFixture::default();
+        opening_fixture.application_staking_policy = Some(StakingPolicy {
+            amount: 100,
+            amount_mode: StakingAmountLimitMode::AtLeast,
+            crowded_out_unstaking_period_length: None,
+            review_period_expired_unstaking_period_length: None,
+        });
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let mut application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+        application_fixture.opt_application_stake_imbalance =
+            Some(stake::NegativeImbalance::<Test>::new(100));
+        let app_application_result = application_fixture.add_application();
+        let application_id = app_application_result.unwrap().application_id_added;
+        let application = <ApplicationById<Test>>::get(application_id);
+        let stake_id = application.active_application_staking_id.unwrap();
+
+        let unstaked_fixture = UnstakedFixture {
+            opening_id,
+            application_id,
+            stake_id,
+        };
+
+        unstaked_fixture.call_and_assert(UnstakedResult::ApplicationIsNotUnstaking);
+    });
+}
+
+#[test]
+fn unstaked_returns_unstaking_in_progress() {
+    build_test_externalities().execute_with(|| {
+        let mut opening_fixture = AddOpeningFixture::default();
+        opening_fixture.application_rationing_policy = Some(hiring::ApplicationRationingPolicy {
+            max_active_applicants: 1,
+        });
+        opening_fixture.application_staking_policy = Some(StakingPolicy {
+            amount: 100,
+            amount_mode: StakingAmountLimitMode::AtLeast,
+            crowded_out_unstaking_period_length: None,
+            review_period_expired_unstaking_period_length: None,
+        });
+        opening_fixture.role_staking_policy = Some(StakingPolicy {
+            amount: 100,
+            amount_mode: StakingAmountLimitMode::AtLeast,
+            crowded_out_unstaking_period_length: None,
+            review_period_expired_unstaking_period_length: None,
+        });
+
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let mut application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+        application_fixture.opt_application_stake_imbalance =
+            Some(stake::NegativeImbalance::<Test>::new(100));
+        application_fixture.opt_role_stake_imbalance =
+            Some(stake::NegativeImbalance::<Test>::new(100));
+
+        let app_application_result = application_fixture.add_application();
+        assert!(app_application_result.is_ok());
+        let first_application_id = app_application_result.unwrap().application_id_added;
+
+        application_fixture.opt_application_stake_imbalance =
+            Some(stake::NegativeImbalance::<Test>::new(101));
+        application_fixture.opt_role_stake_imbalance =
+            Some(stake::NegativeImbalance::<Test>::new(101));
+
+        assert!(application_fixture.add_application().is_ok());
+
+        let application = <ApplicationById<Test>>::get(first_application_id);
+        let stake_id = application.active_application_staking_id.unwrap();
+
+        let unstaked_fixture = UnstakedFixture {
+            opening_id,
+            application_id: first_application_id,
+            stake_id,
+        };
+
+        unstaked_fixture.call_and_assert(UnstakedResult::UnstakingInProgress);
+    });
+}
+
+#[test]
+fn unstaked_returns_unstaked() {
+    build_test_externalities().execute_with(|| {
+        let mut opening_fixture = AddOpeningFixture::default();
+        opening_fixture.application_rationing_policy = Some(hiring::ApplicationRationingPolicy {
+            max_active_applicants: 1,
+        });
+        opening_fixture.application_staking_policy = Some(StakingPolicy {
+            amount: 100,
+            amount_mode: StakingAmountLimitMode::AtLeast,
+            crowded_out_unstaking_period_length: None,
+            review_period_expired_unstaking_period_length: None,
+        });
+
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let mut application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+        application_fixture.opt_application_stake_imbalance =
+            Some(stake::NegativeImbalance::<Test>::new(100));
+
+        let app_application_result = application_fixture.add_application();
+        assert!(app_application_result.is_ok());
+        let first_application_id = app_application_result.unwrap().application_id_added;
+
+        application_fixture.opt_application_stake_imbalance =
+            Some(stake::NegativeImbalance::<Test>::new(101));
+
+        assert!(application_fixture.add_application().is_ok());
+
+        let application = <ApplicationById<Test>>::get(first_application_id);
+        let stake_id = application.active_application_staking_id.unwrap();
+
+        let unstaked_fixture = UnstakedFixture {
+            opening_id,
+            application_id: first_application_id,
+            stake_id,
+        };
+
+        unstaked_fixture.call_and_assert(UnstakedResult::Unstaked);
+
+        TestApplicationDeactivatedHandler::assert_deactivated_application(
+            first_application_id,
+            ApplicationDeactivationCause::CrowdedOut,
+        );
+    });
+}
+
+#[test]
+fn unstaked_returns_unstaked_with_stake_checks() {
+    build_test_externalities().execute_with(|| {
+        let mut opening_fixture = AddOpeningFixture::default();
+        opening_fixture.application_rationing_policy = Some(hiring::ApplicationRationingPolicy {
+            max_active_applicants: 1,
+        });
+        opening_fixture.application_staking_policy = Some(StakingPolicy {
+            amount: 100,
+            amount_mode: StakingAmountLimitMode::AtLeast,
+            crowded_out_unstaking_period_length: None,
+            review_period_expired_unstaking_period_length: None,
+        });
+
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let mut application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+        application_fixture.opt_application_stake_imbalance =
+            Some(stake::NegativeImbalance::<Test>::new(100));
+
+        let app_application_result = application_fixture.add_application();
+        assert!(app_application_result.is_ok());
+        let first_application_id = app_application_result.unwrap().application_id_added;
+
+        application_fixture.opt_application_stake_imbalance =
+            Some(stake::NegativeImbalance::<Test>::new(101));
+
+        assert!(application_fixture.add_application().is_ok());
+
+        let application = <ApplicationById<Test>>::get(first_application_id);
+        let stake_id = application.active_application_staking_id.unwrap();
+
+        let unstaked_fixture = UnstakedFixture {
+            opening_id,
+            application_id: first_application_id,
+            stake_id,
+        };
+
+        unstaked_fixture.call_and_assert(UnstakedResult::Unstaked);
+    });
+}

+ 249 - 0
runtime-modules/hiring/src/test/smoke.rs

@@ -0,0 +1,249 @@
+use super::*;
+use crate::mock::*;
+
+static FIRST_BLOCK_HEIGHT: <Test as system::Trait>::BlockNumber = 1;
+
+use rstd::collections::btree_set::BTreeSet;
+
+/**
+Main hiring workflow:
+1. add_opening
+1.1 (optional) ensure_can_add_application
+2. add_application
+3. begin_review
+4. fill_opening
+Not covered:
+- begin_accepting_applications
+- unstaked
+- cancel_opening
+- deactive_application
+
+Smoke-test concept:
+https://en.wikipedia.org/wiki/Smoke_testing_(software)
+**/
+#[test]
+fn full_hiring_workflow_successful_path() {
+    build_test_externalities().execute_with(|| {
+        // 1. Add opening
+        let expected_opening_id = 0;
+
+        let activation_at = ActivateOpeningAt::CurrentBlock;
+        let max_review_period_length = 672;
+        let application_rationing_policy = None;
+        let application_staking_policy = None;
+        let role_staking_policy = None;
+        let human_readable_text = "human_readable_text!!!".as_bytes().to_vec();
+        let application_readable_text = "hrt!!".as_bytes().to_vec();
+
+        // Add an opening, check that the returned value is Zero
+        assert_eq!(
+            Hiring::add_opening(
+                activation_at,
+                max_review_period_length,
+                application_rationing_policy,
+                application_staking_policy,
+                role_staking_policy,
+                human_readable_text.clone()
+            ),
+            Ok(expected_opening_id)
+        );
+
+        // Check that next opening id has been updated
+        assert_eq!(Hiring::next_opening_id(), expected_opening_id + 1);
+
+        // Check that our opening actually was added
+        assert!(<OpeningById<Test>>::exists(expected_opening_id));
+
+        let found_opening = Hiring::opening_by_id(expected_opening_id);
+
+        // Check opening content
+        assert_eq!(
+            found_opening,
+            Opening {
+                created: FIRST_BLOCK_HEIGHT,
+                stage: OpeningStage::Active {
+                    stage: ActiveOpeningStage::AcceptingApplications {
+                        started_accepting_applicants_at_block: FIRST_BLOCK_HEIGHT
+                    },
+                    applications_added: BTreeSet::new(),
+                    active_application_count: 0,
+                    unstaking_application_count: 0,
+                    deactivated_application_count: 0
+                },
+                max_review_period_length,
+                application_rationing_policy: None,
+                application_staking_policy: None,
+                role_staking_policy: None,
+                human_readable_text: human_readable_text.clone()
+            }
+        );
+        let current_opening_id = expected_opening_id;
+
+        // 1.1 (optional) ensure_can_add_application
+        let ensure_can_add_application_result =
+            Hiring::ensure_can_add_application(current_opening_id, None, None);
+
+        // Check ensure_can_add_application result
+        assert!(ensure_can_add_application_result.is_ok());
+
+        // Check returned content
+        let destructured_app_data = ensure_can_add_application_result.unwrap();
+        let expected = DestructuredApplicationCanBeAddedEvaluation {
+            opening: Opening {
+                created: 1,
+                stage: OpeningStage::Active {
+                    stage: ActiveOpeningStage::AcceptingApplications {
+                        started_accepting_applicants_at_block: 1,
+                    },
+                    applications_added: BTreeSet::new(),
+                    active_application_count: 0,
+                    unstaking_application_count: 0,
+                    deactivated_application_count: 0,
+                },
+                max_review_period_length: 672,
+                application_rationing_policy: None,
+                application_staking_policy: None,
+                role_staking_policy: None,
+                human_readable_text: human_readable_text.clone(),
+            },
+            active_stage: ActiveOpeningStage::AcceptingApplications {
+                started_accepting_applicants_at_block: 1,
+            },
+            applications_added: BTreeSet::new(),
+            active_application_count: 0,
+            unstaking_application_count: 0,
+            deactivated_application_count: 0,
+            would_get_added_success: ApplicationAddedSuccess::Unconditionally,
+        };
+
+        assert_eq!(destructured_app_data, expected);
+
+        // 2. add_application
+        let add_application_result = Hiring::add_application(
+            current_opening_id,
+            None,
+            None,
+            application_readable_text.clone(),
+        );
+
+        // Check add_application result
+        assert!(add_application_result.is_ok());
+
+        // Check that application wasn't crowded_out
+        let app_added = add_application_result.unwrap();
+        assert_eq!(app_added.application_id_crowded_out, None);
+
+        let new_application_id = app_added.application_id_added;
+
+        // Check that our application actually was added
+        assert!(<ApplicationById<Test>>::exists(new_application_id));
+
+        let new_application = Hiring::application_by_id(new_application_id);
+
+        // Check application content
+        assert_eq!(
+            new_application,
+            Application {
+                opening_id: 0,
+                application_index_in_opening: 0,
+                add_to_opening_in_block: 1,
+                active_role_staking_id: None,
+                active_application_staking_id: None,
+                stage: ApplicationStage::Active,
+                human_readable_text: application_readable_text.clone()
+            }
+        );
+
+        // 3. begin_review
+        let begin_review_result = Hiring::begin_review(current_opening_id);
+
+        // Check begin_review result
+        assert!(begin_review_result.is_ok());
+
+        let updated_opening_after_begin_review = Hiring::opening_by_id(current_opening_id);
+
+        let mut expected_added_apps_in_opening = BTreeSet::new();
+        expected_added_apps_in_opening.insert(0);
+
+        // Check updated opening content
+        assert_eq!(
+            updated_opening_after_begin_review,
+            Opening {
+                created: 1,
+                stage: OpeningStage::Active {
+                    stage: ActiveOpeningStage::ReviewPeriod {
+                        started_accepting_applicants_at_block: 1,
+                        started_review_period_at_block: 1
+                    },
+                    applications_added: expected_added_apps_in_opening.clone(),
+                    active_application_count: 1,
+                    unstaking_application_count: 0,
+                    deactivated_application_count: 0
+                },
+                max_review_period_length,
+                application_rationing_policy: None,
+                application_staking_policy: None,
+                role_staking_policy: None,
+                human_readable_text: human_readable_text.clone()
+            }
+        );
+        // 4. fill_opening
+        let applications = [new_application_id]
+            .iter()
+            .map(|&x| x)
+            .collect::<BTreeSet<_>>();
+        let fill_opening_result =
+            Hiring::fill_opening(current_opening_id, applications, None, None, None);
+
+        // Check fill_opening result
+        assert!(fill_opening_result.is_ok());
+
+        let updated_opening_fill_opening = Hiring::opening_by_id(current_opening_id);
+
+        // Check updated opening content
+        assert_eq!(
+            updated_opening_fill_opening,
+            Opening {
+                created: 1,
+                stage: OpeningStage::Active {
+                    stage: ActiveOpeningStage::Deactivated {
+                        cause: OpeningDeactivationCause::Filled,
+                        deactivated_at_block: 1,
+                        started_accepting_applicants_at_block: 1,
+                        started_review_period_at_block: Some(1)
+                    },
+                    applications_added: expected_added_apps_in_opening,
+                    active_application_count: 0,
+                    unstaking_application_count: 0,
+                    deactivated_application_count: 1
+                },
+                max_review_period_length,
+                application_rationing_policy: None,
+                application_staking_policy: None,
+                role_staking_policy: None,
+                human_readable_text: human_readable_text.clone()
+            }
+        );
+
+        let current_application_id = new_application_id;
+        let application_after_fill_opening = Hiring::application_by_id(current_application_id);
+
+        // Check updated application content
+        assert_eq!(
+            application_after_fill_opening,
+            Application {
+                opening_id: 0,
+                application_index_in_opening: 0,
+                add_to_opening_in_block: 1,
+                active_role_staking_id: None,
+                active_application_staking_id: None,
+                stage: ApplicationStage::Inactive {
+                    deactivation_initiated: 1,
+                    deactivated: 1,
+                    cause: ApplicationDeactivationCause::Hired
+                },
+                human_readable_text: application_readable_text.clone()
+            }
+        );
+    });
+}

+ 83 - 0
runtime-modules/hiring/src/test/staking_module/get_opt_stake_amount.rs

@@ -0,0 +1,83 @@
+use super::*;
+use crate::mock::*;
+
+use mockall::predicate::*;
+
+#[test]
+fn get_opt_stake_amount_succeeds_with_empty_stake_id() {
+    build_test_externalities().execute_with(|| {
+        let no_stake = Hiring::get_opt_stake_amount(None);
+
+        assert_eq!(no_stake, 0)
+    })
+}
+
+#[test]
+#[should_panic]
+fn get_opt_stake_amount_panics_with_non_existing_stake() {
+    build_test_externalities().execute_with(|| {
+        Hiring::get_opt_stake_amount(Some(0));
+    });
+}
+
+#[test]
+#[should_panic]
+fn get_opt_stake_amount_panics_with_incorrect_stake_status() {
+    handle_mock(|| {
+        build_test_externalities().execute_with(|| {
+            let mock = {
+                let mut mock = crate::MockStakeHandler::<Test>::new();
+                mock.expect_stake_exists()
+                    .with(eq(0))
+                    .times(1)
+                    .returning(|_| true);
+
+                mock.expect_get_stake()
+                    .with(eq(0))
+                    .times(1)
+                    .returning(|_| stake::Stake {
+                        created: 1,
+                        staking_status: stake::StakingStatus::NotStaked,
+                    });
+
+                Rc::new(RefCell::new(mock))
+            };
+            set_stake_handler_impl(mock);
+
+            Hiring::get_opt_stake_amount(Some(0));
+        })
+    })
+}
+
+#[test]
+fn get_opt_stake_amount_succeeds_with_existing_stake() {
+    handle_mock(|| {
+        build_test_externalities().execute_with(|| {
+            let mock = {
+                let mut mock = crate::MockStakeHandler::<Test>::new();
+                mock.expect_stake_exists()
+                    .with(eq(0))
+                    .times(1)
+                    .returning(|_| true);
+
+                mock.expect_get_stake()
+                    .with(eq(0))
+                    .times(1)
+                    .returning(|_| stake::Stake {
+                        created: 1,
+                        staking_status: stake::StakingStatus::Staked(stake::StakedState {
+                            staked_amount: 100,
+                            staked_status: stake::StakedStatus::Normal,
+                            next_slash_id: 0,
+                            ongoing_slashes: BTreeMap::new(),
+                        }),
+                    });
+
+                Rc::new(RefCell::new(mock))
+            };
+            set_stake_handler_impl(mock.clone());
+
+            assert_eq!(Hiring::get_opt_stake_amount(Some(0)), 100);
+        })
+    });
+}

+ 72 - 0
runtime-modules/hiring/src/test/staking_module/infallible_stake_initiation_on_application.rs

@@ -0,0 +1,72 @@
+use super::*;
+use crate::mock::*;
+
+#[test]
+#[should_panic]
+fn infallible_stake_initiation_on_application_panics_on_existed_stake() {
+    handle_mock(|| {
+        build_test_externalities().execute_with(|| {
+            let mock = {
+                let mut mock = crate::MockStakeHandler::<Test>::new();
+                mock.expect_create_stake().times(1).returning(|| 10);
+                Rc::new(RefCell::new(mock))
+            };
+            set_stake_handler_impl(mock);
+
+            let application_id = 10;
+            <ApplicationIdByStakingId<Test>>::insert(10, application_id);
+
+            Hiring::infallible_stake_initiation_on_application(
+                stake::NegativeImbalance::<Test>::new(100),
+                &application_id,
+            );
+        });
+    });
+}
+
+#[test]
+#[should_panic]
+fn infallible_stake_initiation_on_application_panics_on_unsuccessful_staking() {
+    handle_mock(|| {
+        build_test_externalities().execute_with(|| {
+            let mock = {
+                let mut mock = crate::MockStakeHandler::<Test>::new();
+                mock.expect_create_stake().times(1).returning(|| 10);
+                mock.expect_stake()
+                    .times(1)
+                    .returning(|_, _| Err(StakeActionError::StakeNotFound));
+                Rc::new(RefCell::new(mock))
+            };
+            set_stake_handler_impl(mock);
+
+            let application_id = 10;
+            Hiring::infallible_stake_initiation_on_application(
+                stake::NegativeImbalance::<Test>::new(100),
+                &application_id,
+            );
+        });
+    });
+}
+
+#[test]
+fn infallible_stake_initiation_on_application_succeeds() {
+    handle_mock(|| {
+        build_test_externalities().execute_with(|| {
+            let mock = {
+                let mut mock = crate::MockStakeHandler::<Test>::new();
+                mock.expect_create_stake().times(1).returning(|| 10);
+                mock.expect_stake().times(1).returning(|_, _| Ok(()));
+                Rc::new(RefCell::new(mock))
+            };
+            set_stake_handler_impl(mock);
+
+            let application_id = 10;
+            let staking_result = Hiring::infallible_stake_initiation_on_application(
+                stake::NegativeImbalance::<Test>::new(100),
+                &application_id,
+            );
+
+            assert_eq!(staking_result, 10);
+        });
+    });
+}

+ 111 - 0
runtime-modules/hiring/src/test/staking_module/initiate_application_deactivations.rs

@@ -0,0 +1,111 @@
+use super::*;
+use crate::mock::*;
+
+type ApplicationMap = BTreeMap<ApplicationId, hiring::Application<OpeningId, BlockNumber, StakeId>>;
+
+struct InitiateApplicationDeactivationsFixture {
+    pub applications: ApplicationMap,
+    pub application_stake_unstaking_period: Option<BlockNumber>,
+    pub role_stake_unstaking_period: Option<BlockNumber>,
+    pub cause: ApplicationDeactivationCause,
+}
+
+impl InitiateApplicationDeactivationsFixture {
+    fn default_for_applications(
+        applications: ApplicationMap,
+    ) -> InitiateApplicationDeactivationsFixture {
+        InitiateApplicationDeactivationsFixture {
+            applications,
+            application_stake_unstaking_period: None,
+            role_stake_unstaking_period: None,
+            cause: ApplicationDeactivationCause::External,
+        }
+    }
+
+    fn initiate_application_deactivations(&self) -> ApplicationsDeactivationsInitiationResult {
+        Hiring::initiate_application_deactivations(
+            &self.applications,
+            self.application_stake_unstaking_period,
+            self.role_stake_unstaking_period,
+            self.cause,
+        )
+    }
+
+    fn call_and_assert(&self, expected_result: ApplicationsDeactivationsInitiationResult) {
+        let actual_result = self.initiate_application_deactivations();
+
+        assert_eq!(expected_result, actual_result);
+    }
+}
+
+#[test]
+fn initiate_application_deactivations_succeeds_with_deactivated_result() {
+    build_test_externalities().execute_with(|| {
+        let opening_fixture = AddOpeningFixture::default();
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+        let app_application_result = application_fixture.add_application();
+        let application_id = app_application_result.unwrap().application_id_added;
+
+        let application = <ApplicationById<Test>>::get(application_id);
+
+        let mut applications = ApplicationMap::new();
+        applications.insert(application_id, application);
+
+        let iad_fixture =
+            InitiateApplicationDeactivationsFixture::default_for_applications(applications);
+
+        iad_fixture.call_and_assert(ApplicationsDeactivationsInitiationResult {
+            number_of_deactivated_applications: 1,
+            number_of_unstaking_applications: 0,
+        });
+    });
+}
+
+#[test]
+fn initiate_application_deactivations_succeeds_with_unstaking_result() {
+    handle_mock(|| {
+        build_test_externalities().execute_with(|| {
+            let mut opening_fixture = AddOpeningFixture::default();
+            opening_fixture.application_staking_policy = Some(StakingPolicy {
+                amount: 100,
+                amount_mode: StakingAmountLimitMode::AtLeast,
+                crowded_out_unstaking_period_length: None,
+                review_period_expired_unstaking_period_length: None,
+            });
+
+            let add_opening_result = opening_fixture.add_opening();
+            let opening_id = add_opening_result.unwrap();
+
+            let mut application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+            application_fixture.opt_application_stake_imbalance =
+                Some(stake::NegativeImbalance::<Test>::new(100));
+            let app_application_result = application_fixture.add_application();
+            let application_id = app_application_result.unwrap().application_id_added;
+
+            let application = <ApplicationById<Test>>::get(application_id);
+
+            let mut applications = ApplicationMap::new();
+            applications.insert(application_id, application);
+
+            let iad_fixture =
+                InitiateApplicationDeactivationsFixture::default_for_applications(applications);
+
+            let mock = {
+                let mut mock = crate::MockStakeHandler::<Test>::new();
+                mock.expect_initiate_unstaking()
+                    .times(1)
+                    .returning(|_, _| Ok(()));
+                Rc::new(RefCell::new(mock))
+            };
+            set_stake_handler_impl(mock);
+
+            iad_fixture.call_and_assert(ApplicationsDeactivationsInitiationResult {
+                number_of_deactivated_applications: 0,
+                number_of_unstaking_applications: 1,
+            });
+        });
+    });
+}

+ 8 - 0
runtime-modules/hiring/src/test/staking_module/mod.rs

@@ -0,0 +1,8 @@
+mod get_opt_stake_amount;
+mod infallible_stake_initiation_on_application;
+mod initiate_application_deactivations;
+mod opt_infallible_unstake;
+mod try_to_initiate_application_deactivation;
+mod would_application_get_added;
+
+use super::*;

+ 39 - 0
runtime-modules/hiring/src/test/staking_module/opt_infallible_unstake.rs

@@ -0,0 +1,39 @@
+use super::*;
+use crate::mock::*;
+
+#[test]
+fn opt_infallible_unstake_succeeds_with_empty_stake() {
+    build_test_externalities().execute_with(|| {
+        let unstake_result = Hiring::opt_infallible_unstake(None, None);
+
+        assert_eq!(unstake_result, false);
+    });
+}
+
+#[test]
+#[should_panic]
+fn opt_infallible_unstake_panics_with_invalid_stake() {
+    build_test_externalities().execute_with(|| {
+        Hiring::opt_infallible_unstake(Some(10), None);
+    });
+}
+
+#[test]
+fn opt_infallible_unstake_succeeds() {
+    handle_mock(|| {
+        build_test_externalities().execute_with(|| {
+            let mock = {
+                let mut mock = crate::MockStakeHandler::<Test>::new();
+                mock.expect_initiate_unstaking()
+                    .times(1)
+                    .returning(|_, _| Ok(()));
+                Rc::new(RefCell::new(mock))
+            };
+            set_stake_handler_impl(mock);
+
+            let unstake_result = Hiring::opt_infallible_unstake(Some(10), Some(10));
+
+            assert_eq!(unstake_result, true);
+        });
+    });
+}

+ 391 - 0
runtime-modules/hiring/src/test/staking_module/try_to_initiate_application_deactivation.rs

@@ -0,0 +1,391 @@
+use super::*;
+use crate::mock::*;
+
+struct TryToInitiateApplicationDeactivationFixture<'a> {
+    pub application: &'a Application<OpeningId, BlockNumber, StakeId>,
+    pub application_id: ApplicationId,
+    pub application_stake_unstaking_period: Option<BlockNumber>,
+    pub role_stake_unstaking_period: Option<BlockNumber>,
+    pub cause: hiring::ApplicationDeactivationCause,
+}
+
+impl<'a> TryToInitiateApplicationDeactivationFixture<'a> {
+    pub fn default_for_application(
+        application_id: ApplicationId,
+        application: &'a Application<OpeningId, BlockNumber, StakeId>,
+    ) -> TryToInitiateApplicationDeactivationFixture<'a> {
+        TryToInitiateApplicationDeactivationFixture {
+            application,
+            application_id,
+            application_stake_unstaking_period: None,
+            role_stake_unstaking_period: None,
+            cause: ApplicationDeactivationCause::NotHired,
+        }
+    }
+
+    fn call_and_assert(&self, expected_result: ApplicationDeactivationInitiationResult) {
+        let old_application = <ApplicationById<Test>>::get(self.application_id);
+        let old_opening_state = <OpeningById<Test>>::get(old_application.opening_id);
+
+        let actual_result = self.try_to_initiate_application_deactivation();
+
+        assert_eq!(expected_result, actual_result);
+
+        self.assert_application_content(old_application.clone(), actual_result.clone());
+
+        self.assert_opening_content(old_application.opening_id, old_opening_state, actual_result);
+    }
+
+    fn try_to_initiate_application_deactivation(&self) -> ApplicationDeactivationInitiationResult {
+        Hiring::try_to_initiate_application_deactivation(
+            self.application,
+            self.application_id,
+            self.application_stake_unstaking_period,
+            self.role_stake_unstaking_period,
+            self.cause,
+        )
+    }
+
+    fn assert_application_content(
+        &self,
+        old_application_state: Application<OpeningId, BlockNumber, StakeId>,
+        result: ApplicationDeactivationInitiationResult,
+    ) {
+        let actual_application_state = <ApplicationById<Test>>::get(self.application_id);
+
+        let expected_application_state = match result {
+            ApplicationDeactivationInitiationResult::Deactivated => Application {
+                stage: ApplicationStage::Inactive {
+                    deactivation_initiated: 1,
+                    deactivated: 1,
+                    cause: self.cause,
+                },
+                ..old_application_state
+            },
+            ApplicationDeactivationInitiationResult::Unstaking => Application {
+                stage: ApplicationStage::Unstaking {
+                    deactivation_initiated: 1,
+                    cause: self.cause,
+                },
+                ..old_application_state
+            },
+            ApplicationDeactivationInitiationResult::Ignored => old_application_state,
+        };
+
+        assert_eq!(expected_application_state, actual_application_state);
+    }
+
+    fn assert_opening_content(
+        &self,
+        opening_id: OpeningId,
+        old_opening: Opening<Balance, BlockNumber, ApplicationId>,
+        result: ApplicationDeactivationInitiationResult,
+    ) {
+        // invalid opening stages are not supported
+
+        // check for opening existence
+        if !<OpeningById<Test>>::exists(opening_id) {
+            return;
+        }
+
+        // check only for active opening
+        if let OpeningStage::Active {
+            stage,
+            applications_added,
+            active_application_count,
+            unstaking_application_count,
+            deactivated_application_count,
+        } = old_opening.stage
+        {
+            // check only for accepting application stage
+            if let ActiveOpeningStage::AcceptingApplications { .. } = stage {
+                let mut expected_active_application_count = active_application_count;
+                let mut expected_deactivated_application_count = deactivated_application_count;
+                let mut expected_unstaking_application_count = unstaking_application_count;
+
+                match result {
+                    ApplicationDeactivationInitiationResult::Deactivated => {
+                        expected_active_application_count -= 1;
+                        expected_deactivated_application_count += 1;
+                    }
+                    ApplicationDeactivationInitiationResult::Unstaking => {
+                        expected_active_application_count -= 1;
+                        expected_unstaking_application_count += 1;
+                    }
+                    ApplicationDeactivationInitiationResult::Ignored => {}
+                }
+
+                let expected_opening = Opening {
+                    stage: OpeningStage::Active {
+                        stage: ActiveOpeningStage::AcceptingApplications {
+                            started_accepting_applicants_at_block: 1,
+                        },
+                        applications_added,
+                        active_application_count: expected_active_application_count,
+                        unstaking_application_count: expected_unstaking_application_count,
+                        deactivated_application_count: expected_deactivated_application_count,
+                    },
+                    ..old_opening
+                };
+
+                let new_opening_state = <OpeningById<Test>>::get(opening_id);
+                assert_eq!(new_opening_state, expected_opening);
+            }
+        }
+    }
+}
+
+#[test]
+fn try_to_initiate_application_deactivation_succeeds_with_ignored_result() {
+    build_test_externalities().execute_with(|| {
+        let opening_fixture = AddOpeningFixture::default();
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+        let app_application_result = application_fixture.add_application();
+        let application_id = app_application_result.unwrap().application_id_added;
+
+        let deactivate_application_fixture =
+            DeactivateApplicationFixture::default_for_application_id(application_id);
+
+        assert!(deactivate_application_fixture
+            .deactivate_application()
+            .is_ok());
+
+        let application = <ApplicationById<Test>>::get(application_id);
+        let ttiad_fixture = TryToInitiateApplicationDeactivationFixture::default_for_application(
+            application_id,
+            &application,
+        );
+
+        ttiad_fixture.call_and_assert(ApplicationDeactivationInitiationResult::Ignored);
+    });
+}
+
+#[test]
+#[should_panic]
+fn try_to_initiate_application_deactivation_panics_because_of_not_active_opening() {
+    build_test_externalities().execute_with(|| {
+        let opening_fixture = AddOpeningFixture::default();
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+        let app_application_result = application_fixture.add_application();
+        let application_id = app_application_result.unwrap().application_id_added;
+
+        let application = <ApplicationById<Test>>::get(application_id);
+
+        // provide incorrect opening (invalid stage)
+        let mut opening = <OpeningById<Test>>::get(opening_id);
+        opening.stage = OpeningStage::WaitingToBegin { begins_at_block: 0 };
+        <OpeningById<Test>>::insert(opening_id, opening);
+
+        let ttiad_fixture = TryToInitiateApplicationDeactivationFixture::default_for_application(
+            application_id,
+            &application,
+        );
+
+        ttiad_fixture.call_and_assert(ApplicationDeactivationInitiationResult::Ignored);
+    });
+}
+
+#[test]
+#[should_panic]
+fn try_to_initiate_application_deactivation_panics_because_of_opening_with_no_applications() {
+    build_test_externalities().execute_with(|| {
+        let opening_fixture = AddOpeningFixture::default();
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+        let app_application_result = application_fixture.add_application();
+        let application_id = app_application_result.unwrap().application_id_added;
+
+        let application = <ApplicationById<Test>>::get(application_id);
+
+        // provide incorrect opening (no applications)
+        let add_opening_result = opening_fixture.add_opening();
+        let new_opening_id = add_opening_result.unwrap();
+        let new_opening = <OpeningById<Test>>::get(new_opening_id);
+        <OpeningById<Test>>::insert(opening_id, new_opening);
+
+        let ttiad_fixture = TryToInitiateApplicationDeactivationFixture::default_for_application(
+            application_id,
+            &application,
+        );
+
+        ttiad_fixture.call_and_assert(ApplicationDeactivationInitiationResult::Ignored);
+    });
+}
+
+#[test]
+fn try_to_initiate_application_deactivation_succeeds_with_deactivated_result() {
+    build_test_externalities().execute_with(|| {
+        let opening_fixture = AddOpeningFixture::default();
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+        let app_application_result = application_fixture.add_application();
+        let application_id = app_application_result.unwrap().application_id_added;
+
+        let application = <ApplicationById<Test>>::get(application_id);
+
+        let ttiad_fixture = TryToInitiateApplicationDeactivationFixture::default_for_application(
+            application_id,
+            &application,
+        );
+
+        ttiad_fixture.call_and_assert(ApplicationDeactivationInitiationResult::Deactivated);
+    });
+}
+
+#[test]
+fn try_to_initiate_application_deactivation_succeeds_with_single_application_unstaking() {
+    handle_mock(|| {
+        build_test_externalities().execute_with(|| {
+            let mut opening_fixture = AddOpeningFixture::default();
+            opening_fixture.application_staking_policy = Some(StakingPolicy {
+                amount: 100,
+                amount_mode: StakingAmountLimitMode::AtLeast,
+                crowded_out_unstaking_period_length: None,
+                review_period_expired_unstaking_period_length: None,
+            });
+
+            let add_opening_result = opening_fixture.add_opening();
+            let opening_id = add_opening_result.unwrap();
+
+            let mut application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+            application_fixture.opt_application_stake_imbalance =
+                Some(stake::NegativeImbalance::<Test>::new(100));
+            let app_application_result = application_fixture.add_application();
+            let application_id = app_application_result.unwrap().application_id_added;
+
+            let application = <ApplicationById<Test>>::get(application_id);
+
+            let ttiad_fixture =
+                TryToInitiateApplicationDeactivationFixture::default_for_application(
+                    application_id,
+                    &application,
+                );
+
+            let mock = {
+                let mut mock = crate::MockStakeHandler::<Test>::new();
+                mock.expect_initiate_unstaking()
+                    .times(1)
+                    .returning(|_, _| Ok(()));
+                Rc::new(RefCell::new(mock))
+            };
+            set_stake_handler_impl(mock);
+
+            ttiad_fixture.call_and_assert(ApplicationDeactivationInitiationResult::Unstaking);
+        });
+    });
+}
+
+#[test]
+fn try_to_initiate_application_deactivation_succeeds_with_application_and_role_unstaking() {
+    handle_mock(|| {
+        build_test_externalities().execute_with(|| {
+            let mut opening_fixture = AddOpeningFixture::default();
+            opening_fixture.application_staking_policy = Some(StakingPolicy {
+                amount: 100,
+                amount_mode: StakingAmountLimitMode::AtLeast,
+                crowded_out_unstaking_period_length: None,
+                review_period_expired_unstaking_period_length: None,
+            });
+            opening_fixture.role_staking_policy = Some(StakingPolicy {
+                amount: 100,
+                amount_mode: StakingAmountLimitMode::AtLeast,
+                crowded_out_unstaking_period_length: None,
+                review_period_expired_unstaking_period_length: None,
+            });
+
+            let add_opening_result = opening_fixture.add_opening();
+            let opening_id = add_opening_result.unwrap();
+
+            let mut application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+            application_fixture.opt_application_stake_imbalance =
+                Some(stake::NegativeImbalance::<Test>::new(100));
+            application_fixture.opt_role_stake_imbalance =
+                Some(stake::NegativeImbalance::<Test>::new(100));
+
+            let app_application_result = application_fixture.add_application();
+            let application_id = app_application_result.unwrap().application_id_added;
+
+            let application = <ApplicationById<Test>>::get(application_id);
+
+            let ttiad_fixture =
+                TryToInitiateApplicationDeactivationFixture::default_for_application(
+                    application_id,
+                    &application,
+                );
+
+            let mock = {
+                let mut mock = crate::MockStakeHandler::<Test>::new();
+                mock.expect_initiate_unstaking()
+                    .times(2)
+                    .returning(|_, _| Ok(()));
+                Rc::new(RefCell::new(mock))
+            };
+            set_stake_handler_impl(mock);
+
+            ttiad_fixture.call_and_assert(ApplicationDeactivationInitiationResult::Unstaking);
+        });
+    });
+}
+
+#[test]
+fn try_to_initiate_application_deactivation_succeeds_hired_cause_and_application_only_unstaking() {
+    handle_mock(|| {
+        build_test_externalities().execute_with(|| {
+            let mut opening_fixture = AddOpeningFixture::default();
+            opening_fixture.application_staking_policy = Some(StakingPolicy {
+                amount: 100,
+                amount_mode: StakingAmountLimitMode::AtLeast,
+                crowded_out_unstaking_period_length: None,
+                review_period_expired_unstaking_period_length: None,
+            });
+            opening_fixture.role_staking_policy = Some(StakingPolicy {
+                amount: 100,
+                amount_mode: StakingAmountLimitMode::AtLeast,
+                crowded_out_unstaking_period_length: None,
+                review_period_expired_unstaking_period_length: None,
+            });
+
+            let add_opening_result = opening_fixture.add_opening();
+            let opening_id = add_opening_result.unwrap();
+
+            let mut application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+            application_fixture.opt_application_stake_imbalance =
+                Some(stake::NegativeImbalance::<Test>::new(100));
+            application_fixture.opt_role_stake_imbalance =
+                Some(stake::NegativeImbalance::<Test>::new(100));
+
+            let app_application_result = application_fixture.add_application();
+            let application_id = app_application_result.unwrap().application_id_added;
+
+            let application = <ApplicationById<Test>>::get(application_id);
+
+            let mut ttiad_fixture =
+                TryToInitiateApplicationDeactivationFixture::default_for_application(
+                    application_id,
+                    &application,
+                );
+            ttiad_fixture.cause = ApplicationDeactivationCause::Hired;
+
+            let mock = {
+                let mut mock = crate::MockStakeHandler::<Test>::new();
+                mock.expect_initiate_unstaking()
+                    .times(1)
+                    .returning(|_, _| Ok(()));
+                Rc::new(RefCell::new(mock))
+            };
+            set_stake_handler_impl(mock);
+
+            ttiad_fixture.call_and_assert(ApplicationDeactivationInitiationResult::Unstaking);
+        });
+    });
+}

+ 221 - 0
runtime-modules/hiring/src/test/staking_module/would_application_get_added.rs

@@ -0,0 +1,221 @@
+use super::*;
+use crate::mock::*;
+
+#[derive(Default)]
+struct WouldApplicationGetAddedFixture {
+    possible_opening_application_rationing_policy: Option<ApplicationRationingPolicy>,
+    opening_applicants: BTreeSet<ApplicationId>,
+    opt_role_stake_balance: Option<Balance>,
+    opt_application_stake_balance: Option<Balance>,
+}
+
+impl WouldApplicationGetAddedFixture {
+    fn call_and_assert(&self, expected_result: ApplicationWouldGetAddedEvaluation<Test>) {
+        let wagaf_result = self.would_application_get_added();
+
+        assert_eq!(wagaf_result, expected_result);
+    }
+
+    fn would_application_get_added(&self) -> ApplicationWouldGetAddedEvaluation<Test> {
+        Hiring::would_application_get_added(
+            &self.possible_opening_application_rationing_policy,
+            &self.opening_applicants,
+            &self.opt_role_stake_balance,
+            &self.opt_application_stake_balance,
+        )
+    }
+}
+
+#[test]
+fn would_application_get_added_with_no_rationing_policy() {
+    build_test_externalities().execute_with(|| {
+        let wagaf = WouldApplicationGetAddedFixture::default();
+
+        wagaf.call_and_assert(ApplicationWouldGetAddedEvaluation::Yes(
+            ApplicationAddedSuccess::Unconditionally,
+        ));
+    });
+}
+
+#[test]
+fn would_application_get_added_with_zero_opening_applicants() {
+    build_test_externalities().execute_with(|| {
+        let mut wagaf = WouldApplicationGetAddedFixture::default();
+        wagaf.possible_opening_application_rationing_policy =
+            Some(hiring::ApplicationRationingPolicy {
+                max_active_applicants: 1,
+            });
+
+        wagaf.call_and_assert(ApplicationWouldGetAddedEvaluation::Yes(
+            ApplicationAddedSuccess::Unconditionally,
+        ));
+    });
+}
+
+#[test]
+fn would_application_get_added_with_too_low_stake() {
+    build_test_externalities().execute_with(|| {
+        let mut opening_fixture = AddOpeningFixture::default();
+        opening_fixture.application_rationing_policy = Some(hiring::ApplicationRationingPolicy {
+            max_active_applicants: 1,
+        });
+        opening_fixture.application_staking_policy = Some(StakingPolicy {
+            amount: 100,
+            amount_mode: StakingAmountLimitMode::AtLeast,
+            crowded_out_unstaking_period_length: None,
+            review_period_expired_unstaking_period_length: None,
+        });
+
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let mut application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+        application_fixture.opt_application_stake_imbalance =
+            Some(stake::NegativeImbalance::<Test>::new(100));
+
+        let app_application_result = application_fixture.add_application();
+        let application_id = app_application_result.unwrap().application_id_added;
+
+        let mut wagaf = WouldApplicationGetAddedFixture::default();
+        wagaf.possible_opening_application_rationing_policy =
+            Some(hiring::ApplicationRationingPolicy {
+                max_active_applicants: 1,
+            });
+        wagaf.opening_applicants.insert(application_id);
+        wagaf.opt_application_stake_balance = Some(99);
+
+        wagaf.call_and_assert(ApplicationWouldGetAddedEvaluation::No);
+    });
+}
+
+#[test]
+fn would_application_get_added_with_too_low_stake_with_mocks() {
+    handle_mock(|| {
+        build_test_externalities().execute_with(|| {
+            let mock = {
+                let mut mock = crate::MockStakeHandler::<Test>::new();
+
+                mock.expect_stake().times(1).returning(|_, _| Ok(()));
+                mock.expect_stake_exists().times(2).returning(|_| true);
+                mock.expect_create_stake().times(1).returning(|| 0);
+                mock.expect_get_stake().returning(|_| stake::Stake {
+                    created: 1,
+                    staking_status: stake::StakingStatus::Staked(stake::StakedState {
+                        staked_amount: 101,
+                        staked_status: stake::StakedStatus::Normal,
+                        next_slash_id: 0,
+                        ongoing_slashes: BTreeMap::new(),
+                    }),
+                });
+
+                Rc::new(rstd::cell::RefCell::new(mock))
+            };
+
+            set_stake_handler_impl(mock.clone());
+
+            let mut opening_fixture = AddOpeningFixture::default();
+            opening_fixture.application_rationing_policy =
+                Some(hiring::ApplicationRationingPolicy {
+                    max_active_applicants: 1,
+                });
+            opening_fixture.application_staking_policy = Some(StakingPolicy {
+                amount: 100,
+                amount_mode: StakingAmountLimitMode::AtLeast,
+                crowded_out_unstaking_period_length: None,
+                review_period_expired_unstaking_period_length: None,
+            });
+
+            let add_opening_result = opening_fixture.add_opening();
+            let opening_id = add_opening_result.unwrap();
+
+            let mut application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+            application_fixture.opt_application_stake_imbalance =
+                Some(stake::NegativeImbalance::<Test>::new(100));
+
+            let app_application_result = application_fixture.add_application();
+            let application_id = app_application_result.unwrap().application_id_added;
+
+            let mut wagaf = WouldApplicationGetAddedFixture::default();
+            wagaf.possible_opening_application_rationing_policy =
+                Some(hiring::ApplicationRationingPolicy {
+                    max_active_applicants: 1,
+                });
+            wagaf.opening_applicants.insert(application_id);
+            wagaf.opt_application_stake_balance = Some(99);
+
+            wagaf.call_and_assert(ApplicationWouldGetAddedEvaluation::No);
+        });
+    });
+}
+
+#[test]
+fn would_application_get_added_with_crowding_out() {
+    build_test_externalities().execute_with(|| {
+        let mut opening_fixture = AddOpeningFixture::default();
+        opening_fixture.application_rationing_policy = Some(hiring::ApplicationRationingPolicy {
+            max_active_applicants: 1,
+        });
+        opening_fixture.application_staking_policy = Some(StakingPolicy {
+            amount: 100,
+            amount_mode: StakingAmountLimitMode::AtLeast,
+            crowded_out_unstaking_period_length: None,
+            review_period_expired_unstaking_period_length: None,
+        });
+
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let mut application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+        application_fixture.opt_application_stake_imbalance =
+            Some(stake::NegativeImbalance::<Test>::new(100));
+
+        let app_application_result = application_fixture.add_application();
+        let application_id = app_application_result.unwrap().application_id_added;
+
+        let mut wagaf = WouldApplicationGetAddedFixture::default();
+        wagaf.possible_opening_application_rationing_policy =
+            Some(hiring::ApplicationRationingPolicy {
+                max_active_applicants: 1,
+            });
+        wagaf.opening_applicants.insert(application_id);
+        wagaf.opt_application_stake_balance = Some(101);
+
+        wagaf.call_and_assert(ApplicationWouldGetAddedEvaluation::Yes(
+            ApplicationAddedSuccess::CrowdsOutExistingApplication(application_id),
+        ));
+    });
+}
+#[test]
+#[should_panic]
+fn would_application_get_added_panics_with_bad_params() {
+    build_test_externalities().execute_with(|| {
+        let mut opening_fixture = AddOpeningFixture::default();
+        opening_fixture.application_rationing_policy = Some(hiring::ApplicationRationingPolicy {
+            max_active_applicants: 1,
+        });
+        opening_fixture.application_staking_policy = Some(StakingPolicy {
+            amount: 100,
+            amount_mode: StakingAmountLimitMode::AtLeast,
+            crowded_out_unstaking_period_length: None,
+            review_period_expired_unstaking_period_length: None,
+        });
+
+        let add_opening_result = opening_fixture.add_opening();
+        let opening_id = add_opening_result.unwrap();
+
+        let mut application_fixture = AddApplicationFixture::default_for_opening(opening_id);
+        application_fixture.opt_application_stake_imbalance =
+            Some(stake::NegativeImbalance::<Test>::new(100));
+
+        assert!(application_fixture.add_application().is_ok());
+
+        let mut wagaf = WouldApplicationGetAddedFixture::default();
+        wagaf.possible_opening_application_rationing_policy =
+            Some(hiring::ApplicationRationingPolicy {
+                max_active_applicants: 0,
+            });
+        wagaf.opt_application_stake_balance = Some(100);
+
+        wagaf.would_application_get_added();
+    });
+}

+ 83 - 0
runtime-modules/membership/Cargo.toml

@@ -0,0 +1,83 @@
+[package]
+name = 'substrate-membership-module'
+version = '1.0.0'
+authors = ['Joystream contributors']
+edition = '2018'
+
+[features]
+default = ['std']
+std = [
+	'sr-primitives/std',
+	'srml-support/std',
+	'system/std',
+	'rstd/std',
+	'codec/std',
+	'serde',
+	'timestamp/std',
+	'primitives/std',
+	'common/std',
+]
+
+[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.rstd]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-std'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.codec]
+default-features = false
+features = ['derive']
+package = 'parity-scale-codec'
+version = '1.0.0'
+
+[dependencies.serde]
+features = ['derive']
+optional = true
+version = '1.0.101'
+
+[dependencies.timestamp]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-timestamp'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.primitives]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'substrate-primitives'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[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]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-balances'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'

+ 1 - 1
src/membership/genesis.rs → runtime-modules/membership/src/genesis.rs

@@ -1,6 +1,6 @@
 #![cfg(test)]
 
-use crate::currency::BalanceOf;
+use common::currency::BalanceOf;
 use rstd::prelude::*;
 //pub use super::members::{GenesisConfig, Trait};
 

+ 9 - 0
runtime-modules/membership/src/lib.rs

@@ -0,0 +1,9 @@
+// Ensure we're `no_std` when compiling for Wasm.
+#![cfg_attr(not(feature = "std"), no_std)]
+
+pub mod genesis;
+pub mod members;
+pub mod role_types;
+
+mod mock;
+mod tests;

+ 1 - 1
src/membership/members.rs → runtime-modules/membership/src/members.rs

@@ -1,5 +1,5 @@
-use crate::currency::{BalanceOf, GovernanceCurrency};
 use codec::{Codec, Decode, Encode};
+use common::currency::{BalanceOf, GovernanceCurrency};
 
 use rstd::prelude::*;
 use sr_primitives::traits::{MaybeSerialize, Member, One, SimpleArithmetic};

+ 1 - 1
src/membership/mock.rs → runtime-modules/membership/src/mock.rs

@@ -1,7 +1,7 @@
 #![cfg(test)]
 
 pub use super::members::{self, Trait, DEFAULT_PAID_TERM_ID};
-pub use crate::currency::GovernanceCurrency;
+pub use common::currency::GovernanceCurrency;
 pub use srml_support::traits::Currency;
 pub use system;
 

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


+ 0 - 0
src/membership/tests.rs → runtime-modules/membership/src/tests.rs


+ 58 - 0
runtime-modules/memo/Cargo.toml

@@ -0,0 +1,58 @@
+[package]
+name = 'substrate-memo-module'
+version = '1.0.0'
+authors = ['Joystream contributors']
+edition = '2018'
+
+
+[features]
+default = ['std']
+std = [
+	'sr-primitives/std',
+	'srml-support/std',
+	'system/std',
+	'rstd/std',
+	'codec/std',
+	'serde',
+	'common/std',
+]
+
+[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.rstd]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-std'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.codec]
+default-features = false
+features = ['derive']
+package = 'parity-scale-codec'
+version = '1.0.0'
+
+[dependencies.serde]
+features = ['derive']
+optional = true
+version = '1.0.101'
+
+[dependencies.common]
+default_features = false
+package = 'substrate-common-module'
+path = '../common'

+ 5 - 1
src/memo.rs → runtime-modules/memo/src/lib.rs

@@ -1,8 +1,12 @@
-use crate::currency::GovernanceCurrency;
+// Ensure we're `no_std` when compiling for Wasm.
+#![cfg_attr(not(feature = "std"), no_std)]
+
+use common::currency::GovernanceCurrency;
 use rstd::prelude::*;
 use sr_primitives::traits::Zero;
 use srml_support::traits::Currency;
 use srml_support::{decl_event, decl_module, decl_storage, ensure};
+
 use system::{self, ensure_signed};
 
 pub trait Trait: system::Trait + GovernanceCurrency {

+ 56 - 0
runtime-modules/recurring-reward/Cargo.toml

@@ -0,0 +1,56 @@
+[package]
+name = 'substrate-recurring-reward-module'
+version = '1.0.1'
+authors = ['Joystream contributors']
+edition = '2018'
+
+[dependencies]
+hex-literal = '0.1.0'
+serde = { version = '1.0', optional = true }
+serde_derive = { version = '1.0', optional = true }
+rstd = { package = 'sr-std', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+runtime-primitives = { package = 'sr-primitives', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+srml-support = { package = 'srml-support', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+srml-support-procedural = { package = 'srml-support-procedural', git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+system = { package = 'srml-system', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+balances = { package = 'srml-balances', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+codec = { package = 'parity-scale-codec', version = '1.0.0', default-features = false, features = ['derive'] }
+# https://users.rust-lang.org/t/failure-derive-compilation-error/39062
+quote = '<=1.0.2'
+
+[dependencies.timestamp]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-timestamp'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.runtime-io]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-io'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.minting]
+default_features = false
+package = 'substrate-token-mint-module'
+path = '../token-minting'
+
+[dev-dependencies]
+runtime-io = { package = 'sr-io', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+primitives = { package = 'substrate-primitives', git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+
+[features]
+default = ['std']
+std = [
+	'serde',
+	'serde_derive',
+	'codec/std',
+	'rstd/std',
+	'runtime-io/std',
+	'runtime-primitives/std',
+	'srml-support/std',
+	'system/std',
+  	'balances/std',
+	'timestamp/std',
+	'minting/std',
+]

+ 397 - 0
runtime-modules/recurring-reward/src/lib.rs

@@ -0,0 +1,397 @@
+// Ensure we're `no_std` when compiling for Wasm.
+#![cfg_attr(not(feature = "std"), no_std)]
+use rstd::prelude::*;
+
+use codec::{Codec, Decode, Encode};
+use runtime_primitives::traits::{MaybeSerialize, Member, One, SimpleArithmetic, Zero};
+use srml_support::{decl_module, decl_storage, ensure, Parameter};
+
+use minting::{self, BalanceOf};
+use system;
+
+mod mock;
+mod tests;
+
+pub trait Trait: system::Trait + minting::Trait {
+    type PayoutStatusHandler: PayoutStatusHandler<Self>;
+
+    /// Type of identifier for recipients.
+    type RecipientId: Parameter
+        + Member
+        + SimpleArithmetic
+        + Codec
+        + Default
+        + Copy
+        + MaybeSerialize
+        + PartialEq;
+
+    /// Type for identifier for relationship representing that a recipient recieves recurring reward from a token mint
+    type RewardRelationshipId: Parameter
+        + Member
+        + SimpleArithmetic
+        + Codec
+        + Default
+        + Copy
+        + MaybeSerialize
+        + PartialEq;
+}
+
+/// Handler for aftermath of a payout attempt
+pub trait PayoutStatusHandler<T: Trait> {
+    fn payout_succeeded(
+        id: T::RewardRelationshipId,
+        destination_account: &T::AccountId,
+        amount: BalanceOf<T>,
+    );
+
+    fn payout_failed(
+        id: T::RewardRelationshipId,
+        destination_account: &T::AccountId,
+        amount: BalanceOf<T>,
+    );
+}
+
+/// Makes `()` empty tuple, a PayoutStatusHandler that does nothing.
+impl<T: Trait> PayoutStatusHandler<T> for () {
+    fn payout_succeeded(
+        _id: T::RewardRelationshipId,
+        _destination_account: &T::AccountId,
+        _amount: BalanceOf<T>,
+    ) {
+    }
+
+    fn payout_failed(
+        _id: T::RewardRelationshipId,
+        _destination_account: &T::AccountId,
+        _amount: BalanceOf<T>,
+    ) {
+    }
+}
+
+/// A recipient of recurring rewards
+#[derive(Encode, Decode, Copy, Clone, Debug, Default)]
+pub struct Recipient<Balance> {
+    // stats
+    /// Total payout received by this recipient
+    total_reward_received: Balance,
+
+    /// Total payout missed for this recipient
+    total_reward_missed: Balance,
+}
+
+#[derive(Encode, Decode, Copy, Clone, Debug, Default)]
+pub struct RewardRelationship<AccountId, Balance, BlockNumber, MintId, RecipientId> {
+    /// Identifier for receiver
+    recipient: RecipientId,
+
+    /// Identifier for reward source
+    mint_id: MintId,
+
+    /// Destination account for reward
+    account: AccountId,
+
+    /// The payout amount at the next payout
+    amount_per_payout: Balance,
+
+    /// When set, identifies block when next payout should be processed,
+    /// otherwise there is no pending payout
+    next_payment_at_block: Option<BlockNumber>,
+
+    /// When set, will be the basis for automatically setting next payment,
+    /// otherwise any upcoming payout will be a one off.
+    payout_interval: Option<BlockNumber>,
+
+    // stats
+    /// Total payout received in this relationship
+    total_reward_received: Balance,
+
+    /// Total payout failed in this relationship
+    total_reward_missed: Balance,
+}
+
+impl<AccountId: Clone, Balance: Clone, BlockNumber: Clone, MintId: Clone, RecipientId: Clone>
+    RewardRelationship<AccountId, Balance, BlockNumber, MintId, RecipientId>
+{
+    /// Verifies whether relationship is active
+    pub fn is_active(&self) -> bool {
+        self.next_payment_at_block.is_some()
+    }
+
+    /// Make clone which is activated.
+    pub fn clone_activated(&self, start_at: &BlockNumber) -> Self {
+        Self {
+            next_payment_at_block: Some((*start_at).clone()),
+            ..((*self).clone())
+        }
+    }
+
+    /// Make clone which is deactivated
+    pub fn clone_deactivated(&self) -> Self {
+        Self {
+            next_payment_at_block: None,
+            ..((*self).clone())
+        }
+    }
+}
+
+decl_storage! {
+    trait Store for Module<T: Trait> as RecurringReward {
+        Recipients get(recipients): linked_map T::RecipientId => Recipient<BalanceOf<T>>;
+
+        RecipientsCreated get(recipients_created): T::RecipientId;
+
+        RewardRelationships get(reward_relationships): linked_map T::RewardRelationshipId => RewardRelationship<T::AccountId, BalanceOf<T>, T::BlockNumber, T::MintId, T::RecipientId>;
+
+        RewardRelationshipsCreated get(reward_relationships_created): T::RewardRelationshipId;
+    }
+}
+
+decl_module! {
+    pub struct Module<T: Trait> for enum Call where origin: T::Origin {
+
+        fn on_finalize(now: T::BlockNumber) {
+            Self::do_payouts(now);
+        }
+    }
+}
+
+#[derive(Eq, PartialEq, Debug)]
+pub enum RewardsError {
+    RecipientNotFound,
+    RewardSourceNotFound,
+    NextPaymentNotInFuture,
+    RewardRelationshipNotFound,
+}
+
+impl<T: Trait> Module<T> {
+    /// Adds a new Recipient and returns new recipient identifier.
+    pub fn add_recipient() -> T::RecipientId {
+        let next_id = Self::recipients_created();
+        <RecipientsCreated<T>>::put(next_id + One::one());
+        <Recipients<T>>::insert(&next_id, Recipient::default());
+        next_id
+    }
+
+    /// Adds a new RewardRelationship, for a given source mint, recipient, account.
+    pub fn add_reward_relationship(
+        mint_id: T::MintId,
+        recipient: T::RecipientId,
+        account: T::AccountId,
+        amount_per_payout: BalanceOf<T>,
+        next_payment_at_block: T::BlockNumber,
+        payout_interval: Option<T::BlockNumber>,
+    ) -> Result<T::RewardRelationshipId, RewardsError> {
+        ensure!(
+            <minting::Module<T>>::mint_exists(mint_id),
+            RewardsError::RewardSourceNotFound
+        );
+        ensure!(
+            <Recipients<T>>::exists(recipient),
+            RewardsError::RecipientNotFound
+        );
+        ensure!(
+            next_payment_at_block > <system::Module<T>>::block_number(),
+            RewardsError::NextPaymentNotInFuture
+        );
+
+        let relationship_id = Self::reward_relationships_created();
+        <RewardRelationshipsCreated<T>>::put(relationship_id + One::one());
+        <RewardRelationships<T>>::insert(
+            relationship_id,
+            RewardRelationship {
+                mint_id,
+                recipient,
+                account,
+                amount_per_payout,
+                next_payment_at_block: Some(next_payment_at_block),
+                payout_interval,
+                total_reward_received: Zero::zero(),
+                total_reward_missed: Zero::zero(),
+            },
+        );
+        Ok(relationship_id)
+    }
+
+    /// Removes a relationship from RewardRelashionships and its recipient.
+    pub fn remove_reward_relationship(id: T::RewardRelationshipId) {
+        if <RewardRelationships<T>>::exists(&id) {
+            <Recipients<T>>::remove(<RewardRelationships<T>>::take(&id).recipient);
+        }
+    }
+
+    /// Will attempt to activat a deactivated reward relationship.
+    pub fn try_to_activate_relationship(
+        id: T::RewardRelationshipId,
+        next_payment_at_block: T::BlockNumber,
+    ) -> Result<bool, ()> {
+        // Ensure relationship exists
+        let reward_relationship = Self::ensure_reward_relationship_exists(&id)?;
+
+        let activated = if reward_relationship.is_active() {
+            // Was not activated
+            false
+        } else {
+            // Update as activated
+            let activated_relationship =
+                reward_relationship.clone_activated(&next_payment_at_block);
+
+            RewardRelationships::<T>::insert(id, activated_relationship);
+
+            // We activated
+            true
+        };
+
+        Ok(activated)
+    }
+
+    /// Will attempt to deactivat a activated reward relationship.
+    pub fn try_to_deactivate_relationship(id: T::RewardRelationshipId) -> Result<bool, ()> {
+        // Ensure relationship exists
+        let reward_relationship = Self::ensure_reward_relationship_exists(&id)?;
+
+        let deactivated = if reward_relationship.is_active() {
+            let deactivated_relationship = reward_relationship.clone_deactivated();
+
+            RewardRelationships::<T>::insert(id, deactivated_relationship);
+
+            // Was deactivated
+            true
+        } else {
+            // Was not deactivated
+            false
+        };
+
+        Ok(deactivated)
+    }
+
+    // For reward relationship found with given identifier, new values can be set for
+    // account, payout, block number when next payout will be made and the new interval after
+    // the next scheduled payout. All values are optional, but updating values are combined in this
+    // single method to ensure atomic updates.
+    pub fn set_reward_relationship(
+        id: T::RewardRelationshipId,
+        new_account: Option<T::AccountId>,
+        new_payout: Option<BalanceOf<T>>,
+        new_next_payment_at: Option<Option<T::BlockNumber>>,
+        new_payout_interval: Option<Option<T::BlockNumber>>,
+    ) -> Result<(), RewardsError> {
+        ensure!(
+            <RewardRelationships<T>>::exists(&id),
+            RewardsError::RewardRelationshipNotFound
+        );
+
+        let mut relationship = Self::reward_relationships(&id);
+
+        if let Some(account) = new_account {
+            relationship.account = account;
+        }
+        if let Some(payout) = new_payout {
+            relationship.amount_per_payout = payout;
+        }
+        if let Some(next_payout_at_block) = new_next_payment_at {
+            if let Some(blocknumber) = next_payout_at_block {
+                ensure!(
+                    blocknumber > <system::Module<T>>::block_number(),
+                    RewardsError::NextPaymentNotInFuture
+                );
+            }
+            relationship.next_payment_at_block = next_payout_at_block;
+        }
+        if let Some(payout_interval) = new_payout_interval {
+            relationship.payout_interval = payout_interval;
+        }
+
+        <RewardRelationships<T>>::insert(&id, relationship);
+        Ok(())
+    }
+
+    /*
+    For all relationships where next_payment_at_block is set and matches current block height,
+    a call to pay_reward is made for the suitable amount, recipient and source.
+    The next_payment_in_block is updated based on payout_interval.
+    If the call succeeds, total_reward_received is incremented on both
+    recipient and dependency with amount_per_payout, and a call to T::PayoutStatusHandler is made.
+    Otherwise, analogous steps for failure.
+    */
+    fn do_payouts(now: T::BlockNumber) {
+        for (relationship_id, ref mut relationship) in <RewardRelationships<T>>::enumerate() {
+            assert!(<Recipients<T>>::exists(&relationship.recipient));
+
+            let mut recipient = Self::recipients(relationship.recipient);
+
+            if let Some(next_payment_at_block) = relationship.next_payment_at_block {
+                if next_payment_at_block != now {
+                    continue;
+                }
+
+                // Add the missed payout and try to pay those in addition to scheduled payout?
+                // let payout = relationship.total_reward_missed + relationship.amount_per_payout;
+                let payout = relationship.amount_per_payout;
+
+                // try to make payment
+                if <minting::Module<T>>::transfer_tokens(
+                    relationship.mint_id,
+                    payout,
+                    &relationship.account,
+                )
+                .is_err()
+                {
+                    // add only newly scheduled payout to total missed payout
+                    relationship.total_reward_missed += relationship.amount_per_payout;
+
+                    // update recipient stats
+                    recipient.total_reward_missed += relationship.amount_per_payout;
+
+                    T::PayoutStatusHandler::payout_failed(
+                        relationship_id,
+                        &relationship.account,
+                        payout,
+                    );
+                } else {
+                    // update payout received stats
+                    relationship.total_reward_received += payout;
+                    recipient.total_reward_received += payout;
+
+                    // update missed payout stats
+                    // if relationship.total_reward_missed != Zero::zero() {
+                    //     // update recipient stats
+                    //     recipient.total_reward_missed -= relationship.total_reward_missed;
+
+                    //     // clear missed reward on relationship
+                    //     relationship.total_reward_missed = Zero::zero();
+                    // }
+                    T::PayoutStatusHandler::payout_succeeded(
+                        relationship_id,
+                        &relationship.account,
+                        payout,
+                    );
+                }
+
+                // update next payout blocknumber at interval if set
+                if let Some(payout_interval) = relationship.payout_interval {
+                    relationship.next_payment_at_block = Some(now + payout_interval);
+                } else {
+                    relationship.next_payment_at_block = None;
+                }
+
+                <Recipients<T>>::insert(relationship.recipient, recipient);
+                <RewardRelationships<T>>::insert(relationship_id, relationship);
+            }
+        }
+    }
+}
+
+impl<T: Trait> Module<T> {
+    fn ensure_reward_relationship_exists(
+        id: &T::RewardRelationshipId,
+    ) -> Result<
+        RewardRelationship<T::AccountId, BalanceOf<T>, T::BlockNumber, T::MintId, T::RecipientId>,
+        (),
+    > {
+        ensure!(RewardRelationships::<T>::exists(id), ());
+
+        let relationship = RewardRelationships::<T>::get(id);
+
+        Ok(relationship)
+    }
+}

+ 103 - 0
runtime-modules/recurring-reward/src/mock/mod.rs

@@ -0,0 +1,103 @@
+#![cfg(test)]
+
+// use crate::*;
+use crate::{Module, Trait};
+
+use primitives::H256;
+
+use balances;
+use minting;
+use runtime_primitives::{
+    testing::Header,
+    traits::{BlakeTwo256, IdentityLookup},
+    Perbill,
+};
+use srml_support::{impl_outer_origin, parameter_types};
+
+mod status_handler;
+pub use status_handler::MockStatusHandler;
+
+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 = ();
+}
+
+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: u64 = 2000;
+}
+
+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 Trait for Test {
+    type PayoutStatusHandler = MockStatusHandler;
+    type RecipientId = u64;
+    type RewardRelationshipId = u64;
+}
+
+impl minting::Trait for Test {
+    type Currency = Balances;
+    type MintId = u64;
+}
+
+pub fn build_test_externalities() -> runtime_io::TestExternalities {
+    MockStatusHandler::reset();
+
+    let t = system::GenesisConfig::default()
+        .build_storage::<Test>()
+        .unwrap();
+
+    t.into()
+}
+
+pub type System = system::Module<Test>;
+pub type Balances = balances::Module<Test>;
+pub type Rewards = Module<Test>;
+pub type Minting = minting::Module<Test>;

+ 64 - 0
runtime-modules/recurring-reward/src/mock/status_handler.rs

@@ -0,0 +1,64 @@
+#![cfg(test)]
+
+use super::Test;
+use crate::{PayoutStatusHandler, Trait};
+use std::cell::RefCell;
+
+struct StatusHandlerState<T: Trait> {
+    successes: Vec<T::RewardRelationshipId>,
+    failures: Vec<T::RewardRelationshipId>,
+}
+
+impl<T: Trait> StatusHandlerState<T> {
+    pub fn reset(&mut self) {
+        self.successes = vec![];
+        self.failures = vec![];
+    }
+}
+
+impl<T: Trait> Default for StatusHandlerState<T> {
+    fn default() -> Self {
+        Self {
+            successes: vec![],
+            failures: vec![],
+        }
+    }
+}
+
+thread_local!(static STATUS_HANDLER_STATE: RefCell<StatusHandlerState<Test>> = RefCell::new(Default::default()));
+
+pub struct MockStatusHandler {}
+impl MockStatusHandler {
+    pub fn reset() {
+        STATUS_HANDLER_STATE.with(|cell| {
+            cell.borrow_mut().reset();
+        });
+    }
+    pub fn successes() -> usize {
+        let mut value = 0;
+        STATUS_HANDLER_STATE.with(|cell| {
+            value = cell.borrow_mut().successes.len();
+        });
+        value
+    }
+    pub fn failures() -> usize {
+        let mut value = 0;
+        STATUS_HANDLER_STATE.with(|cell| {
+            value = cell.borrow_mut().failures.len();
+        });
+        value
+    }
+}
+impl PayoutStatusHandler<Test> for MockStatusHandler {
+    fn payout_succeeded(id: u64, _destination_account: &u64, _amount: u64) {
+        STATUS_HANDLER_STATE.with(|cell| {
+            cell.borrow_mut().successes.push(id);
+        });
+    }
+
+    fn payout_failed(id: u64, _destination_account: &u64, _amount: u64) {
+        STATUS_HANDLER_STATE.with(|cell| {
+            cell.borrow_mut().failures.push(id);
+        });
+    }
+}

+ 259 - 0
runtime-modules/recurring-reward/src/tests.rs

@@ -0,0 +1,259 @@
+#![cfg(test)]
+
+use super::*;
+use crate::mock::*;
+use srml_support::traits::Currency;
+
+fn create_new_mint_with_capacity(capacity: u64) -> u64 {
+    let mint_id = Minting::add_mint(capacity, None).ok().unwrap();
+    assert!(Minting::mint_exists(mint_id));
+    assert_eq!(Minting::get_mint_capacity(mint_id).ok().unwrap(), capacity);
+    mint_id
+}
+
+#[test]
+fn adding_recipients() {
+    build_test_externalities().execute_with(|| {
+        let next_id = Rewards::recipients_created();
+        assert!(!<Recipients<Test>>::exists(&next_id));
+        let recipient_id = Rewards::add_recipient();
+        assert!(<Recipients<Test>>::exists(&next_id));
+        assert_eq!(recipient_id, next_id);
+        assert_eq!(Rewards::recipients_created(), next_id + 1);
+    });
+}
+
+#[test]
+fn adding_relationships() {
+    build_test_externalities().execute_with(|| {
+        let recipient_account: u64 = 1;
+        let mint_id = create_new_mint_with_capacity(1000000);
+        let recipient_id = Rewards::add_recipient();
+        let interval: u64 = 600;
+        let next_payment_at: u64 = 2222;
+        let payout = 100;
+
+        let next_relationship_id = Rewards::reward_relationships_created();
+        let relationship = Rewards::add_reward_relationship(
+            mint_id,
+            recipient_id,
+            recipient_account,
+            payout,
+            next_payment_at,
+            Some(interval),
+        );
+        assert!(relationship.is_ok());
+        let relationship_id = relationship.ok().unwrap();
+        assert_eq!(relationship_id, next_relationship_id);
+        assert_eq!(
+            Rewards::reward_relationships_created(),
+            next_relationship_id + 1
+        );
+        assert!(<RewardRelationships<Test>>::exists(&relationship_id));
+        let relationship = Rewards::reward_relationships(&relationship_id);
+        assert_eq!(relationship.next_payment_at_block, Some(next_payment_at));
+        assert_eq!(relationship.amount_per_payout, payout);
+        assert_eq!(relationship.mint_id, mint_id);
+        assert_eq!(relationship.account, recipient_account);
+        assert_eq!(relationship.payout_interval, Some(interval));
+
+        // mint doesn't exist
+        assert_eq!(
+            Rewards::add_reward_relationship(
+                111,
+                recipient_id,
+                recipient_account,
+                100,
+                next_payment_at,
+                None,
+            )
+            .expect_err("should fail if mint doesn't exist"),
+            RewardsError::RewardSourceNotFound
+        );
+    });
+}
+
+#[test]
+fn one_off_payout() {
+    build_test_externalities().execute_with(|| {
+        System::set_block_number(10000);
+        let recipient_account: u64 = 1;
+        let _ = Balances::deposit_creating(&recipient_account, 400);
+        let mint_id = create_new_mint_with_capacity(1000000);
+        let recipient_id = Rewards::add_recipient();
+        let payout: u64 = 1000;
+        let next_payout_at: u64 = 12222;
+        let relationship = Rewards::add_reward_relationship(
+            mint_id,
+            recipient_id,
+            recipient_account,
+            payout,
+            next_payout_at,
+            None,
+        );
+        assert!(relationship.is_ok());
+        let relationship_id = relationship.ok().unwrap();
+
+        let relationship = Rewards::reward_relationships(&relationship_id);
+        assert_eq!(relationship.next_payment_at_block, Some(next_payout_at));
+
+        let starting_balance = Balances::free_balance(&recipient_account);
+
+        // try to catch 'off by one' bugs
+        Rewards::do_payouts(next_payout_at - 1);
+        assert_eq!(Balances::free_balance(&recipient_account), starting_balance);
+        Rewards::do_payouts(next_payout_at + 1);
+        assert_eq!(Balances::free_balance(&recipient_account), starting_balance);
+
+        assert_eq!(MockStatusHandler::successes(), 0);
+
+        Rewards::do_payouts(next_payout_at);
+        assert_eq!(
+            Balances::free_balance(&recipient_account),
+            starting_balance + payout
+        );
+        assert_eq!(MockStatusHandler::successes(), 1);
+
+        let relationship = Rewards::reward_relationships(&relationship_id);
+        assert_eq!(relationship.total_reward_received, payout);
+        assert_eq!(relationship.next_payment_at_block, None);
+
+        let recipient = Rewards::recipients(&recipient_id);
+        assert_eq!(recipient.total_reward_received, payout);
+    });
+}
+
+#[test]
+fn recurring_payout() {
+    build_test_externalities().execute_with(|| {
+        System::set_block_number(10000);
+        let recipient_account: u64 = 1;
+        let _ = Balances::deposit_creating(&recipient_account, 400);
+        let mint_id = create_new_mint_with_capacity(1000000);
+        let recipient_id = Rewards::add_recipient();
+        let payout: u64 = 1000;
+        let next_payout_at: u64 = 12222;
+        let interval: u64 = 600;
+        let relationship = Rewards::add_reward_relationship(
+            mint_id,
+            recipient_id,
+            recipient_account,
+            payout,
+            next_payout_at,
+            Some(interval),
+        );
+        assert!(relationship.is_ok());
+        let relationship_id = relationship.ok().unwrap();
+
+        let relationship = Rewards::reward_relationships(&relationship_id);
+        assert_eq!(relationship.next_payment_at_block, Some(next_payout_at));
+
+        let starting_balance = Balances::free_balance(&recipient_account);
+
+        let number_of_payouts = 3;
+        for i in 0..number_of_payouts {
+            Rewards::do_payouts(next_payout_at + interval * i);
+        }
+        assert_eq!(MockStatusHandler::successes(), number_of_payouts as usize);
+
+        assert_eq!(
+            Balances::free_balance(&recipient_account),
+            starting_balance + payout * number_of_payouts
+        );
+
+        let relationship = Rewards::reward_relationships(&relationship_id);
+        assert_eq!(
+            relationship.total_reward_received,
+            payout * number_of_payouts
+        );
+
+        let recipient = Rewards::recipients(&recipient_id);
+        assert_eq!(recipient.total_reward_received, payout * number_of_payouts);
+    });
+}
+
+#[test]
+fn track_missed_payouts() {
+    build_test_externalities().execute_with(|| {
+        System::set_block_number(10000);
+        let recipient_account: u64 = 1;
+        let _ = Balances::deposit_creating(&recipient_account, 400);
+        let mint_id = create_new_mint_with_capacity(0);
+        let recipient_id = Rewards::add_recipient();
+        let payout: u64 = 1000;
+        let next_payout_at: u64 = 12222;
+        let relationship = Rewards::add_reward_relationship(
+            mint_id,
+            recipient_id,
+            recipient_account,
+            payout,
+            next_payout_at,
+            None,
+        );
+        assert!(relationship.is_ok());
+        let relationship_id = relationship.ok().unwrap();
+
+        let relationship = Rewards::reward_relationships(&relationship_id);
+        assert_eq!(relationship.next_payment_at_block, Some(next_payout_at));
+
+        let starting_balance = Balances::free_balance(&recipient_account);
+
+        Rewards::do_payouts(next_payout_at);
+        assert_eq!(Balances::free_balance(&recipient_account), starting_balance);
+
+        assert_eq!(MockStatusHandler::failures(), 1);
+
+        let relationship = Rewards::reward_relationships(&relationship_id);
+        assert_eq!(relationship.total_reward_received, 0);
+        assert_eq!(relationship.total_reward_missed, payout);
+
+        let recipient = Rewards::recipients(&recipient_id);
+        assert_eq!(recipient.total_reward_received, 0);
+        assert_eq!(recipient.total_reward_missed, payout);
+    });
+}
+
+#[test]
+fn activate_and_deactivate_relationship() {
+    build_test_externalities().execute_with(|| {
+        System::set_block_number(10000);
+        let recipient_account: u64 = 1;
+        let _ = Balances::deposit_creating(&recipient_account, 400);
+        let mint_id = create_new_mint_with_capacity(0);
+        let recipient_id = Rewards::add_recipient();
+        let payout: u64 = 1000;
+        let next_payout_at: u64 = 12222;
+
+        // Add relationship
+        let relationship_id = Rewards::add_reward_relationship(
+            mint_id,
+            recipient_id,
+            recipient_account,
+            payout,
+            next_payout_at,
+            None,
+        )
+        .unwrap();
+
+        // The relationship starts out active
+        assert!(Rewards::reward_relationships(&relationship_id).is_active());
+
+        // We are able to deactivate relationship
+        assert!(Rewards::try_to_deactivate_relationship(relationship_id).unwrap());
+
+        // The relationship is no longer active
+        assert!(!Rewards::reward_relationships(&relationship_id).is_active());
+
+        // We cannot deactivate an already deactivated relationship
+        assert!(!Rewards::try_to_deactivate_relationship(relationship_id).unwrap());
+
+        // We are able to activate relationship
+        assert!(Rewards::try_to_activate_relationship(relationship_id, next_payout_at).unwrap());
+
+        // The relationship is now not active
+        assert!(Rewards::reward_relationships(&relationship_id).is_active());
+
+        // We cannot activate an already active relationship
+        assert!(!Rewards::try_to_activate_relationship(relationship_id, next_payout_at).unwrap());
+    });
+}

+ 88 - 0
runtime-modules/roles/Cargo.toml

@@ -0,0 +1,88 @@
+[package]
+name = 'substrate-roles-module'
+version = '1.0.0'
+authors = ['Joystream contributors']
+edition = '2018'
+
+[features]
+default = ['std']
+std = [
+	'sr-primitives/std',
+	'srml-support/std',
+	'system/std',
+	'rstd/std',
+	'codec/std',
+	'serde',
+	'common/std',
+	'membership/std',
+]
+
+
+[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.rstd]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-std'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.codec]
+default-features = false
+features = ['derive']
+package = 'parity-scale-codec'
+version = '1.0.0'
+
+[dependencies.serde]
+features = ['derive']
+optional = true
+version = '1.0.101'
+
+[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]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-balances'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dev-dependencies.timestamp]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-timestamp'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dev-dependencies.primitives]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'substrate-primitives'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'

+ 1 - 2
src/roles/actors.rs → runtime-modules/roles/src/actors.rs

@@ -1,5 +1,5 @@
-use crate::currency::{BalanceOf, GovernanceCurrency};
 use codec::{Decode, Encode};
+use common::currency::{BalanceOf, GovernanceCurrency};
 use rstd::prelude::*;
 use sr_primitives::traits::{Bounded, Zero};
 use srml_support::traits::{
@@ -8,7 +8,6 @@ use srml_support::traits::{
 use srml_support::{decl_event, decl_module, decl_storage, ensure};
 use system::{self, ensure_root, ensure_signed};
 
-use crate::membership;
 pub use membership::members::Role;
 
 const STAKING_ID: LockIdentifier = *b"role_stk";

+ 8 - 0
runtime-modules/roles/src/lib.rs

@@ -0,0 +1,8 @@
+// Ensure we're `no_std` when compiling for Wasm.
+#![cfg_attr(not(feature = "std"), no_std)]
+
+pub mod actors;
+pub mod traits;
+
+mod mock;
+mod tests;

+ 1 - 2
src/roles/mock.rs → runtime-modules/roles/src/mock.rs

@@ -1,8 +1,7 @@
 #![cfg(test)]
 
 pub use super::actors;
-pub use crate::currency::GovernanceCurrency;
-use crate::membership;
+pub use common::currency::GovernanceCurrency;
 pub use srml_support::traits::Currency;
 pub use system;
 

+ 0 - 0
src/roles/tests.rs → runtime-modules/roles/src/tests.rs


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

@@ -0,0 +1,26 @@
+use crate::actors;
+use system;
+
+// Roles
+pub trait Roles<T: system::Trait> {
+    fn is_role_account(account_id: &T::AccountId) -> bool;
+
+    fn account_has_role(account_id: &T::AccountId, role: actors::Role) -> bool;
+
+    // If available, return a random account ID for the given role.
+    fn random_account_for_role(role: actors::Role) -> Result<T::AccountId, &'static str>;
+}
+
+impl<T: system::Trait> Roles<T> for () {
+    fn is_role_account(_who: &T::AccountId) -> bool {
+        false
+    }
+
+    fn account_has_role(_account_id: &T::AccountId, _role: actors::Role) -> bool {
+        false
+    }
+
+    fn random_account_for_role(_role: actors::Role) -> Result<T::AccountId, &'static str> {
+        Err("not implemented")
+    }
+}

+ 70 - 0
runtime-modules/service-discovery/Cargo.toml

@@ -0,0 +1,70 @@
+[package]
+name = 'substrate-service-discovery-module'
+version = '1.0.0'
+authors = ['Joystream contributors']
+edition = '2018'
+
+[features]
+default = ['std']
+std = [
+	'sr-primitives/std',
+	'srml-support/std',
+	'system/std',
+	'rstd/std',
+	'serde',
+    'codec/std',
+    'primitives/std',
+    'roles/std',
+]
+
+[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.rstd]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-std'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.serde]
+features = ['derive']
+optional = true
+version = '1.0.101'
+
+[dependencies.primitives]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'substrate-primitives'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.codec]
+default-features = false
+features = ['derive']
+package = 'parity-scale-codec'
+version = '1.0.0'
+
+[dev-dependencies.runtime-io]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-io'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.roles]
+default_features = false
+package = 'substrate-roles-module'
+path = '../roles'

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

@@ -1,5 +1,5 @@
-use crate::traits::Roles;
 use codec::{Decode, Encode};
+use roles::traits::Roles;
 use rstd::prelude::*;
 #[cfg(feature = "std")]
 use serde::{Deserialize, Serialize};

+ 7 - 0
runtime-modules/service-discovery/src/lib.rs

@@ -0,0 +1,7 @@
+// Ensure we're `no_std` when compiling for Wasm.
+#![cfg_attr(not(feature = "std"), no_std)]
+
+pub mod discovery;
+
+mod mock;
+mod tests;

+ 2 - 2
src/service_discovery/mock.rs → runtime-modules/service-discovery/src/mock.rs

@@ -1,8 +1,8 @@
 #![cfg(test)]
 
 pub use super::discovery;
-pub use crate::roles::actors;
-pub use crate::traits::Roles;
+pub use roles::actors;
+use roles::traits::Roles;
 
 pub use primitives::{Blake2Hasher, H256};
 pub use sr_primitives::{

+ 0 - 0
src/service_discovery/tests.rs → runtime-modules/service-discovery/src/tests.rs


+ 50 - 0
runtime-modules/stake/Cargo.toml

@@ -0,0 +1,50 @@
+[package]
+name = 'substrate-stake-module'
+version = '1.0.1'
+authors = ['Joystream contributors']
+edition = '2018'
+
+[dependencies]
+hex-literal = '0.1.0'
+serde = { version = '1.0', optional = true }
+serde_derive = { version = '1.0', optional = true }
+rstd = { package = 'sr-std', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+runtime-primitives = { package = 'sr-primitives', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+srml-support = { package = 'srml-support', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+srml-support-procedural = { package = 'srml-support-procedural', git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+system = { package = 'srml-system', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+balances = { package = 'srml-balances', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+codec = { package = 'parity-scale-codec', version = '1.0.0', default-features = false, features = ['derive'] }
+# https://users.rust-lang.org/t/failure-derive-compilation-error/39062
+quote = '<=1.0.2'
+
+[dependencies.timestamp]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-timestamp'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.runtime-io]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-io'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dev-dependencies]
+runtime-io = { package = 'sr-io', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+primitives = { package = 'substrate-primitives', git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+
+[features]
+default = ['std']
+std = [
+	'serde',
+	'serde_derive',
+	'codec/std',
+	'rstd/std',
+	'runtime-io/std',
+	'runtime-primitives/std',
+	'srml-support/std',
+	'system/std',
+  	'balances/std',
+	'timestamp/std',
+]

+ 158 - 0
runtime-modules/stake/src/errors.rs

@@ -0,0 +1,158 @@
+#[derive(Debug, Eq, PartialEq)]
+pub enum StakeActionError<ErrorType> {
+    StakeNotFound,
+    Error(ErrorType),
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum TransferFromAccountError {
+    InsufficientBalance,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum StakingError {
+    CannotStakeZero,
+    CannotStakeLessThanMinimumBalance,
+    AlreadyStaked,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum StakingFromAccountError {
+    StakingError(StakingError),
+    InsufficientBalanceInSourceAccount,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum IncreasingStakeError {
+    NotStaked,
+    CannotChangeStakeByZero,
+    CannotIncreaseStakeWhileUnstaking,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum IncreasingStakeFromAccountError {
+    IncreasingStakeError(IncreasingStakeError),
+    InsufficientBalanceInSourceAccount,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum DecreasingStakeError {
+    NotStaked,
+    CannotChangeStakeByZero,
+    CannotDecreaseStakeWhileOngoingSlahes,
+    InsufficientStake,
+    CannotDecreaseStakeWhileUnstaking,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum InitiateSlashingError {
+    NotStaked,
+    SlashPeriodShouldBeGreaterThanZero,
+    SlashAmountShouldBeGreaterThanZero,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum PauseSlashingError {
+    SlashNotFound,
+    NotStaked,
+    AlreadyPaused,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum ResumeSlashingError {
+    SlashNotFound,
+    NotStaked,
+    NotPaused,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum CancelSlashingError {
+    SlashNotFound,
+    NotStaked,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum UnstakingError {
+    NotStaked,
+    AlreadyUnstaking,
+    CannotUnstakeWhileSlashesOngoing,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum InitiateUnstakingError {
+    UnstakingError(UnstakingError),
+    UnstakingPeriodShouldBeGreaterThanZero,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum PauseUnstakingError {
+    NotStaked,
+    NotUnstaking,
+    AlreadyPaused,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum ResumeUnstakingError {
+    NotStaked,
+    NotUnstaking,
+    NotPaused,
+}
+
+impl<ErrorType> From<ErrorType> for StakeActionError<ErrorType> {
+    fn from(e: ErrorType) -> StakeActionError<ErrorType> {
+        StakeActionError::Error(e)
+    }
+}
+
+impl From<StakingError> for StakeActionError<StakingFromAccountError> {
+    fn from(e: StakingError) -> StakeActionError<StakingFromAccountError> {
+        StakeActionError::Error(StakingFromAccountError::StakingError(e))
+    }
+}
+
+impl From<TransferFromAccountError> for StakeActionError<StakingFromAccountError> {
+    fn from(e: TransferFromAccountError) -> StakeActionError<StakingFromAccountError> {
+        match e {
+            TransferFromAccountError::InsufficientBalance => {
+                StakeActionError::Error(StakingFromAccountError::InsufficientBalanceInSourceAccount)
+            }
+        }
+    }
+}
+
+impl From<IncreasingStakeError> for StakeActionError<IncreasingStakeFromAccountError> {
+    fn from(e: IncreasingStakeError) -> StakeActionError<IncreasingStakeFromAccountError> {
+        StakeActionError::Error(IncreasingStakeFromAccountError::IncreasingStakeError(e))
+    }
+}
+
+impl From<TransferFromAccountError> for StakeActionError<IncreasingStakeFromAccountError> {
+    fn from(e: TransferFromAccountError) -> StakeActionError<IncreasingStakeFromAccountError> {
+        match e {
+            TransferFromAccountError::InsufficientBalance => StakeActionError::Error(
+                IncreasingStakeFromAccountError::InsufficientBalanceInSourceAccount,
+            ),
+        }
+    }
+}
+
+impl From<StakeActionError<IncreasingStakeError>>
+    for StakeActionError<IncreasingStakeFromAccountError>
+{
+    fn from(
+        e: StakeActionError<IncreasingStakeError>,
+    ) -> StakeActionError<IncreasingStakeFromAccountError> {
+        match e {
+            StakeActionError::StakeNotFound => StakeActionError::StakeNotFound,
+            StakeActionError::Error(increasing_stake_error) => StakeActionError::Error(
+                IncreasingStakeFromAccountError::IncreasingStakeError(increasing_stake_error),
+            ),
+        }
+    }
+}
+
+impl From<UnstakingError> for StakeActionError<InitiateUnstakingError> {
+    fn from(e: UnstakingError) -> StakeActionError<InitiateUnstakingError> {
+        StakeActionError::Error(InitiateUnstakingError::UnstakingError(e))
+    }
+}

+ 1083 - 0
runtime-modules/stake/src/lib.rs

@@ -0,0 +1,1083 @@
+// Ensure we're `no_std` when compiling for Wasm.
+#![cfg_attr(not(feature = "std"), no_std)]
+
+use rstd::prelude::*;
+
+use codec::{Codec, Decode, Encode};
+use runtime_primitives::traits::{
+    AccountIdConversion, MaybeSerialize, Member, One, SimpleArithmetic, Zero,
+};
+use runtime_primitives::ModuleId;
+use srml_support::traits::{Currency, ExistenceRequirement, Get, Imbalance, WithdrawReasons};
+use srml_support::{decl_module, decl_storage, ensure, Parameter};
+
+use rstd::collections::btree_map::BTreeMap;
+use system;
+
+mod errors;
+pub use errors::*;
+mod macroes;
+mod mock;
+mod tests;
+
+pub type BalanceOf<T> =
+    <<T as Trait>::Currency as Currency<<T as system::Trait>::AccountId>>::Balance;
+
+pub type NegativeImbalance<T> =
+    <<T as Trait>::Currency as Currency<<T as system::Trait>::AccountId>>::NegativeImbalance;
+
+pub trait Trait: system::Trait + Sized {
+    /// The currency that is managed by the module
+    type Currency: Currency<Self::AccountId>;
+
+    /// ModuleId for computing deterministic AccountId for the module
+    type StakePoolId: Get<[u8; 8]>;
+
+    /// Type that will handle various staking events
+    type StakingEventsHandler: StakingEventsHandler<Self>;
+
+    /// The type used as a stake identifier.
+    type StakeId: Parameter
+        + Member
+        + SimpleArithmetic
+        + Codec
+        + Default
+        + Copy
+        + MaybeSerialize
+        + PartialEq;
+
+    /// The type used as slash identifier.
+    type SlashId: Parameter
+        + Member
+        + SimpleArithmetic
+        + Codec
+        + Default
+        + Copy
+        + MaybeSerialize
+        + PartialEq
+        + Ord; //required to be a key in BTreeMap
+}
+
+pub trait StakingEventsHandler<T: Trait> {
+    /// Handler for unstaking event.
+    /// The handler is informed of the amount that was unstaked, and the value removed from stake is passed as a negative imbalance.
+    /// The handler is responsible to consume part or all of the value (for example by moving it into an account). The remainder
+    /// of the value that is not consumed should be returned as a negative imbalance.
+    fn unstaked(
+        id: &T::StakeId,
+        unstaked_amount: BalanceOf<T>,
+        remaining_imbalance: NegativeImbalance<T>,
+    ) -> NegativeImbalance<T>;
+
+    // Handler for slashing event.
+    // NB: actually_slashed can be less than amount of the slash itself if the
+    // claim amount on the stake cannot cover it fully.
+    fn slashed(
+        id: &T::StakeId,
+        slash_id: &T::SlashId,
+        slashed_amount: BalanceOf<T>,
+        remaining_stake: BalanceOf<T>,
+        remaining_imbalance: NegativeImbalance<T>,
+    ) -> NegativeImbalance<T>;
+}
+
+/// Default implementation just destroys the unstaked or slashed value
+impl<T: Trait> StakingEventsHandler<T> for () {
+    fn unstaked(
+        _id: &T::StakeId,
+        _unstaked_amount: BalanceOf<T>,
+        _remaining_imbalance: NegativeImbalance<T>,
+    ) -> NegativeImbalance<T> {
+        NegativeImbalance::<T>::zero()
+    }
+
+    fn slashed(
+        _id: &T::StakeId,
+        _slash_id: &T::SlashId,
+        _slahed_amount: BalanceOf<T>,
+        _remaining_stake: BalanceOf<T>,
+        _remaining_imbalance: NegativeImbalance<T>,
+    ) -> NegativeImbalance<T> {
+        NegativeImbalance::<T>::zero()
+    }
+}
+
+/// Helper implementation so we can chain multiple handlers by grouping handlers in tuple pairs.
+/// For example for three handlers, A, B and C we can set the StakingEventHandler type on the trait to:
+/// type StakingEventHandler = ((A, B), C)
+/// Individual handlers are expected consume in full or in part the negative imbalance and return any unconsumed value.
+/// The unconsumed value is then passed to the next handler in the chain.
+impl<T: Trait, X: StakingEventsHandler<T>, Y: StakingEventsHandler<T>> StakingEventsHandler<T>
+    for (X, Y)
+{
+    fn unstaked(
+        id: &T::StakeId,
+        unstaked_amount: BalanceOf<T>,
+        imbalance: NegativeImbalance<T>,
+    ) -> NegativeImbalance<T> {
+        let unused_imbalance = X::unstaked(id, unstaked_amount, imbalance);
+        Y::unstaked(id, unstaked_amount, unused_imbalance)
+    }
+
+    fn slashed(
+        id: &T::StakeId,
+        slash_id: &T::SlashId,
+        slashed_amount: BalanceOf<T>,
+        remaining_stake: BalanceOf<T>,
+        imbalance: NegativeImbalance<T>,
+    ) -> NegativeImbalance<T> {
+        let unused_imbalance = X::slashed(id, slash_id, slashed_amount, remaining_stake, imbalance);
+        Y::slashed(
+            id,
+            slash_id,
+            slashed_amount,
+            remaining_stake,
+            unused_imbalance,
+        )
+    }
+}
+
+#[derive(Encode, Decode, Copy, Clone, Debug, Default, Eq, PartialEq)]
+pub struct Slash<BlockNumber, Balance> {
+    /// The block where slashing was initiated.
+    pub started_at_block: BlockNumber,
+
+    /// Whether slashing is in active, or conversley paused state.
+    /// Blocks are only counted towards slashing execution delay when active.
+    pub is_active: bool,
+
+    /// The number blocks which must be finalised while in the active period before the slashing can be executed
+    pub blocks_remaining_in_active_period_for_slashing: BlockNumber,
+
+    /// Amount to slash
+    pub slash_amount: Balance,
+}
+
+#[derive(Encode, Decode, Debug, Default, Eq, PartialEq)]
+pub struct UnstakingState<BlockNumber> {
+    /// The block where the unstaking was initiated
+    pub started_at_block: BlockNumber,
+
+    /// Whether unstaking is in active, or conversely paused state.
+    /// Blocks are only counted towards unstaking period when active.
+    pub is_active: bool,
+
+    /// The number blocks which must be finalised while in the active period before the unstaking is finished
+    pub blocks_remaining_in_active_period_for_unstaking: BlockNumber,
+}
+
+#[derive(Encode, Decode, Debug, Eq, PartialEq)]
+pub enum StakedStatus<BlockNumber> {
+    /// Baseline staking status, nothing is happening.
+    Normal,
+
+    /// Unstaking is under way.
+    Unstaking(UnstakingState<BlockNumber>),
+}
+
+impl<BlockNumber> Default for StakedStatus<BlockNumber> {
+    fn default() -> Self {
+        StakedStatus::Normal
+    }
+}
+
+#[derive(Encode, Decode, Debug, Default, Eq, PartialEq)]
+pub struct StakedState<BlockNumber, Balance, SlashId: Ord> {
+    /// Total amount of funds at stake.
+    pub staked_amount: Balance,
+
+    /// Status of the staking.
+    pub staked_status: StakedStatus<BlockNumber>,
+
+    /// SlashId to use for next Slash that is initiated.
+    /// Will be incremented by one after adding a new Slash.
+    pub next_slash_id: SlashId,
+
+    /// All ongoing slashing.
+    pub ongoing_slashes: BTreeMap<SlashId, Slash<BlockNumber, Balance>>,
+}
+
+impl<BlockNumber, Balance, SlashId> StakedState<BlockNumber, Balance, SlashId>
+where
+    BlockNumber: SimpleArithmetic + Copy,
+    Balance: SimpleArithmetic + Copy,
+    SlashId: Ord + Copy,
+{
+    /// Iterates over all ongoing slashes and decrements blocks_remaining_in_active_period_for_slashing of active slashes (advancing the timer).
+    /// Returns true if there was at least one slashe that was active and had its timer advanced.
+    fn advance_slashing_timer(&mut self) -> bool {
+        let mut did_advance_timers = false;
+
+        for (_slash_id, slash) in self.ongoing_slashes.iter_mut() {
+            if slash.is_active
+                && slash.blocks_remaining_in_active_period_for_slashing > Zero::zero()
+            {
+                slash.blocks_remaining_in_active_period_for_slashing -= One::one();
+                did_advance_timers = true;
+            }
+        }
+
+        did_advance_timers
+    }
+
+    /// Returns pair of slash_id and slashes that should be executed
+    fn get_slashes_to_finalize(&mut self) -> Vec<(SlashId, Slash<BlockNumber, Balance>)> {
+        let slashes_to_finalize = self
+            .ongoing_slashes
+            .iter()
+            .filter(|(_, slash)| {
+                slash.blocks_remaining_in_active_period_for_slashing == Zero::zero()
+            })
+            .map(|(slash_id, _)| *slash_id)
+            .collect::<Vec<_>>();
+
+        // remove and return the slashes
+        slashes_to_finalize
+            .iter()
+            .map(|slash_id| {
+                // assert!(self.ongoing_slashes.contains_key(slash_id))
+                (*slash_id, self.ongoing_slashes.remove(slash_id).unwrap())
+            })
+            .collect()
+    }
+
+    /// Executes a Slash. If remaining at stake drops below the minimum_balance, it will slash the entire staked amount.
+    /// Returns the actual slashed amount.
+    fn apply_slash(
+        &mut self,
+        slash: Slash<BlockNumber, Balance>,
+        minimum_balance: Balance,
+    ) -> Balance {
+        // calculate how much to slash
+        let mut slash_amount = if slash.slash_amount > self.staked_amount {
+            self.staked_amount
+        } else {
+            slash.slash_amount
+        };
+
+        // apply the slashing
+        self.staked_amount -= slash_amount;
+
+        // don't leave less than minimum_balance at stake
+        if self.staked_amount < minimum_balance {
+            slash_amount += self.staked_amount;
+            self.staked_amount = Zero::zero();
+        }
+
+        slash_amount
+    }
+
+    /// For all slahes that should be executed, will apply the Slash to the staked amount, and drop it from the ongoing slashes map.
+    /// Returns a vector of the executed slashes outcome: (SlashId, Slashed Amount, Remaining Staked Amount)
+    fn finalize_slashes(&mut self, minimum_balance: Balance) -> Vec<(SlashId, Balance, Balance)> {
+        let mut finalized_slashes: Vec<(SlashId, Balance, Balance)> = vec![];
+
+        for (slash_id, slash) in self.get_slashes_to_finalize().iter() {
+            // apply the slashing and get back actual amount slashed
+            let slashed_amount = self.apply_slash(*slash, minimum_balance);
+
+            finalized_slashes.push((*slash_id, slashed_amount, self.staked_amount));
+        }
+
+        finalized_slashes
+    }
+}
+
+#[derive(Encode, Decode, Debug, Eq, PartialEq)]
+pub enum StakingStatus<BlockNumber, Balance, SlashId: Ord> {
+    NotStaked,
+
+    Staked(StakedState<BlockNumber, Balance, SlashId>),
+}
+
+impl<BlockNumber, Balance, SlashId: Ord> Default for StakingStatus<BlockNumber, Balance, SlashId> {
+    fn default() -> Self {
+        StakingStatus::NotStaked
+    }
+}
+
+#[derive(Encode, Decode, Default, Debug, Eq, PartialEq)]
+pub struct Stake<BlockNumber, Balance, SlashId: Ord> {
+    /// When role was created
+    pub created: BlockNumber,
+
+    /// Status of any possible ongoing staking
+    pub staking_status: StakingStatus<BlockNumber, Balance, SlashId>,
+}
+
+impl<BlockNumber, Balance, SlashId> Stake<BlockNumber, Balance, SlashId>
+where
+    BlockNumber: Copy + SimpleArithmetic,
+    Balance: Copy + SimpleArithmetic,
+    SlashId: Copy + Ord + Zero + One,
+{
+    fn new(created_at: BlockNumber) -> Self {
+        Self {
+            created: created_at,
+            staking_status: StakingStatus::NotStaked,
+        }
+    }
+
+    fn is_not_staked(&self) -> bool {
+        self.staking_status == StakingStatus::NotStaked
+    }
+
+    /// If staking status is Staked and not currently Unstaking it will increase the staked amount by value.
+    /// On success returns new total staked value.
+    /// Increasing stake by zero is an error.
+    fn increase_stake(&mut self, value: Balance) -> Result<Balance, IncreasingStakeError> {
+        ensure!(
+            value > Zero::zero(),
+            IncreasingStakeError::CannotChangeStakeByZero
+        );
+        match self.staking_status {
+            StakingStatus::Staked(ref mut staked_state) => match staked_state.staked_status {
+                StakedStatus::Normal => {
+                    staked_state.staked_amount += value;
+                    Ok(staked_state.staked_amount)
+                }
+                _ => Err(IncreasingStakeError::CannotIncreaseStakeWhileUnstaking),
+            },
+            _ => Err(IncreasingStakeError::NotStaked),
+        }
+    }
+
+    /// If staking status is Staked and not currently Unstaking, and no ongoing slashes exist, it will decrease the amount at stake
+    /// by provided value. If remaining at stake drops below the minimum_balance it will decrease the stake to zero.
+    /// On success returns (the actual amount of stake decreased, the remaining amount at stake).
+    /// Decreasing stake by zero is an error.
+    fn decrease_stake(
+        &mut self,
+        value: Balance,
+        minimum_balance: Balance,
+    ) -> Result<(Balance, Balance), DecreasingStakeError> {
+        // maybe StakeDecrease
+        ensure!(
+            value > Zero::zero(),
+            DecreasingStakeError::CannotChangeStakeByZero
+        );
+        match self.staking_status {
+            StakingStatus::Staked(ref mut staked_state) => match staked_state.staked_status {
+                StakedStatus::Normal => {
+                    // prevent decreasing stake if there are any ongoing slashes (irrespective if active or not)
+                    if !staked_state.ongoing_slashes.is_empty() {
+                        return Err(DecreasingStakeError::CannotDecreaseStakeWhileOngoingSlahes);
+                    }
+
+                    if value > staked_state.staked_amount {
+                        return Err(DecreasingStakeError::InsufficientStake);
+                    }
+
+                    let stake_to_reduce = if staked_state.staked_amount - value < minimum_balance {
+                        // If staked amount would drop below minimum balance, deduct the entire stake
+                        staked_state.staked_amount
+                    } else {
+                        value
+                    };
+
+                    staked_state.staked_amount -= stake_to_reduce;
+
+                    Ok((stake_to_reduce, staked_state.staked_amount))
+                }
+                _ => return Err(DecreasingStakeError::CannotDecreaseStakeWhileUnstaking),
+            },
+            _ => return Err(DecreasingStakeError::NotStaked),
+        }
+    }
+
+    fn start_staking(
+        &mut self,
+        value: Balance,
+        minimum_balance: Balance,
+    ) -> Result<(), StakingError> {
+        ensure!(value > Zero::zero(), StakingError::CannotStakeZero);
+        ensure!(
+            value >= minimum_balance,
+            StakingError::CannotStakeLessThanMinimumBalance
+        );
+        if self.is_not_staked() {
+            self.staking_status = StakingStatus::Staked(StakedState {
+                staked_amount: value,
+                next_slash_id: Zero::zero(),
+                ongoing_slashes: BTreeMap::new(),
+                staked_status: StakedStatus::Normal,
+            });
+            Ok(())
+        } else {
+            Err(StakingError::AlreadyStaked)
+        }
+    }
+
+    fn initiate_slashing(
+        &mut self,
+        slash_amount: Balance,
+        slash_period: BlockNumber,
+        now: BlockNumber,
+    ) -> Result<SlashId, InitiateSlashingError> {
+        ensure!(
+            slash_period > Zero::zero(),
+            InitiateSlashingError::SlashPeriodShouldBeGreaterThanZero
+        );
+        ensure!(
+            slash_amount > Zero::zero(),
+            InitiateSlashingError::SlashAmountShouldBeGreaterThanZero
+        );
+
+        match self.staking_status {
+            StakingStatus::Staked(ref mut staked_state) => {
+                let slash_id = staked_state.next_slash_id;
+                staked_state.next_slash_id = slash_id + One::one();
+
+                staked_state.ongoing_slashes.insert(
+                    slash_id,
+                    Slash {
+                        is_active: true,
+                        blocks_remaining_in_active_period_for_slashing: slash_period,
+                        slash_amount,
+                        started_at_block: now,
+                    },
+                );
+
+                // pause Unstaking if unstaking is active
+                match staked_state.staked_status {
+                    StakedStatus::Unstaking(ref mut unstaking_state) => {
+                        unstaking_state.is_active = false;
+                    }
+                    _ => (),
+                }
+
+                Ok(slash_id)
+            }
+            _ => Err(InitiateSlashingError::NotStaked),
+        }
+    }
+
+    fn pause_slashing(&mut self, slash_id: &SlashId) -> Result<(), PauseSlashingError> {
+        match self.staking_status {
+            StakingStatus::Staked(ref mut staked_state) => {
+                match staked_state.ongoing_slashes.get_mut(slash_id) {
+                    Some(ref mut slash) => {
+                        if slash.is_active {
+                            slash.is_active = false;
+                            Ok(())
+                        } else {
+                            Err(PauseSlashingError::AlreadyPaused)
+                        }
+                    }
+                    _ => Err(PauseSlashingError::SlashNotFound),
+                }
+            }
+            _ => Err(PauseSlashingError::NotStaked),
+        }
+    }
+
+    fn resume_slashing(&mut self, slash_id: &SlashId) -> Result<(), ResumeSlashingError> {
+        match self.staking_status {
+            StakingStatus::Staked(ref mut staked_state) => {
+                match staked_state.ongoing_slashes.get_mut(slash_id) {
+                    Some(ref mut slash) => {
+                        if slash.is_active {
+                            Err(ResumeSlashingError::NotPaused)
+                        } else {
+                            slash.is_active = true;
+                            Ok(())
+                        }
+                    }
+                    _ => Err(ResumeSlashingError::SlashNotFound),
+                }
+            }
+            _ => Err(ResumeSlashingError::NotStaked),
+        }
+    }
+
+    fn cancel_slashing(&mut self, slash_id: &SlashId) -> Result<(), CancelSlashingError> {
+        match self.staking_status {
+            StakingStatus::Staked(ref mut staked_state) => {
+                if staked_state.ongoing_slashes.remove(slash_id).is_none() {
+                    return Err(CancelSlashingError::SlashNotFound);
+                }
+
+                // 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;
+                        }
+                        _ => (),
+                    }
+                }
+
+                Ok(())
+            }
+            _ => Err(CancelSlashingError::NotStaked),
+        }
+    }
+
+    fn unstake(&mut self) -> Result<Balance, UnstakingError> {
+        let staked_amount = match self.staking_status {
+            StakingStatus::Staked(ref staked_state) => {
+                // prevent unstaking if there are any ongonig slashes (irrespective if active or not)
+                if !staked_state.ongoing_slashes.is_empty() {
+                    return Err(UnstakingError::CannotUnstakeWhileSlashesOngoing);
+                }
+                if StakedStatus::Normal != staked_state.staked_status {
+                    return Err(UnstakingError::AlreadyUnstaking);
+                }
+                Ok(staked_state.staked_amount)
+            }
+            _ => Err(UnstakingError::NotStaked),
+        }?;
+
+        self.staking_status = StakingStatus::NotStaked;
+        Ok(staked_amount)
+    }
+
+    fn initiate_unstaking(
+        &mut self,
+        unstaking_period: BlockNumber,
+        now: BlockNumber,
+    ) -> Result<(), InitiateUnstakingError> {
+        ensure!(
+            unstaking_period > Zero::zero(),
+            InitiateUnstakingError::UnstakingPeriodShouldBeGreaterThanZero
+        );
+        match self.staking_status {
+            StakingStatus::Staked(ref mut staked_state) => {
+                // prevent unstaking if there are any ongonig slashes (irrespective if active or not)
+                if !staked_state.ongoing_slashes.is_empty() {
+                    return Err(InitiateUnstakingError::UnstakingError(
+                        UnstakingError::CannotUnstakeWhileSlashesOngoing,
+                    ));
+                }
+
+                if StakedStatus::Normal != staked_state.staked_status {
+                    return Err(InitiateUnstakingError::UnstakingError(
+                        UnstakingError::AlreadyUnstaking,
+                    ));
+                }
+
+                staked_state.staked_status = StakedStatus::Unstaking(UnstakingState {
+                    started_at_block: now,
+                    is_active: true,
+                    blocks_remaining_in_active_period_for_unstaking: unstaking_period,
+                });
+
+                Ok(())
+            }
+            _ => Err(InitiateUnstakingError::UnstakingError(
+                UnstakingError::NotStaked,
+            )),
+        }
+    }
+
+    fn pause_unstaking(&mut self) -> Result<(), PauseUnstakingError> {
+        match self.staking_status {
+            StakingStatus::Staked(ref mut staked_state) => match staked_state.staked_status {
+                StakedStatus::Unstaking(ref mut unstaking_state) => {
+                    if unstaking_state.is_active {
+                        unstaking_state.is_active = false;
+                        Ok(())
+                    } else {
+                        Err(PauseUnstakingError::AlreadyPaused)
+                    }
+                }
+                _ => Err(PauseUnstakingError::NotUnstaking),
+            },
+            _ => Err(PauseUnstakingError::NotStaked),
+        }
+    }
+
+    fn resume_unstaking(&mut self) -> Result<(), ResumeUnstakingError> {
+        match self.staking_status {
+            StakingStatus::Staked(ref mut staked_state) => match staked_state.staked_status {
+                StakedStatus::Unstaking(ref mut unstaking_state) => {
+                    if !unstaking_state.is_active {
+                        unstaking_state.is_active = true;
+                        Ok(())
+                    } else {
+                        Err(ResumeUnstakingError::NotPaused)
+                    }
+                }
+                _ => Err(ResumeUnstakingError::NotUnstaking),
+            },
+            _ => Err(ResumeUnstakingError::NotStaked),
+        }
+    }
+
+    fn finalize_slashing(
+        &mut self,
+        minimum_balance: Balance,
+    ) -> (bool, Vec<(SlashId, Balance, Balance)>) {
+        match self.staking_status {
+            StakingStatus::Staked(ref mut staked_state) => {
+                // tick the slashing timer
+                let did_update = staked_state.advance_slashing_timer();
+
+                // finalize and apply slashes
+                let slashed = staked_state.finalize_slashes(minimum_balance);
+
+                (did_update, slashed)
+            }
+            _ => (false, vec![]),
+        }
+    }
+
+    fn finalize_unstaking(&mut self) -> (bool, Option<Balance>) {
+        let (did_update, unstaked) = match self.staking_status {
+            StakingStatus::Staked(ref mut staked_state) => match staked_state.staked_status {
+                StakedStatus::Unstaking(ref mut unstaking_state) => {
+                    // if all slashes were processed and there are no more active slashes
+                    // resume unstaking
+                    if staked_state.ongoing_slashes.is_empty() {
+                        unstaking_state.is_active = true;
+                    }
+
+                    // tick the unstaking timer
+                    if unstaking_state.is_active
+                        && unstaking_state.blocks_remaining_in_active_period_for_unstaking
+                            > Zero::zero()
+                    {
+                        // tick the unstaking timer
+                        unstaking_state.blocks_remaining_in_active_period_for_unstaking -=
+                            One::one();
+                    }
+
+                    // finalize unstaking
+                    if unstaking_state.blocks_remaining_in_active_period_for_unstaking
+                        == Zero::zero()
+                    {
+                        (true, Some(staked_state.staked_amount))
+                    } else {
+                        (unstaking_state.is_active, None)
+                    }
+                }
+                _ => (false, None),
+            },
+            _ => (false, None),
+        };
+
+        // if unstaking was finalized transition to NotStaked state
+        if unstaked.is_some() {
+            self.staking_status = StakingStatus::NotStaked;
+        }
+
+        (did_update, unstaked)
+    }
+
+    fn finalize_slashing_and_unstaking(
+        &mut self,
+        minimum_balance: Balance,
+    ) -> (bool, Vec<(SlashId, Balance, Balance)>, Option<Balance>) {
+        let (did_update_slashing_timers, slashed) = self.finalize_slashing(minimum_balance);
+
+        let (did_update_unstaking_timer, unstaked) = self.finalize_unstaking();
+
+        (
+            did_update_slashing_timers || did_update_unstaking_timer,
+            slashed,
+            unstaked,
+        )
+    }
+}
+
+decl_storage! {
+    trait Store for Module<T: Trait> as StakePool {
+        /// Maps identifiers to a stake.
+        pub Stakes get(stakes): linked_map T::StakeId => Stake<T::BlockNumber, BalanceOf<T>, T::SlashId>;
+
+        /// Identifier value for next stake, and count of total stakes created (not necessarily the number of current
+        /// stakes in the Stakes map as stakes can be removed.)
+        pub StakesCreated get(stakes_created): T::StakeId;
+    }
+}
+
+decl_module! {
+    pub struct Module<T: Trait> for enum Call where origin: T::Origin {
+        fn on_finalize(_now: T::BlockNumber) {
+            Self::finalize_slashing_and_unstaking();
+        }
+    }
+}
+
+impl<T: Trait> Module<T> {
+    /// The account ID of theis module which holds all the staked balance. (referred to as the stake pool)
+    ///
+    /// This actually does computation. If you need to keep using it, then make sure you cache the
+    /// value and only call this once. Is it deterministic?
+    pub fn stake_pool_account_id() -> T::AccountId {
+        ModuleId(T::StakePoolId::get()).into_account()
+    }
+
+    pub fn stake_pool_balance() -> BalanceOf<T> {
+        T::Currency::free_balance(&Self::stake_pool_account_id())
+    }
+
+    /// Adds a new Stake which is NotStaked, created at given block, into stakes map.
+    pub fn create_stake() -> T::StakeId {
+        let stake_id = Self::stakes_created();
+        <StakesCreated<T>>::put(stake_id + One::one());
+
+        <Stakes<T>>::insert(&stake_id, Stake::new(<system::Module<T>>::block_number()));
+
+        stake_id
+    }
+
+    /// Given that stake with id exists in stakes and is NotStaked, remove from stakes.
+    pub fn remove_stake(stake_id: &T::StakeId) -> Result<(), StakeActionError<StakingError>> {
+        let stake = ensure_stake_exists!(T, stake_id, StakeActionError::StakeNotFound)?;
+
+        ensure!(
+            stake.is_not_staked(),
+            StakeActionError::Error(StakingError::AlreadyStaked)
+        );
+
+        <Stakes<T>>::remove(stake_id);
+
+        Ok(())
+    }
+
+    /// Dry run to see if staking can be initiated for the specified stake id. This should
+    /// be called before stake() to make sure staking is possible before withdrawing funds.
+    pub fn ensure_can_stake(
+        stake_id: &T::StakeId,
+        value: BalanceOf<T>,
+    ) -> Result<(), StakeActionError<StakingError>> {
+        let mut stake = ensure_stake_exists!(T, stake_id, StakeActionError::StakeNotFound)?;
+
+        stake
+            .start_staking(value, T::Currency::minimum_balance())
+            .err()
+            .map_or(Ok(()), |err| Err(StakeActionError::Error(err)))
+    }
+
+    /// Provided the stake exists and is in state NotStaked the value is transferred
+    /// to the module's account, and the corresponding staked_balance is set to this amount in the new Staked state.
+    /// On error, as the negative imbalance is not returned to the caller, it is the caller's responsibility to return the funds
+    /// back to the source (by creating a new positive imbalance)
+    pub fn stake(
+        stake_id: &T::StakeId,
+        imbalance: NegativeImbalance<T>,
+    ) -> Result<(), StakeActionError<StakingError>> {
+        let mut stake = ensure_stake_exists!(T, stake_id, StakeActionError::StakeNotFound)?;
+
+        let value = imbalance.peek();
+
+        stake.start_staking(value, T::Currency::minimum_balance())?;
+
+        <Stakes<T>>::insert(stake_id, stake);
+
+        Self::deposit_funds_into_stake_pool(imbalance);
+
+        Ok(())
+    }
+
+    pub fn stake_from_account(
+        stake_id: &T::StakeId,
+        source_account_id: &T::AccountId,
+        value: BalanceOf<T>,
+    ) -> Result<(), StakeActionError<StakingFromAccountError>> {
+        let mut stake = ensure_stake_exists!(T, stake_id, StakeActionError::StakeNotFound)?;
+
+        stake.start_staking(value, T::Currency::minimum_balance())?;
+
+        // Its important to only do the transfer as the last step to ensure starting staking was possible.
+        Self::transfer_funds_from_account_into_stake_pool(source_account_id, value)?;
+
+        <Stakes<T>>::insert(stake_id, stake);
+
+        Ok(())
+    }
+
+    /// Moves funds from specified account into the module's account
+    fn transfer_funds_from_account_into_stake_pool(
+        source: &T::AccountId,
+        value: BalanceOf<T>,
+    ) -> Result<(), TransferFromAccountError> {
+        // We don't use T::Currency::transfer() to prevent fees being incurred.
+        let negative_imbalance = T::Currency::withdraw(
+            source,
+            value,
+            WithdrawReasons::all(),
+            ExistenceRequirement::AllowDeath,
+        )
+        .map_err(|_err| TransferFromAccountError::InsufficientBalance)?;
+
+        Self::deposit_funds_into_stake_pool(negative_imbalance);
+        Ok(())
+    }
+
+    fn deposit_funds_into_stake_pool(imbalance: NegativeImbalance<T>) {
+        // move the negative imbalance into the stake pool
+        T::Currency::resolve_creating(&Self::stake_pool_account_id(), imbalance);
+    }
+
+    /// Moves funds from the module's account into specified account. Should never fail if used internally.
+    /// Will panic! if value exceeds balance in the pool.
+    fn transfer_funds_from_pool_into_account(destination: &T::AccountId, value: BalanceOf<T>) {
+        let imbalance = Self::withdraw_funds_from_stake_pool(value);
+        T::Currency::resolve_creating(destination, imbalance);
+    }
+
+    /// Withdraws value from the pool and returns a NegativeImbalance.
+    /// As long as it is only called internally when executing slashes and unstaking, it
+    /// should never fail as the pool balance is always in sync with total amount at stake.
+    fn withdraw_funds_from_stake_pool(value: BalanceOf<T>) -> NegativeImbalance<T> {
+        // We don't use T::Currency::transfer() to prevent fees being incurred.
+        T::Currency::withdraw(
+            &Self::stake_pool_account_id(),
+            value,
+            WithdrawReasons::all(),
+            ExistenceRequirement::AllowDeath,
+        )
+        .expect("pool had less than expected funds!")
+    }
+
+    /// Dry run to see if the state of stake allows for increasing stake. This should be called
+    /// to make sure increasing stake is possible before withdrawing funds.
+    pub fn ensure_can_increase_stake(
+        stake_id: &T::StakeId,
+        value: BalanceOf<T>,
+    ) -> Result<(), StakeActionError<IncreasingStakeError>> {
+        let mut stake = ensure_stake_exists!(T, stake_id, StakeActionError::StakeNotFound)?;
+
+        stake
+            .increase_stake(value)
+            .err()
+            .map_or(Ok(()), |err| Err(StakeActionError::Error(err)))
+    }
+
+    /// Provided the stake exists and is in state Staked.Normal, then the amount is transferred to the module's account,
+    /// and the corresponding staked_amount is increased by the value. New value of staked_amount is returned.
+    /// Caller should call check ensure_can_increase_stake() prior to avoid getting back an error. On error, as the negative imbalance
+    /// is not returned to the caller, it is the caller's responsibility to return the funds back to the source (by creating a new positive imbalance)
+    pub fn increase_stake(
+        stake_id: &T::StakeId,
+        imbalance: NegativeImbalance<T>,
+    ) -> Result<BalanceOf<T>, StakeActionError<IncreasingStakeError>> {
+        let mut stake = ensure_stake_exists!(T, stake_id, StakeActionError::StakeNotFound)?;
+
+        let total_staked_amount = stake.increase_stake(imbalance.peek())?;
+        <Stakes<T>>::insert(stake_id, stake);
+
+        Self::deposit_funds_into_stake_pool(imbalance);
+
+        Ok(total_staked_amount)
+    }
+
+    /// Provided the stake exists and is in state Staked.Normal, and the given source account covers the amount,
+    /// then the amount is transferred to the module's account, and the corresponding staked_amount is increased
+    /// by the amount. New value of staked_amount is returned.
+    pub fn increase_stake_from_account(
+        stake_id: &T::StakeId,
+        source_account_id: &T::AccountId,
+        value: BalanceOf<T>,
+    ) -> Result<BalanceOf<T>, StakeActionError<IncreasingStakeFromAccountError>> {
+        // Compiler error when using macro: cannot infer type for `ErrorType`
+        // let mut stake = ensure_stake_exists!(T, stake_id, StakeActionError::StakeNotFound)?;
+        ensure!(
+            <Stakes<T>>::exists(stake_id),
+            StakeActionError::StakeNotFound
+        );
+
+        let mut stake = Self::stakes(stake_id);
+
+        let total_staked_amount = stake.increase_stake(value)?;
+
+        Self::transfer_funds_from_account_into_stake_pool(&source_account_id, value)?;
+
+        <Stakes<T>>::insert(stake_id, stake);
+
+        Ok(total_staked_amount)
+    }
+
+    pub fn ensure_can_decrease_stake(
+        stake_id: &T::StakeId,
+        value: BalanceOf<T>,
+    ) -> Result<(), StakeActionError<DecreasingStakeError>> {
+        let mut stake = ensure_stake_exists!(T, stake_id, StakeActionError::StakeNotFound)?;
+
+        stake
+            .decrease_stake(value, T::Currency::minimum_balance())
+            .err()
+            .map_or(Ok(()), |err| Err(StakeActionError::Error(err)))
+    }
+
+    pub fn decrease_stake(
+        stake_id: &T::StakeId,
+        value: BalanceOf<T>,
+    ) -> Result<(BalanceOf<T>, NegativeImbalance<T>), StakeActionError<DecreasingStakeError>> {
+        let mut stake = ensure_stake_exists!(T, stake_id, StakeActionError::StakeNotFound)?;
+
+        let (deduct_from_pool, staked_amount) =
+            stake.decrease_stake(value, T::Currency::minimum_balance())?;
+
+        <Stakes<T>>::insert(stake_id, stake);
+
+        let imbalance = Self::withdraw_funds_from_stake_pool(deduct_from_pool);
+
+        Ok((staked_amount, imbalance))
+    }
+
+    /// Provided the stake exists and is in state Staked.Normal, and the given stake holds at least the value,
+    /// then the value is transferred from the module's account to the destination_account, and the corresponding
+    /// staked_amount is decreased by the value. New value of staked_amount is returned.
+    pub fn decrease_stake_to_account(
+        stake_id: &T::StakeId,
+        destination_account_id: &T::AccountId,
+        value: BalanceOf<T>,
+    ) -> Result<BalanceOf<T>, StakeActionError<DecreasingStakeError>> {
+        let mut stake = ensure_stake_exists!(T, stake_id, StakeActionError::StakeNotFound)?;
+
+        let (deduct_from_pool, staked_amount) =
+            stake.decrease_stake(value, T::Currency::minimum_balance())?;
+
+        <Stakes<T>>::insert(stake_id, stake);
+
+        Self::transfer_funds_from_pool_into_account(&destination_account_id, deduct_from_pool);
+
+        Ok(staked_amount)
+    }
+
+    /// Initiate a new slashing of a staked stake.
+    pub fn initiate_slashing(
+        stake_id: &T::StakeId,
+        slash_amount: BalanceOf<T>,
+        slash_period: T::BlockNumber,
+    ) -> Result<T::SlashId, StakeActionError<InitiateSlashingError>> {
+        let mut stake = ensure_stake_exists!(T, stake_id, StakeActionError::StakeNotFound)?;
+
+        let slash_id = stake.initiate_slashing(
+            slash_amount,
+            slash_period,
+            <system::Module<T>>::block_number(),
+        )?;
+
+        <Stakes<T>>::insert(stake_id, stake);
+        Ok(slash_id)
+    }
+
+    /// Pause an ongoing slashing
+    pub fn pause_slashing(
+        stake_id: &T::StakeId,
+        slash_id: &T::SlashId,
+    ) -> Result<(), StakeActionError<PauseSlashingError>> {
+        let mut stake = ensure_stake_exists!(T, stake_id, StakeActionError::StakeNotFound)?;
+
+        stake.pause_slashing(slash_id)?;
+
+        <Stakes<T>>::insert(stake_id, stake);
+
+        Ok(())
+    }
+
+    /// Resume a currently paused ongoing slashing.
+    pub fn resume_slashing(
+        stake_id: &T::StakeId,
+        slash_id: &T::SlashId,
+    ) -> Result<(), StakeActionError<ResumeSlashingError>> {
+        let mut stake = ensure_stake_exists!(T, stake_id, StakeActionError::StakeNotFound)?;
+
+        stake.resume_slashing(slash_id)?;
+
+        <Stakes<T>>::insert(stake_id, stake);
+        Ok(())
+    }
+
+    /// Cancel an ongoing slashing (regardless of whether its active or paused).
+    pub fn cancel_slashing(
+        stake_id: &T::StakeId,
+        slash_id: &T::SlashId,
+    ) -> Result<(), StakeActionError<CancelSlashingError>> {
+        let mut stake = ensure_stake_exists!(T, stake_id, StakeActionError::StakeNotFound)?;
+
+        stake.cancel_slashing(slash_id)?;
+
+        <Stakes<T>>::insert(stake_id, stake);
+
+        Ok(())
+    }
+
+    /// Initiate unstaking of a Staked stake.
+    pub fn initiate_unstaking(
+        stake_id: &T::StakeId,
+        unstaking_period: Option<T::BlockNumber>,
+    ) -> Result<(), StakeActionError<InitiateUnstakingError>> {
+        let mut stake = ensure_stake_exists!(T, stake_id, StakeActionError::StakeNotFound)?;
+
+        if let Some(unstaking_period) = unstaking_period {
+            stake.initiate_unstaking(unstaking_period, <system::Module<T>>::block_number())?;
+            <Stakes<T>>::insert(stake_id, stake);
+        } else {
+            let staked_amount = stake.unstake()?;
+            <Stakes<T>>::insert(stake_id, stake);
+
+            let imbalance = Self::withdraw_funds_from_stake_pool(staked_amount);
+            let _ = T::StakingEventsHandler::unstaked(stake_id, staked_amount, imbalance);
+        }
+
+        Ok(())
+    }
+
+    /// Pause an ongoing Unstaking.
+    pub fn pause_unstaking(
+        stake_id: &T::StakeId,
+    ) -> Result<(), StakeActionError<PauseUnstakingError>> {
+        let mut stake = ensure_stake_exists!(T, stake_id, StakeActionError::StakeNotFound)?;
+
+        stake.pause_unstaking()?;
+
+        <Stakes<T>>::insert(stake_id, stake);
+
+        Ok(())
+    }
+
+    /// Resume a currently paused ongoing unstaking.
+    pub fn resume_unstaking(
+        stake_id: &T::StakeId,
+    ) -> Result<(), StakeActionError<ResumeUnstakingError>> {
+        let mut stake = ensure_stake_exists!(T, stake_id, StakeActionError::StakeNotFound)?;
+
+        stake.resume_unstaking()?;
+
+        <Stakes<T>>::insert(stake_id, stake);
+
+        Ok(())
+    }
+
+    /// Handle timers for finalizing unstaking and slashing.
+    /// Finalised unstaking results in the staked_balance in the given stake to removed from the pool, the corresponding
+    /// imbalance is provided to the unstaked() hook in the StakingEventsHandler.
+    /// Finalised slashing results in the staked_balance in the given stake being correspondingly reduced, and the imbalance
+    /// is provided to the slashed() hook in the StakingEventsHandler.
+    fn finalize_slashing_and_unstaking() {
+        for (stake_id, ref mut stake) in <Stakes<T>>::enumerate() {
+            let (updated, slashed, unstaked) =
+                stake.finalize_slashing_and_unstaking(T::Currency::minimum_balance());
+
+            // update the state before making external calls to StakingEventsHandler
+            if updated {
+                <Stakes<T>>::insert(stake_id, stake)
+            }
+
+            for (slash_id, slashed_amount, staked_amount) in slashed.into_iter() {
+                // remove the slashed amount from the pool
+                let imbalance = Self::withdraw_funds_from_stake_pool(slashed_amount);
+
+                let _ = T::StakingEventsHandler::slashed(
+                    &stake_id,
+                    &slash_id,
+                    slashed_amount,
+                    staked_amount,
+                    imbalance,
+                );
+            }
+
+            if let Some(staked_amount) = unstaked {
+                // remove the unstaked amount from the pool
+                let imbalance = Self::withdraw_funds_from_stake_pool(staked_amount);
+
+                let _ = T::StakingEventsHandler::unstaked(&stake_id, staked_amount, imbalance);
+            }
+        }
+    }
+}

+ 19 - 0
runtime-modules/stake/src/macroes.rs

@@ -0,0 +1,19 @@
+#[macro_export]
+macro_rules! ensure_map_has_mapping_with_key {
+    ($map_variable_name:ident , $runtime_trait:tt, $key:expr, $error:expr) => {{
+        if <$map_variable_name<$runtime_trait>>::exists($key) {
+            let value = <$map_variable_name<$runtime_trait>>::get($key);
+
+            Ok(value)
+        } else {
+            Err($error)
+        }
+    }};
+}
+
+#[macro_export]
+macro_rules! ensure_stake_exists {
+    ($runtime_trait:tt, $stake_id:expr, $error:expr) => {{
+        ensure_map_has_mapping_with_key!(Stakes, $runtime_trait, $stake_id, $error)
+    }};
+}

+ 115 - 0
runtime-modules/stake/src/mock.rs

@@ -0,0 +1,115 @@
+#![cfg(test)]
+
+use crate::*;
+
+use primitives::H256;
+
+use crate::{Module, Trait};
+use balances;
+use runtime_primitives::{
+    testing::Header,
+    traits::{BlakeTwo256, IdentityLookup},
+    Perbill,
+};
+use srml_support::{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;
+}
+
+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 = ();
+}
+
+parameter_types! {
+    pub const ExistentialDeposit: u32 = 500;
+    pub const TransferFee: u32 = 5;
+    pub const CreationFee: u32 = 5;
+    pub const TransactionBaseFee: u32 = 5;
+    pub const TransactionByteFee: u32 = 0;
+    pub const StakePoolId: [u8; 8] = *b"joystake";
+}
+
+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 Trait for Test {
+    type Currency = Balances;
+    type StakePoolId = StakePoolId;
+    type StakingEventsHandler = ();
+    type StakeId = u64;
+    type SlashId = u64;
+}
+
+pub fn build_test_externalities() -> runtime_io::TestExternalities {
+    let t = system::GenesisConfig::default()
+        .build_storage::<Test>()
+        .unwrap();
+
+    t.into()
+}
+
+pub type System = system::Module<Test>;
+pub type Balances = balances::Module<Test>;
+pub type StakePool = Module<Test>;
+
+// Some helper methods for creating Stake states
+pub mod fixtures {
+    use super::*;
+    pub type OngoingSlashes = BTreeMap<
+        <Test as Trait>::SlashId,
+        Slash<<Test as system::Trait>::BlockNumber, BalanceOf<Test>>,
+    >;
+    // pub enum StakeInState {
+    //     NotStaked,
+    //     StakedNormal(BalanceOf<Test>, OngoingSlashes),
+    //     StakedUnstaking(BalanceOf<Test>, OngoingSlashes, <Test as system::Trait>::BlockNumber),
+    // }
+    // fn get_next_slash_id() -> SlashId {
+    // }
+    // pub fn make_stake(state: StakeInState) -> StakeId {
+    //     let id = StakePool::create_stake();
+    //     <Stakes<Test>>::mutate(id, |stake| {});
+    //     id
+    // }
+    // fn stake_in_state_to_stake(StakeInState) -> StakedState {}
+}

+ 804 - 0
runtime-modules/stake/src/tests.rs

@@ -0,0 +1,804 @@
+#![cfg(test)]
+
+use super::*;
+use crate::mock::*;
+use runtime_primitives::traits::OnFinalize;
+use srml_support::{assert_err, assert_ok};
+
+#[test]
+fn stake_pool_works() {
+    build_test_externalities().execute_with(|| {
+        // using deposit_creating
+        assert_eq!(Balances::total_issuance(), 0);
+        assert_eq!(StakePool::stake_pool_balance(), 0);
+
+        // minimum balance (existential deposit) feature applies to stake pool
+        if Balances::minimum_balance() > 0 {
+            let pos_imbalance = Balances::deposit_creating(
+                &StakePool::stake_pool_account_id(),
+                Balances::minimum_balance() - 1,
+            );
+            assert_eq!(pos_imbalance.peek(), 0);
+            assert_eq!(Balances::total_issuance(), 0);
+            assert_eq!(StakePool::stake_pool_balance(), 0);
+        }
+
+        let starting_pool_balance = Balances::minimum_balance() + 1000;
+        let _ =
+            Balances::deposit_creating(&StakePool::stake_pool_account_id(), starting_pool_balance);
+        assert_eq!(Balances::total_issuance(), starting_pool_balance);
+        assert_eq!(StakePool::stake_pool_balance(), starting_pool_balance);
+
+        let staker_starting_balance = Balances::minimum_balance() + 1000;
+        // using transfer_funds_from_account_into_pool()
+        let _ = Balances::deposit_creating(&1, staker_starting_balance);
+        assert_eq!(
+            Balances::total_issuance(),
+            starting_pool_balance + staker_starting_balance
+        );
+
+        let funds = 100;
+
+        assert_ok!(StakePool::transfer_funds_from_account_into_stake_pool(
+            &1, funds
+        ));
+
+        // total issuance unchanged after movement of funds
+        assert_eq!(
+            Balances::total_issuance(),
+            starting_pool_balance + staker_starting_balance
+        );
+
+        // funds moved into stake pool
+        assert_eq!(
+            StakePool::stake_pool_balance(),
+            starting_pool_balance + funds
+        );
+
+        // no fees were deducted
+        assert_eq!(Balances::free_balance(&1), staker_starting_balance - funds);
+
+        StakePool::transfer_funds_from_pool_into_account(&1, funds);
+
+        assert_eq!(Balances::free_balance(&1), staker_starting_balance);
+        assert_eq!(StakePool::stake_pool_balance(), starting_pool_balance);
+    });
+}
+
+#[test]
+fn create_stake() {
+    build_test_externalities().execute_with(|| {
+        let stake_id = StakePool::create_stake();
+        assert_eq!(stake_id, 0);
+        assert!(<Stakes<Test>>::exists(&stake_id));
+
+        assert_eq!(StakePool::stakes_created(), stake_id + 1);
+
+        // Should be NotStaked
+        let stake = StakePool::stakes(&stake_id);
+        assert_eq!(stake.staking_status, StakingStatus::NotStaked);
+    });
+}
+
+#[test]
+fn remove_stake_in_not_staked_state() {
+    build_test_externalities().execute_with(|| {
+        <Stakes<Test>>::insert(
+            &100,
+            Stake {
+                created: 0,
+                staking_status: StakingStatus::NotStaked,
+            },
+        );
+        assert_ok!(StakePool::remove_stake(&100));
+        assert!(!<Stakes<Test>>::exists(&100));
+
+        // when status is Staked, removing should fail
+        <Stakes<Test>>::insert(
+            &200,
+            Stake {
+                created: 0,
+                staking_status: StakingStatus::Staked(Default::default()),
+            },
+        );
+
+        assert_err!(
+            StakePool::remove_stake(&200),
+            StakeActionError::Error(StakingError::AlreadyStaked)
+        );
+        assert!(<Stakes<Test>>::exists(&200));
+    });
+}
+
+#[test]
+fn enter_staked_state() {
+    build_test_externalities().execute_with(|| {
+        <Stakes<Test>>::insert(
+            &100,
+            Stake {
+                created: 0,
+                staking_status: StakingStatus::NotStaked,
+            },
+        );
+
+        let starting_balance: u64 = Balances::minimum_balance();
+        let staker_account: u64 = 1;
+        let stake_value: u64 = Balances::minimum_balance() + 100;
+
+        let _ = Balances::deposit_creating(&staker_account, starting_balance);
+
+        // can't stake zero
+        assert_err!(
+            StakePool::stake_from_account(&100, &staker_account, 0),
+            StakeActionError::Error(StakingFromAccountError::StakingError(
+                StakingError::CannotStakeZero
+            ))
+        );
+
+        // must stake at least the minimum balance
+        if Balances::minimum_balance() > 0 {
+            assert_err!(
+                StakePool::stake_from_account(
+                    &100,
+                    &staker_account,
+                    Balances::minimum_balance() - 1
+                ),
+                StakeActionError::Error(StakingFromAccountError::StakingError(
+                    StakingError::CannotStakeLessThanMinimumBalance
+                ))
+            );
+        }
+
+        // cannot stake with insufficient funds
+        assert_err!(
+            StakePool::stake_from_account(&100, &staker_account, stake_value),
+            StakeActionError::Error(StakingFromAccountError::InsufficientBalanceInSourceAccount)
+        );
+
+        // deposit exact amount to stake
+        let _ = Balances::deposit_creating(&staker_account, stake_value);
+
+        assert_ok!(StakePool::stake_from_account(
+            &100,
+            &staker_account,
+            stake_value
+        ));
+
+        assert_eq!(Balances::free_balance(&staker_account), starting_balance);
+
+        assert_eq!(StakePool::stake_pool_balance(), stake_value);
+    });
+}
+
+#[test]
+fn increasing_stake() {
+    build_test_externalities().execute_with(|| {
+        let starting_pool_stake = Balances::minimum_balance() + 5000;
+        let _ =
+            Balances::deposit_creating(&StakePool::stake_pool_account_id(), starting_pool_stake);
+
+        let starting_stake = Balances::minimum_balance() + 100;
+        <Stakes<Test>>::insert(
+            &100,
+            Stake {
+                created: 0,
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount: starting_stake,
+                    ongoing_slashes: BTreeMap::new(),
+                    next_slash_id: 0,
+                    staked_status: StakedStatus::Normal,
+                }),
+            },
+        );
+
+        let additional_stake: u64 = 500;
+        let starting_balance: u64 = Balances::minimum_balance() + additional_stake;
+        let staker_account: u64 = 1;
+
+        let _ = Balances::deposit_creating(&staker_account, starting_balance);
+
+        assert_err!(
+            StakePool::increase_stake_from_account(&100, &staker_account, 0),
+            StakeActionError::Error(IncreasingStakeFromAccountError::IncreasingStakeError(
+                IncreasingStakeError::CannotChangeStakeByZero
+            ))
+        );
+
+        let total_staked =
+            StakePool::increase_stake_from_account(&100, &staker_account, additional_stake)
+                .ok()
+                .unwrap();
+        assert_eq!(total_staked, starting_stake + additional_stake);
+
+        assert_eq!(
+            Balances::free_balance(&staker_account),
+            starting_balance - additional_stake
+        );
+
+        assert_eq!(
+            StakePool::stake_pool_balance(),
+            starting_pool_stake + additional_stake
+        );
+
+        assert_eq!(
+            StakePool::stakes(&100),
+            Stake {
+                created: 0,
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount: starting_stake + additional_stake,
+                    ongoing_slashes: BTreeMap::new(),
+                    next_slash_id: 0,
+                    staked_status: StakedStatus::Normal,
+                })
+            }
+        );
+
+        // cannot increase stake if insufficent balance
+        assert!(StakePool::increase_stake_from_account(
+            &100,
+            &staker_account,
+            Balances::free_balance(&staker_account) + 1
+        )
+        .is_err());
+    });
+}
+
+#[test]
+fn decreasing_stake() {
+    build_test_externalities().execute_with(|| {
+        let starting_pool_stake = 5000;
+        let _ =
+            Balances::deposit_creating(&StakePool::stake_pool_account_id(), starting_pool_stake);
+
+        let starting_stake = Balances::minimum_balance() + 2000;
+        <Stakes<Test>>::insert(
+            &100,
+            Stake {
+                created: 0,
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount: starting_stake,
+                    ongoing_slashes: BTreeMap::new(),
+                    next_slash_id: 0,
+                    staked_status: StakedStatus::Normal,
+                }),
+            },
+        );
+
+        let starting_balance: u64 = Balances::minimum_balance();
+        let staker_account: u64 = 1;
+        let decrease_stake_by: u64 = 200;
+
+        let _ = Balances::deposit_creating(&staker_account, starting_balance);
+
+        assert_err!(
+            StakePool::decrease_stake_to_account(&100, &staker_account, 0),
+            StakeActionError::Error(DecreasingStakeError::CannotChangeStakeByZero)
+        );
+
+        let total_staked =
+            StakePool::decrease_stake_to_account(&100, &staker_account, decrease_stake_by)
+                .ok()
+                .unwrap();
+        assert_eq!(total_staked, starting_stake - decrease_stake_by);
+
+        assert_eq!(
+            Balances::free_balance(&staker_account),
+            starting_balance + decrease_stake_by
+        );
+
+        assert_eq!(
+            StakePool::stake_pool_balance(),
+            starting_pool_stake - decrease_stake_by
+        );
+
+        assert_eq!(
+            StakePool::stakes(&100),
+            Stake {
+                created: 0,
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount: starting_stake - decrease_stake_by,
+                    ongoing_slashes: BTreeMap::new(),
+                    next_slash_id: 0,
+                    staked_status: StakedStatus::Normal,
+                })
+            }
+        );
+
+        // cannot unstake more than total at stake
+        assert_err!(
+            StakePool::decrease_stake_to_account(&100, &staker_account, total_staked + 1),
+            StakeActionError::Error(DecreasingStakeError::InsufficientStake)
+        );
+
+        // decreasing stake to value less than minimum_balance should reduce entire stake
+        if Balances::minimum_balance() > 0 {
+            let over_minimum = 50;
+            let staked_amount = Balances::minimum_balance() + over_minimum;
+
+            let _ = Balances::deposit_creating(&StakePool::stake_pool_account_id(), staked_amount);
+            <Stakes<Test>>::insert(
+                &200,
+                Stake {
+                    created: 0,
+                    staking_status: StakingStatus::Staked(StakedState {
+                        staked_amount: staked_amount,
+                        ongoing_slashes: BTreeMap::new(),
+                        next_slash_id: 0,
+                        staked_status: StakedStatus::Normal,
+                    }),
+                },
+            );
+
+            assert_eq!(Balances::free_balance(&2), 0);
+            let starting_pool_balance = StakePool::stake_pool_balance();
+            let remaining_stake = StakePool::decrease_stake_to_account(&200, &2, over_minimum + 1)
+                .ok()
+                .unwrap();
+            assert_eq!(remaining_stake, 0);
+            assert_eq!(Balances::free_balance(&2), staked_amount);
+            assert_eq!(
+                StakePool::stake_pool_balance(),
+                starting_pool_balance - staked_amount
+            );
+        }
+    });
+}
+
+#[test]
+fn initiating_pausing_resuming_cancelling_slashes() {
+    build_test_externalities().execute_with(|| {
+        let staked_amount = Balances::minimum_balance() + 10000;
+        let _ = Balances::deposit_creating(&StakePool::stake_pool_account_id(), staked_amount);
+
+        assert_err!(
+            StakePool::initiate_slashing(&100, 5000, 0),
+            StakeActionError::StakeNotFound
+        );
+
+        let stake_id = StakePool::create_stake();
+        <Stakes<Test>>::insert(
+            &stake_id,
+            Stake {
+                created: System::block_number(),
+                staking_status: StakingStatus::NotStaked,
+            },
+        );
+
+        assert_err!(
+            StakePool::initiate_slashing(&stake_id, 5000, 0),
+            StakeActionError::Error(InitiateSlashingError::SlashPeriodShouldBeGreaterThanZero)
+        );
+
+        assert_err!(
+            StakePool::initiate_slashing(&stake_id, 5000, 1),
+            StakeActionError::Error(InitiateSlashingError::NotStaked)
+        );
+
+        <Stakes<Test>>::insert(
+            &stake_id,
+            Stake {
+                created: System::block_number(),
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount: staked_amount,
+                    ongoing_slashes: BTreeMap::new(),
+                    next_slash_id: 0,
+                    staked_status: StakedStatus::Unstaking(UnstakingState {
+                        started_at_block: 0,
+                        blocks_remaining_in_active_period_for_unstaking: 100,
+                        is_active: true,
+                    }),
+                }),
+            },
+        );
+
+        // assert_err!(StakePool::initiate_slashing(&stake_id, 0, 0), StakingError::ZeroSlashing);
+
+        let mut slash_id = 0;
+        assert!(StakePool::initiate_slashing(&stake_id, 5000, 10).is_ok());
+
+        let mut expected_ongoing_slashes: fixtures::OngoingSlashes = BTreeMap::new();
+
+        expected_ongoing_slashes.insert(
+            slash_id,
+            Slash {
+                started_at_block: System::block_number(),
+                is_active: true,
+                blocks_remaining_in_active_period_for_slashing: 10,
+                slash_amount: 5000,
+            },
+        );
+
+        assert_eq!(
+            StakePool::stakes(&stake_id),
+            Stake {
+                created: System::block_number(),
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount: staked_amount,
+                    ongoing_slashes: expected_ongoing_slashes.clone(),
+                    next_slash_id: slash_id + 1,
+                    staked_status: StakedStatus::Unstaking(UnstakingState {
+                        started_at_block: 0,
+                        blocks_remaining_in_active_period_for_unstaking: 100,
+                        is_active: false,
+                    }),
+                })
+            }
+        );
+
+        assert_err!(
+            StakePool::pause_slashing(&stake_id, &999),
+            StakeActionError::Error(PauseSlashingError::SlashNotFound)
+        );
+        assert_err!(
+            StakePool::pause_slashing(&999, &slash_id),
+            StakeActionError::StakeNotFound
+        );
+
+        assert_ok!(StakePool::pause_slashing(&stake_id, &slash_id));
+        expected_ongoing_slashes.insert(
+            slash_id,
+            Slash {
+                started_at_block: System::block_number(),
+                is_active: false,
+                blocks_remaining_in_active_period_for_slashing: 10,
+                slash_amount: 5000,
+            },
+        );
+        assert_eq!(
+            StakePool::stakes(&stake_id),
+            Stake {
+                created: System::block_number(),
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount: staked_amount,
+                    ongoing_slashes: expected_ongoing_slashes.clone(),
+                    next_slash_id: slash_id + 1,
+                    staked_status: StakedStatus::Unstaking(UnstakingState {
+                        started_at_block: 0,
+                        blocks_remaining_in_active_period_for_unstaking: 100,
+                        is_active: false,
+                    }),
+                })
+            }
+        );
+
+        assert_err!(
+            StakePool::resume_slashing(&stake_id, &999),
+            StakeActionError::Error(ResumeSlashingError::SlashNotFound)
+        );
+        assert_err!(
+            StakePool::resume_slashing(&999, &slash_id),
+            StakeActionError::StakeNotFound
+        );
+
+        assert_ok!(StakePool::resume_slashing(&stake_id, &slash_id));
+        expected_ongoing_slashes.insert(
+            slash_id,
+            Slash {
+                started_at_block: System::block_number(),
+                is_active: true,
+                blocks_remaining_in_active_period_for_slashing: 10,
+                slash_amount: 5000,
+            },
+        );
+        assert_eq!(
+            StakePool::stakes(&stake_id),
+            Stake {
+                created: System::block_number(),
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount: staked_amount,
+                    ongoing_slashes: expected_ongoing_slashes.clone(),
+                    next_slash_id: slash_id + 1,
+                    staked_status: StakedStatus::Unstaking(UnstakingState {
+                        started_at_block: 0,
+                        blocks_remaining_in_active_period_for_unstaking: 100,
+                        is_active: false,
+                    }),
+                })
+            }
+        );
+
+        assert_err!(
+            StakePool::cancel_slashing(&stake_id, &999),
+            StakeActionError::Error(CancelSlashingError::SlashNotFound)
+        );
+        assert_err!(
+            StakePool::cancel_slashing(&999, &slash_id),
+            StakeActionError::StakeNotFound
+        );
+
+        assert_ok!(StakePool::cancel_slashing(&stake_id, &slash_id));
+        assert_eq!(
+            StakePool::stakes(&stake_id),
+            Stake {
+                created: System::block_number(),
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount: staked_amount,
+                    ongoing_slashes: BTreeMap::new(),
+                    next_slash_id: slash_id + 1,
+                    staked_status: StakedStatus::Unstaking(UnstakingState {
+                        started_at_block: 0,
+                        blocks_remaining_in_active_period_for_unstaking: 100,
+                        is_active: true,
+                    }),
+                })
+            }
+        );
+
+        expected_ongoing_slashes = BTreeMap::new();
+        let slashing_amount = 5000;
+        slash_id += 1;
+        assert!(StakePool::initiate_slashing(&stake_id, slashing_amount, 2).is_ok());
+
+        StakePool::on_finalize(System::block_number());
+
+        expected_ongoing_slashes.insert(
+            slash_id,
+            Slash {
+                started_at_block: System::block_number(),
+                is_active: true,
+                blocks_remaining_in_active_period_for_slashing: 1,
+                slash_amount: slashing_amount,
+            },
+        );
+        assert_eq!(
+            StakePool::stakes(&stake_id),
+            Stake {
+                created: System::block_number(),
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount: staked_amount,
+                    ongoing_slashes: expected_ongoing_slashes.clone(),
+                    next_slash_id: slash_id + 1,
+                    staked_status: StakedStatus::Unstaking(UnstakingState {
+                        started_at_block: 0,
+                        blocks_remaining_in_active_period_for_unstaking: 100,
+                        is_active: false,
+                    }),
+                })
+            }
+        );
+
+        StakePool::on_finalize(System::block_number());
+        assert_eq!(
+            StakePool::stakes(&stake_id),
+            Stake {
+                created: System::block_number(),
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount: staked_amount - slashing_amount,
+                    ongoing_slashes: BTreeMap::new(),
+                    next_slash_id: slash_id + 1,
+                    staked_status: StakedStatus::Unstaking(UnstakingState {
+                        started_at_block: 0,
+                        blocks_remaining_in_active_period_for_unstaking: 99,
+                        is_active: true
+                    })
+                })
+            }
+        );
+
+        assert_eq!(
+            StakePool::stake_pool_balance(),
+            staked_amount - slashing_amount
+        );
+    });
+}
+
+#[test]
+fn initiating_pausing_resuming_unstaking() {
+    build_test_externalities().execute_with(|| {
+        let staked_amount = Balances::minimum_balance() + 10000;
+        let starting_stake_fund_balance = Balances::minimum_balance() + 3333;
+
+        let _ = Balances::deposit_creating(
+            &StakePool::stake_pool_account_id(),
+            starting_stake_fund_balance + staked_amount,
+        );
+
+        assert_err!(
+            StakePool::initiate_unstaking(&100, Some(1)),
+            StakeActionError::StakeNotFound
+        );
+
+        let stake_id = StakePool::create_stake();
+        <Stakes<Test>>::insert(
+            &stake_id,
+            Stake {
+                created: System::block_number(),
+                staking_status: StakingStatus::NotStaked,
+            },
+        );
+
+        assert_err!(
+            StakePool::initiate_unstaking(&stake_id, Some(0)),
+            StakeActionError::Error(InitiateUnstakingError::UnstakingPeriodShouldBeGreaterThanZero)
+        );
+
+        assert_err!(
+            StakePool::initiate_unstaking(&stake_id, Some(1)),
+            StakeActionError::Error(InitiateUnstakingError::UnstakingError(
+                UnstakingError::NotStaked
+            ))
+        );
+
+        let mut ongoing_slashes = BTreeMap::new();
+        ongoing_slashes.insert(
+            1,
+            Slash {
+                started_at_block: System::block_number(),
+                is_active: true,
+                blocks_remaining_in_active_period_for_slashing: 100,
+                slash_amount: 100,
+            },
+        );
+
+        <Stakes<Test>>::insert(
+            &stake_id,
+            Stake {
+                created: System::block_number(),
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount,
+                    ongoing_slashes,
+                    next_slash_id: 2,
+                    staked_status: StakedStatus::Normal,
+                }),
+            },
+        );
+
+        assert_err!(
+            StakePool::initiate_unstaking(&stake_id, Some(1)),
+            StakeActionError::Error(InitiateUnstakingError::UnstakingError(
+                UnstakingError::CannotUnstakeWhileSlashesOngoing
+            ))
+        );
+
+        assert_ok!(StakePool::cancel_slashing(&stake_id, &1));
+
+        assert_ok!(StakePool::initiate_unstaking(&stake_id, Some(2)));
+
+        assert_eq!(
+            StakePool::stakes(&stake_id),
+            Stake {
+                created: System::block_number(),
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount,
+                    ongoing_slashes: BTreeMap::new(),
+                    next_slash_id: 2,
+                    staked_status: StakedStatus::Unstaking(UnstakingState {
+                        started_at_block: System::block_number(),
+                        blocks_remaining_in_active_period_for_unstaking: 2,
+                        is_active: true
+                    })
+                })
+            }
+        );
+
+        StakePool::on_finalize(System::block_number());
+        assert_eq!(
+            StakePool::stakes(&stake_id),
+            Stake {
+                created: System::block_number(),
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount,
+                    ongoing_slashes: BTreeMap::new(),
+                    next_slash_id: 2,
+                    staked_status: StakedStatus::Unstaking(UnstakingState {
+                        started_at_block: System::block_number(),
+                        blocks_remaining_in_active_period_for_unstaking: 1,
+                        is_active: true
+                    })
+                })
+            }
+        );
+
+        StakePool::finalize_slashing_and_unstaking();
+        assert_eq!(
+            StakePool::stakes(&stake_id),
+            Stake {
+                created: System::block_number(),
+                staking_status: StakingStatus::NotStaked
+            }
+        );
+
+        assert_eq!(StakePool::stake_pool_balance(), starting_stake_fund_balance);
+
+        // unstaked amount is destroyed by StakingEventsHandler
+        assert_eq!(Balances::total_issuance(), starting_stake_fund_balance);
+    });
+}
+
+#[test]
+fn unstake() {
+    build_test_externalities().execute_with(|| {
+        assert_err!(
+            StakePool::initiate_unstaking(&0, None),
+            StakeActionError::StakeNotFound
+        );
+
+        let staked_amount = Balances::minimum_balance() + 10000;
+        let starting_stake_fund_balance = Balances::minimum_balance() + 3333;
+
+        let _ = Balances::deposit_creating(
+            &StakePool::stake_pool_account_id(),
+            starting_stake_fund_balance + staked_amount,
+        );
+
+        let stake_id = StakePool::create_stake();
+
+        assert_err!(
+            StakePool::initiate_unstaking(&stake_id, None),
+            StakeActionError::Error(InitiateUnstakingError::UnstakingError(
+                UnstakingError::NotStaked
+            ))
+        );
+
+        let mut ongoing_slashes = BTreeMap::new();
+        ongoing_slashes.insert(
+            1,
+            Slash {
+                started_at_block: System::block_number(),
+                is_active: true,
+                blocks_remaining_in_active_period_for_slashing: 100,
+                slash_amount: 100,
+            },
+        );
+
+        <Stakes<Test>>::insert(
+            &stake_id,
+            Stake {
+                created: System::block_number(),
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount,
+                    ongoing_slashes,
+                    next_slash_id: 0,
+                    staked_status: StakedStatus::Normal,
+                }),
+            },
+        );
+
+        assert_err!(
+            StakePool::initiate_unstaking(&stake_id, None),
+            StakeActionError::Error(InitiateUnstakingError::UnstakingError(
+                UnstakingError::CannotUnstakeWhileSlashesOngoing
+            ))
+        );
+
+        <Stakes<Test>>::insert(
+            &stake_id,
+            Stake {
+                created: System::block_number(),
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount,
+                    ongoing_slashes: BTreeMap::new(),
+                    next_slash_id: 0,
+                    staked_status: StakedStatus::Unstaking(UnstakingState {
+                        started_at_block: 0,
+                        blocks_remaining_in_active_period_for_unstaking: 100,
+                        is_active: true,
+                    }),
+                }),
+            },
+        );
+
+        assert_err!(
+            StakePool::initiate_unstaking(&stake_id, None),
+            StakeActionError::Error(InitiateUnstakingError::UnstakingError(
+                UnstakingError::AlreadyUnstaking
+            ))
+        );
+
+        <Stakes<Test>>::insert(
+            &stake_id,
+            Stake {
+                created: System::block_number(),
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount,
+                    ongoing_slashes: BTreeMap::new(),
+                    next_slash_id: 0,
+                    staked_status: StakedStatus::Normal,
+                }),
+            },
+        );
+
+        assert_ok!(StakePool::initiate_unstaking(&stake_id, None));
+        assert_eq!(StakePool::stake_pool_balance(), starting_stake_fund_balance);
+    });
+}

+ 96 - 0
runtime-modules/storage/Cargo.toml

@@ -0,0 +1,96 @@
+[package]
+name = 'substrate-storage-module'
+version = '1.0.0'
+authors = ['Joystream contributors']
+edition = '2018'
+
+[features]
+default = ['std']
+std = [
+	'sr-primitives/std',
+	'srml-support/std',
+	'system/std',
+	'rstd/std',
+	'codec/std',
+	'timestamp/std',
+	'serde',
+	'primitives/std',
+	'common/std',
+	'membership/std',
+	'roles/std',
+]
+
+
+[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.rstd]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-std'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.codec]
+default-features = false
+features = ['derive']
+package = 'parity-scale-codec'
+version = '1.0.0'
+
+[dependencies.serde]
+features = ['derive']
+optional = true
+version = '1.0.101'
+
+[dependencies.primitives]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'substrate-primitives'
+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'
+
+[dependencies.roles]
+default_features = false
+package = 'substrate-roles-module'
+path = '../roles'
+
+[dev-dependencies.runtime-io]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-io'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dev-dependencies.balances]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-balances'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'

+ 5 - 5
src/storage/data_directory.rs → runtime-modules/storage/src/data_directory.rs

@@ -1,8 +1,8 @@
-use crate::membership;
-use crate::roles::actors;
-use crate::storage::data_object_type_registry::Trait as DOTRTrait;
-use crate::traits::{ContentIdExists, IsActiveDataObjectType, Roles};
+use crate::data_object_type_registry::Trait as DOTRTrait;
+use crate::traits::{ContentIdExists, IsActiveDataObjectType};
 use codec::{Codec, Decode, Encode};
+use roles::actors;
+use roles::traits::Roles;
 use rstd::prelude::*;
 use sr_primitives::traits::{MaybeSerialize, Member, SimpleArithmetic};
 use srml_support::{decl_event, decl_module, decl_storage, dispatch, ensure, Parameter};
@@ -236,7 +236,7 @@ impl<T: Trait> Module<T> {
 
 #[cfg(test)]
 mod tests {
-    use crate::storage::mock::*;
+    use crate::mock::*;
 
     #[test]
     fn succeed_adding_content() {

+ 5 - 4
src/storage/data_object_storage_registry.rs → runtime-modules/storage/src/data_object_storage_registry.rs

@@ -1,7 +1,8 @@
-use crate::roles::actors;
-use crate::storage::data_directory::Trait as DDTrait;
-use crate::traits::{ContentHasStorage, ContentIdExists, Roles};
+use crate::data_directory::Trait as DDTrait;
+use crate::traits::{ContentHasStorage, ContentIdExists};
 use codec::{Codec, Decode, Encode};
+use roles::actors;
+use roles::traits::Roles;
 use rstd::prelude::*;
 use sr_primitives::traits::{MaybeSerialize, Member, SimpleArithmetic};
 use srml_support::{decl_event, decl_module, decl_storage, ensure, Parameter};
@@ -197,7 +198,7 @@ impl<T: Trait> Module<T> {
 
 #[cfg(test)]
 mod tests {
-    use crate::storage::mock::*;
+    use crate::mock::*;
 
     #[test]
     fn initial_state() {

+ 1 - 1
src/storage/data_object_type_registry.rs → runtime-modules/storage/src/data_object_type_registry.rs

@@ -157,7 +157,7 @@ impl<T: Trait> Module<T> {
 #[cfg(test)]
 mod tests {
     //use super::*;
-    use crate::storage::mock::*;
+    use crate::mock::*;
 
     use system::{self, EventRecord, Phase};
 

+ 9 - 0
runtime-modules/storage/src/lib.rs

@@ -0,0 +1,9 @@
+// Ensure we're `no_std` when compiling for Wasm.
+#![cfg_attr(not(feature = "std"), no_std)]
+
+pub mod data_directory;
+pub mod data_object_storage_registry;
+pub mod data_object_type_registry;
+pub mod traits;
+
+mod mock;

+ 4 - 5
src/storage/mock.rs → runtime-modules/storage/src/mock.rs

@@ -1,11 +1,10 @@
 #![cfg(test)]
 
 pub use super::{data_directory, data_object_storage_registry, data_object_type_registry};
-pub use crate::currency::GovernanceCurrency;
-use crate::membership;
-use crate::membership::members;
-use crate::roles::actors;
 use crate::traits;
+pub use common::currency::GovernanceCurrency;
+use membership::members;
+use roles::actors;
 pub use system;
 
 pub use primitives::{Blake2Hasher, H256};
@@ -42,7 +41,7 @@ pub const TEST_MOCK_LIAISON: u64 = 0xd00du64;
 pub const TEST_MOCK_EXISTING_CID: u64 = 42;
 
 pub struct MockRoles {}
-impl traits::Roles<Test> for MockRoles {
+impl roles::traits::Roles<Test> for MockRoles {
     fn is_role_account(_account_id: &<Test as system::Trait>::AccountId) -> bool {
         false
     }

+ 0 - 0
src/storage/tests.rs → runtime-modules/storage/src/tests.rs


+ 20 - 0
runtime-modules/storage/src/traits.rs

@@ -0,0 +1,20 @@
+use crate::{data_directory, data_object_storage_registry, data_object_type_registry};
+
+// Storage
+pub trait IsActiveDataObjectType<T: data_object_type_registry::Trait> {
+    fn is_active_data_object_type(_which: &T::DataObjectTypeId) -> bool;
+}
+
+pub trait ContentIdExists<T: data_directory::Trait> {
+    fn has_content(_which: &T::ContentId) -> bool;
+
+    fn get_data_object(
+        _which: &T::ContentId,
+    ) -> Result<data_directory::DataObject<T>, &'static str>;
+}
+
+pub trait ContentHasStorage<T: data_object_storage_registry::Trait> {
+    fn has_storage_provider(_which: &T::ContentId) -> bool;
+
+    fn is_ready_at_storage_provider(_which: &T::ContentId, _provider: &T::AccountId) -> bool;
+}

+ 50 - 0
runtime-modules/token-minting/Cargo.toml

@@ -0,0 +1,50 @@
+[package]
+name = 'substrate-token-mint-module'
+version = '1.0.1'
+authors = ['Joystream contributors']
+edition = '2018'
+
+[dependencies]
+hex-literal = '0.1.0'
+serde = { version = '1.0.101', optional = true}
+serde_derive = { version = '1.0.101', optional = true }
+rstd = { package = 'sr-std', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+runtime-primitives = { package = 'sr-primitives', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+srml-support = { package = 'srml-support', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+srml-support-procedural = { package = 'srml-support-procedural', git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+system = { package = 'srml-system', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+balances = { package = 'srml-balances', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+codec = { package = 'parity-scale-codec', version = '1.0.0', default-features = false, features = ['derive'] }
+# https://users.rust-lang.org/t/failure-derive-compilation-error/39062
+quote = '<=1.0.2'
+
+[dependencies.timestamp]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-timestamp'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.runtime-io]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-io'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dev-dependencies]
+runtime-io = { package = 'sr-io', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+primitives = { package = 'substrate-primitives', git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+
+[features]
+default = ['std']
+std = [
+	'serde',
+	'serde_derive',
+	'codec/std',
+	'rstd/std',
+	'runtime-io/std',
+	'runtime-primitives/std',
+	'srml-support/std',
+	'system/std',
+  	'balances/std',
+	'timestamp/std',
+]

+ 260 - 0
runtime-modules/token-minting/src/lib.rs

@@ -0,0 +1,260 @@
+// Ensure we're `no_std` when compiling for Wasm.
+#![cfg_attr(not(feature = "std"), no_std)]
+
+#[cfg(feature = "std")]
+use rstd::prelude::*;
+
+use codec::{Codec, Decode, Encode};
+use runtime_primitives::traits::{MaybeSerialize, Member, One, SimpleArithmetic, Zero};
+use srml_support::traits::Currency;
+use srml_support::{decl_module, decl_storage, ensure, Parameter};
+
+mod mint;
+mod mock;
+mod tests;
+
+pub use mint::*;
+
+use system;
+
+pub trait Trait: system::Trait {
+    /// The currency to mint.
+    type Currency: Currency<Self::AccountId>;
+
+    /// The type used as a mint identifier.
+    type MintId: Parameter
+        + Member
+        + SimpleArithmetic
+        + Codec
+        + Default
+        + Copy
+        + MaybeSerialize
+        + PartialEq;
+}
+
+pub type BalanceOf<T> =
+    <<T as Trait>::Currency as Currency<<T as system::Trait>::AccountId>>::Balance;
+
+#[derive(PartialEq, Eq, Debug)]
+pub enum GeneralError {
+    MintNotFound,
+    NextAdjustmentInPast,
+}
+
+/// Errors that can arise from attempt to mint and transfer tokens from a mint to
+/// an account.
+#[derive(PartialEq, Eq, Debug)]
+pub enum TransferError {
+    MintNotFound,
+    NotEnoughCapacity,
+}
+
+/// Errors that can arise from attempt to transfer capacity between mints.
+#[derive(PartialEq, Eq, Debug)]
+pub enum CapacityTransferError {
+    SourceMintNotFound,
+    DestinationMintNotFound,
+    NotEnoughCapacity,
+}
+
+impl From<MintingError> for CapacityTransferError {
+    fn from(err: MintingError) -> CapacityTransferError {
+        match err {
+            MintingError::NotEnoughCapacity => CapacityTransferError::NotEnoughCapacity,
+        }
+    }
+}
+
+impl From<MintingError> for TransferError {
+    fn from(err: MintingError) -> TransferError {
+        match err {
+            MintingError::NotEnoughCapacity => TransferError::NotEnoughCapacity,
+        }
+    }
+}
+
+#[derive(Encode, Decode, Copy, Clone, Debug, Eq, PartialEq)]
+pub enum Adjustment<Balance: Zero, BlockNumber> {
+    // First adjustment will be after AdjustOnInterval.block_interval
+    Interval(AdjustOnInterval<Balance, BlockNumber>),
+    // First Adjustment will be at absolute blocknumber
+    IntervalAfterFirstAdjustmentAbsolute(AdjustOnInterval<Balance, BlockNumber>, BlockNumber),
+    // First Adjustment will be after a specified number of blocks
+    IntervalAfterFirstAdjustmentRelative(AdjustOnInterval<Balance, BlockNumber>, BlockNumber),
+}
+
+decl_storage! {
+    trait Store for Module<T: Trait> as TokenMint {
+        /// Mints
+        pub Mints get(mints) : linked_map T::MintId => Mint<BalanceOf<T>, T::BlockNumber>;
+
+        /// The number of mints created.
+        pub MintsCreated get(mints_created): T::MintId;
+    }
+}
+
+decl_module! {
+    pub struct Module<T: Trait> for enum Call where origin: T::Origin {
+        fn on_finalize(now: T::BlockNumber) {
+            Self::update_mints(now);
+        }
+    }
+}
+
+impl<T: Trait> Module<T> {
+    fn update_mints(now: T::BlockNumber) {
+        // Are we reading value from storage twice?
+        for (mint_id, ref mut mint) in <Mints<T>>::enumerate() {
+            if mint.maybe_do_capacity_adjustment(now) {
+                <Mints<T>>::insert(&mint_id, mint);
+            }
+        }
+    }
+
+    /// Adds a new mint with given settings to mints, and returns new MintId.
+    pub fn add_mint(
+        initial_capacity: BalanceOf<T>,
+        adjustment: Option<Adjustment<BalanceOf<T>, T::BlockNumber>>,
+    ) -> Result<T::MintId, GeneralError> {
+        let now = <system::Module<T>>::block_number();
+
+        // Ensure the next adjustment if set, is in the future
+        if let Some(adjustment) = adjustment {
+            match adjustment {
+                Adjustment::IntervalAfterFirstAdjustmentAbsolute(_, first_adjustment_in) => {
+                    ensure!(
+                        first_adjustment_in > now,
+                        GeneralError::NextAdjustmentInPast
+                    );
+                }
+                _ => (),
+            }
+        }
+
+        // Determine next adjutment
+        let next_adjustment = adjustment.map(|adjustment| match adjustment {
+            Adjustment::Interval(adjust_on_interval) => NextAdjustment {
+                at_block: now + adjust_on_interval.block_interval,
+                adjustment: adjust_on_interval,
+            },
+            Adjustment::IntervalAfterFirstAdjustmentAbsolute(
+                adjust_on_interval,
+                first_adjustment_at,
+            ) => NextAdjustment {
+                adjustment: adjust_on_interval,
+                at_block: first_adjustment_at,
+            },
+            Adjustment::IntervalAfterFirstAdjustmentRelative(
+                adjust_on_interval,
+                first_adjustment_after,
+            ) => NextAdjustment {
+                adjustment: adjust_on_interval,
+                at_block: now + first_adjustment_after,
+            },
+        });
+
+        // get next mint_id and increment total number of mints created
+        let mint_id = Self::mints_created();
+        <MintsCreated<T>>::put(mint_id + One::one());
+
+        <Mints<T>>::insert(mint_id, Mint::new(initial_capacity, next_adjustment, now));
+
+        Ok(mint_id)
+    }
+
+    /// Removes a mint. Passing a non existent mint has no side effects.
+    pub fn remove_mint(mint_id: T::MintId) {
+        <Mints<T>>::remove(&mint_id);
+    }
+
+    /// Tries to transfer exact requested amount from mint to a recipient account id.
+    /// Returns error if amount exceeds mint capacity or the specified mint doesn't exist.
+    /// Transfering amount of zero has no side effects. Return nothing on success.
+    pub fn transfer_tokens(
+        mint_id: T::MintId,
+        requested_amount: BalanceOf<T>,
+        recipient: &T::AccountId,
+    ) -> Result<(), TransferError> {
+        if requested_amount == Zero::zero() {
+            return Ok(());
+        }
+
+        ensure!(<Mints<T>>::exists(&mint_id), TransferError::MintNotFound);
+
+        let mut mint = Self::mints(&mint_id);
+
+        // Try minting
+        mint.mint_tokens(requested_amount)?;
+
+        <Mints<T>>::insert(&mint_id, mint);
+
+        // Deposit into recipient account
+        T::Currency::deposit_creating(recipient, requested_amount);
+
+        Ok(())
+    }
+
+    /// Provided mint exists, sets its capacity to specied value, return error otherwise.
+    pub fn set_mint_capacity(
+        mint_id: T::MintId,
+        capacity: BalanceOf<T>,
+    ) -> Result<(), GeneralError> {
+        ensure!(<Mints<T>>::exists(&mint_id), GeneralError::MintNotFound);
+
+        <Mints<T>>::mutate(&mint_id, |mint| {
+            mint.set_capacity(capacity);
+        });
+
+        Ok(())
+    }
+
+    /// Provided source and destination mints exist, will attempt to transfer capacity from the source mint
+    /// to the destination mint. Will return errors on non-existence of
+    /// mints or capacity_to_transfer exceeds the source mint's capacity.
+    pub fn transfer_capacity(
+        source: T::MintId,
+        destination: T::MintId,
+        capacity_to_transfer: BalanceOf<T>,
+    ) -> Result<(), CapacityTransferError> {
+        ensure!(
+            <Mints<T>>::exists(&source),
+            CapacityTransferError::SourceMintNotFound
+        );
+        ensure!(
+            <Mints<T>>::exists(&destination),
+            CapacityTransferError::DestinationMintNotFound
+        );
+
+        <Mints<T>>::mutate(&source, |source_mint| {
+            <Mints<T>>::mutate(&destination, |destination_mint| {
+                source_mint.transfer_capacity_to(destination_mint, capacity_to_transfer)
+            })
+        })?;
+
+        Ok(())
+    }
+
+    /// Returns a mint's capacity if it exists, error otherwise.
+    pub fn get_mint_capacity(mint_id: T::MintId) -> Result<BalanceOf<T>, GeneralError> {
+        ensure!(<Mints<T>>::exists(&mint_id), GeneralError::MintNotFound);
+        let mint = Self::mints(&mint_id);
+
+        Ok(mint.capacity())
+    }
+
+    /// Returns a mint's adjustment policy if it exists, error otherwise.
+    pub fn get_mint_next_adjustment(
+        mint_id: T::MintId,
+    ) -> Result<Option<NextAdjustment<BalanceOf<T>, T::BlockNumber>>, GeneralError> {
+        ensure!(<Mints<T>>::exists(&mint_id), GeneralError::MintNotFound);
+
+        let mint = Self::mints(&mint_id);
+
+        Ok(mint.next_adjustment())
+    }
+
+    /// Returns true if a mint exists.
+    pub fn mint_exists(mint_id: T::MintId) -> bool {
+        <Mints<T>>::exists(&mint_id)
+    }
+}

+ 153 - 0
runtime-modules/token-minting/src/mint.rs

@@ -0,0 +1,153 @@
+use codec::{Decode, Encode};
+use runtime_primitives::traits::{SimpleArithmetic, Zero};
+use srml_support::ensure;
+
+#[derive(Encode, Decode, Copy, Clone, Debug, Eq, PartialEq)]
+pub enum AdjustCapacityBy<Balance: Zero> {
+    /// Set capacity of mint to specific value
+    Setting(Balance),
+    /// Add to the capacity of the mint
+    Adding(Balance),
+    /// Reduce capacity of the mint
+    Reducing(Balance),
+}
+
+#[derive(Encode, Decode, Copy, Clone, Debug, Eq, PartialEq)]
+pub struct AdjustOnInterval<Balance: Zero, BlockNumber> {
+    pub block_interval: BlockNumber,
+    pub adjustment_type: AdjustCapacityBy<Balance>,
+}
+
+#[derive(Encode, Decode, Copy, Clone, Debug, Eq, PartialEq)]
+pub struct NextAdjustment<Balance: Zero, BlockNumber> {
+    pub adjustment: AdjustOnInterval<Balance, BlockNumber>,
+    pub at_block: BlockNumber,
+}
+
+#[derive(Encode, Decode, Default, Copy, Clone)]
+// Note we don't use TokenMint<T: Trait> it breaks the Default derivation macro with error T doesn't impl Default
+// Which requires manually implementing Default trait.
+// We want Default trait on TokenMint so we can use it as value in StorageMap without needing to wrap it in an Option
+pub struct Mint<Balance, BlockNumber>
+where
+    Balance: Copy + SimpleArithmetic + Zero,
+    BlockNumber: Copy + SimpleArithmetic,
+{
+    capacity: Balance,
+
+    // Whether there is an upcoming block where an adjustment to the mint will be made
+    // When this is not set, the mint is effectively paused.
+    next_adjustment: Option<NextAdjustment<Balance, BlockNumber>>,
+
+    created_at: BlockNumber,
+
+    total_minted: Balance,
+}
+
+#[derive(PartialEq, Eq, Debug)]
+pub enum MintingError {
+    NotEnoughCapacity,
+}
+
+impl<Balance, BlockNumber> Mint<Balance, BlockNumber>
+where
+    Balance: Copy + SimpleArithmetic + Zero,
+    BlockNumber: Copy + SimpleArithmetic,
+{
+    pub fn new(
+        initial_capacity: Balance,
+        next_adjustment: Option<NextAdjustment<Balance, BlockNumber>>,
+        now: BlockNumber,
+    ) -> Self {
+        Mint {
+            capacity: initial_capacity,
+            created_at: now,
+            total_minted: Zero::zero(),
+            next_adjustment: next_adjustment,
+        }
+    }
+
+    pub fn mint_tokens(&mut self, requested_amount: Balance) -> Result<(), MintingError> {
+        ensure!(
+            self.capacity >= requested_amount,
+            MintingError::NotEnoughCapacity
+        );
+        self.capacity -= requested_amount;
+        self.total_minted += requested_amount;
+        Ok(())
+    }
+
+    pub fn set_capacity(&mut self, new_capacity: Balance) {
+        self.capacity = new_capacity;
+    }
+
+    pub fn capacity(&self) -> Balance {
+        self.capacity
+    }
+
+    pub fn can_mint(&self, amount: Balance) -> bool {
+        self.capacity >= amount
+    }
+
+    pub fn created_at(&self) -> BlockNumber {
+        self.created_at
+    }
+
+    pub fn total_minted(&self) -> Balance {
+        self.total_minted
+    }
+
+    pub fn transfer_capacity_to(
+        &mut self,
+        destination: &mut Self,
+        capacity_to_transfer: Balance,
+    ) -> Result<(), MintingError> {
+        ensure!(
+            self.capacity >= capacity_to_transfer,
+            MintingError::NotEnoughCapacity
+        );
+        self.capacity -= capacity_to_transfer;
+        destination.capacity += capacity_to_transfer;
+        Ok(())
+    }
+
+    pub fn next_adjustment(&self) -> Option<NextAdjustment<Balance, BlockNumber>> {
+        self.next_adjustment
+    }
+
+    pub fn maybe_do_capacity_adjustment(&mut self, now: BlockNumber) -> bool {
+        self.next_adjustment.map_or(false, |next_adjustment| {
+            if now != next_adjustment.at_block {
+                false
+            } else {
+                // update mint capacity
+                self.capacity = Self::adjusted_capacity(
+                    self.capacity,
+                    next_adjustment.adjustment.adjustment_type,
+                );
+
+                // set next adjustment
+                self.next_adjustment = Some(NextAdjustment {
+                    adjustment: next_adjustment.adjustment,
+                    at_block: now + next_adjustment.adjustment.block_interval,
+                });
+
+                true
+            }
+        })
+    }
+
+    fn adjusted_capacity(capacity: Balance, adjustment_type: AdjustCapacityBy<Balance>) -> Balance {
+        match adjustment_type {
+            AdjustCapacityBy::Adding(amount) => capacity + amount,
+            AdjustCapacityBy::Setting(amount) => amount,
+            AdjustCapacityBy::Reducing(amount) => {
+                if amount > capacity {
+                    Zero::zero()
+                } else {
+                    capacity - amount
+                }
+            }
+        }
+    }
+}

+ 90 - 0
runtime-modules/token-minting/src/mock.rs

@@ -0,0 +1,90 @@
+#![cfg(test)]
+
+use crate::*;
+
+use primitives::H256;
+
+use crate::{Module, Trait};
+use balances;
+use runtime_primitives::{
+    testing::Header,
+    traits::{BlakeTwo256, IdentityLookup},
+    Perbill,
+};
+use srml_support::{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;
+}
+
+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 = ();
+}
+
+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: u64 = 2000;
+}
+
+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 Trait for Test {
+    type Currency = Balances;
+    type MintId = u64;
+}
+
+pub fn build_test_externalities() -> runtime_io::TestExternalities {
+    let t = system::GenesisConfig::default()
+        .build_storage::<Test>()
+        .unwrap();
+
+    t.into()
+}
+
+pub type System = system::Module<Test>;
+pub type Balances = balances::Module<Test>;
+pub type Minting = Module<Test>;

+ 202 - 0
runtime-modules/token-minting/src/tests.rs

@@ -0,0 +1,202 @@
+#![cfg(test)]
+
+use super::*;
+use crate::mock::*;
+
+#[test]
+fn adding_and_removing_mints() {
+    build_test_externalities().execute_with(|| {
+        System::set_block_number(1);
+        let capacity: u64 = 5000;
+        let adjustment_amount: u64 = 500;
+
+        let adjustment = AdjustOnInterval {
+            adjustment_type: AdjustCapacityBy::Adding(adjustment_amount),
+            block_interval: 100,
+        };
+
+        let mint_id = Minting::add_mint(capacity, Some(Adjustment::Interval(adjustment)))
+            .ok()
+            .unwrap();
+        assert!(Minting::mint_exists(mint_id));
+
+        assert_eq!(Minting::get_mint_capacity(mint_id).ok().unwrap(), capacity);
+
+        assert_eq!(
+            Minting::get_mint_next_adjustment(mint_id),
+            Ok(Some(NextAdjustment {
+                adjustment,
+                at_block: 1 + 100,
+            }))
+        );
+
+        Minting::remove_mint(mint_id);
+        assert!(!Minting::mint_exists(mint_id));
+    });
+}
+
+#[test]
+fn minting() {
+    build_test_externalities().execute_with(|| {
+        let capacity: u64 = 5000;
+
+        let mint_id = Minting::add_mint(capacity, None).ok().unwrap();
+
+        assert!(Minting::transfer_tokens(mint_id, 1000, &1).is_ok());
+
+        assert_eq!(Balances::free_balance(&1), 1000);
+
+        assert_eq!(Minting::get_mint_capacity(mint_id).ok().unwrap(), 4000);
+    });
+}
+
+#[test]
+fn minting_exact() {
+    build_test_externalities().execute_with(|| {
+        let capacity: u64 = 1000;
+
+        let mint_id = Minting::add_mint(capacity, None).ok().unwrap();
+
+        assert_eq!(
+            Minting::transfer_tokens(mint_id, 2000, &1),
+            Err(TransferError::NotEnoughCapacity)
+        );
+    });
+}
+
+#[test]
+fn adjustment_adding() {
+    build_test_externalities().execute_with(|| {
+        System::set_block_number(0);
+        let capacity: u64 = 5000;
+        let adjustment_amount: u64 = 500;
+
+        let adjustment = AdjustOnInterval {
+            adjustment_type: AdjustCapacityBy::Adding(adjustment_amount),
+            block_interval: 100,
+        };
+
+        let mint_id = Minting::add_mint(capacity, Some(Adjustment::Interval(adjustment)))
+            .ok()
+            .unwrap();
+
+        Minting::update_mints(100);
+        assert_eq!(
+            Minting::get_mint_capacity(mint_id).ok().unwrap(),
+            capacity + (adjustment_amount * 1)
+        );
+
+        // no adjustments should happen
+        Minting::update_mints(100);
+        Minting::update_mints(140);
+        Minting::update_mints(199);
+
+        Minting::update_mints(200);
+        assert_eq!(
+            Minting::get_mint_capacity(mint_id).ok().unwrap(),
+            capacity + (adjustment_amount * 2)
+        );
+    });
+}
+
+#[test]
+fn adjustment_reducing() {
+    build_test_externalities().execute_with(|| {
+        System::set_block_number(0);
+        let capacity: u64 = 5000;
+        let adjustment_amount: u64 = 500;
+
+        let adjustment = AdjustOnInterval {
+            adjustment_type: AdjustCapacityBy::Reducing(adjustment_amount),
+            block_interval: 100,
+        };
+
+        let mint_id = Minting::add_mint(capacity, Some(Adjustment::Interval(adjustment)))
+            .ok()
+            .unwrap();
+
+        Minting::update_mints(100);
+        assert_eq!(
+            Minting::get_mint_capacity(mint_id).ok().unwrap(),
+            capacity - adjustment_amount
+        );
+
+        assert_eq!(
+            Minting::get_mint_capacity(mint_id).ok().unwrap(),
+            capacity - (adjustment_amount * 1)
+        );
+
+        // no adjustments should happen
+        Minting::update_mints(100);
+        Minting::update_mints(140);
+        Minting::update_mints(199);
+
+        Minting::update_mints(200);
+        assert_eq!(
+            Minting::get_mint_capacity(mint_id).ok().unwrap(),
+            capacity - (adjustment_amount * 2)
+        );
+    });
+}
+
+#[test]
+fn adjustment_setting() {
+    build_test_externalities().execute_with(|| {
+        System::set_block_number(0);
+        let capacity: u64 = 2000;
+        let setting_amount: u64 = 5000;
+
+        let adjustment = AdjustOnInterval {
+            adjustment_type: AdjustCapacityBy::Setting(setting_amount),
+            block_interval: 100,
+        };
+
+        let mint_id = Minting::add_mint(capacity, Some(Adjustment::Interval(adjustment)))
+            .ok()
+            .unwrap();
+
+        Minting::update_mints(100);
+        assert_eq!(
+            Minting::get_mint_capacity(mint_id).ok().unwrap(),
+            setting_amount
+        );
+    });
+}
+
+#[test]
+fn adjustment_first_interval() {
+    build_test_externalities().execute_with(|| {
+        System::set_block_number(0);
+        let capacity: u64 = 2000;
+        let amount: u64 = 500;
+
+        let adjustment = AdjustOnInterval {
+            adjustment_type: AdjustCapacityBy::Adding(amount),
+            block_interval: 100,
+        };
+
+        let mint_id = Minting::add_mint(
+            capacity,
+            Some(Adjustment::IntervalAfterFirstAdjustmentAbsolute(
+                adjustment, 1000,
+            )),
+        )
+        .ok()
+        .unwrap();
+
+        Minting::update_mints(100);
+        assert_eq!(Minting::get_mint_capacity(mint_id).ok().unwrap(), capacity);
+
+        Minting::update_mints(1000);
+        assert_eq!(
+            Minting::get_mint_capacity(mint_id).ok().unwrap(),
+            capacity + amount
+        );
+
+        Minting::update_mints(1100);
+        assert_eq!(
+            Minting::get_mint_capacity(mint_id).ok().unwrap(),
+            capacity + 2 * amount
+        );
+    });
+}

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

@@ -0,0 +1,44 @@
+[package]
+name = 'substrate-versioned-store-permissions-module'
+version = '1.0.1'
+authors = ['Joystream contributors']
+edition = '2018'
+
+[dependencies]
+hex-literal = '0.1.0'
+serde = { version = '1.0', optional = true }
+serde_derive = { version = '1.0', optional = true }
+codec = { package = 'parity-scale-codec', version = '1.0.0', default-features = false, features = ['derive'] }
+rstd = { package = 'sr-std', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+runtime-primitives = { package = 'sr-primitives', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+srml-support = { package = 'srml-support', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+srml-support-procedural = { package = 'srml-support-procedural', git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+system = { package = 'srml-system', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+timestamp = { package = 'srml-timestamp', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+runtime-io = { package = 'sr-io', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+# https://users.rust-lang.org/t/failure-derive-compilation-error/39062
+quote = '<=1.0.2'
+
+[dependencies.versioned-store]
+default_features = false
+package ='substrate-versioned-store'
+path = '../versioned-store'
+
+[dev-dependencies]
+runtime-io = { package = 'sr-io', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+primitives = { package = 'substrate-primitives', git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+
+[features]
+default = ['std']
+std = [
+	'serde',
+	'serde_derive',
+	'codec/std',
+	'rstd/std',
+	'runtime-io/std',
+	'runtime-primitives/std',
+	'srml-support/std',
+	'system/std',
+	'timestamp/std',
+	'versioned-store/std',
+]

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

@@ -0,0 +1,28 @@
+use codec::{Decode, Encode};
+use rstd::collections::btree_set::BTreeSet;
+
+/// Reference to a specific property of a specific class.
+#[derive(Encode, Decode, Eq, PartialEq, Ord, PartialOrd, Clone, Debug)]
+pub struct PropertyOfClass<ClassId, PropertyIndex> {
+    pub class_id: ClassId,
+    pub property_index: PropertyIndex,
+}
+
+/// The type of constraint imposed on referencing a class via class property of type "Internal".
+#[derive(Encode, Decode, Eq, PartialEq, Clone, Debug)]
+pub enum ReferenceConstraint<ClassId: Ord, PropertyIndex: Ord> {
+    /// No property can reference the class.
+    NoReferencingAllowed,
+
+    /// Any property of any class may reference the class.
+    NoConstraint,
+
+    /// Only a set of properties of specific classes can reference the class.
+    Restricted(BTreeSet<PropertyOfClass<ClassId, PropertyIndex>>),
+}
+
+impl<ClassId: Ord, PropertyIndex: Ord> Default for ReferenceConstraint<ClassId, PropertyIndex> {
+    fn default() -> Self {
+        ReferenceConstraint::NoReferencingAllowed
+    }
+}

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

@@ -0,0 +1,57 @@
+use codec::{Decode, Encode};
+use rstd::collections::btree_set::BTreeSet;
+use rstd::prelude::*;
+
+#[derive(Encode, Decode, Eq, PartialEq, Clone, Debug)]
+pub struct CredentialSet<Credential>(BTreeSet<Credential>);
+
+impl<Credential> From<Vec<Credential>> for CredentialSet<Credential>
+where
+    Credential: Ord,
+{
+    fn from(v: Vec<Credential>) -> CredentialSet<Credential> {
+        let mut set = CredentialSet(BTreeSet::new());
+        for credential in v.into_iter() {
+            set.insert(credential);
+        }
+        set
+    }
+}
+
+/// Default CredentialSet set is just an empty set.
+impl<Credential: Ord> Default for CredentialSet<Credential> {
+    fn default() -> Self {
+        CredentialSet(BTreeSet::new())
+    }
+}
+
+impl<Credential: Ord> CredentialSet<Credential> {
+    pub fn new() -> Self {
+        Self(BTreeSet::new())
+    }
+
+    pub fn insert(&mut self, value: Credential) -> bool {
+        self.0.insert(value)
+    }
+
+    pub fn contains(&self, value: &Credential) -> bool {
+        self.0.contains(value)
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.0.is_empty()
+    }
+}
+
+/// Type, derived from dispatchable call, identifies the caller
+#[derive(Encode, Decode, Eq, PartialEq, Ord, PartialOrd, Clone, Debug)]
+pub enum AccessLevel<Credential> {
+    /// ROOT origin
+    System,
+    /// Caller identified as the entity maintainer
+    EntityMaintainer, // Maybe enclose EntityId?
+    /// Verified Credential
+    Credential(Credential),
+    /// In cases where a signed extrinsic doesn't provide a Credential
+    Unspecified,
+}

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

@@ -0,0 +1,646 @@
+// Ensure we're `no_std` when compiling for Wasm.
+#![cfg_attr(not(feature = "std"), no_std)]
+
+use codec::Codec;
+use rstd::collections::btree_map::BTreeMap;
+use rstd::prelude::*;
+use runtime_primitives::traits::{MaybeSerialize, Member, SimpleArithmetic};
+use srml_support::{decl_module, decl_storage, dispatch, ensure, Parameter};
+use system;
+
+// EntityId, ClassId -> should be configured on versioned_store::Trait
+pub use versioned_store::{ClassId, ClassPropertyValue, EntityId, Property, PropertyValue};
+
+mod constraint;
+mod credentials;
+mod mock;
+mod operations;
+mod permissions;
+mod tests;
+
+pub use constraint::*;
+pub use credentials::*;
+pub use operations::*;
+pub use permissions::*;
+
+/// Trait for checking if an account has specified Credential
+pub trait CredentialChecker<T: Trait> {
+    fn account_has_credential(account: &T::AccountId, credential: T::Credential) -> bool;
+}
+
+/// An implementation where no account has any credential. Effectively
+/// only the system will be able to perform any action on the versioned store.
+impl<T: Trait> CredentialChecker<T> for () {
+    fn account_has_credential(_account: &T::AccountId, _credential: T::Credential) -> bool {
+        false
+    }
+}
+
+/// An implementation that calls into multiple checkers. This allows for multiple modules
+/// to maintain AccountId to Credential mappings.
+impl<T: Trait, X: CredentialChecker<T>, Y: CredentialChecker<T>> CredentialChecker<T> for (X, Y) {
+    fn account_has_credential(account: &T::AccountId, group: T::Credential) -> bool {
+        X::account_has_credential(account, group) || Y::account_has_credential(account, group)
+    }
+}
+
+/// Trait for externally checking if an account can create new classes in the versioned store.
+pub trait CreateClassPermissionsChecker<T: Trait> {
+    fn account_can_create_class_permissions(account: &T::AccountId) -> bool;
+}
+
+/// An implementation that does not permit any account to create classes. Effectively
+/// only the system can create classes.
+impl<T: Trait> CreateClassPermissionsChecker<T> for () {
+    fn account_can_create_class_permissions(_account: &T::AccountId) -> bool {
+        false
+    }
+}
+
+pub type ClassPermissionsType<T> =
+    ClassPermissions<ClassId, <T as Trait>::Credential, u16, <T as system::Trait>::BlockNumber>;
+
+pub trait Trait: system::Trait + versioned_store::Trait {
+    // type Event: ...
+    // Do we need Events?
+
+    /// Type that represents an actor or group of actors in the system.
+    type Credential: Parameter
+        + Member
+        + SimpleArithmetic
+        + Codec
+        + Default
+        + Copy
+        + Clone
+        + MaybeSerialize
+        + Eq
+        + PartialEq
+        + Ord;
+
+    /// External type for checking if an account has specified credential.
+    type CredentialChecker: CredentialChecker<Self>;
+
+    /// External type used to check if an account has permission to create new Classes.
+    type CreateClassPermissionsChecker: CreateClassPermissionsChecker<Self>;
+}
+
+decl_storage! {
+    trait Store for Module<T: Trait> as VersionedStorePermissions {
+      /// ClassPermissions of corresponding Classes in the versioned store
+      pub ClassPermissionsByClassId get(class_permissions_by_class_id): linked_map ClassId => ClassPermissionsType<T>;
+
+      /// Owner of an entity in the versioned store. If it is None then it is owned by the system.
+      pub EntityMaintainerByEntityId get(entity_maintainer_by_entity_id): linked_map EntityId => Option<T::Credential>;
+    }
+}
+
+decl_module! {
+    pub struct Module<T: Trait> for enum Call where origin: T::Origin {
+
+        /// Sets the admins for a class
+        fn set_class_admins(
+            origin,
+            class_id: ClassId,
+            admins: CredentialSet<T::Credential>
+        ) -> dispatch::Result {
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+
+            Self::mutate_class_permissions(
+                &raw_origin,
+                None,
+                Self::is_system, // root origin
+                class_id,
+                |class_permissions| {
+                    class_permissions.admins = admins;
+                    Ok(())
+                }
+            )
+        }
+
+        // Methods for updating concrete permissions
+
+        fn set_class_entity_permissions(
+            origin,
+            with_credential: Option<T::Credential>,
+            class_id: ClassId,
+            entity_permissions: EntityPermissions<T::Credential>
+        ) -> dispatch::Result {
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+
+            Self::mutate_class_permissions(
+                &raw_origin,
+                with_credential,
+                ClassPermissions::is_admin,
+                class_id,
+                |class_permissions| {
+                    class_permissions.entity_permissions = entity_permissions;
+                    Ok(())
+                }
+            )
+        }
+
+        fn set_class_entities_can_be_created(
+            origin,
+            with_credential: Option<T::Credential>,
+            class_id: ClassId,
+            can_be_created: bool
+        ) -> dispatch::Result {
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+
+            Self::mutate_class_permissions(
+                &raw_origin,
+                with_credential,
+                ClassPermissions::is_admin,
+                class_id,
+                |class_permissions| {
+                    class_permissions.entities_can_be_created = can_be_created;
+                    Ok(())
+                }
+            )
+        }
+
+        fn set_class_add_schemas_set(
+            origin,
+            with_credential: Option<T::Credential>,
+            class_id: ClassId,
+            credential_set: CredentialSet<T::Credential>
+        ) -> dispatch::Result {
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+
+            Self::mutate_class_permissions(
+                &raw_origin,
+                with_credential,
+                ClassPermissions::is_admin,
+                class_id,
+                |class_permissions| {
+                    class_permissions.add_schemas = credential_set;
+                    Ok(())
+                }
+            )
+        }
+
+        fn set_class_create_entities_set(
+            origin,
+            with_credential: Option<T::Credential>,
+            class_id: ClassId,
+            credential_set: CredentialSet<T::Credential>
+        ) -> dispatch::Result {
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+
+            Self::mutate_class_permissions(
+                &raw_origin,
+                with_credential,
+                ClassPermissions::is_admin,
+                class_id,
+                |class_permissions| {
+                    class_permissions.create_entities = credential_set;
+                    Ok(())
+                }
+            )
+        }
+
+        fn set_class_reference_constraint(
+            origin,
+            with_credential: Option<T::Credential>,
+            class_id: ClassId,
+            constraint: ReferenceConstraint<ClassId, u16>
+        ) -> dispatch::Result {
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+
+            Self::mutate_class_permissions(
+                &raw_origin,
+                with_credential,
+                ClassPermissions::is_admin,
+                class_id,
+                |class_permissions| {
+                    class_permissions.reference_constraint = constraint;
+                    Ok(())
+                }
+            )
+        }
+
+        // Setting a new maintainer for an entity may require having additional constraints.
+        // So for now it is disabled.
+        // pub fn set_entity_maintainer(
+        //     origin,
+        //     entity_id: EntityId,
+        //     new_maintainer: Option<T::Credential>
+        // ) -> dispatch::Result {
+        //     ensure_root(origin)?;
+
+        //     // ensure entity exists in the versioned store
+        //     let _ = Self::get_class_id_by_entity_id(entity_id)?;
+
+        //     <EntityMaintainerByEntityId<T>>::mutate(entity_id, |maintainer| {
+        //         *maintainer = new_maintainer;
+        //     });
+
+        //     Ok(())
+        // }
+
+        // Permissioned proxy calls to versioned store
+
+        pub fn create_class(
+            origin,
+            name: Vec<u8>,
+            description: Vec<u8>,
+            class_permissions: ClassPermissionsType<T>
+        ) -> dispatch::Result {
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+
+            let can_create_class = match raw_origin {
+                system::RawOrigin::Root => true,
+                system::RawOrigin::Signed(sender) => {
+                    T::CreateClassPermissionsChecker::account_can_create_class_permissions(&sender)
+                },
+                _ => false
+            };
+
+            if can_create_class {
+                let class_id = <versioned_store::Module<T>>::create_class(name, description)?;
+
+                // is there a need to assert class_id is unique?
+
+                <ClassPermissionsByClassId<T>>::insert(&class_id, class_permissions);
+
+                Ok(())
+            } else {
+                Err("NotPermittedToCreateClass")
+            }
+        }
+
+        pub fn create_class_with_default_permissions(
+            origin,
+            name: Vec<u8>,
+            description: Vec<u8>
+        ) -> dispatch::Result {
+            Self::create_class(origin, name, description, ClassPermissions::default())
+        }
+
+        pub fn add_class_schema(
+            origin,
+            with_credential: Option<T::Credential>,
+            class_id: ClassId,
+            existing_properties: Vec<u16>,
+            new_properties: Vec<Property>
+        ) -> dispatch::Result {
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+
+            Self::if_class_permissions_satisfied(
+                &raw_origin,
+                with_credential,
+                None,
+                ClassPermissions::can_add_class_schema,
+                class_id,
+                |_class_permissions, _access_level| {
+                    // If a new property points at another class,
+                    // at this point we don't enforce anything about reference constraints
+                    // because of the chicken and egg problem. Instead enforcement is done
+                    // at the time of creating an entity.
+                    let _schema_index = <versioned_store::Module<T>>::add_class_schema(class_id, existing_properties, new_properties)?;
+                    Ok(())
+                }
+            )
+        }
+
+        /// Creates a new entity of type class_id. The maintainer is set to be either None if the origin is root, or the provided credential
+        /// associated with signer.
+        pub fn create_entity(
+            origin,
+            with_credential: Option<T::Credential>,
+            class_id: ClassId
+        ) -> dispatch::Result {
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+            let _entity_id = Self::do_create_entity(&raw_origin, with_credential, class_id)?;
+            Ok(())
+        }
+
+        pub fn add_schema_support_to_entity(
+            origin,
+            with_credential: Option<T::Credential>,
+            as_entity_maintainer: bool,
+            entity_id: EntityId,
+            schema_id: u16, // Do not type alias u16!! - u16,
+            property_values: Vec<ClassPropertyValue>
+        ) -> dispatch::Result {
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+            Self::do_add_schema_support_to_entity(&raw_origin, with_credential, as_entity_maintainer, entity_id, schema_id, property_values)
+        }
+
+        pub fn update_entity_property_values(
+            origin,
+            with_credential: Option<T::Credential>,
+            as_entity_maintainer: bool,
+            entity_id: EntityId,
+            property_values: Vec<ClassPropertyValue>
+        ) -> dispatch::Result {
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+            Self::do_update_entity_property_values(&raw_origin, with_credential, as_entity_maintainer, entity_id, property_values)
+        }
+
+        pub fn transaction(origin, operations: Vec<Operation<T::Credential>>) -> dispatch::Result {
+            // This map holds the EntityId of the entity created as a result of executing a CreateEntity Operation
+            // keyed by the indexed of the operation, in the operations vector.
+            let mut entity_created_in_operation: BTreeMap<usize, EntityId> = BTreeMap::new();
+
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+
+            for (op_index, operation) in operations.into_iter().enumerate() {
+                match operation.operation_type {
+                    OperationType::CreateEntity(create_entity_operation) => {
+                        let entity_id = Self::do_create_entity(&raw_origin, operation.with_credential, create_entity_operation.class_id)?;
+                        entity_created_in_operation.insert(op_index, entity_id);
+                    },
+                    OperationType::UpdatePropertyValues(update_property_values_operation) => {
+                        let entity_id = operations::parametrized_entity_to_entity_id(&entity_created_in_operation, update_property_values_operation.entity_id)?;
+                        let property_values = operations::parametrized_property_values_to_property_values(&entity_created_in_operation, update_property_values_operation.new_parametrized_property_values)?;
+                        Self::do_update_entity_property_values(&raw_origin, operation.with_credential, operation.as_entity_maintainer, entity_id, property_values)?;
+                    },
+                    OperationType::AddSchemaSupportToEntity(add_schema_support_to_entity_operation) => {
+                        let entity_id = operations::parametrized_entity_to_entity_id(&entity_created_in_operation, add_schema_support_to_entity_operation.entity_id)?;
+                        let schema_id = add_schema_support_to_entity_operation.schema_id;
+                        let property_values = operations::parametrized_property_values_to_property_values(&entity_created_in_operation, add_schema_support_to_entity_operation.parametrized_property_values)?;
+                        Self::do_add_schema_support_to_entity(&raw_origin, operation.with_credential, operation.as_entity_maintainer, entity_id, schema_id, property_values)?;
+                    }
+                }
+            }
+
+            Ok(())
+        }
+    }
+}
+
+impl<T: Trait> Module<T> {
+    fn ensure_root_or_signed(
+        origin: T::Origin,
+    ) -> Result<system::RawOrigin<T::AccountId>, &'static str> {
+        match origin.into() {
+            Ok(system::RawOrigin::Root) => Ok(system::RawOrigin::Root),
+            Ok(system::RawOrigin::Signed(account_id)) => Ok(system::RawOrigin::Signed(account_id)),
+            _ => Err("BadOrigin:ExpectedRootOrSigned"),
+        }
+    }
+
+    fn do_create_entity(
+        raw_origin: &system::RawOrigin<T::AccountId>,
+        with_credential: Option<T::Credential>,
+        class_id: ClassId,
+    ) -> Result<EntityId, &'static str> {
+        Self::if_class_permissions_satisfied(
+            raw_origin,
+            with_credential,
+            None,
+            ClassPermissions::can_create_entity,
+            class_id,
+            |_class_permissions, access_level| {
+                let entity_id = <versioned_store::Module<T>>::create_entity(class_id)?;
+
+                // Note: mutating value to None is equivalient to removing the value from storage map
+                <EntityMaintainerByEntityId<T>>::mutate(
+                    entity_id,
+                    |maintainer| match access_level {
+                        AccessLevel::System => *maintainer = None,
+                        AccessLevel::Credential(credential) => *maintainer = Some(*credential),
+                        _ => *maintainer = None,
+                    },
+                );
+
+                Ok(entity_id)
+            },
+        )
+    }
+
+    fn do_update_entity_property_values(
+        raw_origin: &system::RawOrigin<T::AccountId>,
+        with_credential: Option<T::Credential>,
+        as_entity_maintainer: bool,
+        entity_id: EntityId,
+        property_values: Vec<ClassPropertyValue>,
+    ) -> dispatch::Result {
+        let class_id = Self::get_class_id_by_entity_id(entity_id)?;
+
+        Self::ensure_internal_property_values_permitted(class_id, &property_values)?;
+
+        let as_entity_maintainer = if as_entity_maintainer {
+            Some(entity_id)
+        } else {
+            None
+        };
+
+        Self::if_class_permissions_satisfied(
+            raw_origin,
+            with_credential,
+            as_entity_maintainer,
+            ClassPermissions::can_update_entity,
+            class_id,
+            |_class_permissions, _access_level| {
+                <versioned_store::Module<T>>::update_entity_property_values(
+                    entity_id,
+                    property_values,
+                )
+            },
+        )
+    }
+
+    fn do_add_schema_support_to_entity(
+        raw_origin: &system::RawOrigin<T::AccountId>,
+        with_credential: Option<T::Credential>,
+        as_entity_maintainer: bool,
+        entity_id: EntityId,
+        schema_id: u16,
+        property_values: Vec<ClassPropertyValue>,
+    ) -> dispatch::Result {
+        // class id of the entity being updated
+        let class_id = Self::get_class_id_by_entity_id(entity_id)?;
+
+        Self::ensure_internal_property_values_permitted(class_id, &property_values)?;
+
+        let as_entity_maintainer = if as_entity_maintainer {
+            Some(entity_id)
+        } else {
+            None
+        };
+
+        Self::if_class_permissions_satisfied(
+            raw_origin,
+            with_credential,
+            as_entity_maintainer,
+            ClassPermissions::can_update_entity,
+            class_id,
+            |_class_permissions, _access_level| {
+                <versioned_store::Module<T>>::add_schema_support_to_entity(
+                    entity_id,
+                    schema_id,
+                    property_values,
+                )
+            },
+        )
+    }
+
+    /// Derives the AccessLevel the caller is attempting to act with.
+    /// It expects only signed or root origin.
+    fn derive_access_level(
+        raw_origin: &system::RawOrigin<T::AccountId>,
+        with_credential: Option<T::Credential>,
+        as_entity_maintainer: Option<EntityId>,
+    ) -> Result<AccessLevel<T::Credential>, &'static str> {
+        match raw_origin {
+            system::RawOrigin::Root => Ok(AccessLevel::System),
+            system::RawOrigin::Signed(account_id) => {
+                if let Some(credential) = with_credential {
+                    if T::CredentialChecker::account_has_credential(&account_id, credential) {
+                        if let Some(entity_id) = as_entity_maintainer {
+                            // is entity maintained by system
+                            ensure!(
+                                <EntityMaintainerByEntityId<T>>::exists(entity_id),
+                                "NotEnityMaintainer"
+                            );
+                            // ensure entity maintainer matches
+                            match Self::entity_maintainer_by_entity_id(entity_id) {
+                                Some(maintainer_credential)
+                                    if credential == maintainer_credential =>
+                                {
+                                    Ok(AccessLevel::EntityMaintainer)
+                                }
+                                _ => Err("NotEnityMaintainer"),
+                            }
+                        } else {
+                            Ok(AccessLevel::Credential(credential))
+                        }
+                    } else {
+                        Err("OriginCannotActWithRequestedCredential")
+                    }
+                } else {
+                    Ok(AccessLevel::Unspecified)
+                }
+            }
+            _ => Err("BadOrigin:ExpectedRootOrSigned"),
+        }
+    }
+
+    /// Returns the stored class permissions if exist, error otherwise.
+    fn ensure_class_permissions(
+        class_id: ClassId,
+    ) -> Result<ClassPermissionsType<T>, &'static str> {
+        ensure!(
+            <ClassPermissionsByClassId<T>>::exists(class_id),
+            "ClassPermissionsNotFoundByClassId"
+        );
+        Ok(Self::class_permissions_by_class_id(class_id))
+    }
+
+    /// Derives the access level of the caller.
+    /// If the predicate passes, the mutate method is invoked.
+    fn mutate_class_permissions<Predicate, Mutate>(
+        raw_origin: &system::RawOrigin<T::AccountId>,
+        with_credential: Option<T::Credential>,
+        // predicate to test
+        predicate: Predicate,
+        // class permissions to perform mutation on if it exists
+        class_id: ClassId,
+        // actual mutation to apply.
+        mutate: Mutate,
+    ) -> dispatch::Result
+    where
+        Predicate:
+            FnOnce(&ClassPermissionsType<T>, &AccessLevel<T::Credential>) -> dispatch::Result,
+        Mutate: FnOnce(&mut ClassPermissionsType<T>) -> dispatch::Result,
+    {
+        let access_level = Self::derive_access_level(raw_origin, with_credential, None)?;
+        let mut class_permissions = Self::ensure_class_permissions(class_id)?;
+
+        predicate(&class_permissions, &access_level)?;
+        mutate(&mut class_permissions)?;
+        class_permissions.last_permissions_update = <system::Module<T>>::block_number();
+        <ClassPermissionsByClassId<T>>::insert(class_id, class_permissions);
+        Ok(())
+    }
+
+    fn is_system(
+        _: &ClassPermissionsType<T>,
+        access_level: &AccessLevel<T::Credential>,
+    ) -> dispatch::Result {
+        if *access_level == AccessLevel::System {
+            Ok(())
+        } else {
+            Err("NotRootOrigin")
+        }
+    }
+
+    /// Derives the access level of the caller.
+    /// If the peridcate passes the callback is invoked. Returns result of the callback
+    /// or error from failed predicate.
+    fn if_class_permissions_satisfied<Predicate, Callback, R>(
+        raw_origin: &system::RawOrigin<T::AccountId>,
+        with_credential: Option<T::Credential>,
+        as_entity_maintainer: Option<EntityId>,
+        // predicate to test
+        predicate: Predicate,
+        // class permissions to test
+        class_id: ClassId,
+        // callback to invoke if predicate passes
+        callback: Callback,
+    ) -> Result<R, &'static str>
+    where
+        Predicate:
+            FnOnce(&ClassPermissionsType<T>, &AccessLevel<T::Credential>) -> dispatch::Result,
+        Callback: FnOnce(
+            &ClassPermissionsType<T>,
+            &AccessLevel<T::Credential>,
+        ) -> Result<R, &'static str>,
+    {
+        let access_level =
+            Self::derive_access_level(raw_origin, with_credential, as_entity_maintainer)?;
+        let class_permissions = Self::ensure_class_permissions(class_id)?;
+
+        predicate(&class_permissions, &access_level)?;
+        callback(&class_permissions, &access_level)
+    }
+
+    fn get_class_id_by_entity_id(entity_id: EntityId) -> Result<ClassId, &'static str> {
+        // use a utility method on versioned_store module
+        ensure!(
+            versioned_store::EntityById::exists(entity_id),
+            "EntityNotFound"
+        );
+        let entity = <versioned_store::Module<T>>::entity_by_id(entity_id);
+        Ok(entity.class_id)
+    }
+
+    // Ensures property_values of type Internal that point to a class,
+    // the target entity and class exists and constraint allows it.
+    fn ensure_internal_property_values_permitted(
+        source_class_id: ClassId,
+        property_values: &[ClassPropertyValue],
+    ) -> dispatch::Result {
+        for property_value in property_values.iter() {
+            if let PropertyValue::Internal(ref target_entity_id) = property_value.value {
+                // get the class permissions for target class
+                let target_class_id = Self::get_class_id_by_entity_id(*target_entity_id)?;
+                // assert class permissions exists for target class
+                let class_permissions = Self::class_permissions_by_class_id(target_class_id);
+
+                // ensure internal reference is permitted
+                match class_permissions.reference_constraint {
+                    ReferenceConstraint::NoConstraint => Ok(()),
+                    ReferenceConstraint::NoReferencingAllowed => {
+                        Err("EntityCannotReferenceTargetEntity")
+                    }
+                    ReferenceConstraint::Restricted(permitted_properties) => {
+                        if permitted_properties.contains(&PropertyOfClass {
+                            class_id: source_class_id,
+                            property_index: property_value.in_class_index,
+                        }) {
+                            Ok(())
+                        } else {
+                            Err("EntityCannotReferenceTargetEntity")
+                        }
+                    }
+                }?;
+            }
+        }
+
+        // if we reach here all Internal properties have passed the constraint check
+        Ok(())
+    }
+}

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