diff --git a/.dockerignore b/.dockerignore index 8357df11b9..6bf1151fe0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1,2 @@ -**target* \ No newline at end of file +**target* +**node_modules* \ No newline at end of file diff --git a/.gitignore b/.gitignore index 58b9ee32cb..e590d097e2 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,12 @@ # runtime built with docker build script joystream_runtime.wasm +# Node modules directory +**/node_modules + +# Generated by yarn +yarn* + # JetBrains IDEs .idea @@ -16,3 +22,6 @@ joystream_runtime.wasm # Visual Studio Code .vscode + +# Compiled WASM code +*.wasm \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 5798a3f78a..bfaefebec6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: rust rust: - - 1.42.0 + - 1.43.0 matrix: include: @@ -12,10 +12,15 @@ matrix: services: docker - os: osx env: TARGET=x86_64-apple-darwin + - os: linux + env: TARGET=wasm-blob + services: docker before_install: - rustup component add rustfmt - cargo fmt --all -- --check + - rustup component add clippy + - BUILD_DUMMY_WASM_BINARY=1 cargo clippy -- -D warnings - rustup default stable - rustup update nightly - rustup target add wasm32-unknown-unknown --toolchain nightly @@ -37,52 +42,55 @@ script: joystream/rust-raspberry \ build --release sudo chmod a+r ${TRAVIS_BUILD_DIR}/target/${TARGET}/release/joystream-node + elif [ "$TARGET" = "wasm-blob" ] + then + docker build --tag joystream/node \ + --file ./devops/dockerfiles/node-and-runtime/Dockerfile \ + . + docker create --name temp-container-joystream-node joystream/node + docker cp temp-container-joystream-node:/joystream/runtime.compact.wasm joystream_runtime.wasm + docker rm temp-container-joystream-node else cargo build --release --target=${TARGET} fi before_deploy: - - cp ./target/${TARGET}/release/joystream-node . - | - if [ "$TARGET" = "arm-unknown-linux-gnueabihf" ] + if [ "$TARGET" = "wasm-blob" ] then - export FILENAME="joystream-node-armv7-linux-gnueabihf" + export ASSET="joystream_runtime.wasm" else - export FILENAME=`./joystream-node --version | sed -e "s/ /-/g"` + cp ./target/${TARGET}/release/joystream-node . + if [ "$TARGET" = "arm-unknown-linux-gnueabihf" ] + then + export FILENAME="joystream-node-armv7-linux-gnueabihf" + else + export FILENAME=`./joystream-node --version | sed -e "s/ /-/g"` + fi + tar -cf ${FILENAME}.tar ./joystream-node + gzip ${FILENAME}.tar + export ASSET=${FILENAME}.tar.gz fi - - tar -cf ${FILENAME}.tar ./joystream-node - - gzip ${FILENAME}.tar deploy: - provider: releases api_key: - secure: QTna4XzKmPrXNA5KnYfLsH8cAKxESLdFbQ5HJ6nvB9reE10SVtg8lZ+ShL+no7TACNBUNt09Qv9HNgs6JcNRJ9QMHEJHKIbMyjplhBtZ+W3l0k+6TL0yeKHZ/OvddDF+vDbpN+y4xBfOf0xqZcNH3lZJTms/NPBn/KT5DpQ3JZ8bibdMP2HSCazfvHLwj38OuLX6VWbFcmN2RAmUR9AXYvk5wWYVw8g1VDzTCxjH1G+dGWH1L9+ZDgFfv7BNSNhPc6V9GghgLVZ+37r/STzTTAQ/gPv+yruglEWUhSAngFfVYUegvTmIeQLi/V+g0tKUS+l7eNX08xz6eZcn0+/32V7P+oEN/dhU84E0kgmiOsiUEGI/KFM+qw9TyX3GtD67UmG5TZrD7OUMIu1qCuPSetsTOK2kvpwlYAn+j5iFB30Uz4hXhOH5dib2zz2I7cYHi1kvzeNQqQOPNDCmaO48bcbRIaeqMAHdsb6scGzh/+CD2V2HOmHlhd+4o1PpX6hAMwmOXAu3bMDi4zlB9Hb1cSZnsYNBHawkD6y45QGepFKpGW/6u5VRPeMK62Gm9wu815C36B4mVg6CVqtZMbk0WYPIk6zfoTft3i04YthKbRO96a5VD9LssVbiSYnudXuZJjSllSZVCi9AKS8JVIS2jC2z+tWkquAesSrwztriRcs= - file: ${FILENAME}.tar.gz + secure: FfxZGQexxAGT0Skbctl1FuqmEvNHejPDPtNG8Du1ACSGjS7Y+M6o/aPqE6HL158AmddOgndsIPR+HM7VfMDAUMkLTbOhv3nMpDBZu1h25vwk+jHOM65tm5LWUu/ROWBpaAQiG7NKrvtfkNfbNBSETsEbWBt/DPrhlIfSbgsXBFDiid7uRrCiwvDUJ097/EUOJ9OVUrk+O4ebSzfIfKPGPtRU2rQQ0eNX7yX3TCm3jbQm/kplkQNRL9mnAJNxtKuvuko4LqZ6jN4XLoLTHUMjO7E0r6wXVB4GVjA4HA214eLlQD6BhgTbWMDxKgWyuKzPG+2GLKyluSSn0RurSl8tYryXKxKxuN3H1FX9r23a8AzGtpRACJtIePC2YmPuQRSnz2Bw8jlSP2WPLJtXGD036J/wVMj6W9TROm7IBigiC7QlqAqCYNByOnoKyhRCgYyAJZb0Jpa3qWaFhA6b6gCGhyH85QCcrc0q6JAB3oqH8Wfm/K2HVzBobmKaSFu5DpwInNnUXnLWGVzhSt3oCq6ld773izReGdLJtLC2vaJ9rZVaVw29s9M662EEuAGgaVLO/sinZJFeIIaCF4i4zUXwXSLIdfKXGOR0ZibkyT2FS6qPGvl/lLN5IREzD7v/rV8htGMLmw4jpPLNskvRjCHX42ewRRYdMvZzQQOAvSlWcsw= + file: ${ASSET} on: tags: true - repo: Joystream/substrate-node-joystream + repo: Joystream/joystream draft: true overwrite: true skip_cleanup: true - provider: releases api_key: - secure: QTna4XzKmPrXNA5KnYfLsH8cAKxESLdFbQ5HJ6nvB9reE10SVtg8lZ+ShL+no7TACNBUNt09Qv9HNgs6JcNRJ9QMHEJHKIbMyjplhBtZ+W3l0k+6TL0yeKHZ/OvddDF+vDbpN+y4xBfOf0xqZcNH3lZJTms/NPBn/KT5DpQ3JZ8bibdMP2HSCazfvHLwj38OuLX6VWbFcmN2RAmUR9AXYvk5wWYVw8g1VDzTCxjH1G+dGWH1L9+ZDgFfv7BNSNhPc6V9GghgLVZ+37r/STzTTAQ/gPv+yruglEWUhSAngFfVYUegvTmIeQLi/V+g0tKUS+l7eNX08xz6eZcn0+/32V7P+oEN/dhU84E0kgmiOsiUEGI/KFM+qw9TyX3GtD67UmG5TZrD7OUMIu1qCuPSetsTOK2kvpwlYAn+j5iFB30Uz4hXhOH5dib2zz2I7cYHi1kvzeNQqQOPNDCmaO48bcbRIaeqMAHdsb6scGzh/+CD2V2HOmHlhd+4o1PpX6hAMwmOXAu3bMDi4zlB9Hb1cSZnsYNBHawkD6y45QGepFKpGW/6u5VRPeMK62Gm9wu815C36B4mVg6CVqtZMbk0WYPIk6zfoTft3i04YthKbRO96a5VD9LssVbiSYnudXuZJjSllSZVCi9AKS8JVIS2jC2z+tWkquAesSrwztriRcs= - file: ${FILENAME}.tar.gz + secure: FfxZGQexxAGT0Skbctl1FuqmEvNHejPDPtNG8Du1ACSGjS7Y+M6o/aPqE6HL158AmddOgndsIPR+HM7VfMDAUMkLTbOhv3nMpDBZu1h25vwk+jHOM65tm5LWUu/ROWBpaAQiG7NKrvtfkNfbNBSETsEbWBt/DPrhlIfSbgsXBFDiid7uRrCiwvDUJ097/EUOJ9OVUrk+O4ebSzfIfKPGPtRU2rQQ0eNX7yX3TCm3jbQm/kplkQNRL9mnAJNxtKuvuko4LqZ6jN4XLoLTHUMjO7E0r6wXVB4GVjA4HA214eLlQD6BhgTbWMDxKgWyuKzPG+2GLKyluSSn0RurSl8tYryXKxKxuN3H1FX9r23a8AzGtpRACJtIePC2YmPuQRSnz2Bw8jlSP2WPLJtXGD036J/wVMj6W9TROm7IBigiC7QlqAqCYNByOnoKyhRCgYyAJZb0Jpa3qWaFhA6b6gCGhyH85QCcrc0q6JAB3oqH8Wfm/K2HVzBobmKaSFu5DpwInNnUXnLWGVzhSt3oCq6ld773izReGdLJtLC2vaJ9rZVaVw29s9M662EEuAGgaVLO/sinZJFeIIaCF4i4zUXwXSLIdfKXGOR0ZibkyT2FS6qPGvl/lLN5IREzD7v/rV8htGMLmw4jpPLNskvRjCHX42ewRRYdMvZzQQOAvSlWcsw= + file: ${ASSET} on: branch: development - repo: Joystream/substrate-node-joystream + repo: Joystream/joystream draft: true prerelease: true overwrite: true skip_cleanup: true - - provider: releases - api_key: - secure: ZoEXp8g+yZOEG8JZ1Fg6tWnW3aYDfupFbZflEejYaAdXhj1nw7G9N10ZX5VDdb/O1iFx8BhfFymQxk0ynxNC8c52LzOjKIhXEporxgvEPdnoPS/N1JhfsOUV0ragwZDLv2tFVi2AT0K4w8WJFJDzrK4qHOMMQgVbVQZtFmDL1whHdfBD5FyFyKmMdZdWBtTGy4s7X0LnmxjNB4/3AMa540T3LowZ5H66MYZkQmAbtg8ib93WomVanhS23vbjNaH9x1Kmzxd2B3pCSgI8uaxBrpmzINvAeSusYVJQt0EF/cAPXmq0+JmGoocvcS1ecg/SNZoKUNmeElB4ns/obg/QAyE+fyQtyl+iDYBilhFLm5xRMUnqkpyeUUD3u824i/Z+/tfLvtm5Egg1QAiXtIIJMeAj1nN8OIeSlHR4phnSTA3jl2PZw9QYidtV9WCqHC0qxtpkYSKkC8ItaefScPB1AuvOvVx8xvnIxfR/tXvL8Y3Y2BvhiLgpky9JkbdMln1b0m0E5c4vyGCEVqHqpbxM63VJkpct8sVx0atGvipWEelVjz5XpkxW2PYbgg4EKUzl3FiYcXwf5Y/ykxaZNZt7I4gv9nz2KkVwUCCPqdwWF7ww1shFWW5tCoCmJuUESOdPFx0jQ7LVWz7SDLDsqvvaW2c2OPxG6DIx9BiTeAE4qIQ= - file: "${FILENAME}.tar.gz" - skip_cleanup: true - draft: true - prerelease: true - overwrite: true - on: - repo: mnaamani/substrate-node-joystream - branch: deploy \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index cc4b914ad6..fa4e3c8bba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -131,7 +131,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d0864d84b8e07b145449be9a8537db86bf9de5ce03b913214694643b4743502" dependencies = [ "quote 1.0.3", - "syn 1.0.16", + "syn 1.0.17", ] [[package]] @@ -159,9 +159,9 @@ checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" [[package]] name = "backtrace" -version = "0.3.45" +version = "0.3.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad235dabf00f36301792cfe82499880ba54c6486be094d1047b02bacb67c14e8" +checksum = "b1e692897359247cc6bb902933361652380af0f1b7651ae5c5013407f30e109e" dependencies = [ "backtrace-sys", "cfg-if", @@ -171,9 +171,9 @@ dependencies = [ [[package]] name = "backtrace-sys" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca797db0057bae1a7aa2eef3283a874695455cecf08a43bfb8507ee0ebc1ed69" +checksum = "7de8aba10a69c8e8d7622c5710229485ec32e9d55fdad160ea559c086fdcd118" dependencies = [ "cc", "libc", @@ -230,9 +230,13 @@ checksum = "5da9b3d9f6f585199287a473f4f8dfab6566cf827d15c00c219f53c645687ead" [[package]] name = "bitvec" -version = "0.15.2" +version = "0.17.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a993f74b4c99c1908d156b8d2e0fb6277736b0ecbd833982fd1241d39b2766a6" +checksum = "41262f11d771fd4a61aa3ce019fca363b4b6c282fca9da2a31186d3965a47a5c" +dependencies = [ + "either", + "radium", +] [[package]] name = "blake2" @@ -309,9 +313,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.2.0" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f359dc14ff8911330a51ef78022d376f25ed00248912803b58f00cb1c27f742" +checksum = "12ae9db68ad7fac5fe51304d20f016c911539251075a214f8e663babefa35187" [[package]] name = "byte-slice-cast" @@ -459,7 +463,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f1af9ac737b2dd2d577701e59fd09ba34822f6f2ebdb30a7647405d9e55e16a" dependencies = [ "const-random-macro", - "proc-macro-hack 0.5.12", + "proc-macro-hack 0.5.15", ] [[package]] @@ -469,7 +473,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25e4c606eb459dd29f7c57b2e0879f2b6f14ee130918c2b78ccb58a9624e6c7a" dependencies = [ "getrandom", - "proc-macro-hack 0.5.12", + "proc-macro-hack 0.5.15", ] [[package]] @@ -640,6 +644,17 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11c0346158a19b3627234e15596f5e465c360fcdb97d817bcb255e0510f5a788" +[[package]] +name = "derivative" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1eae4d76b7cefedd1b4f8cc24378b2fbd1ac1b66e3bbebe8e2192d3be81cb355" +dependencies = [ + "proc-macro2 1.0.10", + "quote 1.0.3", + "syn 1.0.17", +] + [[package]] name = "derive_more" version = "0.14.1" @@ -776,9 +791,9 @@ checksum = "516aa8d7a71cb00a1c4146f0798549b93d083d4f189b3ced8f3de6b8f11ee6c4" [[package]] name = "erased-serde" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd7d80305c9bd8cd78e3c753eb9fb110f83621e5211f1a3afffcc812b104daf9" +checksum = "d88b6d1705e16a4d62e05ea61cc0496c2bd190f4fa8e5c1f11ce747be6bcf3d1" dependencies = [ "serde", ] @@ -809,9 +824,9 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "030a733c8287d6213886dd487564ff5c8f6aae10278b3588ed177f9d18f8d231" dependencies = [ - "proc-macro2 1.0.9", + "proc-macro2 1.0.10", "quote 1.0.3", - "syn 1.0.16", + "syn 1.0.17", "synstructure", ] @@ -1045,10 +1060,10 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a5081aa3de1f7542a794a397cde100ed903b0630152d0973479018fd85423a7" dependencies = [ - "proc-macro-hack 0.5.12", - "proc-macro2 1.0.9", + "proc-macro-hack 0.5.15", + "proc-macro2 1.0.10", "quote 1.0.3", - "syn 1.0.16", + "syn 1.0.17", ] [[package]] @@ -1108,7 +1123,7 @@ dependencies = [ "futures-task", "memchr", "pin-utils", - "proc-macro-hack 0.5.12", + "proc-macro-hack 0.5.15", "proc-macro-nested", "slab", ] @@ -1269,9 +1284,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.1.8" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1010591b26bbfe835e9faeabeb11866061cc7dcebffd56ad7d0942d0e61aefd8" +checksum = "725cf19794cf90aa94e65050cb4191ff5d8fa87a498383774c47b332e3af952e" dependencies = [ "libc", ] @@ -1305,7 +1320,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "961de220ec9a91af2e1e5bd80d02109155695e516771762381ef8581317066e0" dependencies = [ "hex-literal-impl 0.2.1", - "proc-macro-hack 0.5.12", + "proc-macro-hack 0.5.15", ] [[package]] @@ -1323,7 +1338,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d4c5c844e2fee0bf673d54c2c177f1713b3d2af2ff6e666b49cb7572e6cf42d" dependencies = [ - "proc-macro-hack 0.5.12", + "proc-macro-hack 0.5.15", ] [[package]] @@ -1487,9 +1502,9 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ef5550a42e3740a0e71f909d4c861056a284060af885ae7aa6242820f920d9d" dependencies = [ - "proc-macro2 1.0.9", + "proc-macro2 1.0.10", "quote 1.0.3", - "syn 1.0.16", + "syn 1.0.17", ] [[package]] @@ -1554,7 +1569,7 @@ dependencies = [ [[package]] name = "joystream-node" -version = "2.1.3" +version = "2.2.0" dependencies = [ "ctrlc", "derive_more 0.14.1", @@ -1599,7 +1614,7 @@ dependencies = [ [[package]] name = "joystream-node-runtime" -version = "6.8.1" +version = "6.13.0" dependencies = [ "parity-scale-codec", "safe-mix", @@ -1641,6 +1656,9 @@ dependencies = [ "substrate-memo-module", "substrate-offchain-primitives", "substrate-primitives", + "substrate-proposals-codex-module", + "substrate-proposals-discussion-module", + "substrate-proposals-engine-module", "substrate-recurring-reward-module", "substrate-roles-module", "substrate-service-discovery-module", @@ -1655,9 +1673,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cb931d43e71f560c81badb0191596562bafad2be06a3f9025b845c847c60df5" +checksum = "6a27d435371a2fa5b6d2b028a74bbdb1234f308da363226a2854ca3ff8ba7055" dependencies = [ "wasm-bindgen", ] @@ -1720,9 +1738,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8609af8f63b626e8e211f52441fcdb6ec54f1a446606b10d5c89ae9bf8a20058" dependencies = [ "proc-macro-crate", - "proc-macro2 1.0.9", + "proc-macro2 1.0.10", "quote 1.0.3", - "syn 1.0.16", + "syn 1.0.17", ] [[package]] @@ -2357,8 +2375,8 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e37c5d4cd9473c5f4c9c111f033f15d4df9bd378fdf615944e360a4f55a05f0b" dependencies = [ - "proc-macro2 1.0.9", - "syn 1.0.16", + "proc-macro2 1.0.10", + "syn 1.0.17", "synstructure", ] @@ -2504,9 +2522,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5a615a1ad92048ad5d9633251edb7492b8abc057d7a679a9898476aef173935" dependencies = [ "cfg-if", - "proc-macro2 1.0.9", + "proc-macro2 1.0.10", "quote 1.0.3", - "syn 1.0.16", + "syn 1.0.17", ] [[package]] @@ -2642,6 +2660,28 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca565a7df06f3d4b485494f25ba05da1435950f4dc263440eda7a6fa9b8e36e4" +dependencies = [ + "derivative", + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffa5a33ddddfee04c0283a7653987d634e880347e96b5b2ed64de07efb59db9d" +dependencies = [ + "proc-macro-crate", + "proc-macro2 1.0.10", + "quote 1.0.3", + "syn 1.0.17", +] + [[package]] name = "ole32-sys" version = "0.2.0" @@ -2755,9 +2795,9 @@ dependencies = [ [[package]] name = "parity-scale-codec" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f509c5e67ca0605ee17dcd3f91ef41cadd685c75a298fb6261b781a5acb3f910" +checksum = "329c8f7f4244ddb5c37c103641027a76c530e65e8e4b8240b29f81ea40508b17" dependencies = [ "arrayvec 0.5.1", "bitvec", @@ -2773,9 +2813,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a0ec292e92e8ec7c58e576adacc1e3f399c597c8f263c42f18420abe58e7245" dependencies = [ "proc-macro-crate", - "proc-macro2 1.0.9", + "proc-macro2 1.0.10", "quote 1.0.3", - "syn 1.0.16", + "syn 1.0.17", ] [[package]] @@ -2926,24 +2966,24 @@ dependencies = [ [[package]] name = "paste" -version = "0.1.7" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e1afe738d71b1ebab5f1207c055054015427dbfc7bbe9ee1266894156ec046" +checksum = "ab4fb1930692d1b6a9cfabdde3d06ea0a7d186518e2f4d67660d8970e2fa647a" dependencies = [ "paste-impl", - "proc-macro-hack 0.5.12", + "proc-macro-hack 0.5.15", ] [[package]] name = "paste-impl" -version = "0.1.7" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d4dc4a7f6f743211c5aab239640a65091535d97d43d92a52bca435a640892bb" +checksum = "a62486e111e571b1e93b710b61e8f493c0013be39629b714cb166bdb06aa5a8a" dependencies = [ - "proc-macro-hack 0.5.12", - "proc-macro2 1.0.9", + "proc-macro-hack 0.5.15", + "proc-macro2 1.0.10", "quote 1.0.3", - "syn 1.0.16", + "syn 1.0.17", ] [[package]] @@ -3063,9 +3103,9 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aeccfe4d5d8ea175d5f0e4a2ad0637e0f4121d63bd99d356fb1f39ab2e7c6097" dependencies = [ - "proc-macro2 1.0.9", + "proc-macro2 1.0.10", "quote 1.0.3", - "syn 1.0.16", + "syn 1.0.17", ] [[package]] @@ -3079,14 +3119,9 @@ dependencies = [ [[package]] name = "proc-macro-hack" -version = "0.5.12" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f918f2b601f93baa836c1c2945faef682ba5b6d4828ecb45eeb7cc3c71b811b4" -dependencies = [ - "proc-macro2 1.0.9", - "quote 1.0.3", - "syn 1.0.16", -] +checksum = "0d659fe7c6d27f25e9d80a1a094c223f5246f6a6596453e09d7229bf42750b63" [[package]] name = "proc-macro-hack-impl" @@ -3111,9 +3146,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c09721c6781493a2a492a96b5a5bf19b65917fe6728884e7c44dd0c60ca3435" +checksum = "df246d292ff63439fea9bc8c0a270bed0e390d5ebd4db4ba15aba81111b5abe3" dependencies = [ "unicode-xid 0.2.0", ] @@ -3197,9 +3232,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bdc6c187c65bca4260c9011c9e3132efe4909da44726bad24cf7572ae338d7f" dependencies = [ - "proc-macro2 1.0.9", + "proc-macro2 1.0.10", ] +[[package]] +name = "radium" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def50a86306165861203e7f84ecffbbdfdea79f0e51039b33de1e952358c47ac" + [[package]] name = "rand" version = "0.3.23" @@ -3424,9 +3465,9 @@ checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" [[package]] name = "regex" -version = "1.3.5" +version = "1.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8900ebc1363efa7ea1c399ccc32daed870b4002651e0bed86e72d501ebbe0048" +checksum = "7f6946991529684867e47d86474e3a6d0c0ab9b82d5821e314b1ede31fa3a4b3" dependencies = [ "aho-corasick", "memchr", @@ -3451,9 +3492,9 @@ dependencies = [ [[package]] name = "ring" -version = "0.16.11" +version = "0.16.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "741ba1704ae21999c00942f9f5944f801e977f54302af346b596287599ad1862" +checksum = "1ba5a8ec64ee89a76c98c549af81ff14813df09c3e6dc4766c3856da48597a0c" dependencies = [ "cc", "lazy_static", @@ -3606,29 +3647,29 @@ checksum = "a0eddf2e8f50ced781f288c19f18621fa72a3779e3cb58dbf23b07469b0abeb4" [[package]] name = "serde" -version = "1.0.104" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "414115f25f818d7dfccec8ee535d76949ae78584fc4f79a6f45a904bf8ab4449" +checksum = "36df6ac6412072f67cf767ebbde4133a5b2e88e76dc6187fa7104cd16f783399" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.104" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128f9e303a5a29922045a830221b8f78ec74a5f544944f3d5984f8ec3895ef64" +checksum = "9e549e3abf4fb8621bd1609f11dfc9f5e50320802273b12f3811a67e6716ea6c" dependencies = [ - "proc-macro2 1.0.9", + "proc-macro2 1.0.10", "quote 1.0.3", - "syn 1.0.16", + "syn 1.0.17", ] [[package]] name = "serde_json" -version = "1.0.48" +version = "1.0.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9371ade75d4c2d6cb154141b9752cf3781ec9c05e0e5cf35060e1e70ee7b9c25" +checksum = "da07b57ee2623368351e9a0488bb0b261322a15a6e0ae53e243cbdc0f4208da9" dependencies = [ "itoa", "ryu", @@ -3706,7 +3747,7 @@ dependencies = [ [[package]] name = "slog-async" version = "2.3.0" -source = "git+https://github.com/paritytech/slog-async#107848e7ded5e80dc43f6296c2b96039eb92c0a5" +source = "git+https://github.com/paritytech/slog-async#0329dc74feb3afe93d0cd2533a472b7ceab44aaf" dependencies = [ "crossbeam-channel", "slog", @@ -3810,9 +3851,9 @@ source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a1232 dependencies = [ "blake2-rfc", "proc-macro-crate", - "proc-macro2 1.0.9", + "proc-macro2 1.0.10", "quote 1.0.3", - "syn 1.0.16", + "syn 1.0.17", ] [[package]] @@ -4129,9 +4170,9 @@ version = "2.0.0" source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8" dependencies = [ "proc-macro-crate", - "proc-macro2 1.0.9", + "proc-macro2 1.0.10", "quote 1.0.3", - "syn 1.0.16", + "syn 1.0.17", ] [[package]] @@ -4174,11 +4215,11 @@ name = "srml-support-procedural" version = "2.0.0" source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8" dependencies = [ - "proc-macro2 1.0.9", + "proc-macro2 1.0.10", "quote 1.0.3", "sr-api-macros", "srml-support-procedural-tools", - "syn 1.0.16", + "syn 1.0.17", ] [[package]] @@ -4187,10 +4228,10 @@ version = "2.0.0" source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8" dependencies = [ "proc-macro-crate", - "proc-macro2 1.0.9", + "proc-macro2 1.0.10", "quote 1.0.3", "srml-support-procedural-tools-derive", - "syn 1.0.16", + "syn 1.0.17", ] [[package]] @@ -4198,9 +4239,9 @@ name = "srml-support-procedural-tools-derive" version = "2.0.0" source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8" dependencies = [ - "proc-macro2 1.0.9", + "proc-macro2 1.0.10", "quote 1.0.3", - "syn 1.0.16", + "syn 1.0.17", ] [[package]] @@ -4323,9 +4364,9 @@ checksum = "ea692d40005b3ceba90a9fe7a78fa8d4b82b0ce627eebbffc329aab850f3410e" dependencies = [ "heck", "proc-macro-error", - "proc-macro2 1.0.9", + "proc-macro2 1.0.10", "quote 1.0.3", - "syn 1.0.16", + "syn 1.0.17", ] [[package]] @@ -4441,9 +4482,9 @@ version = "2.0.0" source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8" dependencies = [ "proc-macro-crate", - "proc-macro2 1.0.9", + "proc-macro2 1.0.10", "quote 1.0.3", - "syn 1.0.16", + "syn 1.0.17", ] [[package]] @@ -4678,9 +4719,9 @@ name = "substrate-debug-derive" version = "2.0.0" source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8" dependencies = [ - "proc-macro2 1.0.9", + "proc-macro2 1.0.10", "quote 1.0.3", - "syn 1.0.16", + "syn 1.0.17", ] [[package]] @@ -4796,6 +4837,8 @@ dependencies = [ "substrate-common-module", "substrate-membership-module", "substrate-primitives", + "substrate-recurring-reward-module", + "substrate-token-mint-module", ] [[package]] @@ -4870,7 +4913,7 @@ dependencies = [ [[package]] name = "substrate-membership-module" -version = "1.0.0" +version = "1.0.1" dependencies = [ "parity-scale-codec", "serde", @@ -5057,6 +5100,79 @@ dependencies = [ "substrate-debug-derive", ] +[[package]] +name = "substrate-proposals-codex-module" +version = "2.0.0" +dependencies = [ + "num_enum", + "parity-scale-codec", + "serde", + "sr-io", + "sr-primitives", + "sr-staking-primitives", + "sr-std", + "srml-balances", + "srml-staking", + "srml-staking-reward-curve", + "srml-support", + "srml-system", + "srml-timestamp", + "substrate-common-module", + "substrate-content-working-group-module", + "substrate-governance-module", + "substrate-hiring-module", + "substrate-membership-module", + "substrate-primitives", + "substrate-proposals-discussion-module", + "substrate-proposals-engine-module", + "substrate-recurring-reward-module", + "substrate-roles-module", + "substrate-stake-module", + "substrate-token-mint-module", + "substrate-versioned-store", + "substrate-versioned-store-permissions-module", +] + +[[package]] +name = "substrate-proposals-discussion-module" +version = "2.0.0" +dependencies = [ + "num_enum", + "parity-scale-codec", + "serde", + "sr-io", + "sr-primitives", + "sr-std", + "srml-balances", + "srml-support", + "srml-system", + "srml-timestamp", + "substrate-common-module", + "substrate-membership-module", + "substrate-primitives", +] + +[[package]] +name = "substrate-proposals-engine-module" +version = "2.0.0" +dependencies = [ + "mockall", + "num_enum", + "parity-scale-codec", + "serde", + "sr-io", + "sr-primitives", + "sr-std", + "srml-balances", + "srml-support", + "srml-system", + "srml-timestamp", + "substrate-common-module", + "substrate-membership-module", + "substrate-primitives", + "substrate-stake-module", +] + [[package]] name = "substrate-recurring-reward-module" version = "1.0.1" @@ -5080,7 +5196,7 @@ dependencies = [ [[package]] name = "substrate-roles-module" -version = "1.0.0" +version = "1.0.1" dependencies = [ "parity-scale-codec", "serde", @@ -5477,11 +5593,11 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "123bd9499cfb380418d509322d7a6d52e5315f064fe4b3ad18a53d6b92c07859" +checksum = "0df0eb663f387145cab623dea85b09c2c5b4b0aef44e945d928e682fce71bb03" dependencies = [ - "proc-macro2 1.0.9", + "proc-macro2 1.0.10", "quote 1.0.3", "unicode-xid 0.2.0", ] @@ -5492,9 +5608,9 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545" dependencies = [ - "proc-macro2 1.0.9", + "proc-macro2 1.0.10", "quote 1.0.3", - "syn 1.0.16", + "syn 1.0.17", "unicode-xid 0.2.0", ] @@ -6071,9 +6187,9 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" [[package]] name = "wasm-bindgen" -version = "0.2.59" +version = "0.2.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3557c397ab5a8e347d434782bcd31fc1483d927a6826804cec05cc792ee2519d" +checksum = "2cc57ce05287f8376e998cbddfb4c8cb43b84a7ec55cf4551d7c00eef317a47f" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -6081,16 +6197,16 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.59" +version = "0.2.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0da9c9a19850d3af6df1cb9574970b566d617ecfaf36eb0b706b6f3ef9bd2f8" +checksum = "d967d37bf6c16cca2973ca3af071d0a2523392e4a594548155d89a678f4237cd" dependencies = [ "bumpalo", "lazy_static", "log", - "proc-macro2 1.0.9", + "proc-macro2 1.0.10", "quote 1.0.3", - "syn 1.0.16", + "syn 1.0.17", "wasm-bindgen-shared", ] @@ -6109,9 +6225,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.59" +version = "0.2.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f6fde1d36e75a714b5fe0cffbb78978f222ea6baebb726af13c78869fdb4205" +checksum = "8bd151b63e1ea881bb742cd20e1d6127cef28399558f3b5d415289bc41eee3a4" dependencies = [ "quote 1.0.3", "wasm-bindgen-macro-support", @@ -6119,22 +6235,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.59" +version = "0.2.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25bda4168030a6412ea8a047e27238cadf56f0e53516e1e83fec0a8b7c786f6d" +checksum = "d68a5b36eef1be7868f668632863292e37739656a80fc4b9acec7b0bd35a4931" dependencies = [ - "proc-macro2 1.0.9", + "proc-macro2 1.0.10", "quote 1.0.3", - "syn 1.0.16", + "syn 1.0.17", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.59" +version = "0.2.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc9f36ad51f25b0219a3d4d13b90eb44cd075dff8b6280cca015775d7acaddd8" +checksum = "daf76fe7d25ac79748a37538b7daeed1c7a6867c92d3245c12c6222e4a20d639" [[package]] name = "wasm-timer" @@ -6175,9 +6291,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "721c6263e2c66fd44501cc5efbfa2b7dfa775d13e4ea38c46299646ed1f9c70a" +checksum = "2d6f51648d8c56c366144378a33290049eafdd784071077f6fe37dae64c1c4cb" dependencies = [ "js-sys", "wasm-bindgen", @@ -6251,9 +6367,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ccfbf554c6ad11084fb7517daca16cfdcaccbdadba4fc336f032a8b12c2ad80" +checksum = "fa515c5163a99cc82bab70fd3bfdd36d827be85de63737b40fcef2ce084a436e" dependencies = [ "winapi 0.3.8", ] @@ -6353,8 +6469,8 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de251eec69fc7c1bc3923403d18ececb929380e016afe103da75f396704f8ca2" dependencies = [ - "proc-macro2 1.0.9", + "proc-macro2 1.0.10", "quote 1.0.3", - "syn 1.0.16", + "syn 1.0.17", "synstructure", ] diff --git a/Cargo.toml b/Cargo.toml index f2195200f3..9651f9333e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,9 @@ [workspace] members = [ "runtime", + "runtime-modules/proposals/engine", + "runtime-modules/proposals/codex", + "runtime-modules/proposals/discussion", "runtime-modules/common", "runtime-modules/content-working-group", "runtime-modules/forum", diff --git a/README.md b/README.md index 7d42fb45a0..bb97452017 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Joystream [![Build Status](https://travis-ci.org/Joystream/substrate-runtime-joystream.svg?branch=master)](https://travis-ci.org/Joystream/substrate-runtime-joystream) +# Joystream [![Build Status](https://travis-ci.org/Joystream/joystream.svg?branch=master)](https://travis-ci.org/Joystream/joystream) This is the main code reposity for all joystream software. It will house the substrate chain project, the full node and runtime and all reusable substrate runtime modules that make up the joystream runtime. In addition to all front-end apps and infrastructure servers necessary for operating the network. @@ -6,9 +6,9 @@ The repository is currently just a cargo workspace, but eventually will also con ## Build Status -Development [![Development Branch Build Status](https://travis-ci.org/Joystream/substrate-runtime-joystream.svg?branch=development)](https://travis-ci.org/Joystream/substrate-runtime-joystream) +Development [![Development Branch Build Status](https://travis-ci.org/Joystream/joystream.svg?branch=development)](https://travis-ci.org/Joystream/joystream) -More detailed build history on [Travis CI](https://travis-ci.org/github/Joystream/substrate-runtime-joystream/builds) +More detailed build history on [Travis CI](https://travis-ci.org/github/Joystream/joystream/builds) ## Overview @@ -26,7 +26,7 @@ To setup a full node and validator review the [advanced guide from the helpdesk] ### Pre-built Binaries -The latest pre-built binaries can be downloads from the [releases](https://github.com/Joystream/substrate-runtime-joystream/releases) page. +The latest pre-built binaries can be downloaded from the [releases](https://github.com/Joystream/joystream/releases) page. ### Building from source @@ -34,9 +34,9 @@ The latest pre-built binaries can be downloads from the [releases](https://githu Clone the repository and install build tools: ```bash -git clone https://github.com/Joystream/substrate-runtime-joystream.git +git clone https://github.com/Joystream/joystream.git -cd substrate-runtime-joystream/ +cd joystream/ ./setup.sh ``` @@ -55,7 +55,7 @@ Run the node and connect to the public testnet. cargo run --release -- --chain ./rome-tesnet.json ``` -The `rome-testnet.json` chain file can be ontained from the [release page](https://github.com/Joystream/substrate-runtime-joystream/releases/tag/v6.8.0) +The `rome-testnet.json` chain file can be obtained from the [releases page](https://github.com/Joystream/joystream/releases/tag/v6.8.0) ### Installing a release build @@ -85,6 +85,14 @@ This will build and run a fresh new local development chain purging existing one cargo test ``` +### API integration tests + +```bash +./scripts/run-dev-chain.sh +yarn test +``` + +To run the integration tests with a different chain, you can omit step running the local development chain and set the node URL using `NODE_URL` environment variable. ## Joystream Runtime @@ -111,7 +119,7 @@ Deploying the compiled runtime on a live system can be done in one of two ways: ### Versioning the runtime Versioning of the runtime is set in `runtime/src/lib.rs` -For detailed information about how to set correct version numbers when developing a new runtime, [see this](https://github.com/Joystream/substrate-runtime-joystream/issues/1) +For detailed information about how to set correct version numbers when developing a new runtime, [see this](https://github.com/Joystream/joystream/issues/1) ## Coding style diff --git a/cli/.editorconfig b/cli/.editorconfig new file mode 100644 index 0000000000..c3f635323a --- /dev/null +++ b/cli/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/cli/.eslintignore b/cli/.eslintignore new file mode 100644 index 0000000000..502167fa0b --- /dev/null +++ b/cli/.eslintignore @@ -0,0 +1 @@ +/lib diff --git a/cli/.eslintrc b/cli/.eslintrc new file mode 100644 index 0000000000..7b846193cc --- /dev/null +++ b/cli/.eslintrc @@ -0,0 +1,6 @@ +{ + "extends": [ + "oclif", + "oclif-typescript" + ] +} diff --git a/cli/.gitignore b/cli/.gitignore new file mode 100644 index 0000000000..35b0ba96ed --- /dev/null +++ b/cli/.gitignore @@ -0,0 +1,8 @@ +*-debug.log +*-error.log +/.nyc_output +/dist +/lib +/tmp +/yarn.lock +node_modules diff --git a/cli/README.md b/cli/README.md new file mode 100644 index 0000000000..591a949854 --- /dev/null +++ b/cli/README.md @@ -0,0 +1,263 @@ +joystream-cli +============= + +Command Line Interface for Joystream community and governance activities + +[![oclif](https://img.shields.io/badge/cli-oclif-brightgreen.svg)](https://oclif.io) +[![Version](https://img.shields.io/npm/v/joystream-cli.svg)](https://npmjs.org/package/joystream-cli) +[![Downloads/week](https://img.shields.io/npm/dw/joystream-cli.svg)](https://npmjs.org/package/joystream-cli) +[![License](https://img.shields.io/npm/l/joystream-cli.svg)](https://github.com/Joystream/cli/blob/master/package.json) + + +* [Development](#development) +* [Usage](#usage) +* [Commands](#commands) + + +# Development + +To run a command in developemnt environment (without installing the package): + +1. Navigate into the CLI root directory +1. Either execute any command like this: + + ``` + $ ./bin/run COMMAND + ``` + + Or use: + + ``` + $ npm link + ``` + + And then execute any command like this: + + ``` + $ joystream-cli COMMAND + ``` + + +# Usage + +```sh-session +$ npm install -g joystream-cli +$ joystream-cli COMMAND +running command... +$ joystream-cli (-v|--version|version) +joystream-cli/0.0.0 linux-x64 node-v13.12.0 +$ joystream-cli --help [COMMAND] +USAGE + $ joystream-cli COMMAND +... +``` + +# Commands + +* [`joystream-cli account:choose`](#joystream-cli-accountchoose) +* [`joystream-cli account:create NAME`](#joystream-cli-accountcreate-name) +* [`joystream-cli account:current`](#joystream-cli-accountcurrent) +* [`joystream-cli account:export PATH`](#joystream-cli-accountexport-path) +* [`joystream-cli account:forget`](#joystream-cli-accountforget) +* [`joystream-cli account:import BACKUPFILEPATH`](#joystream-cli-accountimport-backupfilepath) +* [`joystream-cli account:transferTokens RECIPIENT AMOUNT`](#joystream-cli-accounttransfertokens-recipient-amount) +* [`joystream-cli api:getUri`](#joystream-cli-apigeturi) +* [`joystream-cli api:inspect`](#joystream-cli-apiinspect) +* [`joystream-cli api:setUri URI`](#joystream-cli-apiseturi-uri) +* [`joystream-cli council:info`](#joystream-cli-councilinfo) +* [`joystream-cli help [COMMAND]`](#joystream-cli-help-command) + +## `joystream-cli account:choose` + +Choose default account to use in the CLI + +``` +USAGE + $ joystream-cli account:choose +``` + +_See code: [src/commands/account/choose.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/account/choose.ts)_ + +## `joystream-cli account:create NAME` + +Create new account + +``` +USAGE + $ joystream-cli account:create NAME + +ARGUMENTS + NAME Account name +``` + +_See code: [src/commands/account/create.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/account/create.ts)_ + +## `joystream-cli account:current` + +Display information about currently choosen default account + +``` +USAGE + $ joystream-cli account:current + +ALIASES + $ joystream-cli account:info + $ joystream-cli account:default +``` + +_See code: [src/commands/account/current.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/account/current.ts)_ + +## `joystream-cli account:export PATH` + +Export account(s) to given location + +``` +USAGE + $ joystream-cli account:export PATH + +ARGUMENTS + PATH Path where the exported files should be placed + +OPTIONS + -a, --all If provided, exports all existing accounts into "exported_accounts" folder inside given path +``` + +_See code: [src/commands/account/export.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/account/export.ts)_ + +## `joystream-cli account:forget` + +Forget (remove) account from the list of available accounts + +``` +USAGE + $ joystream-cli account:forget +``` + +_See code: [src/commands/account/forget.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/account/forget.ts)_ + +## `joystream-cli account:import BACKUPFILEPATH` + +Import account using JSON backup file + +``` +USAGE + $ joystream-cli account:import BACKUPFILEPATH + +ARGUMENTS + BACKUPFILEPATH Path to account backup JSON file +``` + +_See code: [src/commands/account/import.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/account/import.ts)_ + +## `joystream-cli account:transferTokens RECIPIENT AMOUNT` + +Transfer tokens from currently choosen account + +``` +USAGE + $ joystream-cli account:transferTokens RECIPIENT AMOUNT + +ARGUMENTS + RECIPIENT Address of the transfer recipient + AMOUNT Amount of tokens to transfer +``` + +_See code: [src/commands/account/transferTokens.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/account/transferTokens.ts)_ + +## `joystream-cli api:getUri` + +Get current api WS provider uri + +``` +USAGE + $ joystream-cli api:getUri +``` + +_See code: [src/commands/api/getUri.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/api/getUri.ts)_ + +## `joystream-cli api:inspect` + +Lists available node API modules/methods and/or their description(s), or calls one of the API methods (depending on provided arguments and flags) + +``` +USAGE + $ joystream-cli api:inspect + +OPTIONS + -M, --module=module + Specifies the api module, ie. "system", "staking" etc. + If no "--method" flag is provided then all methods in that module will be listed along with the descriptions. + + -a, --callArgs=callArgs + Specifies the arguments to use when calling a method. Multiple arguments can be separated with a comma, ie. + "-a=arg1,arg2". + You can omit this flag even if the method requires some aguments. + In that case you will be promted to provide value for each required argument. + Ommiting this flag is recommended when input parameters are of more complex types (and it's hard to specify them as + just simple comma-separated strings) + + -e, --exec + Provide this flag if you want to execute the actual call, instead of displaying the method description (which is + default) + + -m, --method=method + Specifies the api method to call/describe. + + -t, --type=type + Specifies the type/category of the inspected request (ie. "query", "consts" etc.). + If no "--module" flag is provided then all available modules in that type will be listed. + If this flag is not provided then all available types will be listed. + +EXAMPLES + $ api:inspect + $ api:inspect -t=query + $ api:inspect -t=query -M=members + $ api:inspect -t=query -M=members -m=memberProfile + $ api:inspect -t=query -M=members -m=memberProfile -e + $ api:inspect -t=query -M=members -m=memberProfile -e -a=1 +``` + +_See code: [src/commands/api/inspect.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/api/inspect.ts)_ + +## `joystream-cli api:setUri URI` + +Set api WS provider uri + +``` +USAGE + $ joystream-cli api:setUri URI + +ARGUMENTS + URI Uri of the node api WS provider +``` + +_See code: [src/commands/api/setUri.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/api/setUri.ts)_ + +## `joystream-cli council:info` + +Get current council and council elections information + +``` +USAGE + $ joystream-cli council:info +``` + +_See code: [src/commands/council/info.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/council/info.ts)_ + +## `joystream-cli help [COMMAND]` + +display help for joystream-cli + +``` +USAGE + $ joystream-cli help [COMMAND] + +ARGUMENTS + COMMAND command to show help for + +OPTIONS + --all see all commands in CLI +``` + +_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v2.2.3/src/commands/help.ts)_ + diff --git a/cli/bin/run b/cli/bin/run new file mode 100755 index 0000000000..30b14e1773 --- /dev/null +++ b/cli/bin/run @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +require('@oclif/command').run() +.then(require('@oclif/command/flush')) +.catch(require('@oclif/errors/handle')) diff --git a/cli/bin/run.cmd b/cli/bin/run.cmd new file mode 100644 index 0000000000..968fc30758 --- /dev/null +++ b/cli/bin/run.cmd @@ -0,0 +1,3 @@ +@echo off + +node "%~dp0\run" %* diff --git a/cli/package-lock.json b/cli/package-lock.json new file mode 100644 index 0000000000..5ff94a14a9 --- /dev/null +++ b/cli/package-lock.json @@ -0,0 +1,4671 @@ +{ + "name": "joystream-cli", + "version": "0.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/generator": { + "version": "7.9.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.9.4.tgz", + "integrity": "sha512-rjP8ahaDy/ouhrvCoU1E5mqaitWrxwuNGU+dy1EpaoK48jZay4MdkskKGIMHLZNewg8sAsqpGSREJwP0zH3YQA==", + "dev": true, + "requires": { + "@babel/types": "^7.9.0", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", + "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", + "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz", + "integrity": "sha512-6G8bQKjOh+of4PV/ThDm/rRqlU7+IGoJuofpagU5GlEl29Vv0RGqqt86ZGRV8ZuSOY3o+8yXl5y782SMcG7SHw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", + "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.9.0", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.9.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.4.tgz", + "integrity": "sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==", + "dev": true + }, + "@babel/runtime": { + "version": "7.9.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.2.tgz", + "integrity": "sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "@babel/template": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", + "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6" + } + }, + "@babel/traverse": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.9.0.tgz", + "integrity": "sha512-jAZQj0+kn4WTHO5dUZkZKhbFrqZE7K5LAQ5JysMnmvGij+wOdr+8lWqPeW0BcF4wFwrEXXtdGO7wcV6YPJcf3w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.9.0", + "@babel/helper-function-name": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/parser": "^7.9.0", + "@babel/types": "^7.9.0", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + } + }, + "@babel/types": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.0.tgz", + "integrity": "sha512-BS9JKfXkzzJl8RluW4JGknzpiUV7ZrvTayM6yfqLTVBEnFtyowVIOu6rqxRd5cVO6yGoWf4T8u8dgK9oB+GCng==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.9.0", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, + "@joystream/types": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@joystream/types/-/types-0.6.0.tgz", + "integrity": "sha512-b+6U36GHJLlBPxVqMVQRTZzVxu7BGsjqlC/XJfl/vdx8TOy3P8TIB/3olLU64EPB3cVNadg2p9jqYSsvh9XVAQ==", + "requires": { + "@polkadot/types": "^0.96.1", + "@types/vfile": "^4.0.0", + "ajv": "^6.11.0" + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", + "integrity": "sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.3", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz", + "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz", + "integrity": "sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.3", + "fastq": "^1.6.0" + } + }, + "@oclif/command": { + "version": "1.5.19", + "resolved": "https://registry.npmjs.org/@oclif/command/-/command-1.5.19.tgz", + "integrity": "sha512-6+iaCMh/JXJaB2QWikqvGE9//wLEVYYwZd5sud8aLoLKog1Q75naZh2vlGVtg5Mq/NqpqGQvdIjJb3Bm+64AUQ==", + "requires": { + "@oclif/config": "^1", + "@oclif/errors": "^1.2.2", + "@oclif/parser": "^3.8.3", + "@oclif/plugin-help": "^2", + "debug": "^4.1.1", + "semver": "^5.6.0" + } + }, + "@oclif/config": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@oclif/config/-/config-1.14.0.tgz", + "integrity": "sha512-KsOP/mx9lzTah+EtGqLUXN3PDL0J3zb9/dTneFyiUK2K6T7vFEGhV6OasmqTh4uMZHGYTGrNPV8x/Yw6qZNL6A==", + "requires": { + "@oclif/errors": "^1.0.0", + "@oclif/parser": "^3.8.0", + "debug": "^4.1.1", + "tslib": "^1.9.3" + } + }, + "@oclif/dev-cli": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/@oclif/dev-cli/-/dev-cli-1.22.2.tgz", + "integrity": "sha512-c7633R37RxrQIpwqPKxjNRm6/jb1yuG8fd16hmNz9Nw+/MUhEtQtKHSCe9ScH8n5M06l6LEo4ldk9LEGtpaWwA==", + "dev": true, + "requires": { + "@oclif/command": "^1.5.13", + "@oclif/config": "^1.12.12", + "@oclif/errors": "^1.2.2", + "@oclif/plugin-help": "^2.1.6", + "cli-ux": "^5.2.1", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "github-slugger": "^1.2.1", + "lodash": "^4.17.11", + "normalize-package-data": "^2.5.0", + "qqjs": "^0.3.10", + "tslib": "^1.9.3" + } + }, + "@oclif/errors": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@oclif/errors/-/errors-1.2.2.tgz", + "integrity": "sha512-Eq8BFuJUQcbAPVofDxwdE0bL14inIiwt5EaKRVY9ZDIG11jwdXZqiQEECJx0VfnLyUZdYfRd/znDI/MytdJoKg==", + "requires": { + "clean-stack": "^1.3.0", + "fs-extra": "^7.0.0", + "indent-string": "^3.2.0", + "strip-ansi": "^5.0.0", + "wrap-ansi": "^4.0.0" + } + }, + "@oclif/linewrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@oclif/linewrap/-/linewrap-1.0.0.tgz", + "integrity": "sha512-Ups2dShK52xXa8w6iBWLgcjPJWjais6KPJQq3gQ/88AY6BXoTX+MIGFPrWQO1KLMiQfoTpcLnUwloN4brrVUHw==" + }, + "@oclif/parser": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/@oclif/parser/-/parser-3.8.4.tgz", + "integrity": "sha512-cyP1at3l42kQHZtqDS3KfTeyMvxITGwXwH1qk9ktBYvqgMp5h4vHT+cOD74ld3RqJUOZY/+Zi9lb4Tbza3BtuA==", + "requires": { + "@oclif/linewrap": "^1.0.0", + "chalk": "^2.4.2", + "tslib": "^1.9.3" + } + }, + "@oclif/plugin-help": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@oclif/plugin-help/-/plugin-help-2.2.3.tgz", + "integrity": "sha512-bGHUdo5e7DjPJ0vTeRBMIrfqTRDBfyR5w0MP41u0n3r7YG5p14lvMmiCXxi6WDaP2Hw5nqx3PnkAIntCKZZN7g==", + "requires": { + "@oclif/command": "^1.5.13", + "chalk": "^2.4.1", + "indent-string": "^4.0.0", + "lodash.template": "^4.4.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0", + "widest-line": "^2.0.1", + "wrap-ansi": "^4.0.0" + }, + "dependencies": { + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==" + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + } + } + }, + "@oclif/screen": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@oclif/screen/-/screen-1.0.4.tgz", + "integrity": "sha512-60CHpq+eqnTxLZQ4PGHYNwUX572hgpMHGPtTWMjdTMsAvlm69lZV/4ly6O3sAYkomo4NggGcomrDpBe34rxUqw==" + }, + "@oclif/test": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@oclif/test/-/test-1.2.5.tgz", + "integrity": "sha512-8Y+Ix4A3Zhm87aL0ldVonDK7vFWyLfnFHzP3goYaLyIeh/60KL37lMxfmbp/kBN6/Y0Ru17iR1pdDi/hTDClLQ==", + "dev": true, + "requires": { + "fancy-test": "^1.4.3" + } + }, + "@polkadot/api": { + "version": "0.96.1", + "resolved": "https://registry.npmjs.org/@polkadot/api/-/api-0.96.1.tgz", + "integrity": "sha512-FeYyMfJL0NACJBIuG7C7mp7f9J/WOGUERF/hUP3RlIz4Ld2X0vRjEoOgiG0VIS89I4K31XaNmSjIchH244WtHg==", + "requires": { + "@babel/runtime": "^7.7.1", + "@polkadot/api-derive": "^0.96.1", + "@polkadot/api-metadata": "^0.96.1", + "@polkadot/keyring": "^1.7.0-beta.5", + "@polkadot/rpc-core": "^0.96.1", + "@polkadot/rpc-provider": "^0.96.1", + "@polkadot/types": "^0.96.1", + "@polkadot/util-crypto": "^1.7.0-beta.5" + } + }, + "@polkadot/api-derive": { + "version": "0.96.1", + "resolved": "https://registry.npmjs.org/@polkadot/api-derive/-/api-derive-0.96.1.tgz", + "integrity": "sha512-PGWdUvlD2acUKOgaJcYWuMTfSuQKUpwgwjer5SomHLFn4ZPOz8iDa7mYtrgmxQctRv1zsuck2X01uhxdEdtJZw==", + "requires": { + "@babel/runtime": "^7.7.1", + "@polkadot/api": "^0.96.1", + "@polkadot/types": "^0.96.1" + } + }, + "@polkadot/api-metadata": { + "version": "0.96.1", + "resolved": "https://registry.npmjs.org/@polkadot/api-metadata/-/api-metadata-0.96.1.tgz", + "integrity": "sha512-I9F3twpSCgx4ny25a3moGrhf2vHKFnjooO3W9NaAxIj/us4q4Gqo4+czQajqt8vaJqrNMq/PE7lzVz1NhYDrZQ==", + "requires": { + "@babel/runtime": "^7.7.1", + "@polkadot/types": "^0.96.1", + "@polkadot/util": "^1.7.0-beta.5", + "@polkadot/util-crypto": "^1.7.0-beta.5" + } + }, + "@polkadot/jsonrpc": { + "version": "0.96.1", + "resolved": "https://registry.npmjs.org/@polkadot/jsonrpc/-/jsonrpc-0.96.1.tgz", + "integrity": "sha512-UHpcUGIvkG4dJ5gUhDyfJ1xfr/VcBlJ5lIlGamGsnNacMuIVmmEsftgxtPlJLWHuoA1EBEHY4cbPSv9CUJ0IFw==", + "requires": { + "@babel/runtime": "^7.7.1" + } + }, + "@polkadot/keyring": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@polkadot/keyring/-/keyring-1.8.1.tgz", + "integrity": "sha512-KeDbfP8biY3bXEhMv1ANp9d3kCuXj2oxseuDK0jvxRo7CehVME9UwAMGQK3Y9NCUuYWd+xTO2To0ZOqR7hdmuQ==", + "requires": { + "@babel/runtime": "^7.7.7", + "@polkadot/util": "^1.8.1", + "@polkadot/util-crypto": "^1.8.1" + } + }, + "@polkadot/rpc-core": { + "version": "0.96.1", + "resolved": "https://registry.npmjs.org/@polkadot/rpc-core/-/rpc-core-0.96.1.tgz", + "integrity": "sha512-ygSaJpz/QPEq1p35wYRzONuP2PCtkAJ9eS8swQqUIezTo2ZPUOyBhmnJ3nxj11R8YnQClq4Id0QdsJmH1ClYgw==", + "requires": { + "@babel/runtime": "^7.7.1", + "@polkadot/jsonrpc": "^0.96.1", + "@polkadot/rpc-provider": "^0.96.1", + "@polkadot/types": "^0.96.1", + "@polkadot/util": "^1.7.0-beta.5", + "rxjs": "^6.5.3" + } + }, + "@polkadot/rpc-provider": { + "version": "0.96.1", + "resolved": "https://registry.npmjs.org/@polkadot/rpc-provider/-/rpc-provider-0.96.1.tgz", + "integrity": "sha512-cUhp8FMCYHrXrBTbxZrok/hPIgtOXEUhIXn5/zrffg1Qpbzju/y/bXx7c1Kxl1JF7Bg0vSBRZEGJTn/x0irWRQ==", + "requires": { + "@babel/runtime": "^7.7.1", + "@polkadot/api-metadata": "^0.96.1", + "@polkadot/util": "^1.7.0-beta.5", + "@polkadot/util-crypto": "^1.7.0-beta.5", + "eventemitter3": "^4.0.0", + "isomorphic-fetch": "^2.2.1", + "websocket": "^1.0.30" + } + }, + "@polkadot/ts": { + "version": "0.1.91", + "resolved": "https://registry.npmjs.org/@polkadot/ts/-/ts-0.1.91.tgz", + "integrity": "sha512-UB8zOFZXb/ih03izzAQ1r1DRpiUXBofxAlXjcx4530jopfiNsiU1LZ2J/uS3dVV1QXaGRhkgm8SIJDLsSMRYIQ==", + "dev": true, + "requires": { + "@types/chrome": "^0.0.92" + } + }, + "@polkadot/types": { + "version": "0.96.1", + "resolved": "https://registry.npmjs.org/@polkadot/types/-/types-0.96.1.tgz", + "integrity": "sha512-b8AZBNmMjB0+34Oxue3AYc0gIjDHYCdVGtDpel0omHkLMcEquSvrCniLm+p7g4cfArICiZPFmS9In/OWWdRUVA==", + "requires": { + "@babel/runtime": "^7.7.1", + "@polkadot/util": "^1.7.0-beta.5", + "@polkadot/util-crypto": "^1.7.0-beta.5", + "@types/memoizee": "^0.4.3", + "memoizee": "^0.4.14" + } + }, + "@polkadot/util": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@polkadot/util/-/util-1.8.1.tgz", + "integrity": "sha512-sFpr+JLCG9d+epjboXsmJ1qcKa96r8ZYzXmVo8+aPzI/9jKKyez6Unox/dnfnpKppZB2nJuLcsxQm6nocp2Caw==", + "requires": { + "@babel/runtime": "^7.7.7", + "@types/bn.js": "^4.11.6", + "bn.js": "^4.11.8", + "camelcase": "^5.3.1", + "chalk": "^3.0.0", + "ip-regex": "^4.1.0", + "moment": "^2.24.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@polkadot/util-crypto": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@polkadot/util-crypto/-/util-crypto-1.8.1.tgz", + "integrity": "sha512-ypUs10hV1HPvYc0ZsEu+LTGSEh0rkr0as/FUh7+Z9v3Bxibn3aO+EOxJPQuDbZZ59FSMRmc9SeOSa0wn9ddrnw==", + "requires": { + "@babel/runtime": "^7.7.7", + "@polkadot/util": "^1.8.1", + "@polkadot/wasm-crypto": "^0.14.1", + "@types/bip39": "^2.4.2", + "@types/bs58": "^4.0.0", + "@types/pbkdf2": "^3.0.0", + "@types/secp256k1": "^3.5.0", + "@types/xxhashjs": "^0.2.1", + "base-x": "3.0.5", + "bip39": "^2.5.0", + "blakejs": "^1.1.0", + "bs58": "^4.0.1", + "js-sha3": "^0.8.0", + "secp256k1": "^3.8.0", + "tweetnacl": "^1.0.1", + "xxhashjs": "^0.2.2" + } + }, + "@polkadot/wasm-crypto": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-crypto/-/wasm-crypto-0.14.1.tgz", + "integrity": "sha512-Xng7L2Z8TNZa/5g6pot4O06Jf0ohQRZdvfl8eQL+E/L2mcqJYC1IjkMxJBSBuQEV7hisWzh9mHOy5WCcgPk29Q==" + }, + "@types/bip39": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@types/bip39/-/bip39-2.4.2.tgz", + "integrity": "sha512-Vo9lqOIRq8uoIzEVrV87ZvcIM0PN9t0K3oYZ/CS61fIYKCBdOIM7mlWzXuRvSXrDtVa1uUO2w1cdfufxTC0bzg==", + "requires": { + "@types/node": "*" + } + }, + "@types/bn.js": { + "version": "4.11.6", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.6.tgz", + "integrity": "sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==", + "requires": { + "@types/node": "*" + } + }, + "@types/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-yfAgiWgVLjFCmRv8zAcOIHywYATEwiTVccTLnRp6UxTNavT55M9d/uhK3T03St/+8/z/wW+CRjGKUNmEqoHHCA==", + "requires": { + "base-x": "^3.0.6" + }, + "dependencies": { + "base-x": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.8.tgz", + "integrity": "sha512-Rl/1AWP4J/zRrk54hhlxH4drNxPJXYUaKffODVI53/dAsV4t9fBxyxYKAVPU1XBHxYwOWP9h9H0hM2MVw4YfJA==", + "requires": { + "safe-buffer": "^5.0.1" + } + } + } + }, + "@types/chai": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.11.tgz", + "integrity": "sha512-t7uW6eFafjO+qJ3BIV2gGUyZs27egcNRkUdalkud+Qa3+kg//f129iuOFivHDXQ+vnU3fDXuwgv0cqMCbcE8sw==", + "dev": true + }, + "@types/chrome": { + "version": "0.0.92", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.92.tgz", + "integrity": "sha512-bTv1EljZ03bexRJwS5FwSZmrudtw+QNbzwUY2sxVtXWgtxk752G4I2owhZ+Mlzbf3VKvG+rBYSw/FnvzuZ4xOA==", + "dev": true, + "requires": { + "@types/filesystem": "*" + } + }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" + }, + "@types/eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", + "dev": true + }, + "@types/events": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", + "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", + "dev": true + }, + "@types/filesystem": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.29.tgz", + "integrity": "sha512-85/1KfRedmfPGsbK8YzeaQUyV1FQAvMPMTuWFQ5EkLd2w7szhNO96bk3Rh/SKmOfd9co2rCLf0Voy4o7ECBOvw==", + "dev": true, + "requires": { + "@types/filewriter": "*" + } + }, + "@types/filewriter": { + "version": "0.0.28", + "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.28.tgz", + "integrity": "sha1-wFTor02d11205jq8dviFFocU1LM=", + "dev": true + }, + "@types/glob": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", + "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", + "dev": true, + "requires": { + "@types/events": "*", + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@types/inquirer": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-6.5.0.tgz", + "integrity": "sha512-rjaYQ9b9y/VFGOpqBEXRavc3jh0a+e6evAbI31tMda8VlPaSy0AZJfXsvmIe3wklc7W6C3zCSfleuMXR7NOyXw==", + "requires": { + "@types/through": "*", + "rxjs": "^6.4.0" + } + }, + "@types/json-schema": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz", + "integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==", + "dev": true + }, + "@types/lodash": { + "version": "4.14.149", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.149.tgz", + "integrity": "sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ==", + "dev": true + }, + "@types/memoizee": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@types/memoizee/-/memoizee-0.4.3.tgz", + "integrity": "sha512-N6QT0c9ZbEKl33n1wyoTxZs4cpN+YXjs0Aqy5Qim8ipd9PBNIPqOh/p5Pixc4601tqr5GErsdxUbfqviDfubNw==" + }, + "@types/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", + "dev": true + }, + "@types/mocha": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", + "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", + "dev": true + }, + "@types/node": { + "version": "10.17.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.18.tgz", + "integrity": "sha512-DQ2hl/Jl3g33KuAUOcMrcAOtsbzb+y/ufakzAdeK9z/H/xsvkpbETZZbPNMIiQuk24f5ZRMCcZIViAwyFIiKmg==" + }, + "@types/pbkdf2": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/pbkdf2/-/pbkdf2-3.0.0.tgz", + "integrity": "sha512-6J6MHaAlBJC/eVMy9jOwj9oHaprfutukfW/Dyt0NEnpQ/6HN6YQrpvLwzWdWDeWZIdenjGHlbYDzyEODO5Z+2Q==", + "requires": { + "@types/node": "*" + } + }, + "@types/proper-lockfile": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@types/proper-lockfile/-/proper-lockfile-4.1.1.tgz", + "integrity": "sha512-HAjVfDa73pFgivViHyDu8HHHcds+W4MgOuZZAdyFJrHS8ngtCXmhl4hc2YXqSOwO6Bsa+iF2Sgxb2+gv874VOQ==", + "requires": { + "@types/retry": "*" + } + }, + "@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" + }, + "@types/secp256k1": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@types/secp256k1/-/secp256k1-3.5.3.tgz", + "integrity": "sha512-NGcsPDR0P+Q71O63e2ayshmiZGAwCOa/cLJzOIuhOiDvmbvrCIiVtEpqdCJGogG92Bnr6tw/6lqVBsRMEl15OQ==", + "requires": { + "@types/node": "*" + } + }, + "@types/sinon": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-9.0.0.tgz", + "integrity": "sha512-v2TkYHkts4VXshMkcmot/H+ERZ2SevKa10saGaJPGCJ8vh3lKrC4u663zYEeRZxep+VbG6YRDtQ6gVqw9dYzPA==", + "dev": true, + "requires": { + "@types/sinonjs__fake-timers": "*" + } + }, + "@types/sinonjs__fake-timers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz", + "integrity": "sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA==", + "dev": true + }, + "@types/slug": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@types/slug/-/slug-0.9.1.tgz", + "integrity": "sha512-zR/u8WFQ4/6uCIikjI00a5uB084XjgEGNRAvM4a1BL39Bw9yEiDQFiPS2DgJ8lPDkR2Qd/vZ26dCR9XqlKbDqQ==" + }, + "@types/through": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.30.tgz", + "integrity": "sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg==", + "requires": { + "@types/node": "*" + } + }, + "@types/unist": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz", + "integrity": "sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==" + }, + "@types/vfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/vfile/-/vfile-4.0.0.tgz", + "integrity": "sha512-eleP0/Cz8uVWxARDLi3Axq2+fDdN4ibAXoC6Pv8p6s7znXaUL7XvhgeIhjCiNMnvlLNP+tmCLd+RuCryGgmtEg==", + "requires": { + "vfile": "*" + } + }, + "@types/xxhashjs": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@types/xxhashjs/-/xxhashjs-0.2.1.tgz", + "integrity": "sha512-Akm13wkwsQylVnBokl/aiKLtSxndSjfgTjdvmSxXNehYy4NymwdfdJHwGhpV54wcYfmOByOp3ak8AGdUlvp0sA==", + "requires": { + "@types/node": "*" + } + }, + "@typescript-eslint/eslint-plugin": { + "version": "2.26.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.26.0.tgz", + "integrity": "sha512-4yUnLv40bzfzsXcTAtZyTjbiGUXMrcIJcIMioI22tSOyAxpdXiZ4r7YQUU8Jj6XXrLz9d5aMHPQf5JFR7h27Nw==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "2.26.0", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^3.0.0", + "tsutils": "^3.17.1" + }, + "dependencies": { + "regexpp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", + "dev": true + } + } + }, + "@typescript-eslint/experimental-utils": { + "version": "2.26.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.26.0.tgz", + "integrity": "sha512-RELVoH5EYd+JlGprEyojUv9HeKcZqF7nZUGSblyAw1FwOGNnmQIU8kxJ69fttQvEwCsX5D6ECJT8GTozxrDKVQ==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/typescript-estree": "2.26.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + }, + "dependencies": { + "eslint-scope": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", + "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.0.0.tgz", + "integrity": "sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + } + } + }, + "@typescript-eslint/parser": { + "version": "2.26.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.26.0.tgz", + "integrity": "sha512-+Xj5fucDtdKEVGSh9353wcnseMRkPpEAOY96EEenN7kJVrLqy/EVwtIh3mxcUz8lsFXW1mT5nN5vvEam/a5HiQ==", + "dev": true, + "requires": { + "@types/eslint-visitor-keys": "^1.0.0", + "@typescript-eslint/experimental-utils": "2.26.0", + "@typescript-eslint/typescript-estree": "2.26.0", + "eslint-visitor-keys": "^1.1.0" + } + }, + "@typescript-eslint/typescript-estree": { + "version": "2.26.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.26.0.tgz", + "integrity": "sha512-3x4SyZCLB4zsKsjuhxDLeVJN6W29VwBnYpCsZ7vIdPel9ZqLfIZJgJXO47MNUkurGpQuIBALdPQKtsSnWpE1Yg==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "eslint-visitor-keys": "^1.1.0", + "glob": "^7.1.6", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^6.3.0", + "tsutils": "^3.17.1" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "acorn": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", + "dev": true + }, + "acorn-jsx": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", + "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", + "dev": true + }, + "ajv": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", + "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==" + }, + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "ansicolors": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", + "integrity": "sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk=" + }, + "append-transform": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", + "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==", + "dev": true, + "requires": { + "default-require-extensions": "^2.0.0" + } + }, + "archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "dev": true + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base-x": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.5.tgz", + "integrity": "sha512-C3picSgzPSLE+jW3tcBzJoGwitOtazb5B+5YmAxZm2ybmTi9LNgAtDO/jjVEBZwHoXmDBZ9m/IELj3elJVRBcA==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", + "dev": true + }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bip39": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/bip39/-/bip39-2.6.0.tgz", + "integrity": "sha512-RrnQRG2EgEoqO24ea+Q/fftuPUZLmrEM3qNhhGsA3PbaXaCW791LTzPuVyx/VprXQcTbPJ3K3UeTna8ZnVl2sg==", + "requires": { + "create-hash": "^1.1.0", + "pbkdf2": "^3.0.9", + "randombytes": "^2.0.1", + "safe-buffer": "^5.0.1", + "unorm": "^1.3.3" + } + }, + "bip66": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", + "integrity": "sha1-AfqHSHhcpwlV1QESF9GzE5lpyiI=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "bl": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz", + "integrity": "sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ==", + "dev": true, + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "blakejs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.1.0.tgz", + "integrity": "sha1-ad+S75U6qIylGjLfarHFShVfx6U=" + }, + "bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha1-vhYedsNU9veIrkBx9j806MTwpCo=", + "requires": { + "base-x": "^3.0.2" + } + }, + "buffer": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.5.0.tgz", + "integrity": "sha512-9FTEDjLjwoAkEwyMGDjYJQN2gfRgOKBKRfiglhvibGbpeeU/pQn1bJxQqm32OD/AIeEuHxU9roxXxg34Byp/Ww==", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" + }, + "caching-transform": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-3.0.2.tgz", + "integrity": "sha512-Mtgcv3lh3U0zRii/6qVgQODdPA4G3zhG+jtbCWj39RXuUFTMzH0vcdMtaJS1jPowd+It2Pqr6y3NJMQqOqCE2w==", + "dev": true, + "requires": { + "hasha": "^3.0.0", + "make-dir": "^2.0.0", + "package-hash": "^3.0.0", + "write-file-atomic": "^2.4.2" + }, + "dependencies": { + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "write-file-atomic": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", + "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + } + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "cardinal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz", + "integrity": "sha1-fMEFXYItISlU0HsIXeolHMe8VQU=", + "requires": { + "ansicolors": "~0.3.2", + "redeyed": "~2.1.0" + } + }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "clean-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", + "integrity": "sha1-jffHquUf02h06PjQW5GAvBGj/tc=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "clean-stack": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-1.3.0.tgz", + "integrity": "sha1-noIVAa6XmYbEax1m0tQy2y/UrjE=" + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-progress": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.6.1.tgz", + "integrity": "sha512-OVRgcyeI0viJW47MnyS10Jw/0RTpk7wwNbrCOPyXT0TVi2o3Q/u+Os8vQUFYhvkdXSbguSdFvMv1ia+UuwgIQQ==", + "requires": { + "colors": "^1.1.2", + "string-width": "^4.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, + "cli-ux": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/cli-ux/-/cli-ux-5.4.5.tgz", + "integrity": "sha512-5A6FuU0wPUlfCWUjtizUvNIbXElp6jN9QUJsDibs6F9cVX1kTgaMR3m6KT0R3iriEXpMrmPKV6yYS8XICNuQ6Q==", + "requires": { + "@oclif/command": "^1.5.1", + "@oclif/errors": "^1.2.1", + "@oclif/linewrap": "^1.0.0", + "@oclif/screen": "^1.0.3", + "ansi-escapes": "^3.1.0", + "ansi-styles": "^3.2.1", + "cardinal": "^2.1.1", + "chalk": "^2.4.1", + "clean-stack": "^2.0.0", + "cli-progress": "^3.4.0", + "extract-stack": "^1.0.0", + "fs-extra": "^7.0.1", + "hyperlinker": "^1.0.0", + "indent-string": "^4.0.0", + "is-wsl": "^1.1.0", + "js-yaml": "^3.13.1", + "lodash": "^4.17.11", + "natural-orderby": "^2.0.1", + "password-prompt": "^1.1.2", + "semver": "^5.6.0", + "string-width": "^3.1.0", + "strip-ansi": "^5.1.0", + "supports-color": "^5.5.0", + "supports-hyperlinks": "^1.0.1", + "treeify": "^1.1.0", + "tslib": "^1.9.3" + }, + "dependencies": { + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==" + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==" + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + } + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=" + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + }, + "dependencies": { + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + } + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" + }, + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true + }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, + "cp-file": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/cp-file/-/cp-file-6.2.0.tgz", + "integrity": "sha512-fmvV4caBnofhPe8kOcitBwSn2f39QLjnAnGq3gO9dfd75mUytzKNZB1hde6QHunW2Rt+OwuBOMc3i1tNElbszA==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "make-dir": "^2.0.0", + "nested-error-stacks": "^2.0.0", + "pify": "^4.0.1", + "safe-buffer": "^5.0.1" + }, + "dependencies": { + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + } + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "cuint": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz", + "integrity": "sha1-QICG1AlVDCYxFVYZ6fp7ytw7mRs=" + }, + "d": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "requires": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "default-require-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", + "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=", + "dev": true, + "requires": { + "strip-bom": "^3.0.0" + }, + "dependencies": { + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + } + } + }, + "detect-indent": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.0.0.tgz", + "integrity": "sha512-oSyFlqaTHCItVRGK5RmrmjB+CmaMOW7IaNA/kdxqhoa6d17j/5ce9O9eWXmV/KEdRwqpQA+Vqe8a8Bsybu4YnA==", + "dev": true + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "drbg.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz", + "integrity": "sha1-Pja2xCs3BDgjzbwzLVjzHiRFSAs=", + "requires": { + "browserify-aes": "^1.0.6", + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4" + } + }, + "elliptic": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz", + "integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==", + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + }, + "encoding": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", + "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "requires": { + "iconv-lite": "~0.4.13" + } + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es5-ext": { + "version": "0.10.53", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", + "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", + "requires": { + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.3", + "next-tick": "~1.0.0" + }, + "dependencies": { + "next-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" + } + } + }, + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "es6-symbol": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", + "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "requires": { + "d": "^1.0.1", + "ext": "^1.1.2" + } + }, + "es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "requires": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "eslint": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz", + "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.9.1", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^4.0.3", + "eslint-utils": "^1.3.1", + "eslint-visitor-keys": "^1.0.0", + "espree": "^5.0.1", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob": "^7.1.2", + "globals": "^11.7.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^6.2.2", + "js-yaml": "^3.13.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.11", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "path-is-inside": "^1.0.2", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^5.5.1", + "strip-ansi": "^4.0.0", + "strip-json-comments": "^2.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "inquirer": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", + "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", + "dev": true, + "requires": { + "ansi-escapes": "^3.2.0", + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^2.0.0", + "lodash": "^4.17.12", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^6.4.0", + "string-width": "^2.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "dev": true + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "eslint-ast-utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-ast-utils/-/eslint-ast-utils-1.1.0.tgz", + "integrity": "sha512-otzzTim2/1+lVrlH19EfQQJEhVJSu0zOb9ygb3iapN6UlyaDtyRq4b5U1FuW0v1lRa9Fp/GJyHkSwm6NqABgCA==", + "dev": true, + "requires": { + "lodash.get": "^4.4.2", + "lodash.zip": "^4.2.0" + } + }, + "eslint-config-oclif": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-oclif/-/eslint-config-oclif-3.1.0.tgz", + "integrity": "sha512-Tqgy43cNXsSdhTLWW4RuDYGFhV240sC4ISSv/ZiUEg/zFxExSEUpRE6J+AGnkKY9dYwIW4C9b2YSUVv8z/miMA==", + "dev": true, + "requires": { + "eslint-config-xo-space": "^0.20.0", + "eslint-plugin-mocha": "^5.2.0", + "eslint-plugin-node": "^7.0.1", + "eslint-plugin-unicorn": "^6.0.1" + } + }, + "eslint-config-oclif-typescript": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-oclif-typescript/-/eslint-config-oclif-typescript-0.1.0.tgz", + "integrity": "sha512-BjXNJcH2F02MdaSFml9vJskviUFVkLHbTPGM5tinIt98H6klFNKP7/lQ+fB/Goc2wB45usEuuw6+l/fwAv9i7g==", + "dev": true, + "requires": { + "@typescript-eslint/eslint-plugin": "^2.6.1", + "@typescript-eslint/parser": "^2.6.1", + "eslint-config-oclif": "^3.1.0", + "eslint-config-xo-space": "^0.20.0", + "eslint-plugin-mocha": "^5.2.0", + "eslint-plugin-node": "^7.0.1", + "eslint-plugin-unicorn": "^6.0.1" + } + }, + "eslint-config-xo": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/eslint-config-xo/-/eslint-config-xo-0.24.2.tgz", + "integrity": "sha512-ivQ7qISScW6gfBp+p31nQntz1rg34UCybd3uvlngcxt5Utsf4PMMi9QoAluLFcPUM5Tvqk4JGraR9qu3msKPKQ==", + "dev": true + }, + "eslint-config-xo-space": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/eslint-config-xo-space/-/eslint-config-xo-space-0.20.0.tgz", + "integrity": "sha512-bOsoZA8M6v1HviDUIGVq1fLVnSu3mMZzn85m2tqKb73tSzu4GKD4Jd2Py4ZKjCgvCbRRByEB5HPC3fTMnnJ1uw==", + "dev": true, + "requires": { + "eslint-config-xo": "^0.24.0" + } + }, + "eslint-plugin-es": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-1.4.1.tgz", + "integrity": "sha512-5fa/gR2yR3NxQf+UXkeLeP8FBBl6tSgdrAz1+cF84v1FMM4twGwQoqTnn+QxFLcPOrF4pdKEJKDB/q9GoyJrCA==", + "dev": true, + "requires": { + "eslint-utils": "^1.4.2", + "regexpp": "^2.0.1" + } + }, + "eslint-plugin-mocha": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-5.3.0.tgz", + "integrity": "sha512-3uwlJVLijjEmBeNyH60nzqgA1gacUWLUmcKV8PIGNvj1kwP/CTgAWQHn2ayyJVwziX+KETkr9opNwT1qD/RZ5A==", + "dev": true, + "requires": { + "ramda": "^0.26.1" + } + }, + "eslint-plugin-node": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-7.0.1.tgz", + "integrity": "sha512-lfVw3TEqThwq0j2Ba/Ckn2ABdwmL5dkOgAux1rvOk6CO7A6yGyPI2+zIxN6FyNkp1X1X/BSvKOceD6mBWSj4Yw==", + "dev": true, + "requires": { + "eslint-plugin-es": "^1.3.1", + "eslint-utils": "^1.3.1", + "ignore": "^4.0.2", + "minimatch": "^3.0.4", + "resolve": "^1.8.1", + "semver": "^5.5.0" + }, + "dependencies": { + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + } + } + }, + "eslint-plugin-unicorn": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-6.0.1.tgz", + "integrity": "sha512-hjy9LhTdtL7pz8WTrzS0CGXRkWK3VAPLDjihofj8JC+uxQLfXm0WwZPPPB7xKmcjRyoH+jruPHOCrHNEINpG/Q==", + "dev": true, + "requires": { + "clean-regexp": "^1.0.0", + "eslint-ast-utils": "^1.0.0", + "import-modules": "^1.1.0", + "lodash.camelcase": "^4.1.1", + "lodash.kebabcase": "^4.0.1", + "lodash.snakecase": "^4.0.1", + "lodash.upperfirst": "^4.2.0", + "safe-regex": "^1.1.0" + } + }, + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", + "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", + "dev": true + }, + "espree": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz", + "integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==", + "dev": true, + "requires": { + "acorn": "^6.0.7", + "acorn-jsx": "^5.0.0", + "eslint-visitor-keys": "^1.0.0" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, + "esquery": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.2.0.tgz", + "integrity": "sha512-weltsSqdeWIX9G2qQZz7KlTRJdkkOCTPgLYJUz1Hacf48R4YOwGPHO3+ORfWedqJKbq5WQmsgK90n+pFLIKt/Q==", + "dev": true, + "requires": { + "estraverse": "^5.0.0" + }, + "dependencies": { + "estraverse": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.0.0.tgz", + "integrity": "sha512-j3acdrMzqrxmJTNj5dbr1YbjacrYgAxVMeF0gK16E3j494mOe7xygM/ZLIguEQ0ETwAg2hlJCtHRGav+y0Ny5A==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "eventemitter3": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz", + "integrity": "sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg==" + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "execa": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz", + "integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "dependencies": { + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + } + } + }, + "ext": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", + "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==", + "requires": { + "type": "^2.0.0" + }, + "dependencies": { + "type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/type/-/type-2.0.0.tgz", + "integrity": "sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow==" + } + } + }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "dependencies": { + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "requires": { + "os-tmpdir": "~1.0.2" + } + } + } + }, + "extract-stack": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/extract-stack/-/extract-stack-1.0.0.tgz", + "integrity": "sha1-uXrK+UQe6iMyUpYktzL8WhyBZfo=" + }, + "fancy-test": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/fancy-test/-/fancy-test-1.4.7.tgz", + "integrity": "sha512-drgNrpNbvXXbPAz0rn7jvzjoEihDKpm1fFF+aZ+FVLatjE3jZSc6WwfgC5x7N/+nhmentMx4TXPQ0OkS0SElVQ==", + "dev": true, + "requires": { + "@types/chai": "*", + "@types/lodash": "*", + "@types/mocha": "*", + "@types/node": "*", + "@types/sinon": "*", + "lodash": "^4.17.13", + "mock-stdin": "^0.3.1", + "stdout-stderr": "^0.1.9" + } + }, + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==" + }, + "fast-glob": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.2.tgz", + "integrity": "sha512-UDV82o4uQyljznxwMxyVRJgZZt3O5wENYojjzbaGEGZgeOxkLFf+V4cnUD+krzb2F72E18RhamkMZ7AdeggF7A==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.0", + "merge2": "^1.3.0", + "micromatch": "^4.0.2", + "picomatch": "^2.2.1" + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fastq": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.7.0.tgz", + "integrity": "sha512-YOadQRnHd5q6PogvAR/x62BGituF2ufiEA6s8aavQANw5YKHERI4AREboX6KotzP8oX2klxYF2wcV/7bn1clfQ==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + } + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + }, + "dependencies": { + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, + "foreground-child": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-1.5.6.tgz", + "integrity": "sha1-T9ca0t/elnibmApcCilZN8svXOk=", + "dev": true, + "requires": { + "cross-spawn": "^4", + "signal-exit": "^3.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", + "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + } + } + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "get-stream": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", + "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "github-slugger": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.3.0.tgz", + "integrity": "sha512-gwJScWVNhFYSRDvURk/8yhcFBee6aFjye2a7Lhb2bUyRulpIoek9p0I9Kt7PT67d/nUlZbFu8L9RLiA0woQN8Q==", + "dev": true, + "requires": { + "emoji-regex": ">=6.0.0 <=6.1.1" + }, + "dependencies": { + "emoji-regex": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.1.1.tgz", + "integrity": "sha1-xs0OwbBkLio8Z6ETfvxeeW2k+I4=", + "dev": true + } + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "globby": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.2.tgz", + "integrity": "sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==", + "dev": true, + "requires": { + "@types/glob": "^7.1.1", + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.0.3", + "glob": "^7.1.3", + "ignore": "^5.1.1", + "merge2": "^1.2.3", + "slash": "^3.0.0" + } + }, + "graceful-fs": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", + "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "hasha": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-3.0.0.tgz", + "integrity": "sha1-UqMvq4Vp1BymmmH/GiFPjrfIvTk=", + "dev": true, + "requires": { + "is-stream": "^1.0.1" + } + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "hosted-git-info": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "dev": true + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "http-call": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/http-call/-/http-call-5.3.0.tgz", + "integrity": "sha512-ahwimsC23ICE4kPl9xTBjKB4inbRaeLyZeRunC/1Jy/Z6X8tv22MEAjK+KBOMSVLaqXPTTmd8638waVIKLGx2w==", + "dev": true, + "requires": { + "content-type": "^1.0.4", + "debug": "^4.1.1", + "is-retry-allowed": "^1.1.0", + "is-stream": "^2.0.0", + "parse-json": "^4.0.0", + "tunnel-agent": "^0.6.0" + }, + "dependencies": { + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true + } + } + }, + "hyperlinker": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hyperlinker/-/hyperlinker-1.0.0.tgz", + "integrity": "sha512-Ty8UblRWFEcfSuIaajM34LdPXIhbs1ajEX/BBPv24J+enSVaEVY63xQ6lTO9VRYS5LAoghIG0IDJ+p+IPzKUQQ==" + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "dev": true + }, + "ignore": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz", + "integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==", + "dev": true + }, + "import-fresh": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "import-modules": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/import-modules/-/import-modules-1.1.0.tgz", + "integrity": "sha1-dI23nFzEK7lwHvq0JPiU5yYA6dw=", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "indent-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", + "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "inquirer": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz", + "integrity": "sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==", + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^3.0.0", + "cli-cursor": "^3.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.15", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.5.3", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-escapes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "requires": { + "type-fest": "^0.11.0" + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==" + } + } + }, + "ip-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.1.0.tgz", + "integrity": "sha512-pKnZpbgCTfH/1NLIlOduP/V+WRXzC2MOz3Qo8xmxk8C5GudJLgK5QyLVXOSWy3ParAH7Eemurl3xjv/WXYFvMA==" + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-buffer": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", + "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==" + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" + }, + "is-retry-allowed": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", + "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "isomorphic-fetch": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", + "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", + "requires": { + "node-fetch": "^1.0.1", + "whatwg-fetch": ">=0.10.0" + } + }, + "istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true + }, + "istanbul-lib-hook": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-2.0.7.tgz", + "integrity": "sha512-vrRztU9VRRFDyC+aklfLoeXyNdTfga2EI3udDGn4cZ6fpSXpHLV9X6CHvfoMCPtggg8zvDDmC4b9xfu0z6/llA==", + "dev": true, + "requires": { + "append-transform": "^1.0.0" + } + }, + "istanbul-lib-instrument": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", + "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", + "dev": true, + "requires": { + "@babel/generator": "^7.4.0", + "@babel/parser": "^7.4.3", + "@babel/template": "^7.4.0", + "@babel/traverse": "^7.4.3", + "@babel/types": "^7.4.0", + "istanbul-lib-coverage": "^2.0.5", + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "istanbul-lib-report": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz", + "integrity": "sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "supports-color": "^6.1.0" + }, + "dependencies": { + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", + "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "rimraf": "^2.6.3", + "source-map": "^0.6.1" + }, + "dependencies": { + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.7.tgz", + "integrity": "sha512-uu1F/L1o5Y6LzPVSVZXNOoD/KXpJue9aeLRd0sM9uMXfZvzomB0WxVamWb5ue8kA2vVWEmW7EG+A5n3f1kqHKg==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0" + } + }, + "js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "dev": true + }, + "load-json-file": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-6.2.0.tgz", + "integrity": "sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.15", + "parse-json": "^5.0.0", + "strip-bom": "^4.0.0", + "type-fest": "^0.6.0" + }, + "dependencies": { + "parse-json": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz", + "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1", + "lines-and-columns": "^1.1.6" + } + } + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + }, + "lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=" + }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", + "dev": true + }, + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", + "dev": true + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true + }, + "lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha1-hImxyw0p/4gZXM7KRI/21swpXDY=", + "dev": true + }, + "lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha1-OdcUo1NXFHg3rv1ktdy7Fr7Nj40=", + "dev": true + }, + "lodash.template": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", + "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", + "requires": { + "lodash._reinterpolate": "^3.0.0", + "lodash.templatesettings": "^4.0.0" + } + }, + "lodash.templatesettings": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", + "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", + "requires": { + "lodash._reinterpolate": "^3.0.0" + } + }, + "lodash.upperfirst": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", + "integrity": "sha1-E2Xt9DFIBIHvDRxolXpe2Z1J984=", + "dev": true + }, + "lodash.zip": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz", + "integrity": "sha1-7GZi5IlkCO1KtsVCo5kLcswIACA=", + "dev": true + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=", + "requires": { + "es5-ext": "~0.10.2" + } + }, + "make-dir": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.2.tgz", + "integrity": "sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w==", + "dev": true, + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "memoizee": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.14.tgz", + "integrity": "sha512-/SWFvWegAIYAO4NQMpcX+gcra0yEZu4OntmUdrBaWrJncxOqAziGFlHxc7yjKVK2uu3lpPW27P27wkR82wA8mg==", + "requires": { + "d": "1", + "es5-ext": "^0.10.45", + "es6-weak-map": "^2.0.2", + "event-emitter": "^0.3.5", + "is-promise": "^2.1", + "lru-queue": "0.1", + "next-tick": "1", + "timers-ext": "^0.1.5" + } + }, + "merge-source-map": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", + "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", + "dev": true, + "requires": { + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "merge2": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz", + "integrity": "sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "mkdirp-classic": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.2.tgz", + "integrity": "sha512-ejdnDQcR75gwknmMw/tx02AuRs8jCtqFoFqDZMjiNxsu85sRIJVXDKHuLYvUUPRBUtV2FpSZa9bL1BUa3BdR2g==", + "dev": true + }, + "mocha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "dev": true, + "requires": { + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.5", + "he": "1.1.1", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "supports-color": "5.4.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "mock-stdin": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/mock-stdin/-/mock-stdin-0.3.1.tgz", + "integrity": "sha1-xlfZZC2QeGQ1xkyl6Zu9TQm9fdM=", + "dev": true + }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" + }, + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "natural-orderby": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/natural-orderby/-/natural-orderby-2.0.3.tgz", + "integrity": "sha512-p7KTHxU0CUrcOXe62Zfrb5Z13nLvPhSWR/so3kFulUQU0sgUll2Z0LwpsLN351eOOD+hRGu/F1g+6xDfPeD++Q==" + }, + "nested-error-stacks": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz", + "integrity": "sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug==", + "dev": true + }, + "next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" + }, + "node-fetch": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", + "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", + "requires": { + "encoding": "^0.1.11", + "is-stream": "^1.0.1" + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "nyc": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-14.1.1.tgz", + "integrity": "sha512-OI0vm6ZGUnoGZv/tLdZ2esSVzDwUC88SNs+6JoSOMVxA+gKMB8Tk7jBwgemLx4O40lhhvZCVw1C+OYLOBOPXWw==", + "dev": true, + "requires": { + "archy": "^1.0.0", + "caching-transform": "^3.0.2", + "convert-source-map": "^1.6.0", + "cp-file": "^6.2.0", + "find-cache-dir": "^2.1.0", + "find-up": "^3.0.0", + "foreground-child": "^1.5.6", + "glob": "^7.1.3", + "istanbul-lib-coverage": "^2.0.5", + "istanbul-lib-hook": "^2.0.7", + "istanbul-lib-instrument": "^3.3.0", + "istanbul-lib-report": "^2.0.8", + "istanbul-lib-source-maps": "^3.0.6", + "istanbul-reports": "^2.2.4", + "js-yaml": "^3.13.1", + "make-dir": "^2.1.0", + "merge-source-map": "^1.1.0", + "resolve-from": "^4.0.0", + "rimraf": "^2.6.3", + "signal-exit": "^3.0.2", + "spawn-wrap": "^1.4.2", + "test-exclude": "^5.2.3", + "uuid": "^3.3.2", + "yargs": "^13.2.2", + "yargs-parser": "^13.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + } + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "package-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-3.0.0.tgz", + "integrity": "sha512-lOtmukMDVvtkL84rJHI7dpTYq+0rli8N2wlnqUcBuDWCfVhRUfOmnR9SsoHFMLpACvEV60dX7rd0rFaYDZI+FA==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.15", + "hasha": "^3.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "password-prompt": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/password-prompt/-/password-prompt-1.1.2.tgz", + "integrity": "sha512-bpuBhROdrhuN3E7G/koAju0WjVw9/uQOG5Co5mokNj0MiOSBVZS1JTwM4zl55hu0WFmIEFvO9cU9sJQiBIYeIA==", + "requires": { + "ansi-escapes": "^3.1.0", + "cross-spawn": "^6.0.5" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, + "pbkdf2": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", + "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "proper-lockfile": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.1.tgz", + "integrity": "sha512-1w6rxXodisVpn7QYvLk706mzprPTAPCYAqxMvctmPN3ekuRk/kuGkGc82pangZiAt4R3lwSuUzheTTn0/Yb7Zg==", + "requires": { + "graceful-fs": "^4.1.11", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "qqjs": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/qqjs/-/qqjs-0.3.11.tgz", + "integrity": "sha512-pB2X5AduTl78J+xRSxQiEmga1jQV0j43jOPs/MTgTLApGFEOn6NgdE2dEjp7nvDtjkIOZbvFIojAiYUx6ep3zg==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "debug": "^4.1.1", + "execa": "^0.10.0", + "fs-extra": "^6.0.1", + "get-stream": "^5.1.0", + "glob": "^7.1.2", + "globby": "^10.0.1", + "http-call": "^5.1.2", + "load-json-file": "^6.2.0", + "pkg-dir": "^4.2.0", + "tar-fs": "^2.0.0", + "tmp": "^0.1.0", + "write-json-file": "^4.1.1" + }, + "dependencies": { + "fs-extra": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.1.tgz", + "integrity": "sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + } + } + }, + "ramda": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.26.1.tgz", + "integrity": "sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ==", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "dependencies": { + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + } + } + }, + "read-pkg-up": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", + "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", + "dev": true, + "requires": { + "find-up": "^3.0.0", + "read-pkg": "^3.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + } + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "redeyed": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz", + "integrity": "sha1-iYS1gV2ZyyIEacme7v/jiRPmzAs=", + "requires": { + "esprima": "~4.0.0" + } + }, + "regenerator-runtime": { + "version": "0.13.5", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", + "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==" + }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, + "release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA=", + "dev": true, + "requires": { + "es6-error": "^4.0.1" + } + }, + "replace-ext": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", + "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=" + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "resolve": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz", + "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=" + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "run-async": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.0.tgz", + "integrity": "sha512-xJTbh/d7Lm7SBhc1tNvTpeCHaEzoyxPrqNlvSdMfBTYwaY++UJFyXUOxAtsRUXjlqOfj8luNaR9vjCh4KeV+pg==", + "requires": { + "is-promise": "^2.1.0" + } + }, + "run-parallel": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz", + "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==", + "dev": true + }, + "rxjs": { + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz", + "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==", + "requires": { + "tslib": "^1.9.0" + } + }, + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "secp256k1": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-3.8.0.tgz", + "integrity": "sha512-k5ke5avRZbtl9Tqx/SA7CbY3NF6Ro+Sj9cZxezFzuBlLDmyqPiL8hJJ+EmzD8Ig4LUDByHJ3/iPOVoRixs/hmw==", + "requires": { + "bindings": "^1.5.0", + "bip66": "^1.1.5", + "bn.js": "^4.11.8", + "create-hash": "^1.2.0", + "drbg.js": "^1.0.1", + "elliptic": "^6.5.2", + "nan": "^2.14.0", + "safe-buffer": "^5.1.2" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + } + }, + "slug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/slug/-/slug-2.1.1.tgz", + "integrity": "sha512-yNGhDdS0DR0JyxnPC84qIx/Vd01RHVY4guJeBqBNdBoOLNWnzw5zkWJvxVSmsuUb92bikdnQFnw3PfGY8uZ82g==", + "requires": { + "unicode": ">= 0.3.1" + } + }, + "sort-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-4.0.0.tgz", + "integrity": "sha512-hlJLzrn/VN49uyNkZ8+9b+0q9DjmmYcYOnbMQtpkLrYpPwRApDPZfmqbUfJnAA3sb/nRib+nDot7Zi/1ER1fuA==", + "dev": true, + "requires": { + "is-plain-obj": "^2.0.0" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "source-map-support": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz", + "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "spawn-wrap": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-1.4.3.tgz", + "integrity": "sha512-IgB8md0QW/+tWqcavuFgKYR/qIRvJkRLPJDFaoXtLLUaVcCDK0+HeFTkmQHj3eprcYhc+gOl0aEA1w7qZlYezw==", + "dev": true, + "requires": { + "foreground-child": "^1.5.6", + "mkdirp": "^0.5.0", + "os-homedir": "^1.0.1", + "rimraf": "^2.6.2", + "signal-exit": "^3.0.2", + "which": "^1.3.0" + } + }, + "spdx-correct": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", + "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", + "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", + "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", + "dev": true + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "stdout-stderr": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/stdout-stderr/-/stdout-stderr-0.1.13.tgz", + "integrity": "sha512-Xnt9/HHHYfjZ7NeQLvuQDyL1LnbsbddgMFKCuaQKwGCdJm8LnstZIXop+uOY36UR1UXXoHXfMbC1KlVdVd2JLA==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "supports-hyperlinks": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-1.0.1.tgz", + "integrity": "sha512-HHi5kVSefKaJkGYXbDuKbUGRVxqnWGn3J2e39CYcNJEfWciGq2zYtOhXLTlvrOZW1QU7VX67w7fMmWafHX9Pfw==", + "requires": { + "has-flag": "^2.0.0", + "supports-color": "^5.0.0" + }, + "dependencies": { + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" + } + } + }, + "table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "dependencies": { + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + } + } + }, + "tar-fs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz", + "integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==", + "dev": true, + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.0.0" + } + }, + "tar-stream": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.2.tgz", + "integrity": "sha512-UaF6FoJ32WqALZGOIAApXx+OdxhekNMChu6axLJR85zMMjXKWFGjbIRe+J6P4UnRGg9rAwWvbTT0oI7hD/Un7Q==", + "dev": true, + "requires": { + "bl": "^4.0.1", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + }, + "test-exclude": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", + "integrity": "sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==", + "dev": true, + "requires": { + "glob": "^7.1.3", + "minimatch": "^3.0.4", + "read-pkg-up": "^4.0.0", + "require-main-filename": "^2.0.0" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "timers-ext": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz", + "integrity": "sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==", + "requires": { + "es5-ext": "~0.10.46", + "next-tick": "1" + } + }, + "tmp": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz", + "integrity": "sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==", + "dev": true, + "requires": { + "rimraf": "^2.6.3" + } + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "treeify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/treeify/-/treeify-1.1.0.tgz", + "integrity": "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==" + }, + "ts-node": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.8.2.tgz", + "integrity": "sha512-duVj6BpSpUpD/oM4MfhO98ozgkp3Gt9qIp3jGxwU2DFvl/3IRaEAvbLa8G60uS7C77457e/m5TMowjedeRxI1Q==", + "dev": true, + "requires": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.6", + "yn": "3.1.1" + }, + "dependencies": { + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + } + } + }, + "tslib": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", + "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==" + }, + "tsutils": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" + }, + "type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", + "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true + }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "requires": { + "is-typedarray": "^1.0.0" + } + }, + "typescript": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", + "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", + "dev": true + }, + "unicode": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/unicode/-/unicode-12.1.0.tgz", + "integrity": "sha512-Ty6+Ew21DiYTWLYtd05RF/X4c1ekOvOgANyHbBj0h3MaXpfaGr2Rdmc0hMFuGQLyPLb9cU4ArNxl0bTF5HSzXw==" + }, + "unist-util-stringify-position": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", + "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==", + "requires": { + "@types/unist": "^2.0.2" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + }, + "unorm": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz", + "integrity": "sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA==" + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "vfile": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.1.0.tgz", + "integrity": "sha512-BaTPalregj++64xbGK6uIlsurN3BCRNM/P2Pg8HezlGzKd1O9PrwIac6bd9Pdx2uTb0QHoioZ+rXKolbVXEgJg==", + "requires": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "replace-ext": "1.0.0", + "unist-util-stringify-position": "^2.0.0", + "vfile-message": "^2.0.0" + } + }, + "vfile-message": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz", + "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==", + "requires": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^2.0.0" + } + }, + "websocket": { + "version": "1.0.31", + "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.31.tgz", + "integrity": "sha512-VAouplvGKPiKFDTeCCO65vYHsyay8DqoBSlzIO3fayrfOgU94lQN5a1uWVnFrMLceTJw/+fQXR5PGbUVRaHshQ==", + "requires": { + "debug": "^2.2.0", + "es5-ext": "^0.10.50", + "nan": "^2.14.0", + "typedarray-to-buffer": "^3.1.5", + "yaeti": "^0.0.6" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "whatwg-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz", + "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==" + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "widest-line": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", + "integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==", + "requires": { + "string-width": "^2.1.1" + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wrap-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-4.0.0.tgz", + "integrity": "sha512-uMTsj9rDb0/7kk1PbcbCcwvHUxp60fGDB/NNXpVa0Q+ic/e7y5+BwTxKfQ33VYgDppSwi/FBzpetYzo8s6tfbg==", + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "write-json-file": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/write-json-file/-/write-json-file-4.3.0.tgz", + "integrity": "sha512-PxiShnxf0IlnQuMYOPPhPkhExoCQuTUNPOa/2JWCYTmBquU9njyyDuwRKN26IZBlp4yn1nt+Agh2HOOBl+55HQ==", + "dev": true, + "requires": { + "detect-indent": "^6.0.0", + "graceful-fs": "^4.1.15", + "is-plain-obj": "^2.0.0", + "make-dir": "^3.0.0", + "sort-keys": "^4.0.0", + "write-file-atomic": "^3.0.0" + } + }, + "xxhashjs": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/xxhashjs/-/xxhashjs-0.2.2.tgz", + "integrity": "sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw==", + "requires": { + "cuint": "^0.2.2" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yaeti": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", + "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=" + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + } + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + } + } +} diff --git a/cli/package.json b/cli/package.json new file mode 100644 index 0000000000..942dfc1414 --- /dev/null +++ b/cli/package.json @@ -0,0 +1,90 @@ +{ + "name": "joystream-cli", + "description": "Command Line Interface for Joystream community and governance activities", + "version": "0.0.0", + "author": "Leszek Wiesner", + "bin": { + "joystream-cli": "./bin/run" + }, + "bugs": "https://github.com/Joystream/substrate-runtime-joystream/issues", + "dependencies": { + "@joystream/types": "^0.6.0", + "@oclif/command": "^1.5.19", + "@oclif/config": "^1.14.0", + "@oclif/plugin-help": "^2.2.3", + "@polkadot/api": "^0.96.1", + "@types/inquirer": "^6.5.0", + "@types/proper-lockfile": "^4.1.1", + "@types/slug": "^0.9.1", + "cli-ux": "^5.4.5", + "inquirer": "^7.1.0", + "moment": "^2.24.0", + "proper-lockfile": "^4.1.1", + "slug": "^2.1.1", + "tslib": "^1.11.1" + }, + "devDependencies": { + "@oclif/dev-cli": "^1.22.2", + "@oclif/test": "^1.2.5", + "@types/chai": "^4.2.11", + "@types/mocha": "^5.2.7", + "@types/node": "^10.17.18", + "chai": "^4.2.0", + "eslint": "^5.16.0", + "eslint-config-oclif": "^3.1.0", + "eslint-config-oclif-typescript": "^0.1.0", + "globby": "^10.0.2", + "mocha": "^5.2.0", + "nyc": "^14.1.1", + "ts-node": "^8.8.2", + "typescript": "^3.8.3", + "@polkadot/ts": "^0.1.56" + }, + "engines": { + "node": ">=8.0.0" + }, + "files": [ + "/bin", + "/lib", + "/npm-shrinkwrap.json", + "/oclif.manifest.json" + ], + "homepage": "https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli", + "keywords": [ + "oclif" + ], + "license": "MIT", + "main": "lib/index.js", + "oclif": { + "repositoryPrefix": "<%- repo %>/blob/master/cli/<%- commandPath %>", + "commands": "./lib/commands", + "bin": "joystream-cli", + "plugins": [ + "@oclif/plugin-help" + ], + "topics": { + "council": { + "description": "Council-related information and activities like voting, becoming part of the council etc." + }, + "account": { + "description": "Accounts management - create, import or switch currently used account" + }, + "api": { + "description": "Inspect the substrate node api, perform lower-level api calls or change the current api provider uri" + } + } + }, + "repository": { + "type": "git", + "url": "https://github.com/Joystream/substrate-runtime-joystream", + "directory": "cli" + }, + "scripts": { + "postpack": "rm -f oclif.manifest.json", + "posttest": "eslint . --ext .ts --config .eslintrc", + "prepack": "rm -rf lib && tsc -b && oclif-dev manifest && oclif-dev readme", + "test": "nyc --extension .ts mocha --forbid-only \"test/**/*.test.ts\"", + "version": "oclif-dev readme && git add README.md" + }, + "types": "lib/index.d.ts" +} diff --git a/cli/src/Api.ts b/cli/src/Api.ts new file mode 100644 index 0000000000..b499a50a42 --- /dev/null +++ b/cli/src/Api.ts @@ -0,0 +1,114 @@ +import BN from 'bn.js'; +import { registerJoystreamTypes } from '@joystream/types'; +import { ApiPromise, WsProvider } from '@polkadot/api'; +import { QueryableStorageMultiArg } from '@polkadot/api/types'; +import { formatBalance } from '@polkadot/util'; +import { Hash } from '@polkadot/types/interfaces'; +import { KeyringPair } from '@polkadot/keyring/types'; +import { Codec } from '@polkadot/types/types'; +import { AccountSummary, CouncilInfoObj, CouncilInfoTuple, createCouncilInfoObj } from './Types'; +import { DerivedFees, DerivedBalances } from '@polkadot/api-derive/types'; +import { CLIError } from '@oclif/errors'; +import ExitCodes from './ExitCodes'; + +export const DEFAULT_API_URI = 'wss://rome-rpc-endpoint.joystream.org:9944/'; +export const TOKEN_SYMBOL = 'JOY'; + +// Api wrapper for handling most common api calls and allowing easy API implementation switch in the future + +export default class Api { + private _api: ApiPromise; + + private constructor(originalApi:ApiPromise) { + this._api = originalApi; + } + + public getOriginalApi(): ApiPromise { + return this._api; + } + + private static async initApi(apiUri: string = DEFAULT_API_URI): Promise { + formatBalance.setDefaults({ unit: TOKEN_SYMBOL }); + const wsProvider:WsProvider = new WsProvider(apiUri); + registerJoystreamTypes(); + + return await ApiPromise.create({ provider: wsProvider }); + } + + static async create(apiUri: string = DEFAULT_API_URI): Promise { + const originalApi: ApiPromise = await Api.initApi(apiUri); + return new Api(originalApi); + } + + private async queryMultiOnce(queries: Parameters[0]): Promise { + let results: Codec[] = []; + + const unsub = await this._api.queryMulti( + queries, + (res) => { results = res } + ); + unsub(); + + if (!results.length || results.length !== queries.length) { + throw new CLIError('API querying issue', { exit: ExitCodes.ApiError }); + } + + return results; + } + + async getAccountsBalancesInfo(accountAddresses:string[]): Promise { + let accountsBalances: DerivedBalances[] = await this._api.derive.balances.votingBalances(accountAddresses); + + return accountsBalances; + } + + // Get on-chain data related to given account. + // For now it's just account balances + async getAccountSummary(accountAddresses:string): Promise { + const balances: DerivedBalances = (await this.getAccountsBalancesInfo([accountAddresses]))[0]; + // TODO: Some more information can be fetched here in the future + + return { balances }; + } + + async getCouncilInfo(): Promise { + const queries: { [P in keyof CouncilInfoObj]: QueryableStorageMultiArg<"promise"> } = { + activeCouncil: this._api.query.council.activeCouncil, + termEndsAt: this._api.query.council.termEndsAt, + autoStart: this._api.query.councilElection.autoStart, + newTermDuration: this._api.query.councilElection.newTermDuration, + candidacyLimit: this._api.query.councilElection.candidacyLimit, + councilSize: this._api.query.councilElection.councilSize, + minCouncilStake: this._api.query.councilElection.minCouncilStake, + minVotingStake: this._api.query.councilElection.minVotingStake, + announcingPeriod: this._api.query.councilElection.announcingPeriod, + votingPeriod: this._api.query.councilElection.votingPeriod, + revealingPeriod: this._api.query.councilElection.revealingPeriod, + round: this._api.query.councilElection.round, + stage: this._api.query.councilElection.stage + } + const results: CouncilInfoTuple = await this.queryMultiOnce(Object.values(queries)); + + return createCouncilInfoObj(...results); + } + + // TODO: This formula is probably not too good, so some better implementation will be required in the future + async estimateFee(account: KeyringPair, recipientAddr: string, amount: BN): Promise { + const transfer = this._api.tx.balances.transfer(recipientAddr, amount); + const signature = account.sign(transfer.toU8a()); + const transactionByteSize:BN = new BN(transfer.encodedLength + signature.length); + + const fees: DerivedFees = await this._api.derive.balances.fees(); + + const estimatedFee = fees.transactionBaseFee.add(fees.transactionByteFee.mul(transactionByteSize)); + + return estimatedFee; + } + + async transfer(account: KeyringPair, recipientAddr: string, amount: BN): Promise { + const txHash = await this._api.tx.balances + .transfer(recipientAddr, amount) + .signAndSend(account); + return txHash; + } +} diff --git a/cli/src/ExitCodes.ts b/cli/src/ExitCodes.ts new file mode 100644 index 0000000000..124e76965a --- /dev/null +++ b/cli/src/ExitCodes.ts @@ -0,0 +1,14 @@ +enum ExitCodes { + OK = 0, + + InvalidInput = 400, + FileNotFound = 401, + InvalidFile = 402, + NoAccountFound = 403, + NoAccountSelected = 404, + + UnexpectedException = 500, + FsOperationFailed = 501, + ApiError = 502, +} +export = ExitCodes; diff --git a/cli/src/Types.ts b/cli/src/Types.ts new file mode 100644 index 0000000000..5d38d06aab --- /dev/null +++ b/cli/src/Types.ts @@ -0,0 +1,63 @@ +import BN from 'bn.js'; +import { ElectionStage, Seat } from '@joystream/types'; +import { Option } from '@polkadot/types'; +import { BlockNumber, Balance } from '@polkadot/types/interfaces'; +import { DerivedBalances } from '@polkadot/api-derive/types'; +import { KeyringPair } from '@polkadot/keyring/types'; + +// KeyringPair type extended with mandatory "meta.name" +// It's used for accounts/keys management within CLI. +// If not provided in the account json file, the meta.name value is set to "Unnamed Account" +export type NamedKeyringPair = KeyringPair & { + meta: { + name: string + } +} + +// Summary of the account information fetched from the api for "account:current" purposes (currently just balances) +export type AccountSummary = { + balances: DerivedBalances +} + +// Object/Tuple containing council/councilElection information (council:info). +// The tuple is useful, because that's how api.queryMulti returns the results. +export type CouncilInfoTuple = Parameters; +export type CouncilInfoObj = ReturnType; +// This function allows us to easily transform the tuple into the object +// and simplifies the creation of consitent Object and Tuple types (seen above). +export function createCouncilInfoObj( + activeCouncil: Seat[], + termEndsAt: BlockNumber, + autoStart: Boolean, + newTermDuration: BN, + candidacyLimit: BN, + councilSize: BN, + minCouncilStake: Balance, + minVotingStake: Balance, + announcingPeriod: BlockNumber, + votingPeriod: BlockNumber, + revealingPeriod: BlockNumber, + round: BN, + stage: Option +) { + return { + activeCouncil, + termEndsAt, + autoStart, + newTermDuration, + candidacyLimit, + councilSize, + minCouncilStake, + minVotingStake, + announcingPeriod, + votingPeriod, + revealingPeriod, + round, + stage + }; +} + +// Object with "name" and "value" properties, used for rendering simple CLI tables like: +// Total balance: 100 JOY +// Free calance: 50 JOY +export type NameValueObj = { name: string, value: string }; diff --git a/cli/src/base/AccountsCommandBase.ts b/cli/src/base/AccountsCommandBase.ts new file mode 100644 index 0000000000..5f676cc9e4 --- /dev/null +++ b/cli/src/base/AccountsCommandBase.ts @@ -0,0 +1,217 @@ +import fs from 'fs'; +import path from 'path'; +import slug from 'slug'; +import inquirer from 'inquirer'; +import ExitCodes from '../ExitCodes'; +import { CLIError } from '@oclif/errors'; +import ApiCommandBase from './ApiCommandBase'; +import { Keyring } from '@polkadot/api'; +import { formatBalance } from '@polkadot/util'; +import { NamedKeyringPair } from '../Types'; +import { DerivedBalances } from '@polkadot/api-derive/types'; +import { toFixedLength } from '../helpers/display'; + +const ACCOUNTS_DIRNAME = '/accounts'; + +/** + * Abstract base class for account-related commands. + * + * All the accounts available in the CLI are stored in the form of json backup files inside: + * { this.config.dataDir }/{ ACCOUNTS_DIRNAME } (ie. ~/.local/share/joystream-cli/accounts on Ubuntu) + * Where: this.config.dataDir is provided by oclif and ACCOUNTS_DIRNAME is a const (see above). + */ +export default abstract class AccountsCommandBase extends ApiCommandBase { + getAccountsDirPath(): string { + return path.join(this.config.dataDir, ACCOUNTS_DIRNAME); + } + + getAccountFilePath(account: NamedKeyringPair): string { + return path.join(this.getAccountsDirPath(), this.generateAccountFilename(account)); + } + + generateAccountFilename(account: NamedKeyringPair): string { + return `${ slug(account.meta.name, '_') }__${ account.address }.json`; + } + + private initAccountsFs(): void { + if (!fs.existsSync(this.getAccountsDirPath())) { + fs.mkdirSync(this.getAccountsDirPath()); + } + } + + saveAccount(account: NamedKeyringPair, password: string): void { + try { + fs.writeFileSync(this.getAccountFilePath(account), JSON.stringify(account.toJson(password))); + } catch(e) { + throw this.createDataWriteError(); + } + } + + fetchAccountFromJsonFile(jsonBackupFilePath: string): NamedKeyringPair { + if (!fs.existsSync(jsonBackupFilePath)) { + throw new CLIError('Input file does not exist!', { exit: ExitCodes.FileNotFound }); + } + if (path.extname(jsonBackupFilePath) !== '.json') { + throw new CLIError('Invalid input file: File extension should be .json', { exit: ExitCodes.InvalidFile }); + } + let accountJsonObj: any; + try { + accountJsonObj = require(jsonBackupFilePath); + } catch (e) { + throw new CLIError('Provided backup file is not valid or cannot be accessed', { exit: ExitCodes.InvalidFile }); + } + if (typeof accountJsonObj !== 'object' || accountJsonObj === null) { + throw new CLIError('Provided backup file is not valid', { exit: ExitCodes.InvalidFile }); + } + + // Force some default account name if none is provided in the original backup + if (!accountJsonObj.meta) accountJsonObj.meta = {}; + if (!accountJsonObj.meta.name) accountJsonObj.meta.name = 'Unnamed Account'; + + let keyring = new Keyring(); + let account:NamedKeyringPair; + try { + // Try adding and retrieving the keys in order to validate that the backup file is correct + keyring.addFromJson(accountJsonObj); + account = keyring.getPair(accountJsonObj.address); // We can be sure it's named, because we forced it before + } catch (e) { + throw new CLIError('Provided backup file is not valid', { exit: ExitCodes.InvalidFile }); + } + + return account; + } + + private fetchAccountOrNullFromFile(jsonFilePath: string): NamedKeyringPair | null { + try { + return this.fetchAccountFromJsonFile(jsonFilePath); + } catch (e) { + // Here in case of a typical CLIError we just return null (otherwise we throw) + if (!(e instanceof CLIError)) throw e; + return null; + } + } + + fetchAccounts(): NamedKeyringPair[] { + let files: string[] = []; + const accountDir = this.getAccountsDirPath(); + try { + files = fs.readdirSync(accountDir); + } + catch(e) { + } + + // We have to assert the type, because TS is not aware that we're filtering out the nulls at the end + return files + .map(fileName => { + const filePath = path.join(accountDir, fileName); + return this.fetchAccountOrNullFromFile(filePath); + }) + .filter(accObj => accObj !== null); + } + + getSelectedAccountFilename(): string { + return this.getPreservedState().selectedAccountFilename; + } + + getSelectedAccount(): NamedKeyringPair | null { + const selectedAccountFilename = this.getSelectedAccountFilename(); + + if (!selectedAccountFilename) { + return null; + } + + const account = this.fetchAccountOrNullFromFile( + path.join(this.getAccountsDirPath(), selectedAccountFilename) + ); + + return account; + } + + // Use when account usage is required in given command + async getRequiredSelectedAccount(promptIfMissing: boolean = true): Promise { + let selectedAccount: NamedKeyringPair | null = this.getSelectedAccount(); + if (!selectedAccount) { + this.warn('No default account selected! Use account:choose to set the default account!'); + if (!promptIfMissing) this.exit(ExitCodes.NoAccountSelected); + const accounts: NamedKeyringPair[] = this.fetchAccounts(); + if (!accounts.length) { + this.error('There are no accounts available!', { exit: ExitCodes.NoAccountFound }); + } + + selectedAccount = await this.promptForAccount(accounts); + } + + return selectedAccount; + } + + async setSelectedAccount(account: NamedKeyringPair): Promise { + await this.setPreservedState({ selectedAccountFilename: this.generateAccountFilename(account) }); + } + + async promptForPassword(message:string = 'Your account\'s password') { + const { password } = await inquirer.prompt([ + { name: 'password', type: 'password', message } + ]); + + return password; + } + + async requireConfirmation(message: string = 'Are you sure you want to execute this action?'): Promise { + const { confirmed } = await inquirer.prompt([ + { type: 'confirm', name: 'confirmed', message, default: false } + ]); + if (!confirmed) this.exit(ExitCodes.OK); + } + + async promptForAccount( + accounts: NamedKeyringPair[], + defaultAccount: NamedKeyringPair | null = null, + message: string = 'Select an account', + showBalances: boolean = true + ): Promise { + let balances: DerivedBalances[]; + if (showBalances) { + balances = await this.getApi().getAccountsBalancesInfo(accounts.map(acc => acc.address)); + } + const longestAccNameLength: number = accounts.reduce((prev, curr) => Math.max(curr.meta.name.length, prev), 0); + const accNameColLength: number = Math.min(longestAccNameLength + 1, 20); + const { chosenAccountFilename } = await inquirer.prompt([{ + name: 'chosenAccountFilename', + message, + type: 'list', + choices: accounts.map((account: NamedKeyringPair, i) => ({ + name: ( + `${ toFixedLength(account.meta.name, accNameColLength) } | `+ + `${ account.address } | ` + + ((showBalances || '') && ( + `${ formatBalance(balances[i].availableBalance) } / `+ + `${ formatBalance(balances[i].votingBalance) }` + )) + ), + value: this.generateAccountFilename(account), + short: `${ account.meta.name } (${ account.address })` + })), + default: defaultAccount && this.generateAccountFilename(defaultAccount) + }]); + + return accounts.find(acc => this.generateAccountFilename(acc) === chosenAccountFilename); + } + + async requestAccountDecoding(account: NamedKeyringPair): Promise { + const password: string = await this.promptForPassword(); + try { + account.decodePkcs8(password); + } catch (e) { + this.error('Invalid password!', { exit: ExitCodes.InvalidInput }); + } + } + + async init() { + await super.init(); + try { + this.initAccountsFs(); + } catch (e) { + throw this.createDataDirInitError(); + } + } +} diff --git a/cli/src/base/ApiCommandBase.ts b/cli/src/base/ApiCommandBase.ts new file mode 100644 index 0000000000..017810b9af --- /dev/null +++ b/cli/src/base/ApiCommandBase.ts @@ -0,0 +1,28 @@ +import ExitCodes from '../ExitCodes'; +import { CLIError } from '@oclif/errors'; +import StateAwareCommandBase from './StateAwareCommandBase'; +import Api from '../Api'; +import { ApiPromise } from '@polkadot/api' + +/** + * Abstract base class for commands that require access to the API. + */ +export default abstract class ApiCommandBase extends StateAwareCommandBase { + private api: Api | null = null; + + getApi(): Api { + if (!this.api) throw new CLIError('Tried to get API before initialization.', { exit: ExitCodes.ApiError }); + return this.api; + } + + // Get original api for lower-level api calls + getOriginalApi(): ApiPromise { + return this.getApi().getOriginalApi(); + } + + async init() { + await super.init(); + const apiUri: string = this.getPreservedState().apiUri; + this.api = await Api.create(apiUri); + } +} diff --git a/cli/src/base/DefaultCommandBase.ts b/cli/src/base/DefaultCommandBase.ts new file mode 100644 index 0000000000..24985e1446 --- /dev/null +++ b/cli/src/base/DefaultCommandBase.ts @@ -0,0 +1,15 @@ +import ExitCodes from '../ExitCodes'; +import Command from '@oclif/command'; + +/** + * Abstract base class for pretty much all commands + * (prevents console.log from hanging the process and unifies the default exit code) + */ +export default abstract class DefaultCommandBase extends Command { + async finally(err: any) { + // called after run and catch regardless of whether or not the command errored + // We'll force exit here, in case there is no error, to prevent console.log from hanging the process + if (!err) this.exit(ExitCodes.OK); + super.finally(err); + } +} diff --git a/cli/src/base/StateAwareCommandBase.ts b/cli/src/base/StateAwareCommandBase.ts new file mode 100644 index 0000000000..5c5f924994 --- /dev/null +++ b/cli/src/base/StateAwareCommandBase.ts @@ -0,0 +1,115 @@ +import fs from 'fs'; +import path from 'path'; +import ExitCodes from '../ExitCodes'; +import { CLIError } from '@oclif/errors'; +import { DEFAULT_API_URI } from '../Api'; +import lockFile from 'proper-lockfile'; +import DefaultCommandBase from './DefaultCommandBase'; + +// Type for the state object (which is preserved as json in the state file) +type StateObject = { + selectedAccountFilename: string, + apiUri: string +}; + +// State object default values +const DEFAULT_STATE: StateObject = { + selectedAccountFilename: '', + apiUri: DEFAULT_API_URI +} + +// State file path (relative to this.config.dataDir) +const STATE_FILE = '/state.json'; + +// Possible data directory access errors +enum DataDirErrorType { + Init = 0, + Read = 1, + Write = 2, +} + +/** + * Abstract base class for commands that need to work with the preserved state. + * + * The preserved state is kept in a json file inside the data directory (this.config.dataDir, supplied by oclif). + * The state object contains all the information that needs to be preserved across sessions, ie. the default account + * choosen by the user after executing account:choose command etc. (see "StateObject" type above). + */ +export default abstract class StateAwareCommandBase extends DefaultCommandBase { + getStateFilePath(): string { + return path.join(this.config.dataDir, STATE_FILE); + } + + private createDataDirFsError(errorType: DataDirErrorType, specificPath: string = '') { + const actionStrs: { [x in DataDirErrorType]: string } = { + [DataDirErrorType.Init]: 'initialize', + [DataDirErrorType.Read]: 'read from', + [DataDirErrorType.Write]: 'write into' + }; + + const errorMsg = + `Unexpected error while trying to ${ actionStrs[errorType] } the data directory.`+ + `(${ path.join(this.config.dataDir, specificPath) })! Permissions issue?`; + + return new CLIError(errorMsg, { exit: ExitCodes.FsOperationFailed }); + } + + createDataReadError(specificPath: string = ''): CLIError { + return this.createDataDirFsError(DataDirErrorType.Read, specificPath); + } + + createDataWriteError(specificPath: string = ''): CLIError { + return this.createDataDirFsError(DataDirErrorType.Write, specificPath); + } + + createDataDirInitError(specificPath: string = ''): CLIError { + return this.createDataDirFsError(DataDirErrorType.Init, specificPath); + } + + private initStateFs(): void { + if (!fs.existsSync(this.config.dataDir)) { + fs.mkdirSync(this.config.dataDir); + } + if (!fs.existsSync(this.getStateFilePath())) { + fs.writeFileSync(this.getStateFilePath(), JSON.stringify(DEFAULT_STATE)); + } + } + + getPreservedState(): StateObject { + let preservedState: StateObject; + try { + preservedState = require(this.getStateFilePath()); + } catch(e) { + throw this.createDataReadError(); + } + // The state preserved in a file may be missing some required values ie. + // if the user previously used the older version of the software. + // That's why we combine it with default state before returing. + return { ...DEFAULT_STATE, ...preservedState }; + } + + // Modifies preserved state. Uses file lock in order to avoid updating an older state. + // (which could potentialy change between read and write operation) + async setPreservedState(modifiedState: Partial): Promise { + const stateFilePath = this.getStateFilePath(); + const unlock = await lockFile.lock(stateFilePath); + let oldState: StateObject = this.getPreservedState(); + let newState: StateObject = { ...oldState, ...modifiedState }; + try { + fs.writeFileSync(stateFilePath, JSON.stringify(newState)); + } catch(e) { + await unlock(); + throw this.createDataWriteError(); + } + await unlock(); + } + + async init() { + await super.init(); + try { + await this.initStateFs(); + } catch (e) { + throw this.createDataDirInitError(); + } + } +} diff --git a/cli/src/commands/account/choose.ts b/cli/src/commands/account/choose.ts new file mode 100644 index 0000000000..d1db149ae3 --- /dev/null +++ b/cli/src/commands/account/choose.ts @@ -0,0 +1,25 @@ +import AccountsCommandBase from '../../base/AccountsCommandBase'; +import chalk from 'chalk'; +import ExitCodes from '../../ExitCodes'; +import { NamedKeyringPair } from '../../Types' + +export default class AccountChoose extends AccountsCommandBase { + static description = 'Choose default account to use in the CLI'; + + async run() { + const accounts: NamedKeyringPair[] = this.fetchAccounts(); + const selectedAccount: NamedKeyringPair | null = this.getSelectedAccount(); + + this.log(chalk.white(`Found ${ accounts.length } existing accounts...\n`)); + + if (accounts.length === 0) { + this.warn('No account to choose from. Add accont using account:import or account:create.'); + this.exit(ExitCodes.NoAccountFound); + } + + const choosenAccount: NamedKeyringPair = await this.promptForAccount(accounts, selectedAccount); + + await this.setSelectedAccount(choosenAccount); + this.log(chalk.greenBright("\nAccount switched!")); + } + } diff --git a/cli/src/commands/account/create.ts b/cli/src/commands/account/create.ts new file mode 100644 index 0000000000..eaaf4bf8a0 --- /dev/null +++ b/cli/src/commands/account/create.ts @@ -0,0 +1,47 @@ +import chalk from 'chalk'; +import ExitCodes from '../../ExitCodes'; +import AccountsCommandBase from '../../base/AccountsCommandBase'; +import { Keyring } from '@polkadot/api'; +import { mnemonicGenerate } from '@polkadot/util-crypto' +import { NamedKeyringPair } from '../../Types'; + +type AccountCreateArgs = { + name: string +}; + +export default class AccountCreate extends AccountsCommandBase { + static description = 'Create new account'; + + static args = [ + { + name: 'name', + required: true, + description: 'Account name' + }, + ]; + + validatePass(password: string, password2: string): void { + if (password !== password2) this.error('Passwords are not the same!', { exit: ExitCodes.InvalidInput }); + if (!password) this.error('You didn\'t provide a password', { exit: ExitCodes.InvalidInput }); + } + + async run() { + const args: AccountCreateArgs = this.parse(AccountCreate).args; + const keyring: Keyring = new Keyring(); + const mnemonic: string = mnemonicGenerate(); + + keyring.addFromMnemonic(mnemonic, { name: args.name, whenCreated: Date.now() }); + const keys: NamedKeyringPair = keyring.pairs[0]; // We assigned the name above + + const password = await this.promptForPassword('Set your account\'s password'); + const password2 = await this.promptForPassword('Confirm your password'); + + this.validatePass(password, password2); + + this.saveAccount(keys, password); + + this.log(chalk.greenBright(`\nAccount succesfully created!`)); + this.log(chalk.white(`${chalk.bold('Name: ') }${ args.name }`)); + this.log(chalk.white(`${chalk.bold('Address: ') }${ keys.address }`)); + } + } diff --git a/cli/src/commands/account/current.ts b/cli/src/commands/account/current.ts new file mode 100644 index 0000000000..b820502d0b --- /dev/null +++ b/cli/src/commands/account/current.ts @@ -0,0 +1,41 @@ +import AccountsCommandBase from '../../base/AccountsCommandBase'; +import { AccountSummary, NameValueObj, NamedKeyringPair } from '../../Types'; +import { DerivedBalances } from '@polkadot/api-derive/types'; +import { displayHeader, displayNameValueTable } from '../../helpers/display'; +import { formatBalance } from '@polkadot/util'; +import moment from 'moment'; + +export default class AccountCurrent extends AccountsCommandBase { + static description = 'Display information about currently choosen default account'; + static aliases = ['account:info', 'account:default']; + + async run() { + const currentAccount: NamedKeyringPair = await this.getRequiredSelectedAccount(false); + const summary: AccountSummary = await this.getApi().getAccountSummary(currentAccount.address); + + displayHeader('Account information'); + const creationDate: string = currentAccount.meta.whenCreated ? + moment(currentAccount.meta.whenCreated).format('YYYY-MM-DD HH:mm:ss') + : '?'; + const accountRows: NameValueObj[] = [ + { name: 'Account name:', value: currentAccount.meta.name }, + { name: 'Address:', value: currentAccount.address }, + { name: 'Created:', value: creationDate } + ]; + displayNameValueTable(accountRows); + + displayHeader('Balances'); + const balances: DerivedBalances = summary.balances; + let balancesRows: NameValueObj[] = [ + { name: 'Total balance:', value: formatBalance(balances.votingBalance) }, + { name: 'Transferable balance:', value: formatBalance(balances.availableBalance) } + ]; + if (balances.lockedBalance.gtn(0)) { + balancesRows.push({ name: 'Locked balance:', value: formatBalance(balances.lockedBalance) }); + } + if (balances.reservedBalance.gtn(0)) { + balancesRows.push({ name: 'Reserved balance:', value: formatBalance(balances.reservedBalance) }); + } + displayNameValueTable(balancesRows); + } + } diff --git a/cli/src/commands/account/export.ts b/cli/src/commands/account/export.ts new file mode 100644 index 0000000000..1d71ef51e3 --- /dev/null +++ b/cli/src/commands/account/export.ts @@ -0,0 +1,73 @@ +import fs from 'fs'; +import chalk from 'chalk'; +import path from 'path'; +import ExitCodes from '../../ExitCodes'; +import AccountsCommandBase from '../../base/AccountsCommandBase'; +import { flags } from '@oclif/command'; +import { NamedKeyringPair } from '../../Types'; + +type AccountExportFlags = { all: boolean }; +type AccountExportArgs = { path: string }; + +export default class AccountExport extends AccountsCommandBase { + static description = 'Export account(s) to given location'; + static MULTI_EXPORT_FOLDER_NAME = 'exported_accounts'; + + static args = [ + { + name: 'path', + required: true, + description: 'Path where the exported files should be placed' + } + ]; + + static flags = { + all: flags.boolean({ + char: 'a', + description: `If provided, exports all existing accounts into "${ AccountExport.MULTI_EXPORT_FOLDER_NAME }" folder inside given path`, + }), + }; + + exportAccount(account: NamedKeyringPair, destPath: string): string { + const sourceFilePath: string = this.getAccountFilePath(account); + const destFilePath: string = path.join(destPath, this.generateAccountFilename(account)); + try { + fs.copyFileSync(sourceFilePath, destFilePath); + } + catch (e) { + this.error( + `Error while trying to copy into the export file: (${ destFilePath }). Permissions issue?`, + { exit: ExitCodes.FsOperationFailed } + ); + } + + return destFilePath; + } + + async run() { + const args: AccountExportArgs = this.parse(AccountExport).args; + const flags: AccountExportFlags = this.parse(AccountExport).flags; + const accounts: NamedKeyringPair[] = this.fetchAccounts(); + + if (!accounts.length) { + this.error('No accounts found!', { exit: ExitCodes.NoAccountFound }); + } + + if (flags.all) { + const destPath: string = path.join(args.path, AccountExport.MULTI_EXPORT_FOLDER_NAME); + try { + if (!fs.existsSync(destPath)) fs.mkdirSync(destPath); + } catch(e) { + this.error(`Failed to create the export folder (${ destPath })`, { exit: ExitCodes.FsOperationFailed }); + } + for (let account of accounts) this.exportAccount(account, destPath); + this.log(chalk.greenBright(`All accounts succesfully exported succesfully to: ${ chalk.white(destPath) }!`)); + } + else { + const destPath: string = args.path; + const choosenAccount: NamedKeyringPair = await this.promptForAccount(accounts, null, 'Select an account to export'); + const exportedFilePath: string = this.exportAccount(choosenAccount, destPath); + this.log(chalk.greenBright(`Account succesfully exported to: ${ chalk.white(exportedFilePath) }`)); + } + } + } diff --git a/cli/src/commands/account/forget.ts b/cli/src/commands/account/forget.ts new file mode 100644 index 0000000000..a10f8e98ab --- /dev/null +++ b/cli/src/commands/account/forget.ts @@ -0,0 +1,29 @@ +import fs from 'fs'; +import chalk from 'chalk'; +import ExitCodes from '../../ExitCodes'; +import AccountsCommandBase from '../../base/AccountsCommandBase'; +import { NamedKeyringPair } from '../../Types'; + +export default class AccountForget extends AccountsCommandBase { + static description = 'Forget (remove) account from the list of available accounts'; + + async run() { + const accounts: NamedKeyringPair[] = this.fetchAccounts(); + + if (!accounts.length) { + this.error('No accounts found!', { exit: ExitCodes.NoAccountFound }); + } + + const choosenAccount: NamedKeyringPair = await this.promptForAccount(accounts, null, 'Select an account to forget'); + await this.requireConfirmation('Are you sure you want this account to be forgotten?'); + + const accountFilePath: string = this.getAccountFilePath(choosenAccount); + try { + fs.unlinkSync(accountFilePath); + } catch (e) { + this.error(`Could not remove account file (${ accountFilePath }). Permissions issue?`, { exit: ExitCodes.FsOperationFailed }); + } + + this.log(chalk.greenBright(`\nAccount has been forgotten!`)) + } + } diff --git a/cli/src/commands/account/import.ts b/cli/src/commands/account/import.ts new file mode 100644 index 0000000000..623f0c5e00 --- /dev/null +++ b/cli/src/commands/account/import.ts @@ -0,0 +1,46 @@ +import fs from 'fs'; +import chalk from 'chalk'; +import path from 'path'; +import ExitCodes from '../../ExitCodes'; +import AccountsCommandBase from '../../base/AccountsCommandBase'; +import { NamedKeyringPair } from '../../Types'; + +type AccountImportArgs = { + backupFilePath: string +}; + +export default class AccountImport extends AccountsCommandBase { + static description = 'Import account using JSON backup file'; + + static args = [ + { + name: 'backupFilePath', + required: true, + description: 'Path to account backup JSON file' + }, + ]; + + async run() { + const args: AccountImportArgs = this.parse(AccountImport).args; + const backupAcc: NamedKeyringPair = this.fetchAccountFromJsonFile(args.backupFilePath); + const accountName: string = backupAcc.meta.name; + const accountAddress: string = backupAcc.address; + + const sourcePath: string = args.backupFilePath; + const destPath: string = path.join(this.getAccountsDirPath(), this.generateAccountFilename(backupAcc)); + + try { + fs.copyFileSync(sourcePath, destPath); + } + catch (e) { + this.error( + 'Unexpected error while trying to copy input file! Permissions issue?', + { exit: ExitCodes.FsOperationFailed } + ); + } + + this.log(chalk.bold.greenBright(`ACCOUNT IMPORTED SUCCESFULLY!`)); + this.log(chalk.bold.white(`NAME: `), accountName); + this.log(chalk.bold.white(`ADDRESS: `), accountAddress); + } + } diff --git a/cli/src/commands/account/transferTokens.ts b/cli/src/commands/account/transferTokens.ts new file mode 100644 index 0000000000..953acb22d2 --- /dev/null +++ b/cli/src/commands/account/transferTokens.ts @@ -0,0 +1,68 @@ +import BN from 'bn.js'; +import AccountsCommandBase from '../../base/AccountsCommandBase'; +import chalk from 'chalk'; +import ExitCodes from '../../ExitCodes'; +import { formatBalance } from '@polkadot/util'; +import { Hash } from '@polkadot/types/interfaces'; +import { NamedKeyringPair } from '../../Types'; +import { checkBalance, validateAddress } from '../../helpers/validation'; +import { DerivedBalances } from '@polkadot/api-derive/types'; + +type AccountTransferArgs = { + recipient: string, + amount: string +}; + +export default class AccountTransferTokens extends AccountsCommandBase { + static description = 'Transfer tokens from currently choosen account'; + + static args = [ + { + name: 'recipient', + required: true, + description: 'Address of the transfer recipient' + }, + { + name: 'amount', + required: true, + description: 'Amount of tokens to transfer' + }, + ]; + + async run() { + const args: AccountTransferArgs = this.parse(AccountTransferTokens).args; + const selectedAccount: NamedKeyringPair = await this.getRequiredSelectedAccount(); + const amountBN: BN = new BN(args.amount); + + // Initial validation + validateAddress(args.recipient, 'Invalid recipient address'); + const accBalances: DerivedBalances = (await this.getApi().getAccountsBalancesInfo([ selectedAccount.address ]))[0]; + checkBalance(accBalances, amountBN); + + await this.requestAccountDecoding(selectedAccount); + + this.log(chalk.white('Estimating fee...')); + let estimatedFee: BN; + try { + estimatedFee = await this.getApi().estimateFee(selectedAccount, args.recipient, amountBN); + } + catch (e) { + this.error('Could not estimate the fee.', { exit: ExitCodes.UnexpectedException }); + } + const totalAmount: BN = amountBN.add(estimatedFee); + this.log(chalk.white('Estimated fee:', formatBalance(estimatedFee))); + this.log(chalk.white('Total transfer amount:', formatBalance(totalAmount))); + + checkBalance(accBalances, totalAmount); + + await this.requireConfirmation('Do you confirm the transfer?'); + + try { + const txHash: Hash = await this.getApi().transfer(selectedAccount, args.recipient, amountBN); + this.log(chalk.greenBright('Transaction succesfully sent!')); + this.log(chalk.white('Hash:', txHash.toString())); + } catch (e) { + this.error('Could not send the transaction.', { exit: ExitCodes.UnexpectedException }); + } + } + } diff --git a/cli/src/commands/api/getUri.ts b/cli/src/commands/api/getUri.ts new file mode 100644 index 0000000000..c404799d84 --- /dev/null +++ b/cli/src/commands/api/getUri.ts @@ -0,0 +1,12 @@ +import StateAwareCommandBase from '../../base/StateAwareCommandBase'; +import chalk from 'chalk'; + + +export default class ApiGetUri extends StateAwareCommandBase { + static description = 'Get current api WS provider uri'; + + async run() { + const currentUri:string = this.getPreservedState().apiUri; + this.log(chalk.green(currentUri)); + } + } diff --git a/cli/src/commands/api/inspect.ts b/cli/src/commands/api/inspect.ts new file mode 100644 index 0000000000..cd309cba42 --- /dev/null +++ b/cli/src/commands/api/inspect.ts @@ -0,0 +1,277 @@ +import { flags } from '@oclif/command'; +import { CLIError } from '@oclif/errors'; +import { displayNameValueTable } from '../../helpers/display'; +import { ApiPromise } from '@polkadot/api'; +import { getTypeDef } from '@polkadot/types'; +import { Codec, TypeDef, TypeDefInfo } from '@polkadot/types/types'; +import { ConstantCodec } from '@polkadot/api-metadata/consts/types'; +import ExitCodes from '../../ExitCodes'; +import chalk from 'chalk'; +import { NameValueObj } from '../../Types'; +import inquirer from 'inquirer'; +import ApiCommandBase from '../../base/ApiCommandBase'; + +// Command flags type +type ApiInspectFlags = { + type: string, + module: string, + method: string, + exec: boolean, + callArgs: string +}; + +// Currently "inspectable" api types +const TYPES_AVAILABLE = [ + 'query', + 'consts', +] as const; + +// String literals type based on TYPES_AVAILABLE const. +// It works as if we specified: type ApiType = 'query' | 'consts'...; +type ApiType = typeof TYPES_AVAILABLE[number]; + +// Format of the api input args (as they are specified in the CLI) +type ApiMethodInputSimpleArg = string; +// This recurring type allows the correct handling of nested types like: +// ((Type1, Type2), Option) etc. +type ApiMethodInputArg = ApiMethodInputSimpleArg | ApiMethodInputArg[]; + +export default class ApiInspect extends ApiCommandBase { + static description = + 'Lists available node API modules/methods and/or their description(s), '+ + 'or calls one of the API methods (depending on provided arguments and flags)'; + + static examples = [ + '$ api:inspect', + '$ api:inspect -t=query', + '$ api:inspect -t=query -M=members', + '$ api:inspect -t=query -M=members -m=memberProfile', + '$ api:inspect -t=query -M=members -m=memberProfile -e', + '$ api:inspect -t=query -M=members -m=memberProfile -e -a=1', + ]; + + static flags = { + type: flags.string({ + char: 't', + description: + 'Specifies the type/category of the inspected request (ie. "query", "consts" etc.).\n'+ + 'If no "--module" flag is provided then all available modules in that type will be listed.\n'+ + 'If this flag is not provided then all available types will be listed.', + }), + module: flags.string({ + char: 'M', + description: + 'Specifies the api module, ie. "system", "staking" etc.\n'+ + 'If no "--method" flag is provided then all methods in that module will be listed along with the descriptions.', + dependsOn: ['type'], + }), + method: flags.string({ + char: 'm', + description: 'Specifies the api method to call/describe.', + dependsOn: ['module'], + }), + exec: flags.boolean({ + char: 'e', + description: 'Provide this flag if you want to execute the actual call, instead of displaying the method description (which is default)', + dependsOn: ['method'], + }), + callArgs: flags.string({ + char: 'a', + description: + 'Specifies the arguments to use when calling a method. Multiple arguments can be separated with a comma, ie. "-a=arg1,arg2".\n'+ + 'You can omit this flag even if the method requires some aguments.\n'+ + 'In that case you will be promted to provide value for each required argument.\n' + + 'Ommiting this flag is recommended when input parameters are of more complex types (and it\'s hard to specify them as just simple comma-separated strings)', + dependsOn: ['exec'], + }) + }; + + getMethodMeta(apiType: ApiType, apiModule: string, apiMethod: string) { + if (apiType === 'query') { + return this.getOriginalApi().query[apiModule][apiMethod].creator.meta; + } + else { + // Currently the only other optoin is api.consts + const method:ConstantCodec = this.getOriginalApi().consts[apiModule][apiMethod]; + return method.meta; + } + } + + getMethodDescription(apiType: ApiType, apiModule: string, apiMethod: string): string { + let description:string = this.getMethodMeta(apiType, apiModule, apiMethod).documentation.join(' '); + return description || 'No description available.'; + } + + getQueryMethodParamsTypes(apiModule: string, apiMethod: string): string[] { + const method = this.getOriginalApi().query[apiModule][apiMethod]; + const { type } = method.creator.meta; + if (type.isDoubleMap) { + return [ type.asDoubleMap.key1.toString(), type.asDoubleMap.key2.toString() ]; + } + if (type.isMap) { + return type.asMap.linked.isTrue ? [ `Option<${type.asMap.key.toString()}>` ] : [ type.asMap.key.toString() ]; + } + return []; + } + + getMethodReturnType(apiType: ApiType, apiModule: string, apiMethod: string): string { + if (apiType === 'query') { + const method = this.getOriginalApi().query[apiModule][apiMethod]; + const { meta: { type, modifier } } = method.creator; + if (type.isDoubleMap) { + return type.asDoubleMap.value.toString(); + } + if (modifier.isOptional) { + return `Option<${type.toString()}>`; + } + } + // Fallback for "query" and default for "consts" + return this.getMethodMeta(apiType, apiModule, apiMethod).type.toString(); + } + + // Validate the flags - throws an error if flags.type, flags.module or flags.method is invalid / does not exist in the api. + // Returns type, module and method which validity we can be sure about (notice they may still be "undefined" if weren't provided). + validateFlags(api: ApiPromise, flags: ApiInspectFlags): { apiType: ApiType | undefined, apiModule: string | undefined, apiMethod: string | undefined } { + let apiType: ApiType | undefined = undefined; + const { module: apiModule, method: apiMethod } = flags; + + if (flags.type !== undefined) { + const availableTypes: readonly string[] = TYPES_AVAILABLE; + if (!availableTypes.includes(flags.type)) { + throw new CLIError('Such type is not available', { exit: ExitCodes.InvalidInput }); + } + apiType = flags.type; + if (apiModule !== undefined) { + if (!api[apiType][apiModule]) { + throw new CLIError('Such module was not found', { exit: ExitCodes.InvalidInput }); + } + if (apiMethod !== undefined && !api[apiType][apiModule][apiMethod]) { + throw new CLIError('Such method was not found', { exit: ExitCodes.InvalidInput }); + } + } + } + + return { apiType, apiModule, apiMethod }; + } + + // Prompt for simple value (string) + async promptForSimple(typeName: string): Promise { + const userInput = await inquirer.prompt([{ + name: 'providedValue', + message: `Provide value for ${ typeName }`, + type: 'input' + } ]) + return userInput.providedValue; + } + + // Prompt for optional value (returns undefined if user refused to provide) + async promptForOption(typeDef: TypeDef): Promise { + const userInput = await inquirer.prompt([{ + name: 'confirmed', + message: `Do you want to provide the optional ${ typeDef.type } parameter?`, + type: 'confirm' + } ]); + + if (userInput.confirmed) { + const subtype = typeDef.sub; // We assume that Opion always has a single subtype + let value = await this.promptForParam(subtype.type); + return value; + } + } + + // Prompt for tuple - returns array of values + async promptForTuple(typeDef: TypeDef): Promise<(ApiMethodInputArg)[]> { + let result: ApiMethodInputArg[] = []; + + if (!typeDef.sub) return [ await this.promptForSimple(typeDef.type) ]; + + const subtypes: TypeDef[] = Array.isArray(typeDef.sub) ? typeDef.sub : [ typeDef.sub ]; + + for (let subtype of subtypes) { + let inputParam = await this.promptForParam(subtype.type); + if (inputParam !== undefined) result.push(inputParam); + } + + return result; + } + + // Prompt for param based on "paramType" string (ie. Option) + async promptForParam(paramType: string): Promise { + const typeDef: TypeDef = getTypeDef(paramType); + if (typeDef.info === TypeDefInfo.Option) return await this.promptForOption(typeDef); + else if (typeDef.info === TypeDefInfo.Tuple) return await this.promptForTuple(typeDef); + else return await this.promptForSimple(typeDef.type); + } + + // Request values for params using array of param types (strings) + async requestParamsValues(paramTypes: string[]): Promise { + let result: ApiMethodInputArg[] = []; + for (let [key, paramType] of Object.entries(paramTypes)) { + this.log(chalk.bold.white(`Parameter no. ${ parseInt(key)+1 } (${ paramType }):`)); + let paramValue = await this.promptForParam(paramType); + if (paramValue !== undefined) result.push(paramValue); + } + + return result; + } + + async run() { + const api: ApiPromise = this.getOriginalApi(); + const flags: ApiInspectFlags = this.parse(ApiInspect).flags; + const availableTypes: readonly string[] = TYPES_AVAILABLE; + const { apiType, apiModule, apiMethod } = this.validateFlags(api, flags); + + // Executing a call + if (apiType && apiModule && apiMethod && flags.exec) { + let result: Codec; + + if (apiType === 'query') { + // Api query - call with (or without) arguments + let args: ApiMethodInputArg[] = flags.callArgs ? flags.callArgs.split(',') : []; + const paramsTypes: string[] = this.getQueryMethodParamsTypes(apiModule, apiMethod); + if (args.length < paramsTypes.length) { + this.warn('Some parameters are missing! Please, provide the missing parameters:'); + let missingParamsValues = await this.requestParamsValues(paramsTypes.slice(args.length)); + args = args.concat(missingParamsValues); + } + result = await api.query[apiModule][apiMethod](...args); + } + else { + // Api consts - just assign the value + result = api.consts[apiModule][apiMethod]; + } + + this.log(chalk.green(result.toString())); + } + // Describing a method + else if (apiType && apiModule && apiMethod) { + this.log(chalk.bold.white(`${ apiType }.${ apiModule }.${ apiMethod }`)); + const description: string = this.getMethodDescription(apiType, apiModule, apiMethod); + this.log(`\n${ description }\n`); + let typesRows: NameValueObj[] = []; + if (apiType === 'query') { + typesRows.push({ name: 'Params:', value: this.getQueryMethodParamsTypes(apiModule, apiMethod).join(', ') || '-' }); + } + typesRows.push({ name: 'Returns:', value: this.getMethodReturnType(apiType, apiModule, apiMethod) }); + displayNameValueTable(typesRows); + } + // Displaying all available methods + else if (apiType && apiModule) { + const module = api[apiType][apiModule]; + const rows: NameValueObj[] = Object.keys(module).map((key: string) => { + return { name: key, value: this.getMethodDescription(apiType, apiModule, key) }; + }); + displayNameValueTable(rows); + } + // Displaying all available modules + else if (apiType) { + this.log(chalk.bold.white('Available modules:')); + this.log(Object.keys(api[apiType]).map(key => chalk.white(key)).join('\n')); + } + // Displaying all available types + else { + this.log(chalk.bold.white('Available types:')); + this.log(availableTypes.map(type => chalk.white(type)).join('\n')); + } + } +} diff --git a/cli/src/commands/api/setUri.ts b/cli/src/commands/api/setUri.ts new file mode 100644 index 0000000000..591fc12889 --- /dev/null +++ b/cli/src/commands/api/setUri.ts @@ -0,0 +1,28 @@ +import StateAwareCommandBase from '../../base/StateAwareCommandBase'; +import chalk from 'chalk'; +import { WsProvider } from '@polkadot/api'; +import ExitCodes from '../../ExitCodes'; + +type ApiSetUriArgs = { uri: string }; + +export default class ApiSetUri extends StateAwareCommandBase { + static description = 'Set api WS provider uri'; + static args = [ + { + name: 'uri', + required: true, + description: 'Uri of the node api WS provider' + } + ]; + + async run() { + const args: ApiSetUriArgs = this.parse(ApiSetUri).args; + try { + new WsProvider(args.uri); + } catch(e) { + this.error('The WS provider uri seems to be incorrect', { exit: ExitCodes.InvalidInput }); + } + await this.setPreservedState({ apiUri: args.uri }); + this.log(chalk.greenBright('Api uri successfuly changed! New uri: ') + chalk.white(args.uri)) + } + } diff --git a/cli/src/commands/council/info.ts b/cli/src/commands/council/info.ts new file mode 100644 index 0000000000..d68b0215ac --- /dev/null +++ b/cli/src/commands/council/info.ts @@ -0,0 +1,57 @@ +import { ElectionStage } from '@joystream/types'; +import { formatNumber, formatBalance } from '@polkadot/util'; +import { BlockNumber } from '@polkadot/types/interfaces'; +import { CouncilInfoObj, NameValueObj } from '../../Types'; +import { displayHeader, displayNameValueTable } from '../../helpers/display'; +import ApiCommandBase from '../../base/ApiCommandBase'; + +export default class CouncilInfo extends ApiCommandBase { + static description = 'Get current council and council elections information'; + + displayInfo(infoObj: CouncilInfoObj) { + const { activeCouncil = [], round, stage } = infoObj; + + displayHeader('Council'); + const councilRows: NameValueObj[] = [ + { name: 'Elected:', value: activeCouncil.length ? 'YES' : 'NO' }, + { name: 'Members:', value: activeCouncil.length.toString() }, + { name: 'Term ends at block:', value: `#${formatNumber(infoObj.termEndsAt) }` }, + ]; + displayNameValueTable(councilRows); + + + displayHeader('Election'); + let electionTableRows: NameValueObj[] = [ + { name: 'Running:', value: stage && stage.isSome ? 'YES' : 'NO' }, + { name: 'Election round:', value: formatNumber(round) } + ]; + if (stage && stage.isSome) { + const stageValue = stage.value; + const stageName: string = stageValue.type; + const stageEndsAt = stageValue.value; + electionTableRows.push({ name: 'Stage:', value: stageName }); + electionTableRows.push({ name: 'Stage ends at block:', value: `#${stageEndsAt}` }); + } + displayNameValueTable(electionTableRows); + + displayHeader('Configuration'); + const isAutoStart = (infoObj.autoStart || false).valueOf(); + const configTableRows: NameValueObj[] = [ + { name: 'Auto-start elections:', value: isAutoStart ? 'YES' : 'NO' }, + { name: 'New term duration:', value: formatNumber(infoObj.newTermDuration) }, + { name: 'Candidacy limit:', value: formatNumber(infoObj.candidacyLimit) }, + { name: 'Council size:', value: formatNumber(infoObj.councilSize) }, + { name: 'Min. council stake:', value: formatBalance(infoObj.minCouncilStake) }, + { name: 'Min. voting stake:', value: formatBalance(infoObj.minVotingStake) }, + { name: 'Announcing period:', value: `${ formatNumber(infoObj.announcingPeriod) } blocks` }, + { name: 'Voting period:', value: `${ formatNumber(infoObj.votingPeriod) } blocks` }, + { name: 'Revealing period:', value: `${ formatNumber(infoObj.revealingPeriod) } blocks` } + ]; + displayNameValueTable(configTableRows); + } + + async run() { + const infoObj = await this.getApi().getCouncilInfo(); + this.displayInfo(infoObj); + } + } diff --git a/cli/src/helpers/display.ts b/cli/src/helpers/display.ts new file mode 100644 index 0000000000..13a189c938 --- /dev/null +++ b/cli/src/helpers/display.ts @@ -0,0 +1,33 @@ +import { cli } from 'cli-ux'; +import chalk from 'chalk'; +import { NameValueObj } from '../Types'; + +export function displayHeader(caption: string, placeholderSign: string = '_', size: number = 50) { + let singsPerSide: number = Math.floor((size - (caption.length + 2)) / 2); + let finalStr: string = ''; + for (let i = 0; i < singsPerSide; ++i) finalStr += placeholderSign; + finalStr += ` ${ caption} `; + while (finalStr.length < size) finalStr += placeholderSign; + + process.stdout.write("\n" + chalk.bold.blueBright(finalStr) + "\n\n"); +} + +export function displayNameValueTable(rows: NameValueObj[]) { + cli.table( + rows, + { + name: { minWidth: 30, get: row => chalk.bold.white(row.name) }, + value: { get: row => chalk.white(row.value) } + }, + { 'no-header': true } + ); +} + +export function toFixedLength(text: string, length: number, spacesOnLeft = false): string { + if (text.length > length && length > 3) { + return text.slice(0, length-3) + '...'; + } + while(text.length < length) { spacesOnLeft ? text = ' '+text : text += ' ' }; + + return text; +} diff --git a/cli/src/helpers/validation.ts b/cli/src/helpers/validation.ts new file mode 100644 index 0000000000..cce907b13d --- /dev/null +++ b/cli/src/helpers/validation.ts @@ -0,0 +1,19 @@ +import BN from 'bn.js'; +import ExitCodes from '../ExitCodes'; +import { decodeAddress } from '@polkadot/util-crypto'; +import { DerivedBalances } from '@polkadot/api-derive/types'; +import { CLIError } from '@oclif/errors'; + +export function validateAddress(address: string, errorMessage: string = 'Invalid address'): void { + try { + decodeAddress(address); + } catch (e) { + throw new CLIError(errorMessage, { exit: ExitCodes.InvalidInput }); + } +} + +export function checkBalance(accBalances: DerivedBalances, requiredBalance: BN): void { + if (requiredBalance.gt(accBalances.availableBalance)) { + throw new CLIError('Not enough balance available', { exit: ExitCodes.InvalidInput }); + } +} diff --git a/cli/src/index.ts b/cli/src/index.ts new file mode 100644 index 0000000000..4caa481eee --- /dev/null +++ b/cli/src/index.ts @@ -0,0 +1 @@ +export {run} from '@oclif/command' diff --git a/cli/test/commands/council/info.test.ts b/cli/test/commands/council/info.test.ts new file mode 100644 index 0000000000..2d455f9eb6 --- /dev/null +++ b/cli/test/commands/council/info.test.ts @@ -0,0 +1,11 @@ +import {expect, test} from '@oclif/test' + +describe('info', () => { + test + .stdout() + .command(['council:info']) + .exit(0) + .it('displays "Council" string', ctx => { + expect(ctx.stdout).to.contain('Council') + }) +}) diff --git a/cli/test/mocha.opts b/cli/test/mocha.opts new file mode 100644 index 0000000000..73fb8366ae --- /dev/null +++ b/cli/test/mocha.opts @@ -0,0 +1,5 @@ +--require ts-node/register +--watch-extensions ts +--recursive +--reporter spec +--timeout 5000 diff --git a/cli/test/tsconfig.json b/cli/test/tsconfig.json new file mode 100644 index 0000000000..95898fcedf --- /dev/null +++ b/cli/test/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig", + "compilerOptions": { + "noEmit": true + }, + "references": [ + {"path": ".."} + ] +} diff --git a/cli/tsconfig.json b/cli/tsconfig.json new file mode 100644 index 0000000000..c6477fa01e --- /dev/null +++ b/cli/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "declaration": true, + "importHelpers": true, + "module": "commonjs", + "outDir": "lib", + "rootDir": "src", + "strict": true, + "target": "es2017", + "esModuleInterop": true + }, + "include": [ + "src/**/*" + ] +} diff --git a/devops/dockerfiles/node-and-runtime/Dockerfile b/devops/dockerfiles/node-and-runtime/Dockerfile index ed0a8c1ce3..72f125eb1e 100644 --- a/devops/dockerfiles/node-and-runtime/Dockerfile +++ b/devops/dockerfiles/node-and-runtime/Dockerfile @@ -14,6 +14,13 @@ COPY --from=builder /joystream/target/release/wbuild/joystream-node-runtime/joys # confirm it works RUN /joystream/node --version +# https://manpages.debian.org/stretch/coreutils/b2sum.1.en.html +# RUN apt-get install coreutils +# print the blake2 256 hash of the wasm blob +RUN b2sum -l 256 /joystream/runtime.compact.wasm +# print the blake2 512 hash of the wasm blob +RUN b2sum -l 512 /joystream/runtime.compact.wasm + EXPOSE 30333 9933 9944 # Use these volumes to persits chain state and keystore, eg.: diff --git a/devops/dockerfiles/rust-builder/Dockerfile b/devops/dockerfiles/rust-builder/Dockerfile index 26e35b69ab..c6e0d9283f 100644 --- a/devops/dockerfiles/rust-builder/Dockerfile +++ b/devops/dockerfiles/rust-builder/Dockerfile @@ -1,4 +1,4 @@ -FROM liuchong/rustup:1.42.0 AS builder +FROM liuchong/rustup:1.43.0 AS builder LABEL description="Rust and WASM build environment for joystream and substrate" WORKDIR /setup diff --git a/devops/git-hooks/pre-commit b/devops/git-hooks/pre-commit new file mode 100755 index 0000000000..b8bc924550 --- /dev/null +++ b/devops/git-hooks/pre-commit @@ -0,0 +1,5 @@ +#!/bin/sh +set -e + +echo 'cargo fmt --all -- --check' +cargo fmt --all -- --check \ No newline at end of file diff --git a/devops/git-hooks/pre-push b/devops/git-hooks/pre-push new file mode 100755 index 0000000000..b9ffbcb184 --- /dev/null +++ b/devops/git-hooks/pre-push @@ -0,0 +1,10 @@ +#!/bin/sh +set -e + +export BUILD_DUMMY_WASM_BINARY=1 + +echo '+cargo test --all' +cargo test --all + +echo '+cargo clippy --all -- -D warnings' +cargo clippy --all -- -D warnings \ No newline at end of file diff --git a/node/Cargo.toml b/node/Cargo.toml index 12aa4b4890..3464da2bf2 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -3,7 +3,7 @@ authors = ['Joystream'] build = 'build.rs' edition = '2018' name = 'joystream-node' -version = '2.1.3' +version = '2.2.0' default-run = "joystream-node" [[bin]] diff --git a/node/src/chain_spec.rs b/node/src/chain_spec.rs index 5bc2015558..250fa271d0 100644 --- a/node/src/chain_spec.rs +++ b/node/src/chain_spec.rs @@ -14,13 +14,18 @@ // You should have received a copy of the GNU General Public License // along with Joystream node. If not, see . +// Clippy linter warning. +#![allow(clippy::identity_op)] // disable it because we use such syntax for a code readability + // Example: voting_period: 1 * DAY + use node_runtime::{ versioned_store::InputValidationLengthConstraint as VsInputValidation, ActorsConfig, AuthorityDiscoveryConfig, BabeConfig, Balance, BalancesConfig, ContentWorkingGroupConfig, CouncilConfig, CouncilElectionConfig, DataObjectStorageRegistryConfig, - DataObjectTypeRegistryConfig, GrandpaConfig, ImOnlineConfig, IndicesConfig, MembersConfig, - Perbill, ProposalsConfig, SessionConfig, SessionKeys, Signature, StakerStatus, StakingConfig, - SudoConfig, SystemConfig, VersionedStoreConfig, DAYS, WASM_BINARY, + DataObjectTypeRegistryConfig, ElectionParameters, GrandpaConfig, ImOnlineConfig, IndicesConfig, + MembersConfig, MigrationConfig, Perbill, ProposalsCodexConfig, SessionConfig, SessionKeys, + Signature, StakerStatus, StakingConfig, SudoConfig, SystemConfig, VersionedStoreConfig, DAYS, + WASM_BINARY, }; pub use node_runtime::{AccountId, GenesisConfig}; use primitives::{sr25519, Pair, Public}; @@ -30,7 +35,6 @@ use babe_primitives::AuthorityId as BabeId; use grandpa_primitives::AuthorityId as GrandpaId; use im_online::sr25519::AuthorityId as ImOnlineId; use serde_json as json; -use substrate_service; type AccountPublic = ::Signer; @@ -154,7 +158,7 @@ impl Alternative { } fn new_vs_validation(min: u16, max_min_diff: u16) -> VsInputValidation { - return VsInputValidation { min, max_min_diff }; + VsInputValidation { min, max_min_diff } } pub fn chain_spec_properties() -> json::map::Map { @@ -180,6 +184,9 @@ pub fn testnet_genesis( const STASH: Balance = 20 * DOLLARS; const ENDOWMENT: Balance = 100_000 * DOLLARS; + // default codex proposals config parameters + let cpcp = node_runtime::ProposalsConfigParameters::default(); + GenesisConfig { system: Some(SystemConfig { code: WASM_BINARY.to_vec(), @@ -218,9 +225,7 @@ pub fn testnet_genesis( slash_reward_fraction: Perbill::from_percent(10), ..Default::default() }), - sudo: Some(SudoConfig { - key: root_key.clone(), - }), + sudo: Some(SudoConfig { key: root_key }), babe: Some(BabeConfig { authorities: vec![], }), @@ -235,24 +240,16 @@ pub fn testnet_genesis( }), election: Some(CouncilElectionConfig { auto_start: true, - announcing_period: 3 * DAYS, - voting_period: 1 * DAYS, - revealing_period: 1 * DAYS, - council_size: 12, - candidacy_limit: 25, - min_council_stake: 10 * DOLLARS, - new_term_duration: 14 * DAYS, - min_voting_stake: 1 * DOLLARS, - }), - proposals: Some(ProposalsConfig { - approval_quorum: 66, - min_stake: 2 * DOLLARS, - cancellation_fee: 10 * CENTS, - rejection_fee: 1 * DOLLARS, - voting_period: 2 * DAYS, - name_max_len: 512, - description_max_len: 10_000, - wasm_code_max_len: 2_000_000, + election_parameters: ElectionParameters { + announcing_period: 3 * DAYS, + voting_period: 1 * DAYS, + revealing_period: 1 * DAYS, + council_size: 12, + candidacy_limit: 25, + min_council_stake: 10 * DOLLARS, + new_term_duration: 14 * DAYS, + min_voting_stake: 1 * DOLLARS, + }, }), members: Some(MembersConfig { default_paid_membership_fee: 100u128, @@ -282,7 +279,7 @@ pub fn testnet_genesis( class_description_constraint: new_vs_validation(1, 999), }), content_wg: Some(ContentWorkingGroupConfig { - mint_capacity: 100000, + mint_capacity: 100_000, curator_opening_by_id: vec![], next_curator_opening_id: 0, curator_application_by_id: vec![], @@ -305,5 +302,36 @@ pub fn testnet_genesis( channel_banner_constraint: crate::forum_config::new_validation(5, 1024), channel_title_constraint: crate::forum_config::new_validation(5, 1024), }), + migration: Some(MigrationConfig {}), + proposals_codex: Some(ProposalsCodexConfig { + set_validator_count_proposal_voting_period: cpcp + .set_validator_count_proposal_voting_period, + set_validator_count_proposal_grace_period: cpcp + .set_validator_count_proposal_grace_period, + runtime_upgrade_proposal_voting_period: cpcp.runtime_upgrade_proposal_voting_period, + runtime_upgrade_proposal_grace_period: cpcp.runtime_upgrade_proposal_grace_period, + text_proposal_voting_period: cpcp.text_proposal_voting_period, + text_proposal_grace_period: cpcp.text_proposal_grace_period, + set_election_parameters_proposal_voting_period: cpcp + .set_election_parameters_proposal_voting_period, + set_election_parameters_proposal_grace_period: cpcp + .set_election_parameters_proposal_grace_period, + set_content_working_group_mint_capacity_proposal_voting_period: cpcp + .set_content_working_group_mint_capacity_proposal_voting_period, + set_content_working_group_mint_capacity_proposal_grace_period: cpcp + .set_content_working_group_mint_capacity_proposal_grace_period, + set_lead_proposal_voting_period: cpcp.set_lead_proposal_voting_period, + set_lead_proposal_grace_period: cpcp.set_lead_proposal_voting_period, + spending_proposal_voting_period: cpcp.spending_proposal_voting_period, + spending_proposal_grace_period: cpcp.spending_proposal_grace_period, + evict_storage_provider_proposal_voting_period: cpcp + .evict_storage_provider_proposal_voting_period, + evict_storage_provider_proposal_grace_period: cpcp + .evict_storage_provider_proposal_grace_period, + set_storage_role_parameters_proposal_voting_period: cpcp + .set_storage_role_parameters_proposal_voting_period, + set_storage_role_parameters_proposal_grace_period: cpcp + .set_storage_role_parameters_proposal_grace_period, + }), } } diff --git a/node/src/cli.rs b/node/src/cli.rs index 78339c7a65..9c33d231aa 100644 --- a/node/src/cli.rs +++ b/node/src/cli.rs @@ -90,7 +90,7 @@ where let exit = e .into_exit() .map_err(|_| error::Error::Other("Exit future failed.".into())); - let service = service.map_err(|err| error::Error::Service(err)); + let service = service.map_err(error::Error::Service); let select = service.select(exit).map(|_| ()).map_err(|(err, _)| err); runtime.block_on(select) }; diff --git a/node/src/forum_config/from_serialized.rs b/node/src/forum_config/from_serialized.rs index 06b0ee0b4c..4b512b0a0c 100644 --- a/node/src/forum_config/from_serialized.rs +++ b/node/src/forum_config/from_serialized.rs @@ -1,7 +1,9 @@ +#![allow(clippy::type_complexity)] + use super::new_validation; use node_runtime::{ - forum::{Category, CategoryId, Post, PostId, Thread, ThreadId}, - AccountId, BlockNumber, ForumConfig, Moment, + forum::{Category, CategoryId, Post, Thread}, + AccountId, BlockNumber, ForumConfig, Moment, PostId, ThreadId, }; use serde::Deserialize; use serde_json::Result; @@ -9,8 +11,11 @@ use serde_json::Result; #[derive(Deserialize)] struct ForumData { categories: Vec<(CategoryId, Category)>, - posts: Vec<(PostId, Post)>, - threads: Vec<(ThreadId, Thread)>, + posts: Vec<( + PostId, + Post, + )>, + threads: Vec<(ThreadId, Thread)>, } fn parse_forum_json() -> Result { diff --git a/node/src/forum_config/mod.rs b/node/src/forum_config/mod.rs index e72963da72..c99f69b4ce 100644 --- a/node/src/forum_config/mod.rs +++ b/node/src/forum_config/mod.rs @@ -6,5 +6,5 @@ pub mod from_serialized; use node_runtime::forum::InputValidationLengthConstraint; pub fn new_validation(min: u16, max_min_diff: u16) -> InputValidationLengthConstraint { - return InputValidationLengthConstraint { min, max_min_diff }; + InputValidationLengthConstraint { min, max_min_diff } } diff --git a/node/src/service.rs b/node/src/service.rs index 2d9a6f24b3..703bb6f5f2 100644 --- a/node/src/service.rs +++ b/node/src/service.rs @@ -16,6 +16,12 @@ #![warn(unused_extern_crates)] +// Clippy linter warning. +#![allow(clippy::type_complexity)] // disable it because this is foreign code and can be changed any time + +// Clippy linter warning. +#![allow(clippy::redundant_closure_call)] // disable it because of the substrate lib design + //! Service and ServiceFactory implementation. Specialized wrapper over substrate service. use client_db::Backend; @@ -43,9 +49,9 @@ construct_simple_protocol! { // Declare an instance of the native executor named `Executor`. Include the wasm binary as the // equivalent wasm code. native_executor_instance!( - pub Executor, - node_runtime::api::dispatch, - node_runtime::native_version + pub Executor, + node_runtime::api::dispatch, + node_runtime::native_version ); /// Starts a `ServiceBuilder` for a full service. diff --git a/package.json b/package.json new file mode 100644 index 0000000000..152151b372 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "private": true, + "name": "joystream", + "license": "GPL-3.0-only", + "scripts": { + "test": "yarn && yarn workspaces run test", + "test-migration": "yarn && yarn workspaces run test-migration" + }, + "workspaces": [ + "tests/network-tests" + ], + "devDependencies": { + "husky": "^4.2.5" + }, + "husky": { + "hooks": { + "pre-commit": "devops/git-hooks/pre-commit", + "pre-push": "devops/git-hooks/pre-push" + } + } +} diff --git a/runtime-modules/common/src/currency.rs b/runtime-modules/common/src/currency.rs index ac0e8551ae..50d9ebaef0 100644 --- a/runtime-modules/common/src/currency.rs +++ b/runtime-modules/common/src/currency.rs @@ -1,6 +1,5 @@ use sr_primitives::traits::Convert; use srml_support::traits::{Currency, LockableCurrency, ReservableCurrency}; -use system; pub trait GovernanceCurrency: system::Trait + Sized { type Currency: Currency diff --git a/runtime-modules/common/src/lib.rs b/runtime-modules/common/src/lib.rs index 23177ac457..e48bf36060 100644 --- a/runtime-modules/common/src/lib.rs +++ b/runtime-modules/common/src/lib.rs @@ -2,3 +2,4 @@ #![cfg_attr(not(feature = "std"), no_std)] pub mod currency; +pub mod origin_validator; diff --git a/runtime-modules/common/src/origin_validator.rs b/runtime-modules/common/src/origin_validator.rs new file mode 100644 index 0000000000..336331dda1 --- /dev/null +++ b/runtime-modules/common/src/origin_validator.rs @@ -0,0 +1,5 @@ +/// Abstract validator for the origin(account_id) and actor_id (eg.: thread author id). +pub trait ActorOriginValidator { + /// Check for valid combination of origin and actor_id + fn ensure_actor_origin(origin: Origin, actor_id: ActorId) -> Result; +} diff --git a/runtime-modules/content-working-group/src/genesis.rs b/runtime-modules/content-working-group/src/genesis.rs index fcc4a4396b..6cf74f39c1 100644 --- a/runtime-modules/content-working-group/src/genesis.rs +++ b/runtime-modules/content-working-group/src/genesis.rs @@ -43,11 +43,11 @@ pub struct GenesisConfigBuilder { } impl GenesisConfigBuilder { - /* - pub fn set_mint(mut self, mint: ::MintId) -> Self { - self.mint = mint; + pub fn with_mint_capacity(mut self, capacity: minting::BalanceOf) -> Self { + self.mint_capacity = capacity; self } + /* pub fn set_channel_handle_constraint(mut self, constraint: InputValidationLengthConstraint) -> Self { self.channel_description_constraint = constraint; self diff --git a/runtime-modules/content-working-group/src/lib.rs b/runtime-modules/content-working-group/src/lib.rs index b0c6aeea93..90f2c5e8e9 100755 --- a/runtime-modules/content-working-group/src/lib.rs +++ b/runtime-modules/content-working-group/src/lib.rs @@ -1,3 +1,10 @@ +// Clippy linter warning. TODO: remove after the Constaninople release +#![allow(clippy::type_complexity)] +// disable it because of possible frontend API break + +// Clippy linter warning. TODO: refactor "this function has too many argument" +#![allow(clippy::too_many_arguments)] // disable it because of possible API break + // Ensure we're `no_std` when compiling for Wasm. #![cfg_attr(not(feature = "std"), no_std)] @@ -15,6 +22,7 @@ use serde::{Deserialize, Serialize}; use codec::{Decode, Encode}; // Codec //use rstd::collections::btree_map::BTreeMap; use membership::{members, role_types}; +use rstd::borrow::ToOwned; use rstd::collections::btree_map::BTreeMap; use rstd::collections::btree_set::BTreeSet; use rstd::convert::From; @@ -310,12 +318,12 @@ impl CuratorExitSummary { pub fn new( origin: &CuratorExitInitiationOrigin, initiated_at_block_number: &BlockNumber, - rationale_text: &Vec, + rationale_text: &[u8], ) -> Self { CuratorExitSummary { origin: (*origin).clone(), initiated_at_block_number: (*initiated_at_block_number).clone(), - rationale_text: (*rationale_text).clone(), + rationale_text: rationale_text.to_owned(), } } } @@ -1080,7 +1088,9 @@ decl_event! { CuratorApplicationId = CuratorApplicationId, CuratorId = CuratorId, CuratorApplicationIdToCuratorIdMap = CuratorApplicationIdToCuratorIdMap, + MintBalanceOf = minting::BalanceOf, ::AccountId, + ::MintId, { ChannelCreated(ChannelId), ChannelOwnershipTransferred(ChannelId), @@ -1100,6 +1110,8 @@ decl_event! { CuratorRewardAccountUpdated(CuratorId, AccountId), ChannelUpdatedByCurationActor(ChannelId), ChannelCreationEnabledUpdated(bool), + MintCapacityIncreased(MintId, MintBalanceOf, MintBalanceOf), + MintCapacityDecreased(MintId, MintBalanceOf, MintBalanceOf), } } @@ -1186,12 +1198,12 @@ decl_module! { ChannelById::::insert(next_channel_id, new_channel); // Add id to ChannelIdByHandle under handle - ChannelIdByHandle::::insert(handle.clone(), next_channel_id); + ChannelIdByHandle::::insert(handle, next_channel_id); // Increment NextChannelId NextChannelId::::mutate(|id| *id += as One>::one()); - /// CREDENTIAL STUFF /// + // CREDENTIAL STUFF // // Dial out to membership module and inform about new role as channe owner. let registered_role = >::register_role_on_member(owner, &member_in_role).is_ok(); @@ -1227,7 +1239,7 @@ decl_module! { // Construct new channel with altered properties let new_channel = Channel { owner: new_owner, - role_account: new_role_account.clone(), + role_account: new_role_account, ..channel }; @@ -1304,14 +1316,14 @@ decl_module! { Self::update_channel( &channel_id, - &None, // verified + None, // verified &new_handle, &new_title, &new_description, &new_avatar, &new_banner, - &new_publication_status, - &None // curation_status + new_publication_status, + None // curation_status ); } @@ -1333,14 +1345,14 @@ decl_module! { Self::update_channel( &channel_id, - &new_verified, + new_verified, &None, // handle &None, // title &None, // description, &None, // avatar &None, // banner - &None, // publication_status - &new_curation_status + None, // publication_status + new_curation_status ); } @@ -1460,7 +1472,7 @@ decl_module! { let successful_iter = successful_curator_application_ids .iter() // recover curator application from id - .map(|curator_application_id| { Self::ensure_curator_application_exists(curator_application_id) }) + .map(|curator_application_id| { Self::ensure_curator_application_exists(curator_application_id)}) // remove Err cases, i.e. non-existing applications .filter_map(|result| result.ok()); @@ -1470,8 +1482,7 @@ decl_module! { // Ensure all curator applications exist let number_of_successful_applications = successful_iter .clone() - .collect::>() - .len(); + .count(); ensure!( number_of_successful_applications == num_provided_successful_curator_application_ids, @@ -1489,7 +1500,7 @@ decl_module! { .clone() .map(|(successful_curator_application, _, _)| successful_curator_application.member_id) .filter_map(|successful_member_id| Self::ensure_can_register_curator_role_on_member(&successful_member_id).ok() ) - .collect::>().len(); + .count(); ensure!( num_successful_applications_that_can_register_as_curator == num_provided_successful_curator_application_ids, @@ -1883,7 +1894,7 @@ decl_module! { origin, curator_id: CuratorId, rationale_text: Vec - ) { + ) { // Ensure lead is set and is origin signer Self::ensure_origin_is_set_lead(origin)?; @@ -1906,103 +1917,23 @@ decl_module! { ); } - /* - * Root origin routines for managing lead. - */ - - - /// Introduce a lead when one is not currently set. - pub fn set_lead(origin, member: T::MemberId, role_account: T::AccountId) { - + /// Replace the current lead. First unsets the active lead if there is one. + /// If a value is provided for new_lead it will then set that new lead. + /// It is responsibility of the caller to ensure the new lead can be set + /// to avoid the lead role being vacant at the end of the call. + pub fn replace_lead(origin, new_lead: Option<(T::MemberId, T::AccountId)>) { // Ensure root is origin ensure_root(origin)?; - // Ensure there is no current lead - ensure!( - >::get().is_none(), - MSG_CURRENT_LEAD_ALREADY_SET - ); - - // Ensure that member can actually become lead - let new_lead_id = >::get(); - - let new_lead_role = - role_types::ActorInRole::new(role_types::Role::CuratorLead, new_lead_id); - - let _profile = >::can_register_role_on_member( - &member, - &role_types::ActorInRole::new(role_types::Role::CuratorLead, new_lead_id), - )?; - - // - // == MUTATION SAFE == - // - - // Construct lead - let new_lead = Lead { - role_account: role_account.clone(), - reward_relationship: None, - inducted: >::block_number(), - stage: LeadRoleState::Active, - }; - - // Store lead - >::insert(new_lead_id, new_lead); - - // Update current lead - >::put(new_lead_id); // Some(new_lead_id) - - // Update next lead counter - >::mutate(|id| *id += as One>::one()); - - // Register in role - let registered_role = - >::register_role_on_member(member, &new_lead_role).is_ok(); - - assert!(registered_role); - - // Trigger event - Self::deposit_event(RawEvent::LeadSet(new_lead_id)); - } - - /// Evict the currently unset lead - pub fn unset_lead(origin) { - - // Ensure root is origin - ensure_root(origin)?; - - // Ensure there is a lead set - let (lead_id,lead) = Self::ensure_lead_is_set()?; - - // - // == MUTATION SAFE == - // - - // Unregister from role in membership model - let current_lead_role = role_types::ActorInRole{ - role: role_types::Role::CuratorLead, - actor_id: lead_id - }; - - let unregistered_role = >::unregister_role(current_lead_role).is_ok(); - - assert!(unregistered_role); - - // Update lead stage as exited - let current_block = >::block_number(); - - let new_lead = Lead{ - stage: LeadRoleState::Exited(ExitedLeadRole { initiated_at_block_number: current_block}), - ..lead - }; - - >::insert(lead_id, new_lead); - - // Update current lead - >::take(); // None + // Unset current lead first + if Self::ensure_lead_is_set().is_ok() { + Self::unset_lead()?; + } - // Trigger event - Self::deposit_event(RawEvent::LeadUnset(lead_id)); + // Try to set new lead + if let Some((member_id, role_account)) = new_lead { + Self::set_lead(member_id, role_account)?; + } } /// Add an opening for a curator role. @@ -2022,7 +1953,11 @@ decl_module! { Self::deposit_event(RawEvent::ChannelCreationEnabledUpdated(enabled)); } - /// Add to capacity of current acive mint + /// Add to capacity of current acive mint. + /// This may be deprecated in the future, since set_mint_capacity is sufficient to + /// both increase and decrease capacity. Although when considering that it may be executed + /// by a proposal, given the temporal delay in approving a proposal, it might be more suitable + /// than set_mint_capacity? pub fn increase_mint_capacity( origin, additional_capacity: minting::BalanceOf @@ -2032,7 +1967,42 @@ decl_module! { let mint_id = Self::mint(); let mint = >::mints(mint_id); // must exist let new_capacity = mint.capacity() + additional_capacity; - let _ = >::set_mint_capacity(mint_id, new_capacity); + >::set_mint_capacity(mint_id, new_capacity)?; + + Self::deposit_event(RawEvent::MintCapacityIncreased( + mint_id, additional_capacity, new_capacity + )); + } + + /// Sets the capacity of the current active mint + pub fn set_mint_capacity( + origin, + new_capacity: minting::BalanceOf + ) { + ensure_root(origin)?; + + let mint_id = Self::mint(); + + // Mint must exist - it is set at genesis + let mint = >::mints(mint_id); + + let current_capacity = mint.capacity(); + + if new_capacity != current_capacity { + // Cannot fail if mint exists + >::set_mint_capacity(mint_id, new_capacity)?; + + if new_capacity > current_capacity { + Self::deposit_event(RawEvent::MintCapacityIncreased( + mint_id, new_capacity - current_capacity, new_capacity + )); + } else { + Self::deposit_event(RawEvent::MintCapacityDecreased( + mint_id, current_capacity - new_capacity, new_capacity + )); + } + } + } } } @@ -2079,6 +2049,87 @@ impl versioned_store_permissions::CredentialChecker for Module { } impl Module { + /// Introduce a lead when one is not currently set. + fn set_lead(member: T::MemberId, role_account: T::AccountId) -> dispatch::Result { + // Ensure there is no current lead + ensure!( + >::get().is_none(), + MSG_CURRENT_LEAD_ALREADY_SET + ); + + let new_lead_id = >::get(); + + let new_lead_role = + role_types::ActorInRole::new(role_types::Role::CuratorLead, new_lead_id); + + // + // == MUTATION SAFE == + // + + // Register in role - will fail if member cannot become lead + members::Module::::register_role_on_member(member, &new_lead_role)?; + + // Construct lead + let new_lead = Lead { + role_account, + reward_relationship: None, + inducted: >::block_number(), + stage: LeadRoleState::Active, + }; + + // Store lead + >::insert(new_lead_id, new_lead); + + // Update current lead + >::put(new_lead_id); // Some(new_lead_id) + + // Update next lead counter + >::mutate(|id| *id += as One>::one()); + + // Trigger event + Self::deposit_event(RawEvent::LeadSet(new_lead_id)); + + Ok(()) + } + + /// Evict the currently set lead + fn unset_lead() -> dispatch::Result { + // Ensure there is a lead set + let (lead_id, lead) = Self::ensure_lead_is_set()?; + + // + // == MUTATION SAFE == + // + + // Unregister from role in membership model + let current_lead_role = role_types::ActorInRole { + role: role_types::Role::CuratorLead, + actor_id: lead_id, + }; + + >::unregister_role(current_lead_role)?; + + // Update lead stage as exited + let current_block = >::block_number(); + + let new_lead = Lead { + stage: LeadRoleState::Exited(ExitedLeadRole { + initiated_at_block_number: current_block, + }), + ..lead + }; + + >::insert(lead_id, new_lead); + + // Update current lead + >::take(); // None + + // Trigger event + Self::deposit_event(RawEvent::LeadUnset(lead_id)); + + Ok(()) + } + fn ensure_member_has_no_active_application_on_opening( curator_applications: CuratorApplicationIdSet, member_id: T::MemberId, @@ -2136,7 +2187,7 @@ impl Module { ), &'static str, > { - let next_channel_id = opt_channel_id.unwrap_or(NextChannelId::::get()); + let next_channel_id = opt_channel_id.unwrap_or_else(NextChannelId::::get); Self::ensure_can_register_role_on_member( member_id, @@ -2148,7 +2199,7 @@ impl Module { // TODO: convert InputConstraint ensurer routines into macroes - fn ensure_channel_handle_is_valid(handle: &Vec) -> dispatch::Result { + fn ensure_channel_handle_is_valid(handle: &[u8]) -> dispatch::Result { ChannelHandleConstraint::get().ensure_valid( handle.len(), MSG_CHANNEL_HANDLE_TOO_SHORT, @@ -2212,7 +2263,7 @@ impl Module { } } - fn ensure_curator_application_text_is_valid(text: &Vec) -> dispatch::Result { + fn ensure_curator_application_text_is_valid(text: &[u8]) -> dispatch::Result { CuratorApplicationHumanReadableText::get().ensure_valid( text.len(), MSG_CURATOR_APPLICATION_TEXT_TOO_SHORT, @@ -2220,7 +2271,7 @@ impl Module { ) } - fn ensure_curator_exit_rationale_text_is_valid(text: &Vec) -> dispatch::Result { + fn ensure_curator_exit_rationale_text_is_valid(text: &[u8]) -> dispatch::Result { CuratorExitRationaleText::get().ensure_valid( text.len(), MSG_CURATOR_EXIT_RATIONALE_TEXT_TOO_SHORT, @@ -2228,7 +2279,7 @@ impl Module { ) } - fn ensure_opening_human_readable_text_is_valid(text: &Vec) -> dispatch::Result { + fn ensure_opening_human_readable_text_is_valid(text: &[u8]) -> dispatch::Result { OpeningHumanReadableText::get().ensure_valid( text.len(), MSG_CHANNEL_DESCRIPTION_TOO_SHORT, @@ -2496,7 +2547,7 @@ impl Module { Ok(( curator_application, - curator_application_id.clone(), + *curator_application_id, curator_opening, )) } @@ -2595,7 +2646,7 @@ impl Module { PrincipalId, >, exit_initiation_origin: &CuratorExitInitiationOrigin, - rationale_text: &Vec, + rationale_text: &[u8], ) { // Stop any possible recurring rewards let _did_deactivate_recurring_reward = if let Some(ref reward_relationship_id) = @@ -2603,14 +2654,14 @@ impl Module { { // Attempt to deactivate recurringrewards::Module::::try_to_deactivate_relationship(*reward_relationship_id) - .expect("Relatioship must exist") + .expect("Relationship must exist") } else { // Did not deactivate, there was no reward relationship! false }; - // When the curator is staked, unstaking must first be initated, - // otherwise they can be terminted right away. + // When the curator is staked, unstaking must first be initiated, + // otherwise they can be terminated right away. // Create exit summary for this termination let current_block = >::block_number(); @@ -2619,34 +2670,31 @@ impl Module { CuratorExitSummary::new(exit_initiation_origin, ¤t_block, rationale_text); // Determine new curator stage and event to emit - let (new_curator_stage, unstake_directions, event) = - if let Some(ref stake_profile) = curator.role_stake_profile { - // Determine unstaknig period based on who initiated deactivation - let unstaking_period = match curator_exit_summary.origin { - CuratorExitInitiationOrigin::Lead => stake_profile.termination_unstaking_period, - CuratorExitInitiationOrigin::Curator => stake_profile.exit_unstaking_period, - }; - - ( - CuratorRoleStage::Unstaking(curator_exit_summary), - Some((stake_profile.stake_id.clone(), unstaking_period)), - RawEvent::CuratorUnstaking(curator_id.clone()), - ) - } else { - ( - CuratorRoleStage::Exited(curator_exit_summary.clone()), - None, - match curator_exit_summary.origin { - CuratorExitInitiationOrigin::Lead => { - RawEvent::TerminatedCurator(curator_id.clone()) - } - CuratorExitInitiationOrigin::Curator => { - RawEvent::CuratorExited(curator_id.clone()) - } - }, - ) + let (new_curator_stage, unstake_directions, event) = if let Some(ref stake_profile) = + curator.role_stake_profile + { + // Determine unstaknig period based on who initiated deactivation + let unstaking_period = match curator_exit_summary.origin { + CuratorExitInitiationOrigin::Lead => stake_profile.termination_unstaking_period, + CuratorExitInitiationOrigin::Curator => stake_profile.exit_unstaking_period, }; + ( + CuratorRoleStage::Unstaking(curator_exit_summary), + Some((stake_profile.stake_id, unstaking_period)), + RawEvent::CuratorUnstaking(*curator_id), + ) + } else { + ( + CuratorRoleStage::Exited(curator_exit_summary.clone()), + None, + match curator_exit_summary.origin { + CuratorExitInitiationOrigin::Lead => RawEvent::TerminatedCurator(*curator_id), + CuratorExitInitiationOrigin::Curator => RawEvent::CuratorExited(*curator_id), + }, + ) + }; + // Update curator let new_curator = Curator { stage: new_curator_stage, @@ -2658,7 +2706,7 @@ impl Module { // Unstake if directions provided if let Some(directions) = unstake_directions { // Keep track of curator unstaking - let unstaker = WorkingGroupUnstaker::Curator(curator_id.clone()); + let unstaker = WorkingGroupUnstaker::Curator(*curator_id); UnstakerByStakeId::::insert(directions.0, unstaker); // Unstake @@ -2687,14 +2735,14 @@ impl Module { fn update_channel( channel_id: &ChannelId, - new_verified: &Option, + new_verified: Option, new_handle: &Option>, new_title: &Option, new_description: &Option, new_avatar: &Option, new_banner: &Option, - new_publication_status: &Option, - new_curation_status: &Option, + new_publication_status: Option, + new_curation_status: Option, ) { // Update channel id to handle mapping, if there is a new handle. if let Some(ref handle) = new_handle { diff --git a/runtime-modules/content-working-group/src/mock.rs b/runtime-modules/content-working-group/src/mock.rs index 3f2f4a57f3..d99e240cf2 100644 --- a/runtime-modules/content-working-group/src/mock.rs +++ b/runtime-modules/content-working-group/src/mock.rs @@ -68,7 +68,9 @@ pub type RawLibTestEvent = RawEvent< CuratorApplicationId, CuratorId, CuratorApplicationIdToCuratorIdMap, + minting::BalanceOf, ::AccountId, + ::MintId, >; pub fn get_last_event_or_panic() -> RawLibTestEvent { @@ -220,11 +222,13 @@ impl TestExternalitiesBuilder { self.membership_config = Some(membership_config); self } - pub fn set_content_wg_config(mut self, conteng_wg_config: GenesisConfig) -> Self { + */ + + pub fn with_content_wg_config(mut self, conteng_wg_config: GenesisConfig) -> Self { self.content_wg_config = Some(conteng_wg_config); self } - */ + pub fn build(self) -> runtime_io::TestExternalities { // Add system let mut t = self @@ -260,3 +264,4 @@ impl TestExternalitiesBuilder { pub type System = system::Module; pub type Balances = balances::Module; pub type ContentWorkingGroup = Module; +pub type Minting = minting::Module; diff --git a/runtime-modules/content-working-group/src/tests.rs b/runtime-modules/content-working-group/src/tests.rs index 03cc88e36d..d2b83af4ff 100644 --- a/runtime-modules/content-working-group/src/tests.rs +++ b/runtime-modules/content-working-group/src/tests.rs @@ -1,6 +1,6 @@ #![cfg(test)] -//use super::genesis; +use super::genesis; use super::mock::{self, *}; //use crate::membership; use hiring; @@ -1160,7 +1160,10 @@ struct SetLeadFixture { impl SetLeadFixture { fn call(&self) -> Result<(), &'static str> { - ContentWorkingGroup::set_lead(self.origin.clone(), self.member_id, self.new_role_account) + ContentWorkingGroup::replace_lead( + self.origin.clone(), + Some((self.member_id, self.new_role_account)), + ) } pub fn call_and_assert_success(&self) { @@ -1221,7 +1224,7 @@ struct UnsetLeadFixture { impl UnsetLeadFixture { fn call(&self) -> Result<(), &'static str> { - ContentWorkingGroup::unset_lead(self.origin.clone()) + ContentWorkingGroup::replace_lead(self.origin.clone(), None) } pub fn call_and_assert_success(&self) { @@ -2121,10 +2124,9 @@ pub fn set_lead( // Set lead assert_eq!( - ContentWorkingGroup::set_lead( + ContentWorkingGroup::replace_lead( mock::Origin::system(system::RawOrigin::Root), - member_id, - new_role_account + Some((member_id, new_role_account)) ) .unwrap(), () @@ -2184,3 +2186,87 @@ pub fn generate_too_short_length_buffer(constraint: &InputValidationLengthConstr pub fn generate_too_long_length_buffer(constraint: &InputValidationLengthConstraint) -> Vec { generate_text((constraint.max() + 1) as usize) } + +#[test] +fn increasing_mint_capacity() { + const MINT_CAPACITY: u64 = 50000; + + TestExternalitiesBuilder::::default() + .with_content_wg_config( + genesis::GenesisConfigBuilder::::default() + .with_mint_capacity(MINT_CAPACITY) + .build(), + ) + .build() + .execute_with(|| { + let mint_id = ContentWorkingGroup::mint(); + let mint = Minting::mints(mint_id); + assert_eq!(mint.capacity(), MINT_CAPACITY); + + let increase = 25000; + // Increasing mint capacity + let expected_new_capacity = MINT_CAPACITY + increase; + assert_ok!(ContentWorkingGroup::increase_mint_capacity( + Origin::ROOT, + increase + )); + // Excpected event after increasing + assert_eq!( + get_last_event_or_panic(), + crate::RawEvent::MintCapacityIncreased(mint_id, increase, expected_new_capacity) + ); + // Excpected value of capacity after increasing + let mint = Minting::mints(mint_id); + assert_eq!(mint.capacity(), expected_new_capacity); + }); +} + +#[test] +fn setting_mint_capacity() { + const MINT_CAPACITY: u64 = 50000; + + TestExternalitiesBuilder::::default() + .with_content_wg_config( + genesis::GenesisConfigBuilder::::default() + .with_mint_capacity(MINT_CAPACITY) + .build(), + ) + .build() + .execute_with(|| { + let mint_id = ContentWorkingGroup::mint(); + let mint = Minting::mints(mint_id); + assert_eq!(mint.capacity(), MINT_CAPACITY); + + // Decreasing mint capacity + let new_lower_capacity = 10000; + let decrease = MINT_CAPACITY - new_lower_capacity; + assert_ok!(ContentWorkingGroup::set_mint_capacity( + Origin::ROOT, + new_lower_capacity + )); + // Correct event after decreasing + assert_eq!( + get_last_event_or_panic(), + crate::RawEvent::MintCapacityDecreased(mint_id, decrease, new_lower_capacity) + ); + // Correct value of capacity after decreasing + let mint = Minting::mints(mint_id); + assert_eq!(mint.capacity(), new_lower_capacity); + + // Increasing mint capacity + let new_higher_capacity = 25000; + let increase = new_higher_capacity - mint.capacity(); + assert_ok!(ContentWorkingGroup::set_mint_capacity( + Origin::ROOT, + new_higher_capacity + )); + // Excpected event after increasing + assert_eq!( + get_last_event_or_panic(), + crate::RawEvent::MintCapacityIncreased(mint_id, increase, new_higher_capacity) + ); + // Excpected value of capacity after increasing + let mint = Minting::mints(mint_id); + assert_eq!(mint.capacity(), new_higher_capacity); + }); +} diff --git a/runtime-modules/forum/src/lib.rs b/runtime-modules/forum/src/lib.rs index dde4c88f17..c98c8719a0 100755 --- a/runtime-modules/forum/src/lib.rs +++ b/runtime-modules/forum/src/lib.rs @@ -1,209 +1,7 @@ -// Copyright 2017-2019 Parity Technologies (UK) Ltd. - -// This is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Substrate. If not, see . - -// Copyright 2019 Joystream Contributors - -//! # Runtime Example Module -//! -//! -//! 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:
-//! Add heading with 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. -//! -//! - [`::Trait`](./trait.Trait.html) -//! - [`Call`](./enum.Call.html) -//! - [`Module`](./struct.Module.html) -//! -//! ## Overview -//! -//! -//! 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. -//! -//! -//! -//! -//! ## 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. -//! -//! -//! -//! ## Goals -//! -//! Add goals that the custom module is designed to achieve. -//! -//! -//! -//! ### Scenarios -//! -//! -//! -//! #### -//! -//! 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` -//! -//! -//! -//! -//! ### Types -//! -//! Type aliases. Include any associated types and where the user would typically define them. -//! -//! `ExampleType` -//! -//! -//! -//! -//! ### Dispatchable Functions -//! -//! -//! -//! // A brief description of dispatchable functions and a link to the rustdoc with their actual documentation. -//! -//! MUST have link to Call enum -//! MUST have origin information included in function doc -//! CAN have more info up to the user -//! -//! ### Public Functions -//! -//! -//! -//! 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." -//! -//! -//! -//! 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 module in a custom module. -//! -//! ### Prerequisites -//! -//! Show how to include necessary imports for and derive -//! your module configuration trait with the `INSERT_CUSTOM_MODULE_NAME` trait. -//! -//! ```rust -//! // use ; -//! -//! // pub trait Trait: ::Trait { } -//! ``` -//! -//! ### Simple Code Snippet -//! -//! Show a simple example (e.g. how to query a public getter function of ) -//! -//! ## Genesis Config -//! -//! -//! -//! ## 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 -//! -//! -//! -//! ## Related Modules -//! -//! Interaction with other modules in the form of a bullet point list -//! -//! ## References -//! -//! -//! -//! Links to reference material, if applicable. For example, Phragmen, W3F research, etc. -//! that the implementation is based on. +// Clippy linter warning +#![allow(clippy::type_complexity)] +// disable it because of possible frontend API break +// TODO: remove post-Constaninople // Ensure we're `no_std` when compiling for Wasm. #![cfg_attr(not(feature = "std"), no_std)] @@ -211,10 +9,12 @@ #[cfg(feature = "std")] use serde_derive::{Deserialize, Serialize}; +use rstd::borrow::ToOwned; use rstd::prelude::*; -use codec::{Decode, Encode}; -use srml_support::{decl_event, decl_module, decl_storage, dispatch, ensure}; +use codec::{Codec, Decode, Encode}; +use runtime_primitives::traits::{MaybeSerialize, Member, One, SimpleArithmetic}; +use srml_support::{decl_event, decl_module, decl_storage, dispatch, ensure, Parameter}; mod mock; mod tests; @@ -307,7 +107,6 @@ const ERROR_CATEGORY_CANNOT_BE_UNARCHIVED_WHEN_DELETED: &str = //#[cfg(any(feature = "std", test))] //use sr_primitives::{StorageOverlay, ChildrenStorageOverlay}; -use system; use system::{ensure_root, ensure_signed}; /// Represents a user in this forum. @@ -358,13 +157,10 @@ pub struct PostTextChange { text: Vec, } -/// 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 { +pub struct Post { /// Post identifier id: PostId, @@ -394,13 +190,10 @@ pub struct 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 { +pub struct Thread { /// Thread identifier id: ThreadId, @@ -440,7 +233,7 @@ pub struct Thread { author_id: AccountId, } -impl Thread { +impl Thread { fn num_posts_ever_created(&self) -> u32 { self.num_unmoderated_posts + self.num_moderated_posts } @@ -523,6 +316,26 @@ pub trait Trait: system::Trait + timestamp::Trait + Sized { type Event: From> + Into<::Event>; type MembershipRegistry: ForumUserRegistry; + + /// Thread Id type + type ThreadId: Parameter + + Member + + SimpleArithmetic + + Codec + + Default + + Copy + + MaybeSerialize + + PartialEq; + + /// Post Id type + type PostId: Parameter + + Member + + SimpleArithmetic + + Codec + + Default + + Copy + + MaybeSerialize + + PartialEq; } decl_storage! { @@ -535,16 +348,16 @@ decl_storage! { pub NextCategoryId get(next_category_id) config(): CategoryId; /// Map thread identifier to corresponding thread. - pub ThreadById get(thread_by_id) config(): map ThreadId => Thread; + pub ThreadById get(thread_by_id) config(): map T::ThreadId => Thread; /// Thread identifier value to be used for next Thread in threadById. - pub NextThreadId get(next_thread_id) config(): ThreadId; + pub NextThreadId get(next_thread_id) config(): T::ThreadId; /// Map post identifier to corresponding post. - pub PostById get(post_by_id) config(): map PostId => Post; + pub PostById get(post_by_id) config(): map T::PostId => Post; /// Post identifier value to be used for for next post created. - pub NextPostId get(next_post_id) config(): PostId; + pub NextPostId get(next_post_id) config(): T::PostId; /// Account of forum sudo. pub ForumSudo get(forum_sudo) config(): Option; @@ -588,6 +401,8 @@ decl_event!( pub enum Event where ::AccountId, + ::ThreadId, + ::PostId, { /// A category was introduced CategoryCreated(CategoryId), @@ -633,7 +448,7 @@ decl_module! { */ // Hold on to old value - let old_forum_sudo = >::get().clone(); + let old_forum_sudo = >::get(); // Update forum sudo match new_forum_sudo.clone() { @@ -703,8 +518,8 @@ decl_module! { // Create new category let new_category = Category { id : next_category_id, - title : title.clone(), - description: description.clone(), + title, + description, created_at : Self::current_block_and_time(), deleted: false, archived: false, @@ -753,7 +568,7 @@ decl_module! { // We must skip checking category itself. // NB: This is kind of hacky way to avoid last element, // something clearn can be done later. - let mut path_to_check = category_tree_path.clone(); + let mut path_to_check = category_tree_path; path_to_check.remove(0); Self::ensure_can_mutate_in_path_leaf(&path_to_check)?; @@ -834,7 +649,7 @@ decl_module! { } /// Moderate thread - fn moderate_thread(origin, thread_id: ThreadId, rationale: Vec) -> dispatch::Result { + fn moderate_thread(origin, thread_id: T::ThreadId, rationale: Vec) -> dispatch::Result { // Check that its a valid signature let who = ensure_signed(origin)?; @@ -843,7 +658,7 @@ decl_module! { Self::ensure_is_forum_sudo(&who)?; // Get thread - let mut thread = Self::ensure_thread_exists(&thread_id)?; + let mut thread = Self::ensure_thread_exists(thread_id)?; // Thread is not already moderated ensure!(thread.moderation.is_none(), ERROR_THREAD_ALREADY_MODERATED); @@ -867,7 +682,7 @@ decl_module! { thread.moderation = Some(ModerationAction { moderated_at: Self::current_block_and_time(), moderator_id: who, - rationale: rationale.clone() + rationale }); >::insert(thread_id, thread.clone()); @@ -885,7 +700,7 @@ decl_module! { } /// Edit post text - fn add_post(origin, thread_id: ThreadId, text: Vec) -> dispatch::Result { + fn add_post(origin, thread_id: T::ThreadId, text: Vec) -> dispatch::Result { /* * Update SPEC with new errors, @@ -901,7 +716,7 @@ decl_module! { Self::ensure_post_text_is_valid(&text)?; // Make sure thread exists and is mutable - let thread = Self::ensure_thread_is_mutable(&thread_id)?; + let thread = Self::ensure_thread_is_mutable(thread_id)?; // Get path from parent to root of category tree. let category_tree_path = Self::ensure_valid_category_and_build_category_tree_path(thread.category_id)?; @@ -922,7 +737,7 @@ decl_module! { } /// Edit post text - fn edit_post_text(origin, post_id: PostId, new_text: Vec) -> dispatch::Result { + fn edit_post_text(origin, post_id: T::PostId, new_text: Vec) -> dispatch::Result { /* Edit spec. - forum member guard missing @@ -939,7 +754,7 @@ decl_module! { Self::ensure_post_text_is_valid(&new_text)?; // Make sure there exists a mutable post with post id `post_id` - let post = Self::ensure_post_is_mutable(&post_id)?; + let post = Self::ensure_post_is_mutable(post_id)?; // Signer does not match creator of post with identifier postId ensure!(post.author_id == who, ERROR_ACCOUNT_DOES_NOT_MATCH_POST_AUTHOR); @@ -969,7 +784,7 @@ decl_module! { } /// Moderate post - fn moderate_post(origin, post_id: PostId, rationale: Vec) -> dispatch::Result { + fn moderate_post(origin, post_id: T::PostId, rationale: Vec) -> dispatch::Result { // Check that its a valid signature let who = ensure_signed(origin)?; @@ -978,7 +793,7 @@ decl_module! { Self::ensure_is_forum_sudo(&who)?; // Make sure post exists and is mutable - let post = Self::ensure_post_is_mutable(&post_id)?; + let post = Self::ensure_post_is_mutable(post_id)?; Self::ensure_post_moderation_rationale_is_valid(&rationale)?; @@ -990,7 +805,7 @@ decl_module! { let moderation_action = ModerationAction{ moderated_at: Self::current_block_and_time(), moderator_id: who, - rationale: rationale.clone() + rationale }; >::mutate(post_id, |p| { @@ -1013,7 +828,7 @@ decl_module! { } impl Module { - fn ensure_category_title_is_valid(title: &Vec) -> dispatch::Result { + fn ensure_category_title_is_valid(title: &[u8]) -> dispatch::Result { CategoryTitleConstraint::get().ensure_valid( title.len(), ERROR_CATEGORY_TITLE_TOO_SHORT, @@ -1021,7 +836,7 @@ impl Module { ) } - fn ensure_category_description_is_valid(description: &Vec) -> dispatch::Result { + fn ensure_category_description_is_valid(description: &[u8]) -> dispatch::Result { CategoryDescriptionConstraint::get().ensure_valid( description.len(), ERROR_CATEGORY_DESCRIPTION_TOO_SHORT, @@ -1029,7 +844,7 @@ impl Module { ) } - fn ensure_thread_moderation_rationale_is_valid(rationale: &Vec) -> dispatch::Result { + fn ensure_thread_moderation_rationale_is_valid(rationale: &[u8]) -> dispatch::Result { ThreadModerationRationaleConstraint::get().ensure_valid( rationale.len(), ERROR_THREAD_MODERATION_RATIONALE_TOO_SHORT, @@ -1037,7 +852,7 @@ impl Module { ) } - fn ensure_thread_title_is_valid(title: &Vec) -> dispatch::Result { + fn ensure_thread_title_is_valid(title: &[u8]) -> dispatch::Result { ThreadTitleConstraint::get().ensure_valid( title.len(), ERROR_THREAD_TITLE_TOO_SHORT, @@ -1045,7 +860,7 @@ impl Module { ) } - fn ensure_post_text_is_valid(text: &Vec) -> dispatch::Result { + fn ensure_post_text_is_valid(text: &[u8]) -> dispatch::Result { PostTextConstraint::get().ensure_valid( text.len(), ERROR_POST_TEXT_TOO_SHORT, @@ -1053,7 +868,7 @@ impl Module { ) } - fn ensure_post_moderation_rationale_is_valid(rationale: &Vec) -> dispatch::Result { + fn ensure_post_moderation_rationale_is_valid(rationale: &[u8]) -> dispatch::Result { PostModerationRationaleConstraint::get().ensure_valid( rationale.len(), ERROR_POST_MODERATION_RATIONALE_TOO_SHORT, @@ -1069,8 +884,9 @@ impl Module { } fn ensure_post_is_mutable( - post_id: &PostId, - ) -> Result, &'static str> { + post_id: T::PostId, + ) -> Result, &'static str> + { // Make sure post exists let post = Self::ensure_post_exists(post_id)?; @@ -1078,14 +894,15 @@ impl Module { ensure!(post.moderation.is_none(), ERROR_POST_MODERATED); // and make sure thread is mutable - Self::ensure_thread_is_mutable(&post.thread_id)?; + Self::ensure_thread_is_mutable(post.thread_id)?; Ok(post) } fn ensure_post_exists( - post_id: &PostId, - ) -> Result, &'static str> { + post_id: T::PostId, + ) -> Result, &'static str> + { if >::exists(post_id) { Ok(>::get(post_id)) } else { @@ -1094,10 +911,10 @@ impl Module { } fn ensure_thread_is_mutable( - thread_id: &ThreadId, - ) -> Result, &'static str> { + thread_id: T::ThreadId, + ) -> Result, &'static str> { // Make sure thread exists - let thread = Self::ensure_thread_exists(&thread_id)?; + let thread = Self::ensure_thread_exists(thread_id)?; // and is unmoderated ensure!(thread.moderation.is_none(), ERROR_THREAD_MODERATED); @@ -1109,8 +926,8 @@ impl Module { } fn ensure_thread_exists( - thread_id: &ThreadId, - ) -> Result, &'static str> { + thread_id: T::ThreadId, + ) -> Result, &'static str> { if >::exists(thread_id) { Ok(>::get(thread_id)) } else { @@ -1153,6 +970,9 @@ impl Module { Self::ensure_can_mutate_in_path_leaf(&category_tree_path) } + // Clippy linter warning + #[allow(clippy::ptr_arg)] // disable it because of possible frontend API break + // TODO: remove post-Constaninople fn ensure_can_mutate_in_path_leaf( category_tree_path: &CategoryTreePath, ) -> dispatch::Result { @@ -1167,6 +987,9 @@ impl Module { Ok(()) } + // TODO: remove post-Constaninople + // Clippy linter warning + #[allow(clippy::ptr_arg)] // disable it because of possible frontend API break fn ensure_can_add_subcategory_path_leaf( category_tree_path: &CategoryTreePath, ) -> dispatch::Result { @@ -1194,7 +1017,7 @@ impl Module { // Get path from parent to root of category tree. let category_tree_path = Self::build_category_tree_path(category_id); - assert!(category_tree_path.len() > 0); + assert!(!category_tree_path.is_empty()); Ok(category_tree_path) } @@ -1239,19 +1062,19 @@ impl Module { fn add_new_thread( category_id: CategoryId, - title: &Vec, + title: &[u8], author_id: &T::AccountId, - ) -> Thread { + ) -> Thread { // Get category let category = >::get(category_id); // Create and add new thread - let new_thread_id = NextThreadId::get(); + let new_thread_id = NextThreadId::::get(); let new_thread = Thread { id: new_thread_id, - title: title.clone(), - category_id: category_id, + title: title.to_owned(), + category_id, nr_in_category: category.num_threads_created() + 1, moderation: None, num_unmoderated_posts: 0, @@ -1264,8 +1087,8 @@ impl Module { >::insert(new_thread_id, new_thread.clone()); // Update next thread id - NextThreadId::mutate(|n| { - *n += 1; + NextThreadId::::mutate(|n| { + *n += One::one(); }); // Update unmoderated thread count in corresponding category @@ -1279,21 +1102,21 @@ impl Module { /// 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, + thread_id: T::ThreadId, + text: &[u8], author_id: &T::AccountId, - ) -> Post { + ) -> Post { // Get thread let thread = >::get(thread_id); // Make and add initial post - let new_post_id = NextPostId::get(); + let new_post_id = NextPostId::::get(); let new_post = Post { id: new_post_id, - thread_id: thread_id, + thread_id, nr_in_thread: thread.num_posts_ever_created() + 1, - current_text: text.clone(), + current_text: text.to_owned(), moderation: None, text_change_history: vec![], created_at: Self::current_block_and_time(), @@ -1304,8 +1127,8 @@ impl Module { >::insert(new_post_id, new_post.clone()); // Update next post id - NextPostId::mutate(|n| { - *n += 1; + NextPostId::::mutate(|n| { + *n += One::one(); }); // Update unmoderated post count of thread diff --git a/runtime-modules/forum/src/mock.rs b/runtime-modules/forum/src/mock.rs index 3c0144191d..103279af8b 100644 --- a/runtime-modules/forum/src/mock.rs +++ b/runtime-modules/forum/src/mock.rs @@ -100,6 +100,8 @@ impl timestamp::Trait for Runtime { impl Trait for Runtime { type Event = (); type MembershipRegistry = registry::TestMembershipRegistryModule; + type ThreadId = u64; + type PostId = u64; } #[derive(Clone)] @@ -123,9 +125,9 @@ pub const NOT_MEMBER_ORIGIN: OriginType = OriginType::Signed(222); pub const INVLAID_CATEGORY_ID: CategoryId = 333; -pub const INVLAID_THREAD_ID: ThreadId = 444; +pub const INVLAID_THREAD_ID: RuntimeThreadId = 444; -pub const INVLAID_POST_ID: ThreadId = 555; +pub const INVLAID_POST_ID: RuntimePostId = 555; pub fn generate_text(len: usize) -> Vec { vec![b'x'; len] @@ -228,7 +230,7 @@ impl CreateThreadFixture { pub struct CreatePostFixture { pub origin: OriginType, - pub thread_id: ThreadId, + pub thread_id: RuntimeThreadId, pub text: Vec, pub result: dispatch::Result, } @@ -285,7 +287,7 @@ pub fn assert_create_thread( pub fn assert_create_post( forum_sudo: OriginType, - thread_id: ThreadId, + thread_id: RuntimeThreadId, expected_result: dispatch::Result, ) { CreatePostFixture { @@ -312,7 +314,7 @@ pub fn create_root_category(forum_sudo: OriginType) -> CategoryId { pub fn create_root_category_and_thread( forum_sudo: OriginType, -) -> (OriginType, CategoryId, ThreadId) { +) -> (OriginType, CategoryId, RuntimeThreadId) { let member_origin = create_forum_member(); let category_id = create_root_category(forum_sudo); let thread_id = TestForumModule::next_thread_id(); @@ -331,7 +333,7 @@ pub fn create_root_category_and_thread( pub fn create_root_category_and_thread_and_post( forum_sudo: OriginType, -) -> (OriginType, CategoryId, ThreadId, PostId) { +) -> (OriginType, CategoryId, RuntimeThreadId, RuntimePostId) { let (member_origin, category_id, thread_id) = create_root_category_and_thread(forum_sudo); let post_id = TestForumModule::next_post_id(); @@ -348,7 +350,7 @@ pub fn create_root_category_and_thread_and_post( pub fn moderate_thread( forum_sudo: OriginType, - thread_id: ThreadId, + thread_id: RuntimeThreadId, rationale: Vec, ) -> dispatch::Result { TestForumModule::moderate_thread(mock_origin(forum_sudo), thread_id, rationale) @@ -356,7 +358,7 @@ pub fn moderate_thread( pub fn moderate_post( forum_sudo: OriginType, - post_id: PostId, + post_id: RuntimePostId, rationale: Vec, ) -> dispatch::Result { TestForumModule::moderate_post(mock_origin(forum_sudo), post_id, rationale) @@ -456,23 +458,28 @@ pub type RuntimeThread = Thread< ::BlockNumber, ::Moment, ::AccountId, + RuntimeThreadId, >; pub type RuntimePost = Post< ::BlockNumber, ::Moment, ::AccountId, + RuntimeThreadId, + RuntimePostId, >; pub type RuntimeBlockchainTimestamp = BlockchainTimestamp< ::BlockNumber, ::Moment, >; +pub type RuntimeThreadId = ::ThreadId; +pub type RuntimePostId = ::PostId; pub fn genesis_config( category_by_id: &RuntimeMap, next_category_id: u64, - thread_by_id: &RuntimeMap, + thread_by_id: &RuntimeMap, next_thread_id: u64, - post_by_id: &RuntimeMap, + post_by_id: &RuntimeMap, next_post_id: u64, forum_sudo: ::AccountId, category_title_constraint: &InputValidationLengthConstraint, @@ -484,12 +491,12 @@ pub fn genesis_config( ) -> GenesisConfig { GenesisConfig:: { category_by_id: category_by_id.clone(), - next_category_id: next_category_id, + next_category_id, thread_by_id: thread_by_id.clone(), - next_thread_id: next_thread_id, + next_thread_id, post_by_id: post_by_id.clone(), - next_post_id: next_post_id, - forum_sudo: forum_sudo, + next_post_id, + forum_sudo, category_title_constraint: category_title_constraint.clone(), category_description_constraint: category_description_constraint.clone(), thread_title_constraint: thread_title_constraint.clone(), diff --git a/runtime-modules/governance/Cargo.toml b/runtime-modules/governance/Cargo.toml index 61cce493b9..40653e9894 100644 --- a/runtime-modules/governance/Cargo.toml +++ b/runtime-modules/governance/Cargo.toml @@ -17,6 +17,7 @@ std = [ 'rstd/std', 'common/std', 'membership/std', + 'minting/std', ] [dependencies.sr-primitives] @@ -86,4 +87,14 @@ rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' default_features = false git = 'https://github.com/paritytech/substrate.git' package = 'srml-balances' -rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' \ No newline at end of file +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' + +[dependencies.minting] +default_features = false +package = 'substrate-token-mint-module' +path = '../token-minting' + +[dependencies.recurringrewards] +default_features = false +package = 'substrate-recurring-reward-module' +path = '../recurring-reward' \ No newline at end of file diff --git a/runtime-modules/governance/src/council.rs b/runtime-modules/governance/src/council.rs index 792977f073..b61045ea30 100644 --- a/runtime-modules/governance/src/council.rs +++ b/runtime-modules/governance/src/council.rs @@ -1,6 +1,6 @@ use rstd::prelude::*; -use sr_primitives::traits::Zero; -use srml_support::{decl_event, decl_module, decl_storage, ensure}; +use sr_primitives::traits::{One, Zero}; +use srml_support::{debug, decl_event, decl_module, decl_storage, ensure}; use system::{self, ensure_root}; pub use super::election::{self, CouncilElected, Seat, Seats}; @@ -21,7 +21,7 @@ impl CouncilTermEnded for (X,) { } } -pub trait Trait: system::Trait + GovernanceCurrency { +pub trait Trait: system::Trait + recurringrewards::Trait + GovernanceCurrency { type Event: From> + Into<::Event>; type CouncilTermEnded: CouncilTermEnded; @@ -29,8 +29,28 @@ pub trait Trait: system::Trait + GovernanceCurrency { decl_storage! { trait Store for Module as Council { - ActiveCouncil get(active_council) config(): Seats>; - TermEndsAt get(term_ends_at) config() : T::BlockNumber = T::BlockNumber::from(1); + pub ActiveCouncil get(active_council) config(): Seats>; + + pub TermEndsAt get(term_ends_at) config() : T::BlockNumber = T::BlockNumber::from(1); + + /// The mint that funds council member rewards and spending proposals budget. It is an Option + /// because it was introduced in a runtime upgrade. It will be automatically created when + /// a successful call to set_council_mint_capacity() is made. + pub CouncilMint get(council_mint) : Option<::MintId>; + + /// The reward relationships currently in place. There may not necessarily be a 1-1 correspondance with + /// the active council, since there are multiple ways of setting/adding/removing council members, some of which + /// do not involve creating a relationship. + pub RewardRelationships get(reward_relationships) : map T::AccountId => T::RewardRelationshipId; + + /// Reward amount paid out at each PayoutInterval + pub AmountPerPayout get(amount_per_payout): minting::BalanceOf; + + /// Optional interval in blocks on which a reward payout will be made to each council member + pub PayoutInterval get(payout_interval): Option; + + /// How many blocks after the reward is created, the first payout will be made + pub FirstPayoutAfterRewardCreated get(first_payout_after_reward_created): T::BlockNumber; } } @@ -44,10 +64,23 @@ decl_event!( impl CouncilElected>, T::BlockNumber> for Module { fn council_elected(seats: Seats>, term: T::BlockNumber) { - >::put(seats); + >::put(seats.clone()); let next_term_ends_at = >::block_number() + term; + >::put(next_term_ends_at); + + if let Some(reward_source) = Self::council_mint() { + for seat in seats.iter() { + Self::add_reward_relationship(&seat.member, reward_source); + } + } else { + // Skip trying to create rewards since no mint has been created yet + debug::warn!( + "Not creating reward relationship for council seats because no mint exists" + ); + } + Self::deposit_event(RawEvent::NewCouncilTermStarted(next_term_ends_at)); } } @@ -60,6 +93,60 @@ impl Module { pub fn is_councilor(sender: &T::AccountId) -> bool { Self::active_council().iter().any(|c| c.member == *sender) } + + /// Initializes a new mint, discarding previous mint if it existed. + pub fn create_new_council_mint( + capacity: minting::BalanceOf, + ) -> Result { + let mint_id = >::add_mint(capacity, None)?; + CouncilMint::::put(mint_id); + Ok(mint_id) + } + + fn add_reward_relationship(destination: &T::AccountId, reward_source: T::MintId) { + let recipient = >::add_recipient(); + + // When calculating when first payout occurs, add minimum of one block interval to ensure rewards module + // has a chance to execute its on_finalize routine. + let next_payout_at = system::Module::::block_number() + + Self::first_payout_after_reward_created() + + T::BlockNumber::one(); + + if let Ok(relationship_id) = >::add_reward_relationship( + reward_source, + recipient, + destination.clone(), + Self::amount_per_payout(), + next_payout_at, + Self::payout_interval(), + ) { + RewardRelationships::::insert(destination, relationship_id); + } else { + debug::warn!("Failed to create a reward relationship for council seat"); + } + } + + fn remove_reward_relationships() { + for seat in Self::active_council().into_iter() { + if RewardRelationships::::exists(&seat.member) { + let id = Self::reward_relationships(&seat.member); + >::remove_reward_relationship(id); + } + } + } + + fn on_term_ended(now: T::BlockNumber) { + // Stop paying out rewards when the term ends. + // Note: Is it not simpler to just do a single payout at end of term? + // During the term the recurring reward module could unfairly pay some but not all council members + // If there is insufficient mint capacity.. so doing it at this point offers more control + // and a potentially more fair outcome in such a case. + Self::remove_reward_relationships(); + + Self::deposit_event(RawEvent::CouncilTermEnded(now)); + + T::CouncilTermEnded::council_term_ended(); + } } decl_module! { @@ -68,16 +155,28 @@ decl_module! { fn on_finalize(now: T::BlockNumber) { if now == Self::term_ends_at() { - Self::deposit_event(RawEvent::CouncilTermEnded(now)); - T::CouncilTermEnded::council_term_ended(); + Self::on_term_ended(now); } } // Privileged methods - /// Force set a zero staked council. Stakes in existing council will vanish into thin air! - fn set_council(origin, accounts: Vec) { + /// Force set a zero staked council. Stakes in existing council seats are not returned. + /// Existing council rewards are removed and new council members do NOT get any rewards. + /// Avoid using this call if possible, will be deprecated. The term of the new council is + /// not extended. + pub fn set_council(origin, accounts: Vec) { ensure_root(origin)?; + + // Council is being replaced so remove existing reward relationships if they exist + Self::remove_reward_relationships(); + + if let Some(reward_source) = Self::council_mint() { + for account in accounts.clone() { + Self::add_reward_relationship(&account, reward_source); + } + } + let new_council: Seats> = accounts.into_iter().map(|account| { Seat { member: account, @@ -85,13 +184,20 @@ decl_module! { backers: vec![] } }).collect(); + >::put(new_council); } - /// Adds a zero staked council member + /// Adds a zero staked council member. A member added in this way does not get a recurring reward. fn add_council_member(origin, account: T::AccountId) { ensure_root(origin)?; + ensure!(!Self::is_councilor(&account), "cannot add same account multiple times"); + + if let Some(reward_source) = Self::council_mint() { + Self::add_reward_relationship(&account, reward_source); + } + let seat = Seat { member: account, stake: BalanceOf::::zero(), @@ -102,13 +208,22 @@ decl_module! { >::mutate(|council| council.push(seat)); } + /// Remove a single council member and their reward. fn remove_council_member(origin, account_to_remove: T::AccountId) { ensure_root(origin)?; + ensure!(Self::is_councilor(&account_to_remove), "account is not a councilor"); + + if RewardRelationships::::exists(&account_to_remove) { + let relationship_id = Self::reward_relationships(&account_to_remove); + >::remove_reward_relationship(relationship_id); + } + let filtered_council: Seats> = Self::active_council() .into_iter() .filter(|c| c.member != account_to_remove) .collect(); + >::put(filtered_council); } @@ -118,11 +233,54 @@ decl_module! { ensure!(ends_at > >::block_number(), "must set future block number"); >::put(ends_at); } + + /// Sets the capacity of the the council mint, if it doesn't exist, attempts to + /// create a new one. + pub fn set_council_mint_capacity(origin, capacity: minting::BalanceOf) { + ensure_root(origin)?; + + if let Some(mint_id) = Self::council_mint() { + minting::Module::::set_mint_capacity(mint_id, capacity)?; + } else { + Self::create_new_council_mint(capacity)?; + } + } + + /// Attempts to mint and transfer amount to destination account + fn spend_from_council_mint(origin, amount: minting::BalanceOf, destination: T::AccountId) { + ensure_root(origin)?; + + if let Some(mint_id) = Self::council_mint() { + minting::Module::::transfer_tokens(mint_id, amount, &destination)?; + } else { + return Err("CouncilHasNoMint") + } + } + + /// Sets the council rewards which is only applied on new council being elected. + fn set_council_rewards( + origin, + amount_per_payout: minting::BalanceOf, + payout_interval: Option, + first_payout_after_reward_created: T::BlockNumber + ) { + ensure_root(origin)?; + + AmountPerPayout::::put(amount_per_payout); + FirstPayoutAfterRewardCreated::::put(first_payout_after_reward_created); + + if let Some(payout_interval) = payout_interval { + PayoutInterval::::put(payout_interval); + } else { + PayoutInterval::::take(); + } + } } } #[cfg(test)] mod tests { + use super::*; use crate::mock::*; use srml_support::*; @@ -174,4 +332,44 @@ mod tests { assert!(Council::is_councilor(&6)); }); } + + #[test] + fn council_elected_test() { + initial_test_ext().execute_with(|| { + // Ensure a mint is created so we can create rewards + assert_ok!(Council::set_council_mint_capacity( + system::RawOrigin::Root.into(), + 1000 + )); + + Council::council_elected( + vec![ + Seat { + member: 5, + stake: 0, + backers: vec![], + }, + Seat { + member: 6, + stake: 0, + backers: vec![], + }, + Seat { + member: 7, + stake: 0, + backers: vec![], + }, + ], + 50 as u64, // ::BlockNumber::from(50) + ); + + assert!(Council::is_councilor(&5)); + assert!(Council::is_councilor(&6)); + assert!(Council::is_councilor(&7)); + + assert!(RewardRelationships::::exists(&5)); + assert!(RewardRelationships::::exists(&6)); + assert!(RewardRelationships::::exists(&7)); + }); + } } diff --git a/runtime-modules/governance/src/election.rs b/runtime-modules/governance/src/election.rs index 7e8c5dcf82..9a4e74823b 100644 --- a/runtime-modules/governance/src/election.rs +++ b/runtime-modules/governance/src/election.rs @@ -1,3 +1,34 @@ +//! Council Elections Manager +//! +//! # Election Parameters: +//! We don't currently handle zero periods, zero council term, zero council size and candidacy +//! limit in any special way. The behaviour in such cases: +//! +//! - Setting any period to 0 will mean the election getting stuck in that stage, until force changing +//! the state. +//! +//! - Council Size of 0 - no limit to size of council, all applicants that move beyond +//! announcing stage would become council members, so effectively the candidacy limit will +//! be the size of the council, voting and revealing have no impact on final results. +//! +//! - If candidacy limit is zero and council size > 0, council_size number of applicants will reach the voting stage. +//! and become council members, voting will have no impact on final results. +//! +//! - If both candidacy limit and council size are zero then all applicant become council members +//! since no filtering occurs at end of announcing stage. +//! +//! We only guard against these edge cases in the [`set_election_parameters`] call. +//! +//! [`set_election_parameters`]: struct.Module.html#method.set_election_parameters + +// Clippy linter warning +#![allow(clippy::type_complexity)] +// disable it because of possible frontend API break +// TODO: remove post-Constaninople + +// Clippy linter warning +#![allow(clippy::redundant_closure_call)] // disable it because of the substrate lib design + use rstd::prelude::*; use srml_support::traits::{Currency, ReservableCurrency}; use srml_support::{decl_event, decl_module, decl_storage, dispatch::Result, ensure}; @@ -14,6 +45,7 @@ use super::sealed_vote::SealedVote; use super::stake::Stake; use super::council; +use crate::election_params::ElectionParameters; pub use common::currency::{BalanceOf, GovernanceCurrency}; pub trait Trait: @@ -24,6 +56,8 @@ pub trait Trait: type CouncilElected: CouncilElected>, Self::BlockNumber>; } +pub static MSG_CANNOT_CHANGE_PARAMS_DURING_ELECTION: &str = "CannotChangeParamsDuringElection"; + #[derive(Clone, Copy, Encode, Decode)] pub enum ElectionStage { Announcing(BlockNumber), @@ -73,6 +107,19 @@ impl> CouncilElected, + Y: CouncilElected, + > CouncilElected for (X, Y) +{ + fn council_elected(new_council: Elected, term: Term) { + X::council_elected(new_council.clone(), term.clone()); + Y::council_elected(new_council, term); + } +} #[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))] #[derive(Clone, Copy, Encode, Decode, Default)] @@ -106,16 +153,25 @@ decl_storage! { // TODO value type of this map looks scary, is there any way to simplify the notation? Votes get(votes): map T::Hash => SealedVote, T::Hash, T::AccountId>; - // Current Election Parameters - default "zero" values are not meaningful. Running an election without - // settings reasonable values is a bad idea. Parameters can be set in the TriggerElection hook. - AnnouncingPeriod get(announcing_period) config(): T::BlockNumber = T::BlockNumber::from(100); - VotingPeriod get(voting_period) config(): T::BlockNumber = T::BlockNumber::from(100); - RevealingPeriod get(revealing_period) config(): T::BlockNumber = T::BlockNumber::from(100); - CouncilSize get(council_size) config(): u32 = 10; - CandidacyLimit get (candidacy_limit) config(): u32 = 20; - MinCouncilStake get(min_council_stake) config(): BalanceOf = BalanceOf::::from(100); - NewTermDuration get(new_term_duration) config(): T::BlockNumber = T::BlockNumber::from(1000); - MinVotingStake get(min_voting_stake) config(): BalanceOf = BalanceOf::::from(10); + // Current Election Parameters. + // Should we replace all the individual values with a single ElectionParameters type? + // Having them individually makes it more flexible to add and remove new parameters in future + // without dealing with migration issues. + AnnouncingPeriod get(announcing_period): T::BlockNumber; + VotingPeriod get(voting_period): T::BlockNumber; + RevealingPeriod get(revealing_period): T::BlockNumber; + CouncilSize get(council_size): u32; + CandidacyLimit get (candidacy_limit): u32; + MinCouncilStake get(min_council_stake): BalanceOf; + NewTermDuration get(new_term_duration): T::BlockNumber; + MinVotingStake get(min_voting_stake): BalanceOf; + } + add_extra_genesis { + config(election_parameters): ElectionParameters, T::BlockNumber>; + build(|config: &GenesisConfig| { + config.election_parameters.ensure_valid().expect("Invalid Election Parameters"); + Module::::set_verified_election_parameters(config.election_parameters); + }); } } @@ -156,7 +212,7 @@ impl Module { } fn can_participate(sender: &T::AccountId) -> bool { - !T::Currency::free_balance(sender).is_zero() + !::Currency::free_balance(sender).is_zero() && >::is_member_account(sender) } @@ -187,15 +243,15 @@ impl Module { fn start_election(current_council: Seats>) -> Result { ensure!(!Self::is_election_running(), "election already in progress"); ensure!( - Self::existing_stake_holders().len() == 0, + Self::existing_stake_holders().is_empty(), "stake holders must be empty" ); - ensure!(Self::applicants().len() == 0, "applicants must be empty"); - ensure!(Self::commitments().len() == 0, "commitments must be empty"); + ensure!(Self::applicants().is_empty(), "applicants must be empty"); + ensure!(Self::commitments().is_empty(), "commitments must be empty"); // Take snapshot of seat and backing stakes of an existing council // Its important to note that the election system takes ownership of these stakes, and is responsible - // to return any unused stake to original owners and the end of the election. + // to return any unused stake to original owners at the end of the election. Self::initialize_transferable_stakes(current_council); Self::deposit_event(RawEvent::ElectionStarted()); @@ -249,6 +305,7 @@ impl Module { if len >= applicants.len() { &[] } else { + #[allow(clippy::redundant_closure)] // disable incorrect Clippy linter warning applicants.sort_by_key(|applicant| Self::applicant_stakes(applicant)); &applicants[0..applicants.len() - len] } @@ -303,17 +360,21 @@ impl Module { } } - if new_council.len() == Self::council_size_usize() { - // all applicants in the tally will form the new council - } else if new_council.len() > Self::council_size_usize() { - // we have more than enough applicants to form the new council. - // select top staked - Self::filter_top_staked(&mut new_council, Self::council_size_usize()); - } else { - // Not enough applicants with votes to form a council. - // This may happen if we didn't add applicants with zero votes to the tally, - // or in future if we allow applicants to withdraw candidacy during voting or revealing stages. - // or council size was increased during voting, revealing stages. + match new_council.len() { + ncl if ncl == Self::council_size_usize() => { + // all applicants in the tally will form the new council + } + ncl if ncl > Self::council_size_usize() => { + // we have more than enough applicants to form the new council. + // select top staked + Self::filter_top_staked(&mut new_council, Self::council_size_usize()); + } + _ => { + // Not enough applicants with votes to form a council. + // This may happen if we didn't add applicants with zero votes to the tally, + // or in future if we allow applicants to withdraw candidacy during voting or revealing stages. + // or council size was increased during voting, revealing stages. + } } // unless we want to add more filtering criteria to what is considered a successful election @@ -332,7 +393,7 @@ impl Module { } fn teardown_election( - votes: &Vec>, T::Hash, T::AccountId>>, + votes: &[SealedVote>, T::Hash, T::AccountId>], new_council: &BTreeMap>>, unlock_ts: bool, ) { @@ -356,7 +417,10 @@ impl Module { for stakeholder in Self::existing_stake_holders().iter() { let stake = Self::transferable_stakes(stakeholder); if !stake.seat.is_zero() || !stake.backing.is_zero() { - T::Currency::unreserve(stakeholder, stake.seat + stake.backing); + ::Currency::unreserve( + stakeholder, + stake.seat + stake.backing, + ); } } } @@ -381,7 +445,7 @@ impl Module { // return new stake to account's free balance if !stake.new.is_zero() { - T::Currency::unreserve(applicant, stake.new); + ::Currency::unreserve(applicant, stake.new); } // return unused transferable stake @@ -418,7 +482,7 @@ impl Module { } fn refund_voting_stakes( - sealed_votes: &Vec>, T::Hash, T::AccountId>>, + sealed_votes: &[SealedVote>, T::Hash, T::AccountId>], new_council: &BTreeMap>>, ) { for sealed_vote in sealed_votes.iter() { @@ -435,7 +499,7 @@ impl Module { // return new stake to account's free balance let SealedVote { voter, stake, .. } = sealed_vote; if !stake.new.is_zero() { - T::Currency::unreserve(voter, stake.new); + ::Currency::unreserve(voter, stake.new); } // return unused transferable stake @@ -456,7 +520,7 @@ impl Module { } fn tally_votes( - sealed_votes: &Vec>, T::Hash, T::AccountId>>, + sealed_votes: &[SealedVote>, T::Hash, T::AccountId>], ) -> BTreeMap>> { let mut tally: BTreeMap>> = BTreeMap::new(); @@ -612,7 +676,7 @@ impl Module { *transferable }; - *transferable = *transferable - transferred; + *transferable -= transferred; Stake { new: new_stake - transferred, @@ -626,12 +690,12 @@ impl Module { let new_stake = Self::new_stake_reusing_transferable(&mut transferable_stake.seat, stake); ensure!( - T::Currency::can_reserve(&applicant, new_stake.new), + ::Currency::can_reserve(&applicant, new_stake.new), "not enough free balance to reserve" ); ensure!( - T::Currency::reserve(&applicant, new_stake.new).is_ok(), + ::Currency::reserve(&applicant, new_stake.new).is_ok(), "failed to reserve applicant stake!" ); @@ -648,7 +712,7 @@ impl Module { >::mutate(|applicants| applicants.insert(0, applicant.clone())); } - >::insert(applicant.clone(), total_stake); + >::insert(applicant, total_stake); Ok(()) } @@ -662,12 +726,12 @@ impl Module { Self::new_stake_reusing_transferable(&mut transferable_stake.backing, stake); ensure!( - T::Currency::can_reserve(&voter, vote_stake.new), + ::Currency::can_reserve(&voter, vote_stake.new), "not enough free balance to reserve" ); ensure!( - T::Currency::reserve(&voter, vote_stake.new).is_ok(), + ::Currency::reserve(&voter, vote_stake.new).is_ok(), "failed to reserve voting stake!" ); @@ -703,7 +767,7 @@ impl Module { "vote for non-applicant not allowed" ); - let mut salt = salt.clone(); + let mut salt = salt; // Tries to unseal, if salt is invalid will return error sealed_vote.unseal(vote_for, &mut salt, ::Hashing::hash)?; @@ -713,6 +777,17 @@ impl Module { Ok(()) } + + fn set_verified_election_parameters(params: ElectionParameters, T::BlockNumber>) { + >::put(params.announcing_period); + >::put(params.voting_period); + >::put(params.revealing_period); + >::put(params.min_council_stake); + >::put(params.new_term_duration); + CouncilSize::put(params.council_size); + CandidacyLimit::put(params.candidacy_limit); + >::put(params.min_voting_stake); + } } decl_module! { @@ -803,52 +878,16 @@ decl_module! { >::put(ElectionStage::Voting(ends_at)); } - fn set_param_announcing_period(origin, period: T::BlockNumber) { - ensure_root(origin)?; - ensure!(!Self::is_election_running(), "cannot change params during election"); - ensure!(!period.is_zero(), "period cannot be zero"); - >::put(period); - } - fn set_param_voting_period(origin, period: T::BlockNumber) { - ensure_root(origin)?; - ensure!(!Self::is_election_running(), "cannot change params during election"); - ensure!(!period.is_zero(), "period cannot be zero"); - >::put(period); - } - fn set_param_revealing_period(origin, period: T::BlockNumber) { - ensure_root(origin)?; - ensure!(!Self::is_election_running(), "cannot change params during election"); - ensure!(!period.is_zero(), "period cannot be zero"); - >::put(period); - } - fn set_param_min_council_stake(origin, amount: BalanceOf) { - ensure_root(origin)?; - ensure!(!Self::is_election_running(), "cannot change params during election"); - >::put(amount); - } - fn set_param_new_term_duration(origin, duration: T::BlockNumber) { - ensure_root(origin)?; - ensure!(!Self::is_election_running(), "cannot change params during election"); - ensure!(!duration.is_zero(), "new term duration cannot be zero"); - >::put(duration); - } - fn set_param_council_size(origin, council_size: u32) { + /// Sets new election parameters. Some combination of parameters that are not desirable, so + /// the parameters are checked for validity. + /// The call will fail if an election is in progress. If a council is not being elected for some + /// reaon after multiple rounds, force_stop_election() can be called to stop elections and followed by + /// set_election_parameters(). + pub fn set_election_parameters(origin, params: ElectionParameters, T::BlockNumber>) { ensure_root(origin)?; - ensure!(!Self::is_election_running(), "cannot change params during election"); - ensure!(council_size > 0, "council size cannot be zero"); - ensure!(council_size <= Self::candidacy_limit(), "council size cannot greater than candidacy limit"); - CouncilSize::put(council_size); - } - fn set_param_candidacy_limit(origin, limit: u32) { - ensure_root(origin)?; - ensure!(!Self::is_election_running(), "cannot change params during election"); - ensure!(limit >= Self::council_size(), "candidacy limit cannot be less than council size"); - CandidacyLimit::put(limit); - } - fn set_param_min_voting_stake(origin, amount: BalanceOf) { - ensure_root(origin)?; - ensure!(!Self::is_election_running(), "cannot change params during election"); - >::put(amount); + ensure!(!Self::is_election_running(), MSG_CANNOT_CHANGE_PARAMS_DURING_ELECTION); + params.ensure_valid()?; + Self::set_verified_election_parameters(params); } fn force_stop_election(origin) { @@ -886,11 +925,7 @@ decl_module! { impl council::CouncilTermEnded for Module { fn council_term_ended() { if Self::auto_start() { - if Self::start_election(>::active_council()).is_ok() { - // emit ElectionStarted - } else { - // emit ElectionFailedStart - } + let _ = Self::start_election(>::active_council()); } } } @@ -2042,4 +2077,53 @@ mod tests { assert_ok!(Election::start_election(vec![])); }); } + + #[test] + fn setting_election_parameters() { + initial_test_ext().execute_with(|| { + let default_parameters: ElectionParameters = ElectionParameters::default(); + // default all zeros is invalid + assert!(default_parameters.ensure_valid().is_err()); + + let new_parameters = ElectionParameters { + announcing_period: 1, + voting_period: 2, + revealing_period: 3, + council_size: 4, + candidacy_limit: 5, + min_voting_stake: 6, + min_council_stake: 7, + new_term_duration: 8, + }; + + assert_ok!(Election::set_election_parameters( + Origin::ROOT, + new_parameters + )); + + assert_eq!( + >::get(), + new_parameters.announcing_period + ); + assert_eq!(>::get(), new_parameters.voting_period); + assert_eq!( + >::get(), + new_parameters.revealing_period + ); + assert_eq!( + >::get(), + new_parameters.min_council_stake + ); + assert_eq!( + >::get(), + new_parameters.new_term_duration + ); + assert_eq!(CouncilSize::get(), new_parameters.council_size); + assert_eq!(CandidacyLimit::get(), new_parameters.candidacy_limit); + assert_eq!( + >::get(), + new_parameters.min_voting_stake + ); + }); + } } diff --git a/runtime-modules/governance/src/election_params.rs b/runtime-modules/governance/src/election_params.rs new file mode 100644 index 0000000000..f34646e608 --- /dev/null +++ b/runtime-modules/governance/src/election_params.rs @@ -0,0 +1,48 @@ +use codec::{Decode, Encode}; +#[cfg(feature = "std")] +use serde::{Deserialize, Serialize}; +use sr_primitives::traits::Zero; +use srml_support::{dispatch::Result, ensure}; + +pub static MSG_PERIOD_CANNOT_BE_ZERO: &str = "PeriodCannotBeZero"; +pub static MSG_COUNCIL_SIZE_CANNOT_BE_ZERO: &str = "CouncilSizeCannotBeZero"; +pub static MSG_CANDIDACY_LIMIT_WAS_LOWER_THAN_COUNCIL_SIZE: &str = + "CandidacyWasLessThanCouncilSize"; + +/// Combined Election parameters, as argument for set_election_parameters +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +#[derive(Clone, Copy, Encode, Decode, Default, PartialEq, Debug)] +pub struct ElectionParameters { + pub announcing_period: BlockNumber, + pub voting_period: BlockNumber, + pub revealing_period: BlockNumber, + pub council_size: u32, + pub candidacy_limit: u32, + pub new_term_duration: BlockNumber, + pub min_council_stake: Balance, + pub min_voting_stake: Balance, +} + +impl ElectionParameters { + pub fn ensure_valid(&self) -> Result { + self.ensure_periods_are_valid()?; + self.ensure_council_size_and_candidacy_limit_are_valid()?; + Ok(()) + } + + fn ensure_periods_are_valid(&self) -> Result { + ensure!(!self.announcing_period.is_zero(), MSG_PERIOD_CANNOT_BE_ZERO); + ensure!(!self.voting_period.is_zero(), MSG_PERIOD_CANNOT_BE_ZERO); + ensure!(!self.revealing_period.is_zero(), MSG_PERIOD_CANNOT_BE_ZERO); + Ok(()) + } + + fn ensure_council_size_and_candidacy_limit_are_valid(&self) -> Result { + ensure!(self.council_size > 0, MSG_COUNCIL_SIZE_CANNOT_BE_ZERO); + ensure!( + self.council_size <= self.candidacy_limit, + MSG_CANDIDACY_LIMIT_WAS_LOWER_THAN_COUNCIL_SIZE + ); + Ok(()) + } +} diff --git a/runtime-modules/governance/src/lib.rs b/runtime-modules/governance/src/lib.rs index 9e1d712f8b..de98f9a5d4 100644 --- a/runtime-modules/governance/src/lib.rs +++ b/runtime-modules/governance/src/lib.rs @@ -3,7 +3,7 @@ pub mod council; pub mod election; -pub mod proposals; +pub mod election_params; mod sealed_vote; mod stake; diff --git a/runtime-modules/governance/src/mock.rs b/runtime-modules/governance/src/mock.rs index 5e6dc33dbe..ec637678dc 100644 --- a/runtime-modules/governance/src/mock.rs +++ b/runtime-modules/governance/src/mock.rs @@ -1,6 +1,6 @@ #![cfg(test)] -pub use super::{council, election, proposals}; +pub use super::{council, election}; pub use common::currency::GovernanceCurrency; pub use system; @@ -70,7 +70,15 @@ impl membership::members::Trait for Test { type ActorId = u32; type InitialMembersBalance = InitialMembersBalance; } - +impl minting::Trait for Test { + type Currency = Balances; + type MintId = u64; +} +impl recurringrewards::Trait for Test { + type PayoutStatusHandler = (); + type RecipientId = u64; + type RewardRelationshipId = u64; +} parameter_types! { pub const ExistentialDeposit: u32 = 0; pub const TransferFee: u32 = 0; diff --git a/runtime-modules/governance/src/proposals.rs b/runtime-modules/governance/src/proposals.rs deleted file mode 100644 index e681e51d6c..0000000000 --- a/runtime-modules/governance/src/proposals.rs +++ /dev/null @@ -1,1572 +0,0 @@ -use codec::{Decode, Encode}; -use rstd::prelude::*; -use sr_primitives::{ - print, - traits::{Hash, SaturatedConversion, Zero}, -}; -use srml_support::traits::{Currency, Get, ReservableCurrency}; -use srml_support::{decl_event, decl_module, decl_storage, dispatch, ensure}; -use system::{self, ensure_root, ensure_signed}; - -#[cfg(feature = "std")] -use serde::{Deserialize, Serialize}; - -#[cfg(test)] -use primitives::storage::well_known_keys; - -use super::council; -pub use common::currency::{BalanceOf, GovernanceCurrency}; - -const DEFAULT_APPROVAL_QUORUM: u32 = 60; -const DEFAULT_MIN_STAKE: u32 = 100; -const DEFAULT_CANCELLATION_FEE: u32 = 5; -const DEFAULT_REJECTION_FEE: u32 = 10; - -const DEFAULT_VOTING_PERIOD_IN_DAYS: u32 = 10; -const DEFAULT_VOTING_PERIOD_IN_SECS: u32 = DEFAULT_VOTING_PERIOD_IN_DAYS * 24 * 60 * 60; - -const DEFAULT_NAME_MAX_LEN: u32 = 100; -const DEFAULT_DESCRIPTION_MAX_LEN: u32 = 10_000; -const DEFAULT_WASM_CODE_MAX_LEN: u32 = 2_000_000; - -const MSG_STAKE_IS_TOO_LOW: &str = "Stake is too low"; -const MSG_STAKE_IS_GREATER_THAN_BALANCE: &str = "Balance is too low to be staked"; -const MSG_ONLY_MEMBERS_CAN_PROPOSE: &str = "Only members can make a proposal"; -const MSG_ONLY_COUNCILORS_CAN_VOTE: &str = "Only councilors can vote on proposals"; -const MSG_PROPOSAL_NOT_FOUND: &str = "This proposal does not exist"; -const MSG_PROPOSAL_EXPIRED: &str = "Voting period is expired for this proposal"; -const MSG_PROPOSAL_FINALIZED: &str = "Proposal is finalized already"; -const MSG_YOU_ALREADY_VOTED: &str = "You have already voted on this proposal"; -const MSG_YOU_DONT_OWN_THIS_PROPOSAL: &str = "You do not own this proposal"; -const MSG_PROPOSAL_STATUS_ALREADY_UPDATED: &str = "Proposal status has been updated already"; -const MSG_EMPTY_NAME_PROVIDED: &str = "Proposal cannot have an empty name"; -const MSG_EMPTY_DESCRIPTION_PROVIDED: &str = "Proposal cannot have an empty description"; -const MSG_EMPTY_WASM_CODE_PROVIDED: &str = "Proposal cannot have an empty WASM code"; -const MSG_TOO_LONG_NAME: &str = "Name is too long"; -const MSG_TOO_LONG_DESCRIPTION: &str = "Description is too long"; -const MSG_TOO_LONG_WASM_CODE: &str = "WASM code is too big"; - -#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))] -#[derive(Encode, Decode, Clone, PartialEq, Eq)] -pub enum ProposalStatus { - /// A new proposal that is available for voting. - Active, - /// If cancelled by a proposer. - Cancelled, - /// Not enough votes and voting period expired. - Expired, - /// To clear the quorum requirement, the percentage of council members with revealed votes - /// must be no less than the quorum value for the given proposal type. - Approved, - Rejected, - /// If all revealed votes are slashes, then the proposal is rejected, - /// and the proposal stake is slashed. - Slashed, -} - -impl Default for ProposalStatus { - fn default() -> Self { - ProposalStatus::Active - } -} - -use self::ProposalStatus::*; - -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)] -pub enum VoteKind { - /// Signals presence, but unwillingness to cast judgment on substance of vote. - Abstain, - /// Pass, an alternative or a ranking, for binary, multiple choice - /// and ranked choice propositions, respectively. - Approve, - /// Against proposal. - Reject, - /// Against the proposal, and slash proposal stake. - Slash, -} - -impl Default for VoteKind { - fn default() -> Self { - VoteKind::Abstain - } -} - -use self::VoteKind::*; - -#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))] -#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)] -/// Proposal for node runtime update. -pub struct RuntimeUpgradeProposal { - id: u32, - proposer: AccountId, - stake: Balance, - name: Vec, - description: Vec, - wasm_hash: Hash, - proposed_at: BlockNumber, - status: ProposalStatus, -} - -#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))] -#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)] -pub struct TallyResult { - proposal_id: u32, - abstentions: u32, - approvals: u32, - rejections: u32, - slashes: u32, - status: ProposalStatus, - finalized_at: BlockNumber, -} - -pub trait Trait: - timestamp::Trait + council::Trait + GovernanceCurrency + membership::members::Trait -{ - /// The overarching event type. - type Event: From> + Into<::Event>; -} - -decl_event!( - pub enum Event - where - ::Hash, - ::BlockNumber, - ::AccountId - { - // New events - - /// Params: - /// * Account id of a member who proposed. - /// * Id of a newly created proposal after it was saved in storage. - ProposalCreated(AccountId, u32), - ProposalCanceled(AccountId, u32), - ProposalStatusUpdated(u32, ProposalStatus), - - /// Params: - /// * Voter - an account id of a councilor. - /// * Id of a proposal. - /// * Kind of vote. - Voted(AccountId, u32, VoteKind), - - TallyFinalized(TallyResult), - - /// * Hash - hash of wasm code of runtime update. - RuntimeUpdated(u32, Hash), - - /// Root cancelled proposal - ProposalVetoed(u32), - } -); - -decl_storage! { - trait Store for Module as Proposals { - - // Parameters (defaut values could be exported to config): - - // TODO rename 'approval_quorum' -> 'quorum_percent' ?! - /// A percent (up to 100) of the council participants - /// that must vote affirmatively in order to pass. - ApprovalQuorum get(approval_quorum) config(): u32 = DEFAULT_APPROVAL_QUORUM; - - /// Minimum amount of a balance to be staked in order to make a proposal. - MinStake get(min_stake) config(): BalanceOf = - BalanceOf::::from(DEFAULT_MIN_STAKE); - - /// A fee to be slashed (burn) in case a proposer decides to cancel a proposal. - CancellationFee get(cancellation_fee) config(): BalanceOf = - BalanceOf::::from(DEFAULT_CANCELLATION_FEE); - - /// A fee to be slashed (burn) in case a proposal was rejected. - RejectionFee get(rejection_fee) config(): BalanceOf = - BalanceOf::::from(DEFAULT_REJECTION_FEE); - - /// Max duration of proposal in blocks until it will be expired if not enough votes. - VotingPeriod get(voting_period) config(): T::BlockNumber = - T::BlockNumber::from(DEFAULT_VOTING_PERIOD_IN_SECS / - (::MinimumPeriod::get().saturated_into::() * 2)); - - NameMaxLen get(name_max_len) config(): u32 = DEFAULT_NAME_MAX_LEN; - DescriptionMaxLen get(description_max_len) config(): u32 = DEFAULT_DESCRIPTION_MAX_LEN; - WasmCodeMaxLen get(wasm_code_max_len) config(): u32 = DEFAULT_WASM_CODE_MAX_LEN; - - // Persistent state (always relevant, changes constantly): - - /// Count of all proposals that have been created. - ProposalCount get(proposal_count): u32; - - /// Get proposal details by its id. - Proposals get(proposals): map u32 => RuntimeUpgradeProposal, T::BlockNumber, T::Hash>; - - /// Ids of proposals that are open for voting (have not been finalized yet). - ActiveProposalIds get(active_proposal_ids): Vec = vec![]; - - /// Get WASM code of runtime upgrade by hash of its content. - WasmCodeByHash get(wasm_code_by_hash): map T::Hash => Vec; - - VotesByProposal get(votes_by_proposal): map u32 => Vec<(T::AccountId, VoteKind)>; - - // TODO Rethink: this can be replaced with: votes_by_proposal.find(|vote| vote.0 == proposer) - VoteByAccountAndProposal get(vote_by_account_and_proposal): map (T::AccountId, u32) => VoteKind; - - TallyResults get(tally_results): map u32 => TallyResult; - } -} - -decl_module! { - pub struct Module for enum Call where origin: T::Origin { - - fn deposit_event() = default; - - /// Use next code to create a proposal from Substrate UI's web console: - /// ```js - /// post({ sender: runtime.indices.ss58Decode('F7Gh'), call: calls.proposals.createProposal(2500, "0x123", "0x456", "0x789") }).tie(console.log) - /// ``` - fn create_proposal( - origin, - stake: BalanceOf, - name: Vec, - description: Vec, - wasm_code: Vec - ) { - - let proposer = ensure_signed(origin)?; - ensure!(Self::can_participate(&proposer), MSG_ONLY_MEMBERS_CAN_PROPOSE); - ensure!(stake >= Self::min_stake(), MSG_STAKE_IS_TOO_LOW); - - ensure!(!name.is_empty(), MSG_EMPTY_NAME_PROVIDED); - ensure!(name.len() as u32 <= Self::name_max_len(), MSG_TOO_LONG_NAME); - - ensure!(!description.is_empty(), MSG_EMPTY_DESCRIPTION_PROVIDED); - ensure!(description.len() as u32 <= Self::description_max_len(), MSG_TOO_LONG_DESCRIPTION); - - ensure!(!wasm_code.is_empty(), MSG_EMPTY_WASM_CODE_PROVIDED); - ensure!(wasm_code.len() as u32 <= Self::wasm_code_max_len(), MSG_TOO_LONG_WASM_CODE); - - // Lock proposer's stake: - T::Currency::reserve(&proposer, stake) - .map_err(|_| MSG_STAKE_IS_GREATER_THAN_BALANCE)?; - - let proposal_id = Self::proposal_count() + 1; - ProposalCount::put(proposal_id); - - // See in substrate repo @ srml/contract/src/wasm/code_cache.rs:73 - let wasm_hash = T::Hashing::hash(&wasm_code); - - let new_proposal = RuntimeUpgradeProposal { - id: proposal_id, - proposer: proposer.clone(), - stake, - name, - description, - wasm_hash, - proposed_at: Self::current_block(), - status: Active - }; - - if !>::exists(wasm_hash) { - >::insert(wasm_hash, wasm_code); - } - >::insert(proposal_id, new_proposal); - ActiveProposalIds::mutate(|ids| ids.push(proposal_id)); - Self::deposit_event(RawEvent::ProposalCreated(proposer.clone(), proposal_id)); - - // Auto-vote with Approve if proposer is a councilor: - if Self::is_councilor(&proposer) { - Self::_process_vote(proposer, proposal_id, Approve)?; - } - } - - /// Use next code to create a proposal from Substrate UI's web console: - /// ```js - /// post({ sender: runtime.indices.ss58Decode('F7Gh'), call: calls.proposals.voteOnProposal(1, { option: "Approve", _type: "VoteKind" }) }).tie(console.log) - /// ``` - fn vote_on_proposal(origin, proposal_id: u32, vote: VoteKind) { - let voter = ensure_signed(origin)?; - ensure!(Self::is_councilor(&voter), MSG_ONLY_COUNCILORS_CAN_VOTE); - - ensure!(>::exists(proposal_id), MSG_PROPOSAL_NOT_FOUND); - let proposal = Self::proposals(proposal_id); - - ensure!(proposal.status == Active, MSG_PROPOSAL_FINALIZED); - - let not_expired = !Self::is_voting_period_expired(proposal.proposed_at); - ensure!(not_expired, MSG_PROPOSAL_EXPIRED); - - let did_not_vote_before = !>::exists((voter.clone(), proposal_id)); - ensure!(did_not_vote_before, MSG_YOU_ALREADY_VOTED); - - Self::_process_vote(voter, proposal_id, vote)?; - } - - // TODO add 'reason' why a proposer wants to cancel (UX + feedback)? - /// Cancel a proposal by its original proposer. Some fee will be withdrawn from his balance. - fn cancel_proposal(origin, proposal_id: u32) { - let proposer = ensure_signed(origin)?; - - ensure!(>::exists(proposal_id), MSG_PROPOSAL_NOT_FOUND); - let proposal = Self::proposals(proposal_id); - - ensure!(proposer == proposal.proposer, MSG_YOU_DONT_OWN_THIS_PROPOSAL); - ensure!(proposal.status == Active, MSG_PROPOSAL_FINALIZED); - - // Spend some minimum fee on proposer's balance for canceling a proposal - let fee = Self::cancellation_fee(); - let _ = T::Currency::slash_reserved(&proposer, fee); - - // Return unspent part of remaining staked deposit (after taking some fee) - let left_stake = proposal.stake - fee; - let _ = T::Currency::unreserve(&proposer, left_stake); - - Self::_update_proposal_status(proposal_id, Cancelled)?; - Self::deposit_event(RawEvent::ProposalCanceled(proposer, proposal_id)); - } - - // Called on every block - fn on_finalize(n: T::BlockNumber) { - if let Err(e) = Self::end_block(n) { - print(e); - } - } - - /// Cancel a proposal and return stake without slashing - fn veto_proposal(origin, proposal_id: u32) { - ensure_root(origin)?; - ensure!(>::exists(proposal_id), MSG_PROPOSAL_NOT_FOUND); - let proposal = Self::proposals(proposal_id); - ensure!(proposal.status == Active, MSG_PROPOSAL_FINALIZED); - - let _ = T::Currency::unreserve(&proposal.proposer, proposal.stake); - - Self::_update_proposal_status(proposal_id, Cancelled)?; - - Self::deposit_event(RawEvent::ProposalVetoed(proposal_id)); - } - - fn set_approval_quorum(origin, new_value: u32) { - ensure_root(origin)?; - ensure!(new_value > 0, "approval quorom must be greater than zero"); - ApprovalQuorum::put(new_value); - } - } -} - -impl Module { - fn current_block() -> T::BlockNumber { - >::block_number() - } - - fn can_participate(sender: &T::AccountId) -> bool { - !T::Currency::free_balance(sender).is_zero() - && >::is_member_account(sender) - } - - fn is_councilor(sender: &T::AccountId) -> bool { - >::is_councilor(sender) - } - - fn councilors_count() -> u32 { - >::active_council().len() as u32 - } - - fn approval_quorum_seats() -> u32 { - (Self::approval_quorum() * Self::councilors_count()) / 100 - } - - fn is_voting_period_expired(proposed_at: T::BlockNumber) -> bool { - Self::current_block() >= proposed_at + Self::voting_period() - } - - fn _process_vote(voter: T::AccountId, proposal_id: u32, vote: VoteKind) -> dispatch::Result { - let new_vote = (voter.clone(), vote.clone()); - if >::exists(proposal_id) { - // Append a new vote to other votes on this proposal: - >::mutate(proposal_id, |votes| votes.push(new_vote)); - } else { - // This is the first vote on this proposal: - >::insert(proposal_id, vec![new_vote]); - } - >::insert((voter.clone(), proposal_id), &vote); - Self::deposit_event(RawEvent::Voted(voter, proposal_id, vote)); - Ok(()) - } - - fn end_block(_now: T::BlockNumber) -> dispatch::Result { - // TODO refactor this method - - // TODO iterate over not expired proposals and tally - - Self::tally()?; - // TODO approve or reject a proposal - - Ok(()) - } - - /// Get the voters for the current proposal. - pub fn tally() -> dispatch::Result { - let councilors: u32 = Self::councilors_count(); - let quorum: u32 = Self::approval_quorum_seats(); - - for &proposal_id in Self::active_proposal_ids().iter() { - let votes = Self::votes_by_proposal(proposal_id); - let mut abstentions: u32 = 0; - let mut approvals: u32 = 0; - let mut rejections: u32 = 0; - let mut slashes: u32 = 0; - - for (_, vote) in votes.iter() { - match vote { - Abstain => abstentions += 1, - Approve => approvals += 1, - Reject => rejections += 1, - Slash => slashes += 1, - } - } - - let proposal = Self::proposals(proposal_id); - let is_expired = Self::is_voting_period_expired(proposal.proposed_at); - - // We need to check that the council is not empty because otherwise, - // if there is no votes on a proposal it will be counted as if - // all 100% (zero) councilors voted on the proposal and should be approved. - - let non_empty_council = councilors > 0; - let all_councilors_voted = non_empty_council && votes.len() as u32 == councilors; - let all_councilors_slashed = non_empty_council && slashes == councilors; - let quorum_reached = quorum > 0 && approvals >= quorum; - - // Don't approve a proposal right after quorum reached - // if not all councilors casted their votes. - // Instead let other councilors cast their vote - // up until the proposal's expired. - - let new_status: Option = if all_councilors_slashed { - Some(Slashed) - } else if all_councilors_voted { - if quorum_reached { - Some(Approved) - } else { - Some(Rejected) - } - } else if is_expired { - if quorum_reached { - Some(Approved) - } else { - // Proposal has been expired and quorum not reached. - Some(Expired) - } - } else { - // Councilors still have time to vote on this proposal. - None - }; - - // TODO move next block outside of tally to 'end_block' - if let Some(status) = new_status { - Self::_update_proposal_status(proposal_id, status.clone())?; - let tally_result = TallyResult { - proposal_id, - abstentions, - approvals, - rejections, - slashes, - status, - finalized_at: Self::current_block(), - }; - >::insert(proposal_id, &tally_result); - Self::deposit_event(RawEvent::TallyFinalized(tally_result)); - } - } - - Ok(()) - } - - /// Updates proposal status and removes proposal from active ids. - fn _update_proposal_status(proposal_id: u32, new_status: ProposalStatus) -> dispatch::Result { - let all_active_ids = Self::active_proposal_ids(); - let all_len = all_active_ids.len(); - let other_active_ids: Vec = all_active_ids - .into_iter() - .filter(|&id| id != proposal_id) - .collect(); - - let not_found_in_active = other_active_ids.len() == all_len; - if not_found_in_active { - // Seems like this proposal's status has been updated and removed from active. - Err(MSG_PROPOSAL_STATUS_ALREADY_UPDATED) - } else { - let pid = proposal_id.clone(); - match new_status { - Slashed => Self::_slash_proposal(pid)?, - Rejected | Expired => Self::_reject_proposal(pid)?, - Approved => Self::_approve_proposal(pid)?, - Active | Cancelled => { /* nothing */ } - } - ActiveProposalIds::put(other_active_ids); - >::mutate(proposal_id, |p| p.status = new_status.clone()); - Self::deposit_event(RawEvent::ProposalStatusUpdated(proposal_id, new_status)); - Ok(()) - } - } - - /// Slash a proposal. The staked deposit will be slashed. - fn _slash_proposal(proposal_id: u32) -> dispatch::Result { - let proposal = Self::proposals(proposal_id); - - // Slash proposer's stake: - let _ = T::Currency::slash_reserved(&proposal.proposer, proposal.stake); - - Ok(()) - } - - /// Reject a proposal. The staked deposit will be returned to a proposer. - fn _reject_proposal(proposal_id: u32) -> dispatch::Result { - let proposal = Self::proposals(proposal_id); - let proposer = proposal.proposer; - - // Spend some minimum fee on proposer's balance to prevent spamming attacks: - let fee = Self::rejection_fee(); - let _ = T::Currency::slash_reserved(&proposer, fee); - - // Return unspent part of remaining staked deposit (after taking some fee): - let left_stake = proposal.stake - fee; - let _ = T::Currency::unreserve(&proposer, left_stake); - - Ok(()) - } - - /// Approve a proposal. The staked deposit will be returned. - fn _approve_proposal(proposal_id: u32) -> dispatch::Result { - let proposal = Self::proposals(proposal_id); - let wasm_code = Self::wasm_code_by_hash(proposal.wasm_hash); - - // Return staked deposit to proposer: - let _ = T::Currency::unreserve(&proposal.proposer, proposal.stake); - - // Update wasm code of node's runtime: - >::set_code(system::RawOrigin::Root.into(), wasm_code)?; - - Self::deposit_event(RawEvent::RuntimeUpdated(proposal_id, proposal.wasm_hash)); - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - - use super::*; - use primitives::H256; - // The testing primitives are very useful for avoiding having to work with signatures - // or public keys. `u64` is used as the `AccountId` and no `Signature`s are requried. - use sr_primitives::{ - testing::Header, - traits::{BlakeTwo256, IdentityLookup}, - Perbill, - }; - use srml_support::*; - - impl_outer_origin! { - pub enum Origin for Test {} - } - - // Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted. - #[derive(Clone, PartialEq, Eq, Debug)] - pub struct Test; - - parameter_types! { - pub const BlockHashCount: u64 = 250; - pub const MaximumBlockWeight: u32 = 1024; - pub const MaximumBlockLength: u32 = 2 * 1024; - pub const AvailableBlockRatio: Perbill = Perbill::one(); - pub const MinimumPeriod: u64 = 5; - } - - impl system::Trait for Test { - type Origin = Origin; - type Index = u64; - type BlockNumber = u64; - type Call = (); - type Hash = H256; - type Hashing = BlakeTwo256; - type AccountId = u64; - type Lookup = IdentityLookup; - type Header = Header; - type Event = (); - type BlockHashCount = BlockHashCount; - type MaximumBlockWeight = MaximumBlockWeight; - type MaximumBlockLength = MaximumBlockLength; - type AvailableBlockRatio = AvailableBlockRatio; - type Version = (); - } - - impl timestamp::Trait for Test { - type Moment = u64; - type OnTimestampSet = (); - type MinimumPeriod = MinimumPeriod; - } - - parameter_types! { - pub const ExistentialDeposit: u32 = 0; - pub const TransferFee: u32 = 0; - pub const CreationFee: u32 = 0; - pub const TransactionBaseFee: u32 = 1; - pub const TransactionByteFee: u32 = 0; - pub const InitialMembersBalance: u32 = 0; - } - - impl balances::Trait for Test { - /// The type for recording an account's balance. - type Balance = u64; - /// What to do if an account's free balance gets zeroed. - type OnFreeBalanceZero = (); - /// What to do if a new account is created. - type OnNewAccount = (); - /// The ubiquitous event type. - type Event = (); - - type DustRemoval = (); - type TransferPayment = (); - type ExistentialDeposit = ExistentialDeposit; - type TransferFee = TransferFee; - type CreationFee = CreationFee; - } - - impl council::Trait for Test { - type Event = (); - type CouncilTermEnded = (); - } - - impl GovernanceCurrency for Test { - type Currency = balances::Module; - } - - impl membership::members::Trait for Test { - type Event = (); - type MemberId = u32; - type PaidTermId = u32; - type SubscriptionId = u32; - type ActorId = u32; - type InitialMembersBalance = InitialMembersBalance; - } - - impl Trait for Test { - type Event = (); - } - - type System = system::Module; - type Balances = balances::Module; - type Proposals = Module; - - const COUNCILOR1: u64 = 1; - const COUNCILOR2: u64 = 2; - const COUNCILOR3: u64 = 3; - const COUNCILOR4: u64 = 4; - const COUNCILOR5: u64 = 5; - - const PROPOSER1: u64 = 11; - const PROPOSER2: u64 = 12; - - const NOT_COUNCILOR: u64 = 22; - - const ALL_COUNCILORS: [u64; 5] = [COUNCILOR1, COUNCILOR2, COUNCILOR3, COUNCILOR4, COUNCILOR5]; - - // TODO Figure out how to test Events in test... (low priority) - // mod proposals { - // pub use ::Event; - // } - // impl_outer_event!{ - // pub enum TestEvent for Test { - // balances,system,proposals, - // } - // } - - // This function basically just builds a genesis storage key/value store according to - // our desired mockup. - fn new_test_ext() -> runtime_io::TestExternalities { - let mut t = system::GenesisConfig::default() - .build_storage::() - .unwrap(); - - // balances doesn't contain GenesisConfig anymore - // // We use default for brevity, but you can configure as desired if needed. - // balances::GenesisConfig::::default() - // .assimilate_storage(&mut t) - // .unwrap(); - - let council_mock: council::Seats = ALL_COUNCILORS - .iter() - .map(|&c| council::Seat { - member: c, - stake: 0u64, - backers: vec![], - }) - .collect(); - - council::GenesisConfig:: { - active_council: council_mock, - term_ends_at: 0, - } - .assimilate_storage(&mut t) - .unwrap(); - - membership::members::GenesisConfig:: { - default_paid_membership_fee: 0, - members: vec![ - (PROPOSER1, "alice".into(), "".into(), "".into()), - (PROPOSER2, "bobby".into(), "".into(), "".into()), - (COUNCILOR1, "councilor1".into(), "".into(), "".into()), - (COUNCILOR2, "councilor2".into(), "".into(), "".into()), - (COUNCILOR3, "councilor3".into(), "".into(), "".into()), - (COUNCILOR4, "councilor4".into(), "".into(), "".into()), - (COUNCILOR5, "councilor5".into(), "".into(), "".into()), - ], - } - .assimilate_storage(&mut t) - .unwrap(); - // t.extend(GenesisConfig::{ - // // Here we can override defaults. - // }.build_storage().unwrap().0); - - t.into() - } - - /// A shortcut to get minimum stake in tests. - fn min_stake() -> u64 { - Proposals::min_stake() - } - - /// A shortcut to get cancellation fee in tests. - fn cancellation_fee() -> u64 { - Proposals::cancellation_fee() - } - - /// A shortcut to get rejection fee in tests. - fn rejection_fee() -> u64 { - Proposals::rejection_fee() - } - - /// Initial balance of Proposer 1. - fn initial_balance() -> u64 { - (min_stake() as f64 * 2.5) as u64 - } - - fn name() -> Vec { - b"Proposal Name".to_vec() - } - - fn description() -> Vec { - b"Proposal Description".to_vec() - } - - fn wasm_code() -> Vec { - b"Proposal Wasm Code".to_vec() - } - - fn _create_default_proposal() -> dispatch::Result { - _create_proposal(None, None, None, None, None) - } - - fn _create_proposal( - origin: Option, - stake: Option, - name: Option>, - description: Option>, - wasm_code: Option>, - ) -> dispatch::Result { - Proposals::create_proposal( - Origin::signed(origin.unwrap_or(PROPOSER1)), - stake.unwrap_or(min_stake()), - name.unwrap_or(self::name()), - description.unwrap_or(self::description()), - wasm_code.unwrap_or(self::wasm_code()), - ) - } - - fn get_runtime_code() -> Option> { - storage::unhashed::get_raw(well_known_keys::CODE) - } - - macro_rules! assert_runtime_code_empty { - () => { - assert_eq!(get_runtime_code(), Some(vec![])) - }; - } - - macro_rules! assert_runtime_code { - ($code:expr) => { - assert_eq!(get_runtime_code(), Some($code)) - }; - } - - #[test] - fn check_default_values() { - new_test_ext().execute_with(|| { - assert_eq!(Proposals::approval_quorum(), DEFAULT_APPROVAL_QUORUM); - assert_eq!( - Proposals::min_stake(), - BalanceOf::::from(DEFAULT_MIN_STAKE) - ); - assert_eq!( - Proposals::cancellation_fee(), - BalanceOf::::from(DEFAULT_CANCELLATION_FEE) - ); - assert_eq!( - Proposals::rejection_fee(), - BalanceOf::::from(DEFAULT_REJECTION_FEE) - ); - assert_eq!(Proposals::name_max_len(), DEFAULT_NAME_MAX_LEN); - assert_eq!( - Proposals::description_max_len(), - DEFAULT_DESCRIPTION_MAX_LEN - ); - assert_eq!(Proposals::wasm_code_max_len(), DEFAULT_WASM_CODE_MAX_LEN); - assert_eq!(Proposals::proposal_count(), 0); - assert!(Proposals::active_proposal_ids().is_empty()); - }); - } - - #[test] - fn member_create_proposal() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - assert_ok!(_create_default_proposal()); - assert_eq!(Proposals::active_proposal_ids().len(), 1); - assert_eq!(Proposals::active_proposal_ids()[0], 1); - - let wasm_hash = BlakeTwo256::hash(&wasm_code()); - let expected_proposal = RuntimeUpgradeProposal { - id: 1, - proposer: PROPOSER1, - stake: min_stake(), - name: name(), - description: description(), - wasm_hash, - proposed_at: 1, - status: Active, - }; - assert_eq!(Proposals::proposals(1), expected_proposal); - - // Check that stake amount has been locked on proposer's balance: - assert_eq!( - Balances::free_balance(PROPOSER1), - initial_balance() - min_stake() - ); - assert_eq!(Balances::reserved_balance(PROPOSER1), min_stake()); - - // TODO expect event ProposalCreated(AccountId, u32) - }); - } - - #[test] - fn not_member_cannot_create_proposal() { - new_test_ext().execute_with(|| { - // In this test a proposer has an empty balance - // thus he is not considered as a member. - assert_eq!( - _create_default_proposal(), - Err(MSG_ONLY_MEMBERS_CAN_PROPOSE) - ); - }); - } - - #[test] - fn cannot_create_proposal_with_small_stake() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - assert_eq!( - _create_proposal(None, Some(min_stake() - 1), None, None, None), - Err(MSG_STAKE_IS_TOO_LOW) - ); - - // Check that balances remain unchanged afer a failed attempt to create a proposal: - assert_eq!(Balances::free_balance(PROPOSER1), initial_balance()); - assert_eq!(Balances::reserved_balance(PROPOSER1), 0); - }); - } - - #[test] - fn cannot_create_proposal_when_stake_is_greater_than_balance() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - assert_eq!( - _create_proposal(None, Some(initial_balance() + 1), None, None, None), - Err(MSG_STAKE_IS_GREATER_THAN_BALANCE) - ); - - // Check that balances remain unchanged afer a failed attempt to create a proposal: - assert_eq!(Balances::free_balance(PROPOSER1), initial_balance()); - assert_eq!(Balances::reserved_balance(PROPOSER1), 0); - }); - } - - #[test] - fn cannot_create_proposal_with_empty_values() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - // Empty name: - assert_eq!( - _create_proposal(None, None, Some(vec![]), None, None), - Err(MSG_EMPTY_NAME_PROVIDED) - ); - - // Empty description: - assert_eq!( - _create_proposal(None, None, None, Some(vec![]), None), - Err(MSG_EMPTY_DESCRIPTION_PROVIDED) - ); - - // Empty WASM code: - assert_eq!( - _create_proposal(None, None, None, None, Some(vec![])), - Err(MSG_EMPTY_WASM_CODE_PROVIDED) - ); - }); - } - - #[test] - fn cannot_create_proposal_with_too_long_values() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - // Too long name: - assert_eq!( - _create_proposal(None, None, Some(too_long_name()), None, None), - Err(MSG_TOO_LONG_NAME) - ); - - // Too long description: - assert_eq!( - _create_proposal(None, None, None, Some(too_long_description()), None), - Err(MSG_TOO_LONG_DESCRIPTION) - ); - - // Too long WASM code: - assert_eq!( - _create_proposal(None, None, None, None, Some(too_long_wasm_code())), - Err(MSG_TOO_LONG_WASM_CODE) - ); - }); - } - - fn too_long_name() -> Vec { - vec![65; Proposals::name_max_len() as usize + 1] - } - - fn too_long_description() -> Vec { - vec![65; Proposals::description_max_len() as usize + 1] - } - - fn too_long_wasm_code() -> Vec { - vec![65; Proposals::wasm_code_max_len() as usize + 1] - } - - // ------------------------------------------------------------------- - // Cancellation - - #[test] - fn owner_cancel_proposal() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - assert_ok!(_create_default_proposal()); - assert_ok!(Proposals::cancel_proposal(Origin::signed(PROPOSER1), 1)); - assert_eq!(Proposals::proposals(1).status, Cancelled); - assert!(Proposals::active_proposal_ids().is_empty()); - - // Check that proposer's balance reduced by cancellation fee and other part of his stake returned to his balance: - assert_eq!( - Balances::free_balance(PROPOSER1), - initial_balance() - cancellation_fee() - ); - assert_eq!(Balances::reserved_balance(PROPOSER1), 0); - - // TODO expect event ProposalCancelled(AccountId, u32) - }); - } - - #[test] - fn owner_cannot_cancel_proposal_if_its_finalized() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - assert_ok!(_create_default_proposal()); - assert_ok!(Proposals::cancel_proposal(Origin::signed(PROPOSER1), 1)); - assert_eq!(Proposals::proposals(1).status, Cancelled); - - // Get balances updated after cancelling a proposal: - let updated_free_balance = Balances::free_balance(PROPOSER1); - let updated_reserved_balance = Balances::reserved_balance(PROPOSER1); - - assert_eq!( - Proposals::cancel_proposal(Origin::signed(PROPOSER1), 1), - Err(MSG_PROPOSAL_FINALIZED) - ); - - // Check that proposer's balance and locked stake haven't been changed: - assert_eq!(Balances::free_balance(PROPOSER1), updated_free_balance); - assert_eq!( - Balances::reserved_balance(PROPOSER1), - updated_reserved_balance - ); - }); - } - - #[test] - fn not_owner_cannot_cancel_proposal() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - let _ = Balances::deposit_creating(&PROPOSER2, initial_balance()); - assert_ok!(_create_default_proposal()); - assert_eq!( - Proposals::cancel_proposal(Origin::signed(PROPOSER2), 1), - Err(MSG_YOU_DONT_OWN_THIS_PROPOSAL) - ); - }); - } - - // ------------------------------------------------------------------- - // Voting - - #[test] - fn councilor_vote_on_proposal() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - assert_ok!(_create_default_proposal()); - - assert_ok!(Proposals::vote_on_proposal( - Origin::signed(COUNCILOR1), - 1, - Approve - )); - - // Check that a vote has been saved: - assert_eq!(Proposals::votes_by_proposal(1), vec![(COUNCILOR1, Approve)]); - assert_eq!( - Proposals::vote_by_account_and_proposal((COUNCILOR1, 1)), - Approve - ); - - // TODO expect event Voted(PROPOSER1, 1, Approve) - }); - } - - #[test] - fn councilor_cannot_vote_on_proposal_twice() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - assert_ok!(_create_default_proposal()); - - assert_ok!(Proposals::vote_on_proposal( - Origin::signed(COUNCILOR1), - 1, - Approve - )); - assert_eq!( - Proposals::vote_on_proposal(Origin::signed(COUNCILOR1), 1, Approve), - Err(MSG_YOU_ALREADY_VOTED) - ); - }); - } - - #[test] - fn autovote_with_approve_when_councilor_creates_proposal() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&COUNCILOR1, initial_balance()); - - assert_ok!(_create_proposal(Some(COUNCILOR1), None, None, None, None)); - - // Check that a vote has been sent automatically, - // such as the proposer is a councilor: - assert_eq!(Proposals::votes_by_proposal(1), vec![(COUNCILOR1, Approve)]); - assert_eq!( - Proposals::vote_by_account_and_proposal((COUNCILOR1, 1)), - Approve - ); - }); - } - - #[test] - fn not_councilor_cannot_vote_on_proposal() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - assert_ok!(_create_default_proposal()); - assert_eq!( - Proposals::vote_on_proposal(Origin::signed(NOT_COUNCILOR), 1, Approve), - Err(MSG_ONLY_COUNCILORS_CAN_VOTE) - ); - }); - } - - #[test] - fn councilor_cannot_vote_on_proposal_if_it_has_been_cancelled() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - assert_ok!(_create_default_proposal()); - assert_ok!(Proposals::cancel_proposal(Origin::signed(PROPOSER1), 1)); - assert_eq!( - Proposals::vote_on_proposal(Origin::signed(COUNCILOR1), 1, Approve), - Err(MSG_PROPOSAL_FINALIZED) - ); - }); - } - - #[test] - fn councilor_cannot_vote_on_proposal_if_tally_has_been_finalized() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - assert_ok!(_create_default_proposal()); - - // All councilors vote with 'Approve' on proposal: - let mut expected_votes: Vec<(u64, VoteKind)> = vec![]; - for &councilor in ALL_COUNCILORS.iter() { - expected_votes.push((councilor, Approve)); - assert_ok!(Proposals::vote_on_proposal( - Origin::signed(councilor), - 1, - Approve - )); - assert_eq!( - Proposals::vote_by_account_and_proposal((councilor, 1)), - Approve - ); - } - assert_eq!(Proposals::votes_by_proposal(1), expected_votes); - - System::set_block_number(2); - let _ = Proposals::end_block(2); - - assert!(Proposals::active_proposal_ids().is_empty()); - assert_eq!(Proposals::proposals(1).status, Approved); - - // Try to vote on finalized proposal: - assert_eq!( - Proposals::vote_on_proposal(Origin::signed(COUNCILOR1), 1, Reject), - Err(MSG_PROPOSAL_FINALIZED) - ); - }); - } - - // ------------------------------------------------------------------- - // Tally + Outcome: - - #[test] - fn approve_proposal_when_all_councilors_approved_it() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - assert_ok!(_create_default_proposal()); - - // All councilors approved: - let mut expected_votes: Vec<(u64, VoteKind)> = vec![]; - for &councilor in ALL_COUNCILORS.iter() { - expected_votes.push((councilor, Approve)); - assert_ok!(Proposals::vote_on_proposal( - Origin::signed(councilor), - 1, - Approve - )); - assert_eq!( - Proposals::vote_by_account_and_proposal((councilor, 1)), - Approve - ); - } - assert_eq!(Proposals::votes_by_proposal(1), expected_votes); - - assert_runtime_code_empty!(); - - System::set_block_number(2); - let _ = Proposals::end_block(2); - - // Check that runtime code has been updated after proposal approved. - assert_runtime_code!(wasm_code()); - - assert!(Proposals::active_proposal_ids().is_empty()); - assert_eq!(Proposals::proposals(1).status, Approved); - assert_eq!( - Proposals::tally_results(1), - TallyResult { - proposal_id: 1, - abstentions: 0, - approvals: ALL_COUNCILORS.len() as u32, - rejections: 0, - slashes: 0, - status: Approved, - finalized_at: 2 - } - ); - - // Check that proposer's stake has been added back to his balance: - assert_eq!(Balances::free_balance(PROPOSER1), initial_balance()); - assert_eq!(Balances::reserved_balance(PROPOSER1), 0); - - // TODO expect event ProposalStatusUpdated(1, Approved) - }); - } - - #[test] - fn approve_proposal_when_all_councilors_voted_and_only_quorum_approved() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - assert_ok!(_create_default_proposal()); - - // Only a quorum of councilors approved, others rejected: - let councilors = Proposals::councilors_count(); - let approvals = Proposals::approval_quorum_seats(); - let rejections = councilors - approvals; - for i in 0..councilors as usize { - let vote = if (i as u32) < approvals { - Approve - } else { - Reject - }; - assert_ok!(Proposals::vote_on_proposal( - Origin::signed(ALL_COUNCILORS[i]), - 1, - vote - )); - } - assert_eq!(Proposals::votes_by_proposal(1).len() as u32, councilors); - - assert_runtime_code_empty!(); - - System::set_block_number(2); - let _ = Proposals::end_block(2); - - // Check that runtime code has been updated after proposal approved. - assert_runtime_code!(wasm_code()); - - assert!(Proposals::active_proposal_ids().is_empty()); - assert_eq!(Proposals::proposals(1).status, Approved); - assert_eq!( - Proposals::tally_results(1), - TallyResult { - proposal_id: 1, - abstentions: 0, - approvals: approvals, - rejections: rejections, - slashes: 0, - status: Approved, - finalized_at: 2 - } - ); - - // Check that proposer's stake has been added back to his balance: - assert_eq!(Balances::free_balance(PROPOSER1), initial_balance()); - assert_eq!(Balances::reserved_balance(PROPOSER1), 0); - - // TODO expect event ProposalStatusUpdated(1, Approved) - }); - } - - #[test] - fn approve_proposal_when_voting_period_expired_if_only_quorum_voted() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - assert_ok!(_create_default_proposal()); - - // Only quorum of councilors approved, other councilors didn't vote: - let approvals = Proposals::approval_quorum_seats(); - for i in 0..approvals as usize { - let vote = if (i as u32) < approvals { - Approve - } else { - Slash - }; - assert_ok!(Proposals::vote_on_proposal( - Origin::signed(ALL_COUNCILORS[i]), - 1, - vote - )); - } - assert_eq!(Proposals::votes_by_proposal(1).len() as u32, approvals); - - assert_runtime_code_empty!(); - - let expiration_block = System::block_number() + Proposals::voting_period(); - System::set_block_number(2); - let _ = Proposals::end_block(2); - - // Check that runtime code has NOT been updated yet, - // because not all councilors voted and voting period is not expired yet. - assert_runtime_code_empty!(); - - System::set_block_number(expiration_block); - let _ = Proposals::end_block(expiration_block); - - // Check that runtime code has been updated after proposal approved. - assert_runtime_code!(wasm_code()); - - assert!(Proposals::active_proposal_ids().is_empty()); - assert_eq!(Proposals::proposals(1).status, Approved); - assert_eq!( - Proposals::tally_results(1), - TallyResult { - proposal_id: 1, - abstentions: 0, - approvals: approvals, - rejections: 0, - slashes: 0, - status: Approved, - finalized_at: expiration_block - } - ); - - // Check that proposer's stake has been added back to his balance: - assert_eq!(Balances::free_balance(PROPOSER1), initial_balance()); - assert_eq!(Balances::reserved_balance(PROPOSER1), 0); - - // TODO expect event ProposalStatusUpdated(1, Approved) - }); - } - - #[test] - fn reject_proposal_when_all_councilors_voted_and_quorum_not_reached() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - assert_ok!(_create_default_proposal()); - - // Less than a quorum of councilors approved, while others abstained: - let councilors = Proposals::councilors_count(); - let approvals = Proposals::approval_quorum_seats() - 1; - let abstentions = councilors - approvals; - for i in 0..councilors as usize { - let vote = if (i as u32) < approvals { - Approve - } else { - Abstain - }; - assert_ok!(Proposals::vote_on_proposal( - Origin::signed(ALL_COUNCILORS[i]), - 1, - vote - )); - } - assert_eq!(Proposals::votes_by_proposal(1).len() as u32, councilors); - - assert_runtime_code_empty!(); - - System::set_block_number(2); - let _ = Proposals::end_block(2); - - // Check that runtime code has NOT been updated after proposal slashed. - assert_runtime_code_empty!(); - - assert!(Proposals::active_proposal_ids().is_empty()); - assert_eq!(Proposals::proposals(1).status, Rejected); - assert_eq!( - Proposals::tally_results(1), - TallyResult { - proposal_id: 1, - abstentions: abstentions, - approvals: approvals, - rejections: 0, - slashes: 0, - status: Rejected, - finalized_at: 2 - } - ); - - // Check that proposer's balance reduced by burnt stake: - assert_eq!( - Balances::free_balance(PROPOSER1), - initial_balance() - rejection_fee() - ); - assert_eq!(Balances::reserved_balance(PROPOSER1), 0); - - // TODO expect event ProposalStatusUpdated(1, Rejected) - }); - } - - #[test] - fn reject_proposal_when_all_councilors_rejected_it() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - assert_ok!(_create_default_proposal()); - - // All councilors rejected: - let mut expected_votes: Vec<(u64, VoteKind)> = vec![]; - for &councilor in ALL_COUNCILORS.iter() { - expected_votes.push((councilor, Reject)); - assert_ok!(Proposals::vote_on_proposal( - Origin::signed(councilor), - 1, - Reject - )); - assert_eq!( - Proposals::vote_by_account_and_proposal((councilor, 1)), - Reject - ); - } - assert_eq!(Proposals::votes_by_proposal(1), expected_votes); - - assert_runtime_code_empty!(); - - System::set_block_number(2); - let _ = Proposals::end_block(2); - - // Check that runtime code has NOT been updated after proposal rejected. - assert_runtime_code_empty!(); - - assert!(Proposals::active_proposal_ids().is_empty()); - assert_eq!(Proposals::proposals(1).status, Rejected); - assert_eq!( - Proposals::tally_results(1), - TallyResult { - proposal_id: 1, - abstentions: 0, - approvals: 0, - rejections: ALL_COUNCILORS.len() as u32, - slashes: 0, - status: Rejected, - finalized_at: 2 - } - ); - - // Check that proposer's balance reduced by burnt stake: - assert_eq!( - Balances::free_balance(PROPOSER1), - initial_balance() - rejection_fee() - ); - assert_eq!(Balances::reserved_balance(PROPOSER1), 0); - - // TODO expect event ProposalStatusUpdated(1, Rejected) - }); - } - - #[test] - fn slash_proposal_when_all_councilors_slashed_it() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - assert_ok!(_create_default_proposal()); - - // All councilors slashed: - let mut expected_votes: Vec<(u64, VoteKind)> = vec![]; - for &councilor in ALL_COUNCILORS.iter() { - expected_votes.push((councilor, Slash)); - assert_ok!(Proposals::vote_on_proposal( - Origin::signed(councilor), - 1, - Slash - )); - assert_eq!( - Proposals::vote_by_account_and_proposal((councilor, 1)), - Slash - ); - } - assert_eq!(Proposals::votes_by_proposal(1), expected_votes); - - assert_runtime_code_empty!(); - - System::set_block_number(2); - let _ = Proposals::end_block(2); - - // Check that runtime code has NOT been updated after proposal slashed. - assert_runtime_code_empty!(); - - assert!(Proposals::active_proposal_ids().is_empty()); - assert_eq!(Proposals::proposals(1).status, Slashed); - assert_eq!( - Proposals::tally_results(1), - TallyResult { - proposal_id: 1, - abstentions: 0, - approvals: 0, - rejections: 0, - slashes: ALL_COUNCILORS.len() as u32, - status: Slashed, - finalized_at: 2 - } - ); - - // Check that proposer's balance reduced by burnt stake: - assert_eq!( - Balances::free_balance(PROPOSER1), - initial_balance() - min_stake() - ); - assert_eq!(Balances::reserved_balance(PROPOSER1), 0); - - // TODO expect event ProposalStatusUpdated(1, Slashed) - // TODO fix: event log assertion doesn't work and return empty event in every record - // assert_eq!(*System::events().last().unwrap(), - // EventRecord { - // phase: Phase::ApplyExtrinsic(0), - // event: RawEvent::ProposalStatusUpdated(1, Slashed), - // } - // ); - }); - } - - // In this case a proposal will be marked as 'Expired' - // and it will be processed in the same way as if it has been rejected. - #[test] - fn expire_proposal_when_not_all_councilors_voted_and_quorum_not_reached() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - assert_ok!(_create_default_proposal()); - - // Less than a quorum of councilors approved: - let approvals = Proposals::approval_quorum_seats() - 1; - for i in 0..approvals as usize { - let vote = if (i as u32) < approvals { - Approve - } else { - Slash - }; - assert_ok!(Proposals::vote_on_proposal( - Origin::signed(ALL_COUNCILORS[i]), - 1, - vote - )); - } - assert_eq!(Proposals::votes_by_proposal(1).len() as u32, approvals); - - assert_runtime_code_empty!(); - - let expiration_block = System::block_number() + Proposals::voting_period(); - System::set_block_number(expiration_block); - let _ = Proposals::end_block(expiration_block); - - // Check that runtime code has NOT been updated after proposal slashed. - assert_runtime_code_empty!(); - - assert!(Proposals::active_proposal_ids().is_empty()); - assert_eq!(Proposals::proposals(1).status, Expired); - assert_eq!( - Proposals::tally_results(1), - TallyResult { - proposal_id: 1, - abstentions: 0, - approvals: approvals, - rejections: 0, - slashes: 0, - status: Expired, - finalized_at: expiration_block - } - ); - - // Check that proposer's balance reduced by burnt stake: - assert_eq!( - Balances::free_balance(PROPOSER1), - initial_balance() - rejection_fee() - ); - assert_eq!(Balances::reserved_balance(PROPOSER1), 0); - - // TODO expect event ProposalStatusUpdated(1, Rejected) - }); - } -} diff --git a/runtime-modules/hiring/src/lib.rs b/runtime-modules/hiring/src/lib.rs index 8230d28612..fc7b48a9fa 100755 --- a/runtime-modules/hiring/src/lib.rs +++ b/runtime-modules/hiring/src/lib.rs @@ -27,7 +27,6 @@ use mockall::*; use stake::{InitiateUnstakingError, Stake, StakeActionError, StakingError, Trait as StakeTrait}; use codec::Codec; -use system; use runtime_primitives::traits::Zero; use runtime_primitives::traits::{MaybeSerialize, Member, One, SimpleArithmetic}; @@ -52,7 +51,6 @@ mod mock; mod test; pub use hiring::*; -use stake; /// Main trait of hiring substrate module pub trait Trait: system::Trait + stake::Trait + Sized { diff --git a/runtime-modules/membership/Cargo.toml b/runtime-modules/membership/Cargo.toml index 8fc8e9adc5..9961e03f1d 100644 --- a/runtime-modules/membership/Cargo.toml +++ b/runtime-modules/membership/Cargo.toml @@ -1,6 +1,6 @@ [package] name = 'substrate-membership-module' -version = '1.0.0' +version = '1.0.1' authors = ['Joystream contributors'] edition = '2018' diff --git a/runtime-modules/membership/src/lib.rs b/runtime-modules/membership/src/lib.rs index a777802fd3..f3259534d5 100644 --- a/runtime-modules/membership/src/lib.rs +++ b/runtime-modules/membership/src/lib.rs @@ -5,5 +5,5 @@ pub mod genesis; pub mod members; pub mod role_types; -mod mock; +pub(crate) mod mock; mod tests; diff --git a/runtime-modules/membership/src/members.rs b/runtime-modules/membership/src/members.rs index bda32cac2a..af1908361c 100644 --- a/runtime-modules/membership/src/members.rs +++ b/runtime-modules/membership/src/members.rs @@ -1,13 +1,17 @@ +// Clippy linter requirement +#![allow(clippy::redundant_closure_call)] // disable it because of the substrate lib design + // example: pub PaidMembershipTermsById get(paid_membership_terms_by_id) build(|config: &GenesisConfig| {} + use codec::{Codec, Decode, Encode}; use common::currency::{BalanceOf, GovernanceCurrency}; +use rstd::borrow::ToOwned; use rstd::prelude::*; use sr_primitives::traits::{MaybeSerialize, Member, One, SimpleArithmetic}; use srml_support::traits::{Currency, Get}; use srml_support::{decl_event, decl_module, decl_storage, dispatch, ensure, Parameter}; use system::{self, ensure_root, ensure_signed}; -use timestamp; pub use super::role_types::*; @@ -252,7 +256,7 @@ decl_module! { let _ = T::Currency::slash(&who, terms.fee); let member_id = Self::insert_member(&who, &user_info, EntryMethod::Paid(paid_terms_id)); - Self::deposit_event(RawEvent::MemberRegistered(member_id, who.clone())); + Self::deposit_event(RawEvent::MemberRegistered(member_id, who)); } /// Change member's about text @@ -448,12 +452,13 @@ impl Module { Ok(terms) } + #[allow(clippy::ptr_arg)] // cannot change to the "&[u8]" suggested by clippy fn ensure_unique_handle(handle: &Vec) -> dispatch::Result { ensure!(!>::exists(handle), "handle already registered"); Ok(()) } - fn validate_handle(handle: &Vec) -> dispatch::Result { + fn validate_handle(handle: &[u8]) -> dispatch::Result { ensure!( handle.len() >= Self::min_handle_length() as usize, "handle too short" @@ -465,13 +470,13 @@ impl Module { Ok(()) } - fn validate_text(text: &Vec) -> Vec { - let mut text = text.clone(); + fn validate_text(text: &[u8]) -> Vec { + let mut text = text.to_owned(); text.truncate(Self::max_about_text_length() as usize); text } - fn validate_avatar(uri: &Vec) -> dispatch::Result { + fn validate_avatar(uri: &[u8]) -> dispatch::Result { ensure!( uri.len() <= Self::max_avatar_uri_length() as usize, "avatar uri too long" @@ -533,7 +538,7 @@ impl Module { new_member_id } - fn _change_member_about_text(id: T::MemberId, text: &Vec) -> dispatch::Result { + fn _change_member_about_text(id: T::MemberId, text: &[u8]) -> dispatch::Result { let mut profile = Self::ensure_profile(id)?; let text = Self::validate_text(text); profile.about = text; @@ -542,10 +547,10 @@ impl Module { Ok(()) } - fn _change_member_avatar(id: T::MemberId, uri: &Vec) -> dispatch::Result { + fn _change_member_avatar(id: T::MemberId, uri: &[u8]) -> dispatch::Result { let mut profile = Self::ensure_profile(id)?; Self::validate_avatar(uri)?; - profile.avatar_uri = uri.clone(); + profile.avatar_uri = uri.to_owned(); Self::deposit_event(RawEvent::MemberUpdatedAvatar(id)); >::insert(id, profile); Ok(()) @@ -593,7 +598,7 @@ impl Module { ensure_signed(origin).map_err(|_| MemberControllerAccountDidNotSign::UnsignedOrigin)?; // Ensure member exists - let profile = Self::ensure_profile(member_id.clone()) + let profile = Self::ensure_profile(*member_id) .map_err(|_| MemberControllerAccountDidNotSign::MemberIdInvalid)?; ensure!( @@ -609,7 +614,7 @@ impl Module { member_id: &T::MemberId, ) -> Result<(), MemberControllerAccountMismatch> { // Ensure member exists - let profile = Self::ensure_profile(member_id.clone()) + let profile = Self::ensure_profile(*member_id) .map_err(|_| MemberControllerAccountMismatch::MemberIdInvalid)?; ensure!( @@ -625,7 +630,7 @@ impl Module { member_id: &T::MemberId, ) -> Result<(), MemberRootAccountMismatch> { // Ensure member exists - let profile = Self::ensure_profile(member_id.clone()) + let profile = Self::ensure_profile(*member_id) .map_err(|_| MemberRootAccountMismatch::MemberIdInvalid)?; ensure!( diff --git a/runtime-modules/membership/src/role_types.rs b/runtime-modules/membership/src/role_types.rs index 803e048b8e..01446006f7 100644 --- a/runtime-modules/membership/src/role_types.rs +++ b/runtime-modules/membership/src/role_types.rs @@ -1,7 +1,13 @@ +#![allow(clippy::new_without_default)] // disable because Default for enums doesn't make sense + use codec::{Decode, Encode}; use rstd::collections::btree_set::BTreeSet; use rstd::prelude::*; +#[cfg(feature = "std")] +use serde::{Deserialize, Serialize}; + +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] pub enum Role { StorageProvider, diff --git a/runtime-modules/proposals/codex/Cargo.toml b/runtime-modules/proposals/codex/Cargo.toml new file mode 100644 index 0000000000..58f71c9517 --- /dev/null +++ b/runtime-modules/proposals/codex/Cargo.toml @@ -0,0 +1,184 @@ +[package] +name = 'substrate-proposals-codex-module' +version = '2.0.0' +authors = ['Joystream contributors'] +edition = '2018' + +[features] +default = ['std'] +no_std = [] +std = [ + 'codec/std', + 'rstd/std', + 'srml-support/std', + 'primitives/std', + 'sr-primitives/std', + 'system/std', + 'timestamp/std', + 'staking/std', + 'serde', + 'proposal_engine/std', + 'proposal_discussion/std', + 'stake/std', + 'balances/std', + 'membership/std', + 'governance/std', + 'mint/std', + 'roles/std', + 'common/std', + 'content_working_group/std', +] + + +[dependencies.num_enum] +default_features = false +version = "0.4.2" + +[dependencies.serde] +features = ['derive'] +optional = true +version = '1.0.101' + +[dependencies.codec] +default-features = false +features = ['derive'] +package = 'parity-scale-codec' +version = '1.0.0' + +[dependencies.primitives] +default_features = false +git = 'https://github.com/paritytech/substrate.git' +package = 'substrate-primitives' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' + +[dependencies.rstd] +default_features = false +git = 'https://github.com/paritytech/substrate.git' +package = 'sr-std' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' + +[dependencies.sr-primitives] +default_features = false +git = 'https://github.com/paritytech/substrate.git' +package = 'sr-primitives' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' + +[dependencies.srml-support] +default_features = false +git = 'https://github.com/paritytech/substrate.git' +package = 'srml-support' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' + +[dependencies.system] +default_features = false +git = 'https://github.com/paritytech/substrate.git' +package = 'srml-system' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' + +[dependencies.timestamp] +default_features = false +git = 'https://github.com/paritytech/substrate.git' +package = 'srml-timestamp' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' + +[dependencies.balances] +package = 'srml-balances' +default-features = false +git = 'https://github.com/paritytech/substrate.git' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' + +[dependencies.staking] +default_features = false +git = 'https://github.com/paritytech/substrate.git' +package = 'srml-staking' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' + +[dependencies.stake] +default_features = false +package = 'substrate-stake-module' +path = '../../stake' + +[dependencies.membership] +default_features = false +package = 'substrate-membership-module' +path = '../../membership' + +[dependencies.governance] +default_features = false +package = 'substrate-governance-module' +path = '../../governance' + +[dependencies.mint] +default_features = false +package = 'substrate-token-mint-module' +path = '../../token-minting' + +[dependencies.proposal_engine] +default_features = false +package = 'substrate-proposals-engine-module' +path = '../engine' + +[dependencies.proposal_discussion] +default_features = false +package = 'substrate-proposals-discussion-module' +path = '../discussion' + +[dependencies.common] +default_features = false +package = 'substrate-common-module' +path = '../../common' + +[dependencies.content_working_group] +default_features = false +package = 'substrate-content-working-group-module' +path = '../../content-working-group' + +[dependencies.roles] +default_features = false +package = 'substrate-roles-module' +path = '../../roles' + +[dev-dependencies.hiring] +default_features = false +package = 'substrate-hiring-module' +path = '../../hiring' + +[dev-dependencies.versioned_store] +default_features = false +package ='substrate-versioned-store' +path = '../../versioned-store' + +[dependencies.versioned_store] +default_features = false +package ='substrate-versioned-store' +path = '../../versioned-store' + +[dev-dependencies.versioned_store_permissions] +default_features = false +package = 'substrate-versioned-store-permissions-module' +path = '../../versioned-store-permissions' + +[dev-dependencies.recurring_rewards] +default_features = false +package = 'substrate-recurring-reward-module' +path = '../../recurring-reward' + +[dev-dependencies.sr-staking-primitives] +default_features = false +git = 'https://github.com/paritytech/substrate.git' +package = 'sr-staking-primitives' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' + +[dev-dependencies.runtime-io] +default_features = false +git = 'https://github.com/paritytech/substrate.git' +package = 'sr-io' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' + +# don't rename the dependency it is causing some strange compiler error: +# https://github.com/rust-lang/rust/issues/64450 +[dev-dependencies.srml-staking-reward-curve] +package = 'srml-staking-reward-curve' +git = 'https://github.com/paritytech/substrate.git' +default_features = false +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' \ No newline at end of file diff --git a/runtime-modules/proposals/codex/src/lib.rs b/runtime-modules/proposals/codex/src/lib.rs new file mode 100644 index 0000000000..beb4e7b8b7 --- /dev/null +++ b/runtime-modules/proposals/codex/src/lib.rs @@ -0,0 +1,1090 @@ +//! # Proposals codex module +//! Proposals `codex` module for the Joystream platform. Version 2. +//! Component of the proposals system. It contains preset proposal types. +//! +//! ## Overview +//! +//! The proposals codex module serves as a facade and entry point of the proposals system. It uses +//! proposals `engine` module to maintain a lifecycle of the proposal and to execute proposals. +//! During the proposal creation, `codex` also create a discussion thread using the `discussion` +//! proposals module. `Codex` uses predefined parameters (eg.:`voting_period`) for each proposal and +//! encodes extrinsic calls from dependency modules in order to create proposals inside the `engine` +//! module. For each proposal, [its crucial details](./enum.ProposalDetails.html) are saved to the +//! `ProposalDetailsByProposalId` map. +//! +//! ### Supported extrinsics (proposal types) +//! - [create_text_proposal](./struct.Module.html#method.create_text_proposal) +//! - [create_runtime_upgrade_proposal](./struct.Module.html#method.create_runtime_upgrade_proposal) +//! - [create_set_election_parameters_proposal](./struct.Module.html#method.create_set_election_parameters_proposal) +//! - [create_set_content_working_group_mint_capacity_proposal](./struct.Module.html#method.create_set_content_working_group_mint_capacity_proposal) +//! - [create_spending_proposal](./struct.Module.html#method.create_spending_proposal) +//! - [create_set_lead_proposal](./struct.Module.html#method.create_set_lead_proposal) +//! - [create_evict_storage_provider_proposal](./struct.Module.html#method.create_evict_storage_provider_proposal) +//! - [create_set_validator_count_proposal](./struct.Module.html#method.create_set_validator_count_proposal) +//! - [create_set_storage_role_parameters_proposal](./struct.Module.html#method.create_set_storage_role_parameters_proposal) +//! +//! ### Proposal implementations of this module +//! - execute_text_proposal - prints the proposal to the log +//! - execute_runtime_upgrade_proposal - sets the runtime code +//! +//! ### Dependencies: +//! - [proposals engine](../substrate_proposals_engine_module/index.html) +//! - [proposals discussion](../substrate_proposals_discussion_module/index.html) +//! - [membership](../substrate_membership_module/index.html) +//! - [governance](../substrate_governance_module/index.html) +//! - [content_working_group](../substrate_content_working_group_module/index.html) +//! +//! ### Notes +//! The module uses [ProposalEncoder](./trait.ProposalEncoder.html) to encode the proposal using +//! its details. Encoded byte vector is passed to the _proposals engine_ as serialized executable code. + +// Clippy linter warning. TODO: remove after the Constaninople release +#![allow(clippy::type_complexity)] +// disable it because of possible frontend API break + +// Clippy linter warning. TODO: refactor "this function has too many argument" +#![allow(clippy::too_many_arguments)] // disable it because of possible API break + +// Ensure we're `no_std` when compiling for Wasm. +#![cfg_attr(not(feature = "std"), no_std)] + +// Do not delete! Cannot be uncommented by default, because of Parity decl_module! issue. +// #![warn(missing_docs)] + +mod proposal_types; + +#[cfg(test)] +mod tests; + +use common::origin_validator::ActorOriginValidator; +use governance::election_params::ElectionParameters; +use proposal_engine::ProposalParameters; +use roles::actors::RoleParameters; +use rstd::clone::Clone; +use rstd::prelude::*; +use rstd::str::from_utf8; +use rstd::vec::Vec; +use sr_primitives::traits::Zero; +use srml_support::dispatch::DispatchResult; +use srml_support::traits::{Currency, Get}; +use srml_support::{decl_error, decl_module, decl_storage, ensure, print}; +use system::{ensure_root, RawOrigin}; + +pub use crate::proposal_types::ProposalsConfigParameters; +pub use proposal_types::{ProposalDetails, ProposalDetailsOf, ProposalEncoder}; + +// 'Set working group mint capacity' proposal limit +const CONTENT_WORKING_GROUP_MINT_CAPACITY_MAX_VALUE: u32 = 1_000_000; +// Max allowed value for 'spending' proposal +const MAX_SPENDING_PROPOSAL_VALUE: u32 = 2_000_000_u32; +// Max validator count for the 'set validator count' proposal +const MAX_VALIDATOR_COUNT: u32 = 100; +// min_actors min value for the 'set storage role parameters' proposal +const ROLE_PARAMETERS_MIN_ACTORS_MAX_VALUE: u32 = 2; +// max_actors min value for the 'set storage role parameters' proposal +const ROLE_PARAMETERS_MAX_ACTORS_MIN_VALUE: u32 = 2; +// max_actors max value for the 'set storage role parameters' proposal +const ROLE_PARAMETERS_MAX_ACTORS_MAX_VALUE: u32 = 100; +// reward_period min value for the 'set storage role parameters' proposal +const ROLE_PARAMETERS_REWARD_PERIOD_MIN_VALUE: u32 = 600; +// reward_period max value for the 'set storage role parameters' proposal +const ROLE_PARAMETERS_REWARD_PERIOD_MAX_VALUE: u32 = 3600; +// bonding_period min value for the 'set storage role parameters' proposal +const ROLE_PARAMETERS_BONDING_PERIOD_MIN_VALUE: u32 = 600; +// bonding_period max value for the 'set storage role parameters' proposal +const ROLE_PARAMETERS_BONDING_PERIOD_MAX_VALUE: u32 = 28800; +// unbonding_period min value for the 'set storage role parameters' proposal +const ROLE_PARAMETERS_UNBONDING_PERIOD_MIN_VALUE: u32 = 600; +// unbonding_period max value for the 'set storage role parameters' proposal +const ROLE_PARAMETERS_UNBONDING_PERIOD_MAX_VALUE: u32 = 28800; +// min_service_period min value for the 'set storage role parameters' proposal +const ROLE_PARAMETERS_MIN_SERVICE_PERIOD_MIN_VALUE: u32 = 600; +// min_service_period max value for the 'set storage role parameters' proposal +const ROLE_PARAMETERS_MIN_SERVICE_PERIOD_MAX_VALUE: u32 = 28800; +// startup_grace_period min value for the 'set storage role parameters' proposal +const ROLE_PARAMETERS_STARTUP_GRACE_PERIOD_MIN_VALUE: u32 = 600; +// startup_grace_period max value for the 'set storage role parameters' proposal +const ROLE_PARAMETERS_STARTUP_GRACE_PERIOD_MAX_VALUE: u32 = 28800; +// min_stake min value for the 'set storage role parameters' proposal +const ROLE_PARAMETERS_MIN_STAKE_MIN_VALUE: u32 = 0; +// min_stake max value for the 'set storage role parameters' proposal +const ROLE_PARAMETERS_MIN_STAKE_MAX_VALUE: u32 = 10_000_000; +// entry_request_fee min value for the 'set storage role parameters' proposal +const ROLE_PARAMETERS_ENTRY_REQUEST_FEE_MIN_VALUE: u32 = 0; +// entry_request_fee max value for the 'set storage role parameters' proposal +const ROLE_PARAMETERS_ENTRY_REQUEST_FEE_MAX_VALUE: u32 = 100_000; +// reward min value for the 'set storage role parameters' proposal +const ROLE_PARAMETERS_REWARD_MIN_VALUE: u32 = 0; +// reward max value for the 'set storage role parameters' proposal +const ROLE_PARAMETERS_REWARD_MAX_VALUE: u32 = 1000; +// council_size min value for the 'set election parameters' proposal +const ELECTION_PARAMETERS_COUNCIL_SIZE_MIN_VALUE: u32 = 4; +// council_size max value for the 'set election parameters' proposal +const ELECTION_PARAMETERS_COUNCIL_SIZE_MAX_VALUE: u32 = 20; +// candidacy_limit min value for the 'set election parameters' proposal +const ELECTION_PARAMETERS_CANDIDACY_LIMIT_MIN_VALUE: u32 = 25; +// candidacy_limit max value for the 'set election parameters' proposal +const ELECTION_PARAMETERS_CANDIDACY_LIMIT_MAX_VALUE: u32 = 100; +// min_voting_stake min value for the 'set election parameters' proposal +const ELECTION_PARAMETERS_MIN_STAKE_MIN_VALUE: u32 = 1; +// min_voting_stake max value for the 'set election parameters' proposal +const ELECTION_PARAMETERS_MIN_STAKE_MAX_VALUE: u32 = 100_000_u32; +// new_term_duration min value for the 'set election parameters' proposal +const ELECTION_PARAMETERS_NEW_TERM_DURATION_MIN_VALUE: u32 = 14400; +// new_term_duration max value for the 'set election parameters' proposal +const ELECTION_PARAMETERS_NEW_TERM_DURATION_MAX_VALUE: u32 = 432_000; +// revealing_period min value for the 'set election parameters' proposal +const ELECTION_PARAMETERS_REVEALING_PERIOD_MIN_VALUE: u32 = 14400; +// revealing_period max value for the 'set election parameters' proposal +const ELECTION_PARAMETERS_REVEALING_PERIOD_MAX_VALUE: u32 = 28800; +// voting_period min value for the 'set election parameters' proposal +const ELECTION_PARAMETERS_VOTING_PERIOD_MIN_VALUE: u32 = 14400; +// voting_period max value for the 'set election parameters' proposal +const ELECTION_PARAMETERS_VOTING_PERIOD_MAX_VALUE: u32 = 28800; +// announcing_period min value for the 'set election parameters' proposal +const ELECTION_PARAMETERS_ANNOUNCING_PERIOD_MIN_VALUE: u32 = 14400; +// announcing_period max value for the 'set election parameters' proposal +const ELECTION_PARAMETERS_ANNOUNCING_PERIOD_MAX_VALUE: u32 = 43200; +// min_council_stake min value for the 'set election parameters' proposal +const ELECTION_PARAMETERS_MIN_COUNCIL_STAKE_MIN_VALUE: u32 = 1; +// min_council_stake max value for the 'set election parameters' proposal +const ELECTION_PARAMETERS_MIN_COUNCIL_STAKE_MAX_VALUE: u32 = 100_000_u32; + +/// 'Proposals codex' substrate module Trait +pub trait Trait: + system::Trait + + proposal_engine::Trait + + proposal_discussion::Trait + + membership::members::Trait + + governance::election::Trait + + content_working_group::Trait + + roles::actors::Trait + + staking::Trait +{ + /// Defines max allowed text proposal length. + type TextProposalMaxLength: Get; + + /// Defines max wasm code length of the runtime upgrade proposal. + type RuntimeUpgradeWasmProposalMaxLength: Get; + + /// Validates member id and origin combination + type MembershipOriginValidator: ActorOriginValidator< + Self::Origin, + MemberId, + Self::AccountId, + >; + + /// Encodes the proposal usint its details + type ProposalEncoder: ProposalEncoder; +} + +/// Balance alias for `stake` module +pub type BalanceOf = + <::Currency as Currency<::AccountId>>::Balance; + +/// Currency alias for `stake` module +pub type CurrencyOf = ::Currency; + +/// Balance alias for GovernanceCurrency from `common` module. TODO: replace with BalanceOf +pub type BalanceOfGovernanceCurrency = + <::Currency as Currency< + ::AccountId, + >>::Balance; + +/// Balance alias for token mint balance from `token mint` module. TODO: replace with BalanceOf +pub type BalanceOfMint = + <::Currency as Currency<::AccountId>>::Balance; + +/// Negative imbalance alias for staking +pub type NegativeImbalance = + <::Currency as Currency<::AccountId>>::NegativeImbalance; + +type MemberId = ::MemberId; + +decl_error! { + /// Codex module predefined errors + pub enum Error { + /// The size of the provided text for text proposal exceeded the limit + TextProposalSizeExceeded, + + /// Provided text for text proposal is empty + TextProposalIsEmpty, + + /// The size of the provided WASM code for the runtime upgrade proposal exceeded the limit + RuntimeProposalSizeExceeded, + + /// Provided WASM code for the runtime upgrade proposal is empty + RuntimeProposalIsEmpty, + + /// Invalid balance value for the spending proposal + InvalidSpendingProposalBalance, + + /// Invalid validator count for the 'set validator count' proposal + InvalidValidatorCount, + + /// Require root origin in extrinsics + RequireRootOrigin, + + /// Invalid storage role parameter - min_actors + InvalidStorageRoleParameterMinActors, + + /// Invalid storage role parameter - max_actors + InvalidStorageRoleParameterMaxActors, + + /// Invalid storage role parameter - reward_period + InvalidStorageRoleParameterRewardPeriod, + + /// Invalid storage role parameter - bonding_period + InvalidStorageRoleParameterBondingPeriod, + + /// Invalid storage role parameter - unbonding_period + InvalidStorageRoleParameterUnbondingPeriod, + + /// Invalid storage role parameter - min_service_period + InvalidStorageRoleParameterMinServicePeriod, + + /// Invalid storage role parameter - startup_grace_period + InvalidStorageRoleParameterStartupGracePeriod, + + /// Invalid council election parameter - council_size + InvalidCouncilElectionParameterCouncilSize, + + /// Invalid council election parameter - candidacy-limit + InvalidCouncilElectionParameterCandidacyLimit, + + /// Invalid council election parameter - min-voting_stake + InvalidCouncilElectionParameterMinVotingStake, + + /// Invalid council election parameter - new_term_duration + InvalidCouncilElectionParameterNewTermDuration, + + /// Invalid council election parameter - min_council_stake + InvalidCouncilElectionParameterMinCouncilStake, + + /// Invalid council election parameter - revealing_period + InvalidCouncilElectionParameterRevealingPeriod, + + /// Invalid council election parameter - voting_period + InvalidCouncilElectionParameterVotingPeriod, + + /// Invalid council election parameter - announcing_period + InvalidCouncilElectionParameterAnnouncingPeriod, + + /// Invalid council election parameter - min_stake + InvalidStorageRoleParameterMinStake, + + /// Invalid council election parameter - reward + InvalidStorageRoleParameterReward, + + /// Invalid council election parameter - entry_request_fee + InvalidStorageRoleParameterEntryRequestFee, + + /// Invalid working group mint capacity parameter + InvalidStorageWorkingGroupMintCapacity, + + /// Invalid 'set lead proposal' parameter - proposed lead cannot be a councilor + InvalidSetLeadParameterCannotBeCouncilor + } +} + +impl From for Error { + fn from(error: system::Error) -> Self { + match error { + system::Error::Other(msg) => Error::Other(msg), + system::Error::RequireRootOrigin => Error::RequireRootOrigin, + _ => Error::Other(error.into()), + } + } +} + +impl From for Error { + fn from(error: proposal_engine::Error) -> Self { + match error { + proposal_engine::Error::Other(msg) => Error::Other(msg), + proposal_engine::Error::RequireRootOrigin => Error::RequireRootOrigin, + _ => Error::Other(error.into()), + } + } +} + +impl From for Error { + fn from(error: proposal_discussion::Error) -> Self { + match error { + proposal_discussion::Error::Other(msg) => Error::Other(msg), + proposal_discussion::Error::RequireRootOrigin => Error::RequireRootOrigin, + _ => Error::Other(error.into()), + } + } +} + +// Storage for the proposals codex module +decl_storage! { + pub trait Store for Module as ProposalCodex{ + /// Map proposal id to its discussion thread id + pub ThreadIdByProposalId get(fn thread_id_by_proposal_id): + map T::ProposalId => T::ThreadId; + + /// Map proposal id to proposal details + pub ProposalDetailsByProposalId get(fn proposal_details_by_proposal_id): + map T::ProposalId => ProposalDetails< + BalanceOfMint, + BalanceOfGovernanceCurrency, + T::BlockNumber, + T::AccountId, + T::MemberId + >; + + /// Voting period for the 'set validator count' proposal + pub SetValidatorCountProposalVotingPeriod get(set_validator_count_proposal_voting_period) + config(): T::BlockNumber; + + /// Grace period for the 'set validator count' proposal + pub SetValidatorCountProposalGracePeriod get(set_validator_count_proposal_grace_period) + config(): T::BlockNumber; + + /// Voting period for the 'runtime upgrade' proposal + pub RuntimeUpgradeProposalVotingPeriod get(runtime_upgrade_proposal_voting_period) + config(): T::BlockNumber; + + /// Grace period for the 'runtime upgrade' proposal + pub RuntimeUpgradeProposalGracePeriod get(runtime_upgrade_proposal_grace_period) + config(): T::BlockNumber; + + /// Voting period for the 'set election parameters' proposal + pub SetElectionParametersProposalVotingPeriod get(set_election_parameters_proposal_voting_period) + config(): T::BlockNumber; + + /// Grace period for the 'set election parameters' proposal + pub SetElectionParametersProposalGracePeriod get(set_election_parameters_proposal_grace_period) + config(): T::BlockNumber; + + /// Voting period for the 'text' proposal + pub TextProposalVotingPeriod get(text_proposal_voting_period) config(): T::BlockNumber; + + /// Grace period for the 'text' proposal + pub TextProposalGracePeriod get(text_proposal_grace_period) config(): T::BlockNumber; + + /// Voting period for the 'set content working group mint capacity' proposal + pub SetContentWorkingGroupMintCapacityProposalVotingPeriod get(set_content_working_group_mint_capacity_proposal_voting_period) + config(): T::BlockNumber; + + /// Grace period for the 'set content working group mint capacity' proposal + pub SetContentWorkingGroupMintCapacityProposalGracePeriod get(set_content_working_group_mint_capacity_proposal_grace_period) + config(): T::BlockNumber; + + /// Voting period for the 'set lead' proposal + pub SetLeadProposalVotingPeriod get(set_lead_proposal_voting_period) + config(): T::BlockNumber; + + /// Grace period for the 'set lead' proposal + pub SetLeadProposalGracePeriod get(set_lead_proposal_grace_period) + config(): T::BlockNumber; + + /// Voting period for the 'spending' proposal + pub SpendingProposalVotingPeriod get(spending_proposal_voting_period) config(): T::BlockNumber; + + /// Grace period for the 'spending' proposal + pub SpendingProposalGracePeriod get(spending_proposal_grace_period) config(): T::BlockNumber; + + /// Voting period for the 'evict storage provider' proposal + pub EvictStorageProviderProposalVotingPeriod get(evict_storage_provider_proposal_voting_period) + config(): T::BlockNumber; + + /// Grace period for the 'evict storage provider' proposal + pub EvictStorageProviderProposalGracePeriod get(evict_storage_provider_proposal_grace_period) + config(): T::BlockNumber; + + /// Voting period for the 'set storage role parameters' proposal + pub SetStorageRoleParametersProposalVotingPeriod get(set_storage_role_parameters_proposal_voting_period) + config(): T::BlockNumber; + + /// Grace period for the 'set storage role parameters' proposal + pub SetStorageRoleParametersProposalGracePeriod get(set_storage_role_parameters_proposal_grace_period) + config(): T::BlockNumber; + } +} + +decl_module! { + /// Proposal codex substrate module Call + pub struct Module for enum Call where origin: T::Origin { + /// Predefined errors + type Error = Error; + + /// Exports max allowed text proposal length const. + const TextProposalMaxLength: u32 = T::TextProposalMaxLength::get(); + + /// Exports max wasm code length of the runtime upgrade proposal const. + const RuntimeUpgradeWasmProposalMaxLength: u32 = T::RuntimeUpgradeWasmProposalMaxLength::get(); + + /// Create 'Text (signal)' proposal type. + pub fn create_text_proposal( + origin, + member_id: MemberId, + title: Vec, + description: Vec, + stake_balance: Option>, + text: Vec, + ) { + ensure!(!text.is_empty(), Error::TextProposalIsEmpty); + ensure!(text.len() as u32 <= T::TextProposalMaxLength::get(), + Error::TextProposalSizeExceeded); + + let proposal_parameters = proposal_types::parameters::text_proposal::(); + let proposal_details = ProposalDetails::, BalanceOfGovernanceCurrency, T::BlockNumber, T::AccountId, MemberId>::Text(text); + let proposal_code = T::ProposalEncoder::encode_proposal(proposal_details.clone()); + + Self::create_proposal( + origin, + member_id, + title, + description, + stake_balance, + proposal_code, + proposal_parameters, + proposal_details, + )?; + } + + /// Create 'Runtime upgrade' proposal type. Runtime upgrade can be initiated only by + /// members from the hardcoded list `RuntimeUpgradeProposalAllowedProposers` + pub fn create_runtime_upgrade_proposal( + origin, + member_id: MemberId, + title: Vec, + description: Vec, + stake_balance: Option>, + wasm: Vec, + ) { + ensure!(!wasm.is_empty(), Error::RuntimeProposalIsEmpty); + ensure!(wasm.len() as u32 <= T::RuntimeUpgradeWasmProposalMaxLength::get(), + Error::RuntimeProposalSizeExceeded); + + let proposal_parameters = proposal_types::parameters::runtime_upgrade_proposal::(); + let proposal_details = ProposalDetails::RuntimeUpgrade(wasm); + let proposal_code = T::ProposalEncoder::encode_proposal(proposal_details.clone()); + + Self::create_proposal( + origin, + member_id, + title, + description, + stake_balance, + proposal_code, + proposal_parameters, + proposal_details, + )?; + } + + /// Create 'Set election parameters' proposal type. This proposal uses `set_election_parameters()` + /// extrinsic from the `governance::election module`. + pub fn create_set_election_parameters_proposal( + origin, + member_id: MemberId, + title: Vec, + description: Vec, + stake_balance: Option>, + election_parameters: ElectionParameters, T::BlockNumber>, + ) { + election_parameters.ensure_valid()?; + + Self::ensure_council_election_parameters_valid(&election_parameters)?; + + let proposal_details = ProposalDetails::SetElectionParameters(election_parameters); + let proposal_code = T::ProposalEncoder::encode_proposal(proposal_details.clone()); + let proposal_parameters = + proposal_types::parameters::set_election_parameters_proposal::(); + + Self::create_proposal( + origin, + member_id, + title, + description, + stake_balance, + proposal_code, + proposal_parameters, + proposal_details, + )?; + } + + /// Create 'Set content working group mint capacity' proposal type. + /// This proposal uses `set_mint_capacity()` extrinsic from the `content-working-group` module. + pub fn create_set_content_working_group_mint_capacity_proposal( + origin, + member_id: MemberId, + title: Vec, + description: Vec, + stake_balance: Option>, + mint_balance: BalanceOfMint, + ) { + ensure!( + mint_balance <= >::from(CONTENT_WORKING_GROUP_MINT_CAPACITY_MAX_VALUE), + Error::InvalidStorageWorkingGroupMintCapacity + ); + + let proposal_parameters = + proposal_types::parameters::set_content_working_group_mint_capacity_proposal::(); + let proposal_details = ProposalDetails::SetContentWorkingGroupMintCapacity(mint_balance); + let proposal_code = T::ProposalEncoder::encode_proposal(proposal_details.clone()); + + Self::create_proposal( + origin, + member_id, + title, + description, + stake_balance, + proposal_code, + proposal_parameters, + proposal_details, + )?; + } + + /// Create 'Spending' proposal type. + /// This proposal uses `spend_from_council_mint()` extrinsic from the `governance::council` module. + pub fn create_spending_proposal( + origin, + member_id: MemberId, + title: Vec, + description: Vec, + stake_balance: Option>, + balance: BalanceOfMint, + destination: T::AccountId, + ) { + ensure!(balance != BalanceOfMint::::zero(), Error::InvalidSpendingProposalBalance); + ensure!( + balance <= >::from(MAX_SPENDING_PROPOSAL_VALUE), + Error::InvalidSpendingProposalBalance + ); + + let proposal_parameters = + proposal_types::parameters::spending_proposal::(); + let proposal_details = ProposalDetails::Spending(balance, destination); + let proposal_code = T::ProposalEncoder::encode_proposal(proposal_details.clone()); + + Self::create_proposal( + origin, + member_id, + title, + description, + stake_balance, + proposal_code, + proposal_parameters, + proposal_details, + )?; + } + + /// Create 'Set lead' proposal type. + /// This proposal uses `replace_lead()` extrinsic from the `content_working_group` module. + pub fn create_set_lead_proposal( + origin, + member_id: MemberId, + title: Vec, + description: Vec, + stake_balance: Option>, + new_lead: Option<(T::MemberId, T::AccountId)> + ) { + if let Some(lead) = new_lead.clone() { + let account_id = lead.1; + ensure!( + !>::is_councilor(&account_id), + Error::InvalidSetLeadParameterCannotBeCouncilor + ); + } + + let proposal_parameters = + proposal_types::parameters::set_lead_proposal::(); + let proposal_details = ProposalDetails::SetLead(new_lead); + let proposal_code = T::ProposalEncoder::encode_proposal(proposal_details.clone()); + + Self::create_proposal( + origin, + member_id, + title, + description, + stake_balance, + proposal_code, + proposal_parameters, + proposal_details, + )?; + } + + /// Create 'Evict storage provider' proposal type. + /// This proposal uses `remove_actor()` extrinsic from the `roles::actors` module. + pub fn create_evict_storage_provider_proposal( + origin, + member_id: MemberId, + title: Vec, + description: Vec, + stake_balance: Option>, + actor_account: T::AccountId, + ) { + let proposal_parameters = + proposal_types::parameters::evict_storage_provider_proposal::(); + let proposal_details = ProposalDetails::EvictStorageProvider(actor_account); + let proposal_code = T::ProposalEncoder::encode_proposal(proposal_details.clone()); + + Self::create_proposal( + origin, + member_id, + title, + description, + stake_balance, + proposal_code, + proposal_parameters, + proposal_details, + )?; + } + + /// Create 'Evict storage provider' proposal type. + /// This proposal uses `set_validator_count()` extrinsic from the Substrate `staking` module. + pub fn create_set_validator_count_proposal( + origin, + member_id: MemberId, + title: Vec, + description: Vec, + stake_balance: Option>, + new_validator_count: u32, + ) { + ensure!( + new_validator_count >= >::minimum_validator_count(), + Error::InvalidValidatorCount + ); + + ensure!( + new_validator_count <= MAX_VALIDATOR_COUNT, + Error::InvalidValidatorCount + ); + + let proposal_parameters = + proposal_types::parameters::set_validator_count_proposal::(); + let proposal_details = ProposalDetails::SetValidatorCount(new_validator_count); + let proposal_code = T::ProposalEncoder::encode_proposal(proposal_details.clone()); + + Self::create_proposal( + origin, + member_id, + title, + description, + stake_balance, + proposal_code, + proposal_parameters, + proposal_details, + )?; + } + + /// Create 'Set storage roles parameters' proposal type. + /// This proposal uses `set_role_parameters()` extrinsic from the Substrate `roles::actors` module. + pub fn create_set_storage_role_parameters_proposal( + origin, + member_id: MemberId, + title: Vec, + description: Vec, + stake_balance: Option>, + role_parameters: RoleParameters, T::BlockNumber> + ) { + Self::ensure_storage_role_parameters_valid(&role_parameters)?; + + let proposal_parameters = + proposal_types::parameters::set_storage_role_parameters_proposal::(); + let proposal_details = ProposalDetails::SetStorageRoleParameters(role_parameters); + let proposal_code = T::ProposalEncoder::encode_proposal(proposal_details.clone()); + + Self::create_proposal( + origin, + member_id, + title, + description, + stake_balance, + proposal_code, + proposal_parameters, + proposal_details, + )?; + } + +// *************** Extrinsic to execute + + /// Text proposal extrinsic. Should be used as callable object to pass to the `engine` module. + pub fn execute_text_proposal( + origin, + text: Vec, + ) { + ensure_root(origin)?; + print("Text proposal: "); + let text_string_result = from_utf8(text.as_slice()); + if let Ok(text_string) = text_string_result{ + print(text_string); + } + } + + /// Runtime upgrade proposal extrinsic. + /// Should be used as callable object to pass to the `engine` module. + pub fn execute_runtime_upgrade_proposal( + origin, + wasm: Vec, + ) { + let (cloned_origin1, cloned_origin2) = Self::double_origin(origin); + ensure_root(cloned_origin1)?; + + print("Runtime upgrade proposal execution started."); + + >::set_code(cloned_origin2, wasm)?; + + print("Runtime upgrade proposal execution finished."); + } + } +} + +impl Module { + // Multiplies the T::Origin. + // In our current substrate version system::Origin doesn't support clone(), + // but it will be supported in latest up-to-date substrate version. + // TODO: delete when T::Origin will support the clone() + fn double_origin(origin: T::Origin) -> (T::Origin, T::Origin) { + let coerced_origin = origin.into().ok().unwrap_or(RawOrigin::None); + + let (cloned_origin1, cloned_origin2) = match coerced_origin { + RawOrigin::None => (RawOrigin::None, RawOrigin::None), + RawOrigin::Root => (RawOrigin::Root, RawOrigin::Root), + RawOrigin::Signed(account_id) => ( + RawOrigin::Signed(account_id.clone()), + RawOrigin::Signed(account_id), + ), + }; + + (cloned_origin1.into(), cloned_origin2.into()) + } + + // Generic template proposal builder + fn create_proposal( + origin: T::Origin, + member_id: MemberId, + title: Vec, + description: Vec, + stake_balance: Option>, + proposal_code: Vec, + proposal_parameters: ProposalParameters>, + proposal_details: ProposalDetails< + BalanceOfMint, + BalanceOfGovernanceCurrency, + T::BlockNumber, + T::AccountId, + T::MemberId, + >, + ) -> DispatchResult { + let account_id = T::MembershipOriginValidator::ensure_actor_origin(origin, member_id)?; + + >::ensure_create_proposal_parameters_are_valid( + &proposal_parameters, + &title, + &description, + stake_balance, + )?; + + >::ensure_can_create_thread(member_id, &title)?; + + let discussion_thread_id = + >::create_thread(member_id, title.clone())?; + + let proposal_id = >::create_proposal( + account_id, + member_id, + proposal_parameters, + title, + description, + stake_balance, + proposal_code, + )?; + + >::insert(proposal_id, discussion_thread_id); + >::insert(proposal_id, proposal_details); + + Ok(()) + } + + // validates storage role parameters for the 'Set storage role parameters' proposal + fn ensure_storage_role_parameters_valid( + role_parameters: &RoleParameters, T::BlockNumber>, + ) -> Result<(), Error> { + ensure!( + role_parameters.min_actors < ROLE_PARAMETERS_MIN_ACTORS_MAX_VALUE, + Error::InvalidStorageRoleParameterMinActors + ); + + ensure!( + role_parameters.max_actors >= ROLE_PARAMETERS_MAX_ACTORS_MIN_VALUE, + Error::InvalidStorageRoleParameterMaxActors + ); + + ensure!( + role_parameters.max_actors < ROLE_PARAMETERS_MAX_ACTORS_MAX_VALUE, + Error::InvalidStorageRoleParameterMaxActors + ); + + ensure!( + role_parameters.reward_period + >= T::BlockNumber::from(ROLE_PARAMETERS_REWARD_PERIOD_MIN_VALUE), + Error::InvalidStorageRoleParameterRewardPeriod + ); + + ensure!( + role_parameters.reward_period + <= T::BlockNumber::from(ROLE_PARAMETERS_REWARD_PERIOD_MAX_VALUE), + Error::InvalidStorageRoleParameterRewardPeriod + ); + + ensure!( + role_parameters.bonding_period + >= T::BlockNumber::from(ROLE_PARAMETERS_BONDING_PERIOD_MIN_VALUE), + Error::InvalidStorageRoleParameterBondingPeriod + ); + + ensure!( + role_parameters.bonding_period + <= T::BlockNumber::from(ROLE_PARAMETERS_BONDING_PERIOD_MAX_VALUE), + Error::InvalidStorageRoleParameterBondingPeriod + ); + + ensure!( + role_parameters.unbonding_period + >= T::BlockNumber::from(ROLE_PARAMETERS_UNBONDING_PERIOD_MIN_VALUE), + Error::InvalidStorageRoleParameterUnbondingPeriod + ); + + ensure!( + role_parameters.unbonding_period + <= T::BlockNumber::from(ROLE_PARAMETERS_UNBONDING_PERIOD_MAX_VALUE), + Error::InvalidStorageRoleParameterUnbondingPeriod + ); + + ensure!( + role_parameters.min_service_period + >= T::BlockNumber::from(ROLE_PARAMETERS_MIN_SERVICE_PERIOD_MIN_VALUE), + Error::InvalidStorageRoleParameterMinServicePeriod + ); + + ensure!( + role_parameters.min_service_period + <= T::BlockNumber::from(ROLE_PARAMETERS_MIN_SERVICE_PERIOD_MAX_VALUE), + Error::InvalidStorageRoleParameterMinServicePeriod + ); + + ensure!( + role_parameters.startup_grace_period + >= T::BlockNumber::from(ROLE_PARAMETERS_STARTUP_GRACE_PERIOD_MIN_VALUE), + Error::InvalidStorageRoleParameterStartupGracePeriod + ); + + ensure!( + role_parameters.startup_grace_period + <= T::BlockNumber::from(ROLE_PARAMETERS_STARTUP_GRACE_PERIOD_MAX_VALUE), + Error::InvalidStorageRoleParameterStartupGracePeriod + ); + + ensure!( + role_parameters.min_stake + > >::from(ROLE_PARAMETERS_MIN_STAKE_MIN_VALUE), + Error::InvalidStorageRoleParameterMinStake + ); + + ensure!( + role_parameters.min_stake + <= >::from(ROLE_PARAMETERS_MIN_STAKE_MAX_VALUE), + Error::InvalidStorageRoleParameterMinStake + ); + + ensure!( + role_parameters.entry_request_fee + > >::from( + ROLE_PARAMETERS_ENTRY_REQUEST_FEE_MIN_VALUE + ), + Error::InvalidStorageRoleParameterEntryRequestFee + ); + + ensure!( + role_parameters.entry_request_fee + <= >::from( + ROLE_PARAMETERS_ENTRY_REQUEST_FEE_MAX_VALUE + ), + Error::InvalidStorageRoleParameterEntryRequestFee + ); + + ensure!( + role_parameters.reward + > >::from(ROLE_PARAMETERS_REWARD_MIN_VALUE), + Error::InvalidStorageRoleParameterReward + ); + + ensure!( + role_parameters.reward + < >::from(ROLE_PARAMETERS_REWARD_MAX_VALUE), + Error::InvalidStorageRoleParameterReward + ); + + Ok(()) + } + + /* + entry_request_fee [tJOY] >0 <1% NA + * Not enforced by runtime. Should not be displayed in the UI, or at least grayed out. + ** Should not be displayed in the UI, or at least grayed out. + */ + + // validates council election parameters for the 'Set election parameters' proposal + pub(crate) fn ensure_council_election_parameters_valid( + election_parameters: &ElectionParameters, T::BlockNumber>, + ) -> Result<(), Error> { + ensure!( + election_parameters.council_size >= ELECTION_PARAMETERS_COUNCIL_SIZE_MIN_VALUE, + Error::InvalidCouncilElectionParameterCouncilSize + ); + + ensure!( + election_parameters.council_size <= ELECTION_PARAMETERS_COUNCIL_SIZE_MAX_VALUE, + Error::InvalidCouncilElectionParameterCouncilSize + ); + + ensure!( + election_parameters.candidacy_limit >= ELECTION_PARAMETERS_CANDIDACY_LIMIT_MIN_VALUE, + Error::InvalidCouncilElectionParameterCandidacyLimit + ); + + ensure!( + election_parameters.candidacy_limit <= ELECTION_PARAMETERS_CANDIDACY_LIMIT_MAX_VALUE, + Error::InvalidCouncilElectionParameterCandidacyLimit + ); + + ensure!( + election_parameters.min_voting_stake + >= >::from(ELECTION_PARAMETERS_MIN_STAKE_MIN_VALUE), + Error::InvalidCouncilElectionParameterMinVotingStake + ); + + ensure!( + election_parameters.min_voting_stake + <= >::from(ELECTION_PARAMETERS_MIN_STAKE_MAX_VALUE), + Error::InvalidCouncilElectionParameterMinVotingStake + ); + + ensure!( + election_parameters.new_term_duration + >= T::BlockNumber::from(ELECTION_PARAMETERS_NEW_TERM_DURATION_MIN_VALUE), + Error::InvalidCouncilElectionParameterNewTermDuration + ); + + ensure!( + election_parameters.new_term_duration + <= T::BlockNumber::from(ELECTION_PARAMETERS_NEW_TERM_DURATION_MAX_VALUE), + Error::InvalidCouncilElectionParameterNewTermDuration + ); + + ensure!( + election_parameters.revealing_period + >= T::BlockNumber::from(ELECTION_PARAMETERS_REVEALING_PERIOD_MIN_VALUE), + Error::InvalidCouncilElectionParameterRevealingPeriod + ); + + ensure!( + election_parameters.revealing_period + <= T::BlockNumber::from(ELECTION_PARAMETERS_REVEALING_PERIOD_MAX_VALUE), + Error::InvalidCouncilElectionParameterRevealingPeriod + ); + + ensure!( + election_parameters.voting_period + >= T::BlockNumber::from(ELECTION_PARAMETERS_VOTING_PERIOD_MIN_VALUE), + Error::InvalidCouncilElectionParameterVotingPeriod + ); + + ensure!( + election_parameters.voting_period + <= T::BlockNumber::from(ELECTION_PARAMETERS_VOTING_PERIOD_MAX_VALUE), + Error::InvalidCouncilElectionParameterVotingPeriod + ); + + ensure!( + election_parameters.announcing_period + >= T::BlockNumber::from(ELECTION_PARAMETERS_ANNOUNCING_PERIOD_MIN_VALUE), + Error::InvalidCouncilElectionParameterAnnouncingPeriod + ); + + ensure!( + election_parameters.announcing_period + <= T::BlockNumber::from(ELECTION_PARAMETERS_ANNOUNCING_PERIOD_MAX_VALUE), + Error::InvalidCouncilElectionParameterAnnouncingPeriod + ); + + ensure!( + election_parameters.min_council_stake + >= >::from( + ELECTION_PARAMETERS_MIN_COUNCIL_STAKE_MIN_VALUE + ), + Error::InvalidCouncilElectionParameterMinCouncilStake + ); + + ensure!( + election_parameters.min_council_stake + <= >::from( + ELECTION_PARAMETERS_MIN_COUNCIL_STAKE_MAX_VALUE + ), + Error::InvalidCouncilElectionParameterMinCouncilStake + ); + + Ok(()) + } + + /// Sets default config values for the proposals. + /// Should be called on the migration to the new runtime version. + pub fn set_default_config_values() { + let p = ProposalsConfigParameters::default(); + + >::put(T::BlockNumber::from( + p.set_validator_count_proposal_voting_period, + )); + >::put(T::BlockNumber::from( + p.set_validator_count_proposal_grace_period, + )); + >::put(T::BlockNumber::from( + p.runtime_upgrade_proposal_voting_period, + )); + >::put(T::BlockNumber::from( + p.runtime_upgrade_proposal_grace_period, + )); + >::put(T::BlockNumber::from(p.text_proposal_voting_period)); + >::put(T::BlockNumber::from(p.text_proposal_grace_period)); + >::put(T::BlockNumber::from( + p.set_election_parameters_proposal_voting_period, + )); + >::put(T::BlockNumber::from( + p.set_election_parameters_proposal_grace_period, + )); + >::put(T::BlockNumber::from( + p.set_content_working_group_mint_capacity_proposal_voting_period, + )); + >::put(T::BlockNumber::from( + p.set_content_working_group_mint_capacity_proposal_grace_period, + )); + >::put(T::BlockNumber::from( + p.set_lead_proposal_voting_period, + )); + >::put(T::BlockNumber::from( + p.set_lead_proposal_grace_period, + )); + >::put(T::BlockNumber::from( + p.spending_proposal_voting_period, + )); + >::put(T::BlockNumber::from( + p.spending_proposal_grace_period, + )); + >::put(T::BlockNumber::from( + p.evict_storage_provider_proposal_voting_period, + )); + >::put(T::BlockNumber::from( + p.evict_storage_provider_proposal_grace_period, + )); + >::put(T::BlockNumber::from( + p.set_storage_role_parameters_proposal_voting_period, + )); + >::put(T::BlockNumber::from( + p.set_storage_role_parameters_proposal_grace_period, + )); + } +} diff --git a/runtime-modules/proposals/codex/src/proposal_types/mod.rs b/runtime-modules/proposals/codex/src/proposal_types/mod.rs new file mode 100644 index 0000000000..5bbf4fa43f --- /dev/null +++ b/runtime-modules/proposals/codex/src/proposal_types/mod.rs @@ -0,0 +1,148 @@ +#![warn(missing_docs)] + +pub(crate) mod parameters; + +use codec::{Decode, Encode}; +use rstd::vec::Vec; +#[cfg(feature = "std")] +use serde::{Deserialize, Serialize}; + +use crate::ElectionParameters; +use roles::actors::RoleParameters; + +/// Encodes proposal using its details information. +pub trait ProposalEncoder { + /// Encodes proposal using its details information. + fn encode_proposal(proposal_details: ProposalDetailsOf) -> Vec; +} + +/// _ProposalDetails_ alias for type simplification +pub type ProposalDetailsOf = ProposalDetails< + crate::BalanceOfMint, + crate::BalanceOfGovernanceCurrency, + ::BlockNumber, + ::AccountId, + crate::MemberId, +>; + +/// Proposal details provide voters the information required for the perceived voting. +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +#[derive(Encode, Decode, Clone, PartialEq, Debug)] +pub enum ProposalDetails { + /// The text of the `text` proposal + Text(Vec), + + /// The wasm code for the `runtime upgrade` proposal + RuntimeUpgrade(Vec), + + /// Election parameters for the `set election parameters` proposal + SetElectionParameters(ElectionParameters), + + /// Balance and destination account for the `spending` proposal + Spending(MintedBalance, AccountId), + + /// New leader memberId and account_id for the `set lead` proposal + SetLead(Option<(MemberId, AccountId)>), + + /// Balance for the `set content working group mint capacity` proposal + SetContentWorkingGroupMintCapacity(MintedBalance), + + /// AccountId for the `evict storage provider` proposal + EvictStorageProvider(AccountId), + + /// Validator count for the `set validator count` proposal + SetValidatorCount(u32), + + /// Role parameters for the `set storage role parameters` proposal + SetStorageRoleParameters(RoleParameters), +} + +impl Default + for ProposalDetails +{ + fn default() -> Self { + ProposalDetails::Text(b"invalid proposal details".to_vec()) + } +} + +/// Contains proposal config parameters. Default values are used by migration and genesis config. +pub struct ProposalsConfigParameters { + /// 'Set validator count' proposal voting period + pub set_validator_count_proposal_voting_period: u32, + + /// 'Set validator count' proposal grace period + pub set_validator_count_proposal_grace_period: u32, + + /// 'Runtime upgrade' proposal voting period + pub runtime_upgrade_proposal_voting_period: u32, + + /// 'Runtime upgrade' proposal grace period + pub runtime_upgrade_proposal_grace_period: u32, + + /// 'Text' proposal voting period + pub text_proposal_voting_period: u32, + + /// 'Text' proposal grace period + pub text_proposal_grace_period: u32, + + /// 'Set election parameters' proposal voting period + pub set_election_parameters_proposal_voting_period: u32, + + /// 'Set election parameters' proposal grace period + pub set_election_parameters_proposal_grace_period: u32, + + /// 'Set content working group mint capacity' proposal voting period + pub set_content_working_group_mint_capacity_proposal_voting_period: u32, + + /// 'Set content working group mint capacity' proposal grace period + pub set_content_working_group_mint_capacity_proposal_grace_period: u32, + + /// 'Set lead' proposal voting period + pub set_lead_proposal_voting_period: u32, + + /// 'Set lead' proposal grace period + pub set_lead_proposal_grace_period: u32, + + /// 'Spending' proposal voting period + pub spending_proposal_voting_period: u32, + + /// 'Spending' proposal grace period + pub spending_proposal_grace_period: u32, + + /// 'Evict storage provider' proposal voting period + pub evict_storage_provider_proposal_voting_period: u32, + + /// 'Evict storage provider' proposal grace period + pub evict_storage_provider_proposal_grace_period: u32, + + /// 'Set storage role parameters' proposal voting period + pub set_storage_role_parameters_proposal_voting_period: u32, + + /// 'Set storage role parameters' proposal grace period + pub set_storage_role_parameters_proposal_grace_period: u32, +} + +impl Default for ProposalsConfigParameters { + fn default() -> Self { + ProposalsConfigParameters { + set_validator_count_proposal_voting_period: 43200u32, + set_validator_count_proposal_grace_period: 0u32, + runtime_upgrade_proposal_voting_period: 72000u32, + runtime_upgrade_proposal_grace_period: 72000u32, + text_proposal_voting_period: 72000u32, + text_proposal_grace_period: 0u32, + set_election_parameters_proposal_voting_period: 72000u32, + set_election_parameters_proposal_grace_period: 201_601_u32, + set_content_working_group_mint_capacity_proposal_voting_period: 43200u32, + set_content_working_group_mint_capacity_proposal_grace_period: 0u32, + set_lead_proposal_voting_period: 43200u32, + set_lead_proposal_grace_period: 0u32, + spending_proposal_voting_period: 72000u32, + spending_proposal_grace_period: 14400u32, + evict_storage_provider_proposal_voting_period: 43200u32, + evict_storage_provider_proposal_grace_period: 0u32, + set_storage_role_parameters_proposal_voting_period: 43200u32, + set_storage_role_parameters_proposal_grace_period: 14400u32, + } + } +} diff --git a/runtime-modules/proposals/codex/src/proposal_types/parameters.rs b/runtime-modules/proposals/codex/src/proposal_types/parameters.rs new file mode 100644 index 0000000000..b75d68ae21 --- /dev/null +++ b/runtime-modules/proposals/codex/src/proposal_types/parameters.rs @@ -0,0 +1,127 @@ +use crate::{BalanceOf, Module, ProposalParameters}; + +// Proposal parameters for the 'Set validator count' proposal +pub(crate) fn set_validator_count_proposal( +) -> ProposalParameters> { + ProposalParameters { + voting_period: >::set_validator_count_proposal_voting_period(), + grace_period: >::set_validator_count_proposal_grace_period(), + approval_quorum_percentage: 66, + approval_threshold_percentage: 80, + slashing_quorum_percentage: 60, + slashing_threshold_percentage: 80, + required_stake: Some(>::from(100_000_u32)), + } +} + +// Proposal parameters for the upgrade runtime proposal +pub(crate) fn runtime_upgrade_proposal( +) -> ProposalParameters> { + ProposalParameters { + voting_period: >::runtime_upgrade_proposal_voting_period(), + grace_period: >::runtime_upgrade_proposal_grace_period(), + approval_quorum_percentage: 80, + approval_threshold_percentage: 100, + slashing_quorum_percentage: 60, + slashing_threshold_percentage: 80, + required_stake: Some(>::from(1_000_000_u32)), + } +} + +// Proposal parameters for the text proposal +pub(crate) fn text_proposal() -> ProposalParameters> { + ProposalParameters { + voting_period: >::text_proposal_voting_period(), + grace_period: >::text_proposal_grace_period(), + approval_quorum_percentage: 60, + approval_threshold_percentage: 80, + slashing_quorum_percentage: 60, + slashing_threshold_percentage: 80, + required_stake: Some(>::from(25000u32)), + } +} + +// Proposal parameters for the 'Set Election Parameters' proposal +pub(crate) fn set_election_parameters_proposal( +) -> ProposalParameters> { + ProposalParameters { + voting_period: >::set_election_parameters_proposal_voting_period(), + grace_period: >::set_election_parameters_proposal_grace_period(), + approval_quorum_percentage: 66, + approval_threshold_percentage: 80, + slashing_quorum_percentage: 60, + slashing_threshold_percentage: 80, + required_stake: Some(>::from(200_000_u32)), + } +} + +// Proposal parameters for the 'Set content working group mint capacity' proposal +pub(crate) fn set_content_working_group_mint_capacity_proposal( +) -> ProposalParameters> { + ProposalParameters { + voting_period: >::set_content_working_group_mint_capacity_proposal_voting_period( + ), + grace_period: >::set_content_working_group_mint_capacity_proposal_grace_period(), + approval_quorum_percentage: 60, + approval_threshold_percentage: 75, + slashing_quorum_percentage: 60, + slashing_threshold_percentage: 80, + required_stake: Some(>::from(50000u32)), + } +} + +// Proposal parameters for the 'Spending' proposal +pub(crate) fn spending_proposal( +) -> ProposalParameters> { + ProposalParameters { + voting_period: >::spending_proposal_voting_period(), + grace_period: >::spending_proposal_grace_period(), + approval_quorum_percentage: 60, + approval_threshold_percentage: 80, + slashing_quorum_percentage: 60, + slashing_threshold_percentage: 80, + required_stake: Some(>::from(25000u32)), + } +} + +// Proposal parameters for the 'Set lead' proposal +pub(crate) fn set_lead_proposal( +) -> ProposalParameters> { + ProposalParameters { + voting_period: >::set_lead_proposal_voting_period(), + grace_period: >::set_lead_proposal_grace_period(), + approval_quorum_percentage: 60, + approval_threshold_percentage: 75, + slashing_quorum_percentage: 60, + slashing_threshold_percentage: 80, + required_stake: Some(>::from(50000u32)), + } +} + +// Proposal parameters for the 'Evict storage provider' proposal +pub(crate) fn evict_storage_provider_proposal( +) -> ProposalParameters> { + ProposalParameters { + voting_period: >::evict_storage_provider_proposal_voting_period(), + grace_period: >::evict_storage_provider_proposal_grace_period(), + approval_quorum_percentage: 50, + approval_threshold_percentage: 75, + slashing_quorum_percentage: 60, + slashing_threshold_percentage: 80, + required_stake: Some(>::from(25000u32)), + } +} + +// Proposal parameters for the 'Set storage role parameters' proposal +pub(crate) fn set_storage_role_parameters_proposal( +) -> ProposalParameters> { + ProposalParameters { + voting_period: >::set_storage_role_parameters_proposal_voting_period(), + grace_period: >::set_storage_role_parameters_proposal_grace_period(), + approval_quorum_percentage: 66, + approval_threshold_percentage: 80, + slashing_quorum_percentage: 60, + slashing_threshold_percentage: 80, + required_stake: Some(>::from(100_000_u32)), + } +} diff --git a/runtime-modules/proposals/codex/src/tests/mock.rs b/runtime-modules/proposals/codex/src/tests/mock.rs new file mode 100644 index 0000000000..adebe2ca20 --- /dev/null +++ b/runtime-modules/proposals/codex/src/tests/mock.rs @@ -0,0 +1,296 @@ +#![cfg(test)] +// srml_staking_reward_curve::build! - substrate macro produces a warning. +// TODO: remove after post-Rome substrate upgrade +#![allow(array_into_iter)] + +use crate::{ProposalDetailsOf, ProposalEncoder}; +pub use primitives::{Blake2Hasher, H256}; +use proposal_engine::VotersParameters; +use sr_primitives::curve::PiecewiseLinear; +pub use sr_primitives::{ + testing::{Digest, DigestItem, Header, UintAuthorityId}, + traits::{BlakeTwo256, Convert, IdentityLookup, OnFinalize}, + weights::Weight, + BuildStorage, DispatchError, Perbill, +}; +use sr_staking_primitives::SessionIndex; +use srml_support::{impl_outer_dispatch, impl_outer_origin, parameter_types}; +pub use system; + +impl_outer_origin! { + pub enum Origin for Test {} +} + +// Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Test; +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const MaximumBlockWeight: u32 = 1024; + pub const MaximumBlockLength: u32 = 2 * 1024; + pub const AvailableBlockRatio: Perbill = Perbill::one(); + pub const MinimumPeriod: u64 = 5; + pub const StakePoolId: [u8; 8] = *b"joystake"; +} + +impl_outer_dispatch! { + pub enum Call for Test where origin: Origin { + codex::ProposalCodex, + proposals::ProposalsEngine, + } +} + +impl common::currency::GovernanceCurrency for Test { + type Currency = balances::Module; +} + +impl membership::members::Trait for Test { + type Event = (); + type MemberId = u64; + type PaidTermId = u64; + type SubscriptionId = u64; + type ActorId = u64; + type InitialMembersBalance = (); +} + +parameter_types! { + pub const ExistentialDeposit: u32 = 0; + pub const TransferFee: u32 = 0; + pub const CreationFee: u32 = 0; +} + +impl balances::Trait for Test { + /// The type for recording an account's balance. + type Balance = u64; + /// What to do if an account's free balance gets zeroed. + type OnFreeBalanceZero = (); + /// What to do if a new account is created. + type OnNewAccount = (); + + type Event = (); + + type DustRemoval = (); + type TransferPayment = (); + type ExistentialDeposit = ExistentialDeposit; + type TransferFee = TransferFee; + type CreationFee = CreationFee; +} + +impl stake::Trait for Test { + type Currency = Balances; + type StakePoolId = StakePoolId; + type StakingEventsHandler = (); + type StakeId = u64; + type SlashId = u64; +} + +parameter_types! { + pub const CancellationFee: u64 = 5; + pub const RejectionFee: u64 = 3; + pub const TitleMaxLength: u32 = 100; + pub const DescriptionMaxLength: u32 = 10000; + pub const MaxActiveProposalLimit: u32 = 100; +} + +impl proposal_engine::Trait for Test { + type Event = (); + type ProposerOriginValidator = (); + type VoterOriginValidator = (); + type TotalVotersCounter = MockVotersParameters; + type ProposalId = u32; + type StakeHandlerProvider = proposal_engine::DefaultStakeHandlerProvider; + type CancellationFee = CancellationFee; + type RejectionFee = RejectionFee; + type TitleMaxLength = TitleMaxLength; + type DescriptionMaxLength = DescriptionMaxLength; + type MaxActiveProposalLimit = MaxActiveProposalLimit; + type DispatchableCallCode = crate::Call; +} + +impl Default for crate::Call { + fn default() -> Self { + panic!("shouldn't call default for Call"); + } +} + +impl mint::Trait for Test { + type Currency = Balances; + type MintId = u64; +} + +impl governance::council::Trait for Test { + type Event = (); + type CouncilTermEnded = (); +} + +impl common::origin_validator::ActorOriginValidator for () { + fn ensure_actor_origin(origin: Origin, _: u64) -> Result { + let account_id = system::ensure_signed(origin)?; + + Ok(account_id) + } +} + +parameter_types! { + pub const MaxPostEditionNumber: u32 = 5; + pub const MaxThreadInARowNumber: u32 = 3; + pub const ThreadTitleLengthLimit: u32 = 200; + pub const PostLengthLimit: u32 = 2000; +} + +impl proposal_discussion::Trait for Test { + type Event = (); + type PostAuthorOriginValidator = (); + type ThreadId = u64; + type PostId = u64; + type MaxPostEditionNumber = MaxPostEditionNumber; + type ThreadTitleLengthLimit = ThreadTitleLengthLimit; + type PostLengthLimit = PostLengthLimit; + type MaxThreadInARowNumber = MaxThreadInARowNumber; +} + +pub struct MockVotersParameters; +impl VotersParameters for MockVotersParameters { + fn total_voters_count() -> u32 { + 4 + } +} + +parameter_types! { + pub const TextProposalMaxLength: u32 = 20_000; + pub const RuntimeUpgradeWasmProposalMaxLength: u32 = 20_000; +} + +impl governance::election::Trait for Test { + type Event = (); + type CouncilElected = (); +} + +impl content_working_group::Trait for Test { + type Event = (); +} + +impl recurring_rewards::Trait for Test { + type PayoutStatusHandler = (); + type RecipientId = u64; + type RewardRelationshipId = u64; +} + +impl versioned_store_permissions::Trait for Test { + type Credential = u64; + type CredentialChecker = (); + type CreateClassPermissionsChecker = (); +} + +impl versioned_store::Trait for Test { + type Event = (); +} + +impl hiring::Trait for Test { + type OpeningId = u64; + type ApplicationId = u64; + type ApplicationDeactivatedHandler = (); + type StakeHandlerProvider = hiring::Module; +} + +impl roles::actors::Trait for Test { + type Event = (); + type OnActorRemoved = (); +} + +impl roles::actors::ActorRemoved for () { + fn actor_removed(_: &u64) {} +} + +srml_staking_reward_curve::build! { + const I_NPOS: PiecewiseLinear<'static> = curve!( + min_inflation: 0_025_000, + max_inflation: 0_100_000, + ideal_stake: 0_500_000, + falloff: 0_050_000, + max_piece_count: 40, + test_precision: 0_005_000, + ); +} + +parameter_types! { + pub const SessionsPerEra: SessionIndex = 3; + pub const BondingDuration: staking::EraIndex = 3; + pub const RewardCurve: &'static PiecewiseLinear<'static> = &I_NPOS; +} +impl staking::Trait for Test { + type Currency = balances::Module; + type Time = timestamp::Module; + type CurrencyToVote = (); + type RewardRemainder = (); + type Event = (); + type Slash = (); + type Reward = (); + type SessionsPerEra = SessionsPerEra; + type BondingDuration = BondingDuration; + type SessionInterface = Self; + type RewardCurve = RewardCurve; +} + +impl staking::SessionInterface for Test { + fn disable_validator(_: &u64) -> Result { + unimplemented!() + } + + fn validators() -> Vec { + unimplemented!() + } + + fn prune_historical_up_to(_: u32) { + unimplemented!() + } +} + +impl crate::Trait for Test { + type TextProposalMaxLength = TextProposalMaxLength; + type RuntimeUpgradeWasmProposalMaxLength = RuntimeUpgradeWasmProposalMaxLength; + type MembershipOriginValidator = (); + type ProposalEncoder = (); +} + +impl ProposalEncoder for () { + fn encode_proposal(_proposal_details: ProposalDetailsOf) -> Vec { + Vec::new() + } +} + +impl system::Trait for Test { + type Origin = Origin; + type Index = u64; + type BlockNumber = u64; + type Call = (); + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type Event = (); + type BlockHashCount = BlockHashCount; + type MaximumBlockWeight = MaximumBlockWeight; + type MaximumBlockLength = MaximumBlockLength; + type AvailableBlockRatio = AvailableBlockRatio; + type Version = (); +} + +impl timestamp::Trait for Test { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; +} + +pub fn initial_test_ext() -> runtime_io::TestExternalities { + let t = system::GenesisConfig::default() + .build_storage::() + .unwrap(); + + t.into() +} + +pub type ProposalCodex = crate::Module; +pub type ProposalsEngine = proposal_engine::Module; +pub type Balances = balances::Module; diff --git a/runtime-modules/proposals/codex/src/tests/mod.rs b/runtime-modules/proposals/codex/src/tests/mod.rs new file mode 100644 index 0000000000..1ea0ab553c --- /dev/null +++ b/runtime-modules/proposals/codex/src/tests/mod.rs @@ -0,0 +1,1147 @@ +mod mock; + +use governance::election_params::ElectionParameters; +use srml_support::traits::Currency; +use srml_support::StorageMap; +use system::RawOrigin; + +use crate::*; +use crate::{BalanceOf, Error, ProposalDetails}; +use proposal_engine::ProposalParameters; +use roles::actors::RoleParameters; +use srml_support::dispatch::DispatchResult; + +use crate::proposal_types::ProposalsConfigParameters; +pub use mock::*; + +pub(crate) fn increase_total_balance_issuance(balance: u64) { + increase_total_balance_issuance_using_account_id(999, balance); +} + +pub(crate) fn increase_total_balance_issuance_using_account_id(account_id: u64, balance: u64) { + let initial_balance = Balances::total_issuance(); + { + let _ = ::Currency::deposit_creating(&account_id, balance); + } + assert_eq!(Balances::total_issuance(), initial_balance + balance); +} + +struct ProposalTestFixture +where + InsufficientRightsCall: Fn() -> DispatchResult, + EmptyStakeCall: Fn() -> DispatchResult, + InvalidStakeCall: Fn() -> DispatchResult, + SuccessfulCall: Fn() -> DispatchResult, +{ + insufficient_rights_call: InsufficientRightsCall, + empty_stake_call: EmptyStakeCall, + invalid_stake_call: InvalidStakeCall, + successful_call: SuccessfulCall, + proposal_parameters: ProposalParameters, + proposal_details: ProposalDetails, +} + +impl + ProposalTestFixture +where + InsufficientRightsCall: Fn() -> DispatchResult, + EmptyStakeCall: Fn() -> DispatchResult, + InvalidStakeCall: Fn() -> DispatchResult, + SuccessfulCall: Fn() -> DispatchResult, +{ + fn check_for_invalid_stakes(&self) { + assert_eq!((self.empty_stake_call)(), Err(Error::Other("EmptyStake"))); + + assert_eq!( + (self.invalid_stake_call)(), + Err(Error::Other("StakeDiffersFromRequired")) + ); + } + + fn check_call_for_insufficient_rights(&self) { + assert_eq!( + (self.insufficient_rights_call)(), + Err(Error::Other("RequireSignedOrigin")) + ); + } + + fn check_for_successful_call(&self) { + let account_id = 1; + let _imbalance = ::Currency::deposit_creating(&account_id, 50000); + + assert_eq!((self.successful_call)(), Ok(())); + + // a discussion was created + let thread_id = >::get(1); + assert_eq!(thread_id, 1); + + let proposal_id = 1; + let proposal = ProposalsEngine::proposals(proposal_id); + // check for correct proposal parameters + assert_eq!(proposal.parameters, self.proposal_parameters); + + // proposal details was set + let details = >::get(proposal_id); + assert_eq!(details, self.proposal_details); + } + + pub fn check_all(&self) { + self.check_call_for_insufficient_rights(); + self.check_for_invalid_stakes(); + self.check_for_successful_call(); + } +} + +#[test] +fn create_text_proposal_common_checks_succeed() { + initial_test_ext().execute_with(|| { + increase_total_balance_issuance(500000); + + let proposal_fixture = ProposalTestFixture { + insufficient_rights_call: || { + ProposalCodex::create_text_proposal( + RawOrigin::None.into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + None, + b"text".to_vec(), + ) + }, + empty_stake_call: || { + ProposalCodex::create_text_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + None, + b"text".to_vec(), + ) + }, + invalid_stake_call: || { + ProposalCodex::create_text_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + Some(>::from(5000u32)), + b"text".to_vec(), + ) + }, + successful_call: || { + ProposalCodex::create_text_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + Some(>::from(25000u32)), + b"text".to_vec(), + ) + }, + proposal_parameters: crate::proposal_types::parameters::text_proposal::(), + proposal_details: ProposalDetails::Text(b"text".to_vec()), + }; + proposal_fixture.check_all(); + }); +} + +#[test] +fn create_text_proposal_codex_call_fails_with_incorrect_text_size() { + initial_test_ext().execute_with(|| { + let origin = RawOrigin::Signed(1).into(); + + let long_text = [0u8; 30000].to_vec(); + assert_eq!( + ProposalCodex::create_text_proposal( + origin, + 1, + b"title".to_vec(), + b"body".to_vec(), + None, + long_text, + ), + Err(Error::TextProposalSizeExceeded) + ); + + assert_eq!( + ProposalCodex::create_text_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + None, + Vec::new(), + ), + Err(Error::TextProposalIsEmpty) + ); + }); +} + +#[test] +fn create_runtime_upgrade_common_checks_succeed() { + initial_test_ext().execute_with(|| { + increase_total_balance_issuance_using_account_id(1, 5000000); + + let proposal_fixture = ProposalTestFixture { + insufficient_rights_call: || { + ProposalCodex::create_runtime_upgrade_proposal( + RawOrigin::None.into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + None, + b"wasm".to_vec(), + ) + }, + empty_stake_call: || { + ProposalCodex::create_runtime_upgrade_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + None, + b"wasm".to_vec(), + ) + }, + invalid_stake_call: || { + ProposalCodex::create_runtime_upgrade_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + Some(>::from(500u32)), + b"wasm".to_vec(), + ) + }, + successful_call: || { + ProposalCodex::create_runtime_upgrade_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + Some(>::from(1_000_000_u32)), + b"wasm".to_vec(), + ) + }, + proposal_parameters: crate::proposal_types::parameters::runtime_upgrade_proposal::(), + proposal_details: ProposalDetails::RuntimeUpgrade(b"wasm".to_vec()), + }; + proposal_fixture.check_all(); + }); +} + +#[test] +fn create_upgrade_runtime_proposal_codex_call_fails_with_incorrect_wasm_size() { + initial_test_ext().execute_with(|| { + let origin = RawOrigin::Signed(1).into(); + + let long_wasm = [0u8; 30000].to_vec(); + assert_eq!( + ProposalCodex::create_runtime_upgrade_proposal( + origin, + 1, + b"title".to_vec(), + b"body".to_vec(), + None, + long_wasm, + ), + Err(Error::RuntimeProposalSizeExceeded) + ); + + assert_eq!( + ProposalCodex::create_runtime_upgrade_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + None, + Vec::new(), + ), + Err(Error::RuntimeProposalIsEmpty) + ); + }); +} + +#[test] +fn create_set_election_parameters_proposal_common_checks_succeed() { + initial_test_ext().execute_with(|| { + increase_total_balance_issuance_using_account_id(1, 500000); + + let proposal_fixture = ProposalTestFixture { + insufficient_rights_call: || { + ProposalCodex::create_set_election_parameters_proposal( + RawOrigin::None.into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + None, + get_valid_election_parameters(), + ) + }, + empty_stake_call: || { + ProposalCodex::create_set_election_parameters_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + None, + get_valid_election_parameters(), + ) + }, + invalid_stake_call: || { + ProposalCodex::create_set_election_parameters_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + Some(>::from(50000u32)), + get_valid_election_parameters(), + ) + }, + successful_call: || { + ProposalCodex::create_set_election_parameters_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + Some(>::from(200_000_u32)), + get_valid_election_parameters(), + ) + }, + proposal_parameters: + crate::proposal_types::parameters::set_election_parameters_proposal::(), + proposal_details: ProposalDetails::SetElectionParameters( + get_valid_election_parameters(), + ), + }; + proposal_fixture.check_all(); + }); +} + +fn assert_failed_election_parameters_call( + election_parameters: ElectionParameters, + error: Error, +) { + assert_eq!( + ProposalCodex::create_set_election_parameters_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + Some(>::from(3750u32)), + election_parameters, + ), + Err(error) + ); +} + +fn get_valid_election_parameters() -> ElectionParameters { + ElectionParameters { + announcing_period: 14400, + voting_period: 14400, + revealing_period: 14400, + council_size: 4, + candidacy_limit: 25, + new_term_duration: 14400, + min_council_stake: 1, + min_voting_stake: 1, + } +} + +#[test] +fn create_set_election_parameters_call_fails_with_incorrect_parameters() { + initial_test_ext().execute_with(|| { + increase_total_balance_issuance_using_account_id(1, 500000); + + let mut election_parameters = get_valid_election_parameters(); + election_parameters.council_size = 2; + assert_failed_election_parameters_call( + election_parameters, + Error::InvalidCouncilElectionParameterCouncilSize, + ); + + election_parameters.council_size = 21; + assert_failed_election_parameters_call( + election_parameters, + Error::InvalidCouncilElectionParameterCouncilSize, + ); + + election_parameters = get_valid_election_parameters(); + election_parameters.candidacy_limit = 22; + assert_failed_election_parameters_call( + election_parameters, + Error::InvalidCouncilElectionParameterCandidacyLimit, + ); + + election_parameters = get_valid_election_parameters(); + election_parameters.candidacy_limit = 122; + assert_failed_election_parameters_call( + election_parameters, + Error::InvalidCouncilElectionParameterCandidacyLimit, + ); + + election_parameters = get_valid_election_parameters(); + election_parameters.min_voting_stake = 0; + assert_failed_election_parameters_call( + election_parameters, + Error::InvalidCouncilElectionParameterMinVotingStake, + ); + + election_parameters = get_valid_election_parameters(); + election_parameters.min_voting_stake = 200000; + assert_failed_election_parameters_call( + election_parameters, + Error::InvalidCouncilElectionParameterMinVotingStake, + ); + + election_parameters = get_valid_election_parameters(); + election_parameters.new_term_duration = 10000; + assert_failed_election_parameters_call( + election_parameters, + Error::InvalidCouncilElectionParameterNewTermDuration, + ); + + election_parameters = get_valid_election_parameters(); + election_parameters.new_term_duration = 500000; + assert_failed_election_parameters_call( + election_parameters, + Error::InvalidCouncilElectionParameterNewTermDuration, + ); + + election_parameters = get_valid_election_parameters(); + election_parameters.min_council_stake = 0; + assert_failed_election_parameters_call( + election_parameters, + Error::InvalidCouncilElectionParameterMinCouncilStake, + ); + + election_parameters = get_valid_election_parameters(); + election_parameters.min_council_stake = 200000; + assert_failed_election_parameters_call( + election_parameters, + Error::InvalidCouncilElectionParameterMinCouncilStake, + ); + + election_parameters = get_valid_election_parameters(); + election_parameters.voting_period = 10000; + assert_failed_election_parameters_call( + election_parameters, + Error::InvalidCouncilElectionParameterVotingPeriod, + ); + + election_parameters = get_valid_election_parameters(); + election_parameters.voting_period = 50000; + assert_failed_election_parameters_call( + election_parameters, + Error::InvalidCouncilElectionParameterVotingPeriod, + ); + + election_parameters = get_valid_election_parameters(); + election_parameters.revealing_period = 10000; + assert_failed_election_parameters_call( + election_parameters, + Error::InvalidCouncilElectionParameterRevealingPeriod, + ); + + election_parameters = get_valid_election_parameters(); + election_parameters.revealing_period = 50000; + assert_failed_election_parameters_call( + election_parameters, + Error::InvalidCouncilElectionParameterRevealingPeriod, + ); + + election_parameters = get_valid_election_parameters(); + election_parameters.announcing_period = 10000; + assert_failed_election_parameters_call( + election_parameters, + Error::InvalidCouncilElectionParameterAnnouncingPeriod, + ); + + election_parameters = get_valid_election_parameters(); + election_parameters.announcing_period = 50000; + assert_failed_election_parameters_call( + election_parameters, + Error::InvalidCouncilElectionParameterAnnouncingPeriod, + ); + }); +} + +#[test] +fn create_working_group_mint_capacity_proposal_fails_with_invalid_parameters() { + initial_test_ext().execute_with(|| { + increase_total_balance_issuance_using_account_id(1, 500000); + + assert_eq!( + ProposalCodex::create_set_content_working_group_mint_capacity_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + Some(>::from(50000u32)), + (crate::CONTENT_WORKING_GROUP_MINT_CAPACITY_MAX_VALUE + 1) as u64, + ), + Err(Error::InvalidStorageWorkingGroupMintCapacity) + ); + }); +} + +#[test] +fn create_set_content_working_group_mint_capacity_proposal_common_checks_succeed() { + initial_test_ext().execute_with(|| { + increase_total_balance_issuance(500000); + + let proposal_fixture = ProposalTestFixture { + insufficient_rights_call: || { + ProposalCodex::create_set_content_working_group_mint_capacity_proposal( + RawOrigin::None.into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + None, + 0, + ) + }, + empty_stake_call: || { + ProposalCodex::create_set_content_working_group_mint_capacity_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + None, + 0, + ) + }, + invalid_stake_call: || { + ProposalCodex::create_set_content_working_group_mint_capacity_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + Some(>::from(5000u32)), + 0, + ) + }, + successful_call: || { + ProposalCodex::create_set_content_working_group_mint_capacity_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + Some(>::from(50000u32)), + 10, + ) + }, + proposal_parameters: crate::proposal_types::parameters::set_content_working_group_mint_capacity_proposal::(), + proposal_details: ProposalDetails::SetContentWorkingGroupMintCapacity(10), + }; + proposal_fixture.check_all(); + }); +} + +#[test] +fn create_spending_proposal_common_checks_succeed() { + initial_test_ext().execute_with(|| { + increase_total_balance_issuance(500000); + + let proposal_fixture = ProposalTestFixture { + insufficient_rights_call: || { + ProposalCodex::create_spending_proposal( + RawOrigin::None.into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + None, + 20, + 10, + ) + }, + empty_stake_call: || { + ProposalCodex::create_spending_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + None, + 20, + 10, + ) + }, + invalid_stake_call: || { + ProposalCodex::create_spending_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + Some(>::from(5000u32)), + 20, + 10, + ) + }, + successful_call: || { + ProposalCodex::create_spending_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + Some(>::from(25000u32)), + 100, + 2, + ) + }, + proposal_parameters: crate::proposal_types::parameters::spending_proposal::(), + proposal_details: ProposalDetails::Spending(100, 2), + }; + proposal_fixture.check_all(); + }); +} + +#[test] +fn create_spending_proposal_call_fails_with_incorrect_balance() { + initial_test_ext().execute_with(|| { + increase_total_balance_issuance_using_account_id(500000, 1); + + assert_eq!( + ProposalCodex::create_spending_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + Some(>::from(1250u32)), + 0, + 2, + ), + Err(Error::InvalidSpendingProposalBalance) + ); + + assert_eq!( + ProposalCodex::create_spending_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + Some(>::from(1250u32)), + 2000001, + 2, + ), + Err(Error::InvalidSpendingProposalBalance) + ); + }); +} + +#[test] +fn create_set_lead_proposal_fails_with_proposed_councilor() { + initial_test_ext().execute_with(|| { + increase_total_balance_issuance_using_account_id(1, 500000); + + let lead_account_id = 20; + >::set_council( + RawOrigin::Root.into(), + vec![lead_account_id], + ) + .unwrap(); + + assert_eq!( + ProposalCodex::create_set_lead_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + Some(>::from(1250u32)), + Some((20, lead_account_id)), + ), + Err(Error::InvalidSetLeadParameterCannotBeCouncilor) + ); + }); +} + +#[test] +fn create_set_lead_proposal_common_checks_succeed() { + initial_test_ext().execute_with(|| { + increase_total_balance_issuance(500000); + + let proposal_fixture = ProposalTestFixture { + insufficient_rights_call: || { + ProposalCodex::create_set_lead_proposal( + RawOrigin::None.into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + None, + Some((20, 10)), + ) + }, + empty_stake_call: || { + ProposalCodex::create_set_lead_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + None, + Some((20, 10)), + ) + }, + invalid_stake_call: || { + ProposalCodex::create_set_lead_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + Some(>::from(5000u32)), + Some((20, 10)), + ) + }, + successful_call: || { + ProposalCodex::create_set_lead_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + Some(>::from(50000u32)), + Some((20, 10)), + ) + }, + proposal_parameters: crate::proposal_types::parameters::set_lead_proposal::(), + proposal_details: ProposalDetails::SetLead(Some((20, 10))), + }; + proposal_fixture.check_all(); + }); +} + +#[test] +fn create_evict_storage_provider_proposal_common_checks_succeed() { + initial_test_ext().execute_with(|| { + increase_total_balance_issuance(500000); + + let proposal_fixture = ProposalTestFixture { + insufficient_rights_call: || { + ProposalCodex::create_evict_storage_provider_proposal( + RawOrigin::None.into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + None, + 1, + ) + }, + empty_stake_call: || { + ProposalCodex::create_evict_storage_provider_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + None, + 1, + ) + }, + invalid_stake_call: || { + ProposalCodex::create_evict_storage_provider_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + Some(>::from(5000u32)), + 1, + ) + }, + successful_call: || { + ProposalCodex::create_evict_storage_provider_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + Some(>::from(25000u32)), + 1, + ) + }, + proposal_parameters: crate::proposal_types::parameters::evict_storage_provider_proposal::(), + proposal_details: ProposalDetails::EvictStorageProvider(1), + }; + proposal_fixture.check_all(); + }); +} + +#[test] +fn create_set_validator_count_proposal_common_checks_succeed() { + initial_test_ext().execute_with(|| { + increase_total_balance_issuance_using_account_id(1, 500000); + + let proposal_fixture = ProposalTestFixture { + insufficient_rights_call: || { + ProposalCodex::create_set_validator_count_proposal( + RawOrigin::None.into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + None, + 4, + ) + }, + empty_stake_call: || { + ProposalCodex::create_set_validator_count_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + None, + 4, + ) + }, + invalid_stake_call: || { + ProposalCodex::create_set_validator_count_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + Some(>::from(5000u32)), + 4, + ) + }, + successful_call: || { + ProposalCodex::create_set_validator_count_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + Some(>::from(100_000_u32)), + 4, + ) + }, + proposal_parameters: crate::proposal_types::parameters::set_validator_count_proposal::< + Test, + >(), + proposal_details: ProposalDetails::SetValidatorCount(4), + }; + proposal_fixture.check_all(); + }); +} + +#[test] +fn create_set_validator_count_proposal_failed_with_invalid_validator_count() { + initial_test_ext().execute_with(|| { + assert_eq!( + ProposalCodex::create_set_validator_count_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + Some(>::from(500u32)), + 3, + ), + Err(Error::InvalidValidatorCount) + ); + + assert_eq!( + ProposalCodex::create_set_validator_count_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + Some(>::from(1001u32)), + 3, + ), + Err(Error::InvalidValidatorCount) + ); + }); +} + +#[test] +fn create_set_storage_role_parameters_proposal_common_checks_succeed() { + initial_test_ext().execute_with(|| { + increase_total_balance_issuance_using_account_id(1, 500000); + let role_parameters = RoleParameters { + min_actors: 1, + ..RoleParameters::default() + }; + let proposal_fixture = ProposalTestFixture { + insufficient_rights_call: || { + ProposalCodex::create_set_storage_role_parameters_proposal( + RawOrigin::None.into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + None, + role_parameters.clone(), + ) + }, + empty_stake_call: || { + ProposalCodex::create_set_storage_role_parameters_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + None, + role_parameters.clone(), + ) + }, + invalid_stake_call: || { + ProposalCodex::create_set_storage_role_parameters_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + Some(>::from(5000u32)), + role_parameters.clone(), + ) + }, + successful_call: || { + ProposalCodex::create_set_storage_role_parameters_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + Some(>::from(100_000_u32)), + role_parameters.clone(), + ) + }, + proposal_parameters: + crate::proposal_types::parameters::set_storage_role_parameters_proposal::(), + proposal_details: ProposalDetails::SetStorageRoleParameters(role_parameters), + }; + proposal_fixture.check_all(); + }); +} + +fn assert_failed_set_storage_parameters_call( + role_parameters: RoleParameters, + error: Error, +) { + assert_eq!( + ProposalCodex::create_set_storage_role_parameters_proposal( + RawOrigin::Signed(1).into(), + 1, + b"title".to_vec(), + b"body".to_vec(), + Some(>::from(100_000_u32)), + role_parameters, + ), + Err(error) + ); +} + +#[test] +fn create_set_storage_role_parameters_proposal_fails_with_invalid_parameters() { + initial_test_ext().execute_with(|| { + increase_total_balance_issuance_using_account_id(1, 500000); + + let working_role_parameters = RoleParameters { + min_actors: 1, + ..RoleParameters::default() + }; + let mut role_parameters = working_role_parameters.clone(); + role_parameters.min_actors = 2; + assert_failed_set_storage_parameters_call( + role_parameters, + Error::InvalidStorageRoleParameterMinActors, + ); + + role_parameters = working_role_parameters.clone(); + role_parameters.max_actors = 1; + assert_failed_set_storage_parameters_call( + role_parameters, + Error::InvalidStorageRoleParameterMaxActors, + ); + + role_parameters = working_role_parameters.clone(); + role_parameters.max_actors = 100; + assert_failed_set_storage_parameters_call( + role_parameters, + Error::InvalidStorageRoleParameterMaxActors, + ); + + role_parameters = working_role_parameters.clone(); + role_parameters.reward_period = 599; + assert_failed_set_storage_parameters_call( + role_parameters, + Error::InvalidStorageRoleParameterRewardPeriod, + ); + + role_parameters.reward_period = 28801; + assert_failed_set_storage_parameters_call( + role_parameters, + Error::InvalidStorageRoleParameterRewardPeriod, + ); + + role_parameters = working_role_parameters.clone(); + role_parameters.bonding_period = 599; + assert_failed_set_storage_parameters_call( + role_parameters, + Error::InvalidStorageRoleParameterBondingPeriod, + ); + + role_parameters = working_role_parameters.clone(); + role_parameters.bonding_period = 28801; + assert_failed_set_storage_parameters_call( + role_parameters, + Error::InvalidStorageRoleParameterBondingPeriod, + ); + + role_parameters = working_role_parameters.clone(); + role_parameters.unbonding_period = 599; + assert_failed_set_storage_parameters_call( + role_parameters, + Error::InvalidStorageRoleParameterUnbondingPeriod, + ); + + role_parameters = working_role_parameters.clone(); + role_parameters.unbonding_period = 28801; + assert_failed_set_storage_parameters_call( + role_parameters, + Error::InvalidStorageRoleParameterUnbondingPeriod, + ); + + role_parameters = working_role_parameters.clone(); + role_parameters.min_service_period = 599; + assert_failed_set_storage_parameters_call( + role_parameters, + Error::InvalidStorageRoleParameterMinServicePeriod, + ); + + role_parameters = working_role_parameters.clone(); + role_parameters.min_service_period = 28801; + assert_failed_set_storage_parameters_call( + role_parameters, + Error::InvalidStorageRoleParameterMinServicePeriod, + ); + + role_parameters = working_role_parameters.clone(); + role_parameters.startup_grace_period = 599; + assert_failed_set_storage_parameters_call( + role_parameters, + Error::InvalidStorageRoleParameterStartupGracePeriod, + ); + + role_parameters = working_role_parameters.clone(); + role_parameters.startup_grace_period = 28801; + assert_failed_set_storage_parameters_call( + role_parameters, + Error::InvalidStorageRoleParameterStartupGracePeriod, + ); + + role_parameters = working_role_parameters.clone(); + role_parameters.min_stake = 0; + assert_failed_set_storage_parameters_call( + role_parameters, + Error::InvalidStorageRoleParameterMinStake, + ); + + role_parameters = working_role_parameters.clone(); + role_parameters.min_stake = 10000001; + assert_failed_set_storage_parameters_call( + role_parameters, + Error::InvalidStorageRoleParameterMinStake, + ); + + role_parameters = working_role_parameters.clone(); + role_parameters.entry_request_fee = 0; + assert_failed_set_storage_parameters_call( + role_parameters, + Error::InvalidStorageRoleParameterEntryRequestFee, + ); + + role_parameters = working_role_parameters.clone(); + role_parameters.entry_request_fee = 100001; + assert_failed_set_storage_parameters_call( + role_parameters, + Error::InvalidStorageRoleParameterEntryRequestFee, + ); + + role_parameters = working_role_parameters.clone(); + role_parameters.reward = 0; + assert_failed_set_storage_parameters_call( + role_parameters, + Error::InvalidStorageRoleParameterReward, + ); + + role_parameters = working_role_parameters; + role_parameters.reward = 1001; + assert_failed_set_storage_parameters_call( + role_parameters, + Error::InvalidStorageRoleParameterReward, + ); + }); +} + +#[test] +fn set_default_proposal_parameters_succeeded() { + initial_test_ext().execute_with(|| { + let p = ProposalsConfigParameters::default(); + + // nothing is set + assert_eq!(>::get(), 0); + + ProposalCodex::set_default_config_values(); + + assert_eq!( + >::get(), + p.set_validator_count_proposal_voting_period as u64 + ); + assert_eq!( + >::get(), + p.set_validator_count_proposal_grace_period as u64 + ); + assert_eq!( + >::get(), + p.runtime_upgrade_proposal_voting_period as u64 + ); + assert_eq!( + >::get(), + p.runtime_upgrade_proposal_grace_period as u64 + ); + assert_eq!( + >::get(), + p.text_proposal_voting_period as u64 + ); + assert_eq!( + >::get(), + p.text_proposal_grace_period as u64 + ); + assert_eq!( + >::get(), + p.set_election_parameters_proposal_voting_period as u64 + ); + assert_eq!( + >::get(), + p.set_election_parameters_proposal_grace_period as u64 + ); + assert_eq!( + >::get(), + p.set_content_working_group_mint_capacity_proposal_voting_period as u64 + ); + assert_eq!( + >::get(), + p.set_content_working_group_mint_capacity_proposal_grace_period as u64 + ); + assert_eq!( + >::get(), + p.set_lead_proposal_voting_period as u64 + ); + assert_eq!( + >::get(), + p.set_lead_proposal_grace_period as u64 + ); + assert_eq!( + >::get(), + p.spending_proposal_voting_period as u64 + ); + assert_eq!( + >::get(), + p.spending_proposal_grace_period as u64 + ); + assert_eq!( + >::get(), + p.evict_storage_provider_proposal_voting_period as u64 + ); + assert_eq!( + >::get(), + p.evict_storage_provider_proposal_grace_period as u64 + ); + assert_eq!( + >::get(), + p.set_storage_role_parameters_proposal_voting_period as u64 + ); + assert_eq!( + >::get(), + p.set_storage_role_parameters_proposal_grace_period as u64 + ); + }); +} diff --git a/runtime-modules/proposals/discussion/Cargo.toml b/runtime-modules/proposals/discussion/Cargo.toml new file mode 100644 index 0000000000..c5bad3b951 --- /dev/null +++ b/runtime-modules/proposals/discussion/Cargo.toml @@ -0,0 +1,94 @@ +[package] +name = 'substrate-proposals-discussion-module' +version = '2.0.0' +authors = ['Joystream contributors'] +edition = '2018' + +[features] +default = ['std'] +no_std = [] +std = [ + 'codec/std', + 'rstd/std', + 'srml-support/std', + 'primitives/std', + 'sr-primitives/std', + 'system/std', + 'timestamp/std', + 'serde', + 'membership/std', + 'common/std', +] + +[dependencies.num_enum] +default_features = false +version = "0.4.2" + +[dependencies.serde] +features = ['derive'] +optional = true +version = '1.0.101' + +[dependencies.codec] +default-features = false +features = ['derive'] +package = 'parity-scale-codec' +version = '1.0.0' + +[dependencies.primitives] +default_features = false +git = 'https://github.com/paritytech/substrate.git' +package = 'substrate-primitives' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' + +[dependencies.rstd] +default_features = false +git = 'https://github.com/paritytech/substrate.git' +package = 'sr-std' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' + +[dependencies.sr-primitives] +default_features = false +git = 'https://github.com/paritytech/substrate.git' +package = 'sr-primitives' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' + +[dependencies.srml-support] +default_features = false +git = 'https://github.com/paritytech/substrate.git' +package = 'srml-support' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' + +[dependencies.system] +default_features = false +git = 'https://github.com/paritytech/substrate.git' +package = 'srml-system' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' + +[dependencies.timestamp] +default_features = false +git = 'https://github.com/paritytech/substrate.git' +package = 'srml-timestamp' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' + +[dependencies.membership] +default_features = false +package = 'substrate-membership-module' +path = '../../membership' + +[dependencies.common] +default_features = false +package = 'substrate-common-module' +path = '../../common' + +[dev-dependencies.runtime-io] +default_features = false +git = 'https://github.com/paritytech/substrate.git' +package = 'sr-io' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' + +[dev-dependencies.balances] +package = 'srml-balances' +default-features = false +git = 'https://github.com/paritytech/substrate.git' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' \ No newline at end of file diff --git a/runtime-modules/proposals/discussion/src/lib.rs b/runtime-modules/proposals/discussion/src/lib.rs new file mode 100644 index 0000000000..faada10586 --- /dev/null +++ b/runtime-modules/proposals/discussion/src/lib.rs @@ -0,0 +1,364 @@ +//! # Proposals discussion module +//! Proposals `discussion` module for the Joystream platform. Version 2. +//! It contains discussion subsystem of the proposals. +//! +//! ## Overview +//! +//! The proposals discussion module is used by the codex module to provide a platform for discussions +//! about different proposals. It allows to create discussion threads and then add and update related +//! posts. +//! +//! ## Supported extrinsics +//! - [add_post](./struct.Module.html#method.add_post) - adds a post to an existing discussion thread +//! - [update_post](./struct.Module.html#method.update_post) - updates existing post +//! +//! ## Public API methods +//! - [create_thread](./struct.Module.html#method.create_thread) - creates a discussion thread +//! - [ensure_can_create_thread](./struct.Module.html#method.ensure_can_create_thread) - ensures safe thread creation +//! +//! ## Usage +//! +//! ``` +//! use srml_support::{decl_module, dispatch::Result}; +//! use system::ensure_root; +//! use substrate_proposals_discussion_module::{self as discussions}; +//! +//! pub trait Trait: discussions::Trait + membership::members::Trait {} +//! +//! decl_module! { +//! pub struct Module for enum Call where origin: T::Origin { +//! pub fn create_discussion(origin, title: Vec, author_id : T::MemberId) -> Result { +//! ensure_root(origin)?; +//! >::ensure_can_create_thread(author_id, &title)?; +//! >::create_thread(author_id, title)?; +//! Ok(()) +//! } +//! } +//! } +//! # fn main() {} +//! ``` + +//! + +// Ensure we're `no_std` when compiling for Wasm. +#![cfg_attr(not(feature = "std"), no_std)] + +// Do not delete! Cannot be uncommented by default, because of Parity decl_module! issue. +//#![warn(missing_docs)] + +#[cfg(test)] +mod tests; +mod types; + +use rstd::clone::Clone; +use rstd::prelude::*; +use rstd::vec::Vec; +use srml_support::{decl_error, decl_event, decl_module, decl_storage, ensure, Parameter}; + +use srml_support::traits::Get; +use types::{Post, Thread, ThreadCounter}; + +use common::origin_validator::ActorOriginValidator; +use srml_support::dispatch::DispatchResult; + +type MemberId = ::MemberId; + +decl_event!( + /// Proposals engine events + pub enum Event + where + ::ThreadId, + MemberId = MemberId, + ::PostId, + { + /// Emits on thread creation. + ThreadCreated(ThreadId, MemberId), + + /// Emits on post creation. + PostCreated(PostId, MemberId), + + /// Emits on post update. + PostUpdated(PostId, MemberId), + } +); + +/// 'Proposal discussion' substrate module Trait +pub trait Trait: system::Trait + membership::members::Trait { + /// Engine event type. + type Event: From> + Into<::Event>; + + /// Validates post author id and origin combination + type PostAuthorOriginValidator: ActorOriginValidator< + Self::Origin, + MemberId, + Self::AccountId, + >; + + /// Discussion thread Id type + type ThreadId: From + Into + Parameter + Default + Copy; + + /// Post Id type + type PostId: From + Parameter + Default + Copy; + + /// Defines post edition number limit. + type MaxPostEditionNumber: Get; + + /// Defines thread title length limit. + type ThreadTitleLengthLimit: Get; + + /// Defines post length limit. + type PostLengthLimit: Get; + + /// Defines max thread by same author in a row number limit. + type MaxThreadInARowNumber: Get; +} + +decl_error! { + /// Discussion module predefined errors + pub enum Error { + /// Author should match the post creator + NotAuthor, + + /// Post edition limit reached + PostEditionNumberExceeded, + + /// Discussion cannot have an empty title + EmptyTitleProvided, + + /// Title is too long + TitleIsTooLong, + + /// Thread doesn't exist + ThreadDoesntExist, + + /// Post doesn't exist + PostDoesntExist, + + /// Post cannot be empty + EmptyPostProvided, + + /// Post is too long + PostIsTooLong, + + /// Max number of threads by same author in a row limit exceeded + MaxThreadInARowLimitExceeded, + + /// Require root origin in extrinsics + RequireRootOrigin, + } +} + +impl From for Error { + fn from(error: system::Error) -> Self { + match error { + system::Error::Other(msg) => Error::Other(msg), + system::Error::RequireRootOrigin => Error::RequireRootOrigin, + _ => Error::Other(error.into()), + } + } +} + +// Storage for the proposals discussion module +decl_storage! { + pub trait Store for Module as ProposalDiscussion { + /// Map thread identifier to corresponding thread. + pub ThreadById get(thread_by_id): map T::ThreadId => + Thread, T::BlockNumber>; + + /// Count of all threads that have been created. + pub ThreadCount get(fn thread_count): u64; + + /// Map thread id and post id to corresponding post. + pub PostThreadIdByPostId: double_map T::ThreadId, twox_128(T::PostId) => + Post, T::BlockNumber, T::ThreadId>; + + /// Count of all posts that have been created. + pub PostCount get(fn post_count): u64; + + /// Last author thread counter (part of the antispam mechanism) + pub LastThreadAuthorCounter get(fn last_thread_author_counter): + Option>>; + } +} + +decl_module! { + /// 'Proposal discussion' substrate module + pub struct Module for enum Call where origin: T::Origin { + /// Predefined errors + type Error = Error; + + /// Emits an event. Default substrate implementation. + fn deposit_event() = default; + + /// Exports post edition number limit const. + const MaxPostEditionNumber: u32 = T::MaxPostEditionNumber::get(); + + /// Exports thread title length limit const. + const ThreadTitleLengthLimit: u32 = T::ThreadTitleLengthLimit::get(); + + /// Exports post length limit const. + const PostLengthLimit: u32 = T::PostLengthLimit::get(); + + /// Exports max thread by same author in a row number limit const. + const MaxThreadInARowNumber: u32 = T::MaxThreadInARowNumber::get(); + + /// Adds a post with author origin check. + pub fn add_post( + origin, + post_author_id: MemberId, + thread_id : T::ThreadId, + text : Vec + ) { + T::PostAuthorOriginValidator::ensure_actor_origin( + origin, + post_author_id, + )?; + ensure!(>::exists(thread_id), Error::ThreadDoesntExist); + + ensure!(!text.is_empty(),Error::EmptyPostProvided); + ensure!( + text.len() as u32 <= T::PostLengthLimit::get(), + Error::PostIsTooLong + ); + + // mutation + + let next_post_count_value = Self::post_count() + 1; + let new_post_id = next_post_count_value; + + let new_post = Post { + text, + created_at: Self::current_block(), + updated_at: Self::current_block(), + author_id: post_author_id, + edition_number : 0, + thread_id, + }; + + let post_id = T::PostId::from(new_post_id); + >::insert(thread_id, post_id, new_post); + PostCount::put(next_post_count_value); + Self::deposit_event(RawEvent::PostCreated(post_id, post_author_id)); + } + + /// Updates a post with author origin check. Update attempts number is limited. + pub fn update_post( + origin, + post_author_id: MemberId, + thread_id: T::ThreadId, + post_id : T::PostId, + text : Vec + ){ + T::PostAuthorOriginValidator::ensure_actor_origin( + origin, + post_author_id, + )?; + + ensure!(>::exists(thread_id), Error::ThreadDoesntExist); + ensure!(>::exists(thread_id, post_id), Error::PostDoesntExist); + + ensure!(!text.is_empty(), Error::EmptyPostProvided); + ensure!( + text.len() as u32 <= T::PostLengthLimit::get(), + Error::PostIsTooLong + ); + + let post = >::get(&thread_id, &post_id); + + ensure!(post.author_id == post_author_id, Error::NotAuthor); + ensure!(post.edition_number < T::MaxPostEditionNumber::get(), + Error::PostEditionNumberExceeded); + + let new_post = Post { + text, + updated_at: Self::current_block(), + edition_number: post.edition_number + 1, + ..post + }; + + // mutation + + >::insert(thread_id, post_id, new_post); + Self::deposit_event(RawEvent::PostUpdated(post_id, post_author_id)); + } + } +} + +impl Module { + /// Create the discussion thread. Cannot add more threads than 'predefined limit = MaxThreadInARowNumber' + /// times in a row by the same author. + pub fn create_thread( + thread_author_id: MemberId, + title: Vec, + ) -> Result { + Self::ensure_can_create_thread(thread_author_id, &title)?; + + let next_thread_count_value = Self::thread_count() + 1; + let new_thread_id = next_thread_count_value; + + let new_thread = Thread { + title, + created_at: Self::current_block(), + author_id: thread_author_id, + }; + + // get new 'threads in a row' counter for the author + let current_thread_counter = Self::get_updated_thread_counter(thread_author_id); + + // mutation + + let thread_id = T::ThreadId::from(new_thread_id); + >::insert(thread_id, new_thread); + ThreadCount::put(next_thread_count_value); + >::put(current_thread_counter); + Self::deposit_event(RawEvent::ThreadCreated(thread_id, thread_author_id)); + + Ok(thread_id) + } + + /// Ensures thread can be created. + /// Checks: + /// - title is valid + /// - max thread in a row by the same author + pub fn ensure_can_create_thread( + thread_author_id: MemberId, + title: &[u8], + ) -> DispatchResult { + ensure!(!title.is_empty(), Error::EmptyTitleProvided); + ensure!( + title.len() as u32 <= T::ThreadTitleLengthLimit::get(), + Error::TitleIsTooLong + ); + + // get new 'threads in a row' counter for the author + let current_thread_counter = Self::get_updated_thread_counter(thread_author_id); + + ensure!( + current_thread_counter.counter as u32 <= T::MaxThreadInARowNumber::get(), + Error::MaxThreadInARowLimitExceeded + ); + + Ok(()) + } +} + +impl Module { + // Wrapper-function over system::block_number() + fn current_block() -> T::BlockNumber { + >::block_number() + } + + // returns incremented thread counter if last thread author equals with provided parameter + fn get_updated_thread_counter(author_id: MemberId) -> ThreadCounter> { + // if thread counter exists + if let Some(last_thread_author_counter) = Self::last_thread_author_counter() { + // if last(previous) author is the same as current author + if last_thread_author_counter.author_id == author_id { + return last_thread_author_counter.increment(); + } + } + + // else return new counter (set with 1 thread number) + ThreadCounter::new(author_id) + } +} diff --git a/runtime-modules/proposals/discussion/src/tests/mock.rs b/runtime-modules/proposals/discussion/src/tests/mock.rs new file mode 100644 index 0000000000..e94e62d4f1 --- /dev/null +++ b/runtime-modules/proposals/discussion/src/tests/mock.rs @@ -0,0 +1,145 @@ +#![cfg(test)] + +pub use system; + +pub use primitives::{Blake2Hasher, H256}; +pub use sr_primitives::{ + testing::{Digest, DigestItem, Header, UintAuthorityId}, + traits::{BlakeTwo256, Convert, IdentityLookup, OnFinalize}, + weights::Weight, + BuildStorage, Perbill, +}; + +use crate::ActorOriginValidator; +use srml_support::{impl_outer_event, impl_outer_origin, parameter_types}; + +impl_outer_origin! { + pub enum Origin for Test {} +} + +// Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Test; +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const MaximumBlockWeight: u32 = 1024; + pub const MaximumBlockLength: u32 = 2 * 1024; + pub const AvailableBlockRatio: Perbill = Perbill::one(); + pub const MinimumPeriod: u64 = 5; + pub const StakePoolId: [u8; 8] = *b"joystake"; +} + +parameter_types! { + pub const MaxPostEditionNumber: u32 = 5; + pub const MaxThreadInARowNumber: u32 = 3; + pub const ThreadTitleLengthLimit: u32 = 200; + pub const PostLengthLimit: u32 = 2000; +} + +mod discussion { + pub use crate::Event; +} + +mod membership_mod { + pub use membership::members::Event; +} + +impl_outer_event! { + pub enum TestEvent for Test { + discussion, + balances, + membership_mod, + } +} + +parameter_types! { + pub const ExistentialDeposit: u32 = 0; + pub const TransferFee: u32 = 0; + pub const CreationFee: u32 = 0; +} + +impl balances::Trait for Test { + type Balance = u64; + /// What to do if an account's free balance gets zeroed. + type OnFreeBalanceZero = (); + type OnNewAccount = (); + type TransferPayment = (); + type DustRemoval = (); + type Event = TestEvent; + type ExistentialDeposit = ExistentialDeposit; + type TransferFee = TransferFee; + type CreationFee = CreationFee; +} + +impl common::currency::GovernanceCurrency for Test { + type Currency = balances::Module; +} + +impl membership::members::Trait for Test { + type Event = TestEvent; + type MemberId = u64; + type PaidTermId = u64; + type SubscriptionId = u64; + type ActorId = u64; + type InitialMembersBalance = (); +} + +impl crate::Trait for Test { + type Event = TestEvent; + type PostAuthorOriginValidator = (); + type ThreadId = u64; + type PostId = u64; + type MaxPostEditionNumber = MaxPostEditionNumber; + type ThreadTitleLengthLimit = ThreadTitleLengthLimit; + type PostLengthLimit = PostLengthLimit; + type MaxThreadInARowNumber = MaxThreadInARowNumber; +} + +impl ActorOriginValidator for () { + fn ensure_actor_origin(origin: Origin, actor_id: u64) -> Result { + if system::ensure_none(origin).is_ok() { + return Ok(1); + } + + if actor_id == 1 { + return Ok(1); + } + + Err("Invalid author") + } +} + +impl system::Trait for Test { + type Origin = Origin; + type Call = (); + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type Event = TestEvent; + type BlockHashCount = BlockHashCount; + type MaximumBlockWeight = MaximumBlockWeight; + type MaximumBlockLength = MaximumBlockLength; + type AvailableBlockRatio = AvailableBlockRatio; + type Version = (); +} + +impl timestamp::Trait for Test { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; +} + +pub fn initial_test_ext() -> runtime_io::TestExternalities { + let t = system::GenesisConfig::default() + .build_storage::() + .unwrap(); + + t.into() +} + +pub type Discussions = crate::Module; +pub type System = system::Module; diff --git a/runtime-modules/proposals/discussion/src/tests/mod.rs b/runtime-modules/proposals/discussion/src/tests/mod.rs new file mode 100644 index 0000000000..a3d1b7edf8 --- /dev/null +++ b/runtime-modules/proposals/discussion/src/tests/mod.rs @@ -0,0 +1,417 @@ +mod mock; + +use mock::*; + +use crate::*; +use system::RawOrigin; +use system::{EventRecord, Phase}; + +struct EventFixture; +impl EventFixture { + fn assert_events(expected_raw_events: Vec>) { + let expected_events = expected_raw_events + .iter() + .map(|ev| EventRecord { + phase: Phase::ApplyExtrinsic(0), + event: TestEvent::discussion(ev.clone()), + topics: vec![], + }) + .collect::>>(); + + assert_eq!(System::events(), expected_events); + } +} + +struct TestPostEntry { + pub post_id: u64, + pub text: Vec, + pub edition_number: u32, +} + +struct TestThreadEntry { + pub thread_id: u64, + pub title: Vec, +} + +fn assert_thread_content(thread_entry: TestThreadEntry, post_entries: Vec) { + assert!(>::exists(thread_entry.thread_id)); + + let actual_thread = >::get(thread_entry.thread_id); + let expected_thread = Thread { + title: thread_entry.title, + created_at: 1, + author_id: 1, + }; + assert_eq!(actual_thread, expected_thread); + + for post_entry in post_entries { + let actual_post = + >::get(thread_entry.thread_id, post_entry.post_id); + let expected_post = Post { + text: post_entry.text, + created_at: 1, + updated_at: 1, + author_id: 1, + thread_id: thread_entry.thread_id, + edition_number: post_entry.edition_number, + }; + + assert_eq!(actual_post, expected_post); + } +} + +struct DiscussionFixture { + pub title: Vec, + pub origin: RawOrigin, + pub author_id: u64, +} + +impl Default for DiscussionFixture { + fn default() -> Self { + DiscussionFixture { + title: b"title".to_vec(), + origin: RawOrigin::Signed(1), + author_id: 1, + } + } +} + +impl DiscussionFixture { + fn with_title(self, title: Vec) -> Self { + DiscussionFixture { title, ..self } + } + + fn create_discussion_and_assert(&self, result: Result) -> Option { + let create_discussion_result = + Discussions::create_thread(self.author_id, self.title.clone()); + + assert_eq!(create_discussion_result, result); + + create_discussion_result.ok() + } +} + +struct PostFixture { + pub text: Vec, + pub origin: RawOrigin, + pub thread_id: u64, + pub post_id: Option, + pub author_id: u64, +} + +impl PostFixture { + fn default_for_thread(thread_id: u64) -> Self { + PostFixture { + text: b"text".to_vec(), + author_id: 1, + thread_id, + origin: RawOrigin::Signed(1), + post_id: None, + } + } + + fn with_text(self, text: Vec) -> Self { + PostFixture { text, ..self } + } + + fn with_origin(self, origin: RawOrigin) -> Self { + PostFixture { origin, ..self } + } + + fn with_author(self, author_id: u64) -> Self { + PostFixture { author_id, ..self } + } + + fn change_thread_id(self, thread_id: u64) -> Self { + PostFixture { thread_id, ..self } + } + + fn change_post_id(self, post_id: u64) -> Self { + PostFixture { + post_id: Some(post_id), + ..self + } + } + + fn add_post_and_assert(&mut self, result: Result<(), Error>) -> Option { + let add_post_result = Discussions::add_post( + self.origin.clone().into(), + self.author_id, + self.thread_id, + self.text.clone(), + ); + + assert_eq!(add_post_result, result); + + if result.is_ok() { + self.post_id = Some(::get()); + } + + self.post_id + } + + fn update_post_with_text_and_assert(&mut self, new_text: Vec, result: Result<(), Error>) { + let add_post_result = Discussions::update_post( + self.origin.clone().into(), + self.author_id, + self.thread_id, + self.post_id.unwrap(), + new_text, + ); + + assert_eq!(add_post_result, result); + } + + fn update_post_and_assert(&mut self, result: Result<(), Error>) { + self.update_post_with_text_and_assert(self.text.clone(), result); + } +} + +#[test] +fn create_discussion_call_succeeds() { + initial_test_ext().execute_with(|| { + let discussion_fixture = DiscussionFixture::default(); + + discussion_fixture.create_discussion_and_assert(Ok(1)); + }); +} + +#[test] +fn create_post_call_succeeds() { + initial_test_ext().execute_with(|| { + let discussion_fixture = DiscussionFixture::default(); + + let thread_id = discussion_fixture + .create_discussion_and_assert(Ok(1)) + .unwrap(); + + let mut post_fixture = PostFixture::default_for_thread(thread_id); + + post_fixture.add_post_and_assert(Ok(())); + }); +} + +#[test] +fn update_post_call_succeeds() { + initial_test_ext().execute_with(|| { + let discussion_fixture = DiscussionFixture::default(); + + let thread_id = discussion_fixture + .create_discussion_and_assert(Ok(1)) + .unwrap(); + + let mut post_fixture = PostFixture::default_for_thread(thread_id); + + post_fixture.add_post_and_assert(Ok(())); + post_fixture.update_post_and_assert(Ok(())); + + EventFixture::assert_events(vec![ + RawEvent::ThreadCreated(1, 1), + RawEvent::PostCreated(1, 1), + RawEvent::PostUpdated(1, 1), + ]); + }); +} + +#[test] +fn update_post_call_fails_because_of_post_edition_limit() { + initial_test_ext().execute_with(|| { + let discussion_fixture = DiscussionFixture::default(); + + let thread_id = discussion_fixture + .create_discussion_and_assert(Ok(1)) + .unwrap(); + + let mut post_fixture = PostFixture::default_for_thread(thread_id); + + post_fixture.add_post_and_assert(Ok(())); + + for _ in 1..6 { + post_fixture.update_post_and_assert(Ok(())); + } + + post_fixture.update_post_and_assert(Err(Error::PostEditionNumberExceeded)); + }); +} + +#[test] +fn update_post_call_fails_because_of_the_wrong_author() { + initial_test_ext().execute_with(|| { + let discussion_fixture = DiscussionFixture::default(); + + let thread_id = discussion_fixture + .create_discussion_and_assert(Ok(1)) + .unwrap(); + + let mut post_fixture = PostFixture::default_for_thread(thread_id); + + post_fixture.add_post_and_assert(Ok(())); + + post_fixture = post_fixture.with_author(2); + + post_fixture.update_post_and_assert(Err(Error::Other("Invalid author"))); + + post_fixture = post_fixture.with_origin(RawOrigin::None).with_author(2); + + post_fixture.update_post_and_assert(Err(Error::NotAuthor)); + }); +} + +#[test] +fn thread_content_check_succeeded() { + initial_test_ext().execute_with(|| { + let discussion_fixture = DiscussionFixture::default(); + + let thread_id = discussion_fixture + .create_discussion_and_assert(Ok(1)) + .unwrap(); + + let mut post_fixture1 = PostFixture::default_for_thread(thread_id); + let post_id1 = post_fixture1.add_post_and_assert(Ok(())).unwrap(); + + let mut post_fixture2 = PostFixture::default_for_thread(thread_id); + let post_id2 = post_fixture2.add_post_and_assert(Ok(())).unwrap(); + post_fixture1.update_post_with_text_and_assert(b"new_text".to_vec(), Ok(())); + + assert_thread_content( + TestThreadEntry { + thread_id, + title: b"title".to_vec(), + }, + vec![ + TestPostEntry { + post_id: post_id1, + text: b"new_text".to_vec(), + edition_number: 1, + }, + TestPostEntry { + post_id: post_id2, + text: b"text".to_vec(), + edition_number: 0, + }, + ], + ); + }); +} + +#[test] +fn create_discussion_call_with_bad_title_failed() { + initial_test_ext().execute_with(|| { + let mut discussion_fixture = DiscussionFixture::default().with_title(Vec::new()); + discussion_fixture.create_discussion_and_assert(Err(Error::EmptyTitleProvided)); + + discussion_fixture = DiscussionFixture::default().with_title([0; 201].to_vec()); + discussion_fixture.create_discussion_and_assert(Err(Error::TitleIsTooLong)); + }); +} + +#[test] +fn add_post_call_with_invalid_thread_failed() { + initial_test_ext().execute_with(|| { + let discussion_fixture = DiscussionFixture::default(); + discussion_fixture + .create_discussion_and_assert(Ok(1)) + .unwrap(); + + let mut post_fixture = PostFixture::default_for_thread(2); + post_fixture.add_post_and_assert(Err(Error::ThreadDoesntExist)); + }); +} + +#[test] +fn update_post_call_with_invalid_post_failed() { + initial_test_ext().execute_with(|| { + let discussion_fixture = DiscussionFixture::default(); + let thread_id = discussion_fixture + .create_discussion_and_assert(Ok(1)) + .unwrap(); + + let mut post_fixture1 = PostFixture::default_for_thread(thread_id); + post_fixture1.add_post_and_assert(Ok(())).unwrap(); + + let mut post_fixture2 = post_fixture1.change_post_id(2); + post_fixture2.update_post_and_assert(Err(Error::PostDoesntExist)); + }); +} + +#[test] +fn update_post_call_with_invalid_thread_failed() { + initial_test_ext().execute_with(|| { + let discussion_fixture = DiscussionFixture::default(); + let thread_id = discussion_fixture + .create_discussion_and_assert(Ok(1)) + .unwrap(); + + let mut post_fixture1 = PostFixture::default_for_thread(thread_id); + post_fixture1.add_post_and_assert(Ok(())).unwrap(); + + let mut post_fixture2 = post_fixture1.change_thread_id(2); + post_fixture2.update_post_and_assert(Err(Error::ThreadDoesntExist)); + }); +} + +#[test] +fn add_post_call_with_invalid_text_failed() { + initial_test_ext().execute_with(|| { + let discussion_fixture = DiscussionFixture::default(); + let thread_id = discussion_fixture + .create_discussion_and_assert(Ok(1)) + .unwrap(); + + let mut post_fixture1 = PostFixture::default_for_thread(thread_id).with_text(Vec::new()); + post_fixture1.add_post_and_assert(Err(Error::EmptyPostProvided)); + + let mut post_fixture2 = + PostFixture::default_for_thread(thread_id).with_text([0; 2001].to_vec()); + post_fixture2.add_post_and_assert(Err(Error::PostIsTooLong)); + }); +} + +#[test] +fn update_post_call_with_invalid_text_failed() { + initial_test_ext().execute_with(|| { + let discussion_fixture = DiscussionFixture::default(); + let thread_id = discussion_fixture + .create_discussion_and_assert(Ok(1)) + .unwrap(); + + let mut post_fixture1 = PostFixture::default_for_thread(thread_id); + post_fixture1.add_post_and_assert(Ok(())); + + let mut post_fixture2 = post_fixture1.with_text(Vec::new()); + post_fixture2.update_post_and_assert(Err(Error::EmptyPostProvided)); + + let mut post_fixture3 = post_fixture2.with_text([0; 2001].to_vec()); + post_fixture3.update_post_and_assert(Err(Error::PostIsTooLong)); + }); +} + +#[test] +fn add_discussion_thread_fails_because_of_max_thread_by_same_author_in_a_row_limit_exceeded() { + initial_test_ext().execute_with(|| { + let discussion_fixture = DiscussionFixture::default(); + for idx in 1..=3 { + discussion_fixture + .create_discussion_and_assert(Ok(idx)) + .unwrap(); + } + + discussion_fixture.create_discussion_and_assert(Err(Error::MaxThreadInARowLimitExceeded)); + }); +} + +#[test] +fn discussion_thread_and_post_counters_are_valid() { + initial_test_ext().execute_with(|| { + let discussion_fixture = DiscussionFixture::default(); + let thread_id = discussion_fixture + .create_discussion_and_assert(Ok(1)) + .unwrap(); + + let mut post_fixture1 = PostFixture::default_for_thread(thread_id); + let _ = post_fixture1.add_post_and_assert(Ok(())).unwrap(); + + assert_eq!(Discussions::thread_count(), 1); + assert_eq!(Discussions::post_count(), 1); + }); +} diff --git a/runtime-modules/proposals/discussion/src/types.rs b/runtime-modules/proposals/discussion/src/types.rs new file mode 100644 index 0000000000..5b8add6e5c --- /dev/null +++ b/runtime-modules/proposals/discussion/src/types.rs @@ -0,0 +1,102 @@ +#![warn(missing_docs)] + +use codec::{Decode, Encode}; +#[cfg(feature = "std")] +use serde::{Deserialize, Serialize}; + +use rstd::prelude::*; + +/// Represents a discussion thread +#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))] +#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)] +pub struct Thread { + /// Title + pub title: Vec, + + /// When thread was established. + pub created_at: BlockNumber, + + /// Author of the thread. + pub author_id: ThreadAuthorId, +} + +/// Post for the discussion thread +#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))] +#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)] +pub struct Post { + /// Text + pub text: Vec, + + /// When post was added. + pub created_at: BlockNumber, + + /// When post was updated last time. + pub updated_at: BlockNumber, + + /// Author of the post. + pub author_id: PostAuthorId, + + /// Parent thread id for this post + pub thread_id: ThreadId, + + /// Defines how many times this post was edited. Zero on creation. + pub edition_number: u32, +} + +/// Post for the discussion thread +#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))] +#[derive(Encode, Decode, Default, Clone, Copy, PartialEq, Eq)] +pub struct ThreadCounter { + /// Author of the threads. + pub author_id: ThreadAuthorId, + + /// ThreadCount + pub counter: u32, +} + +impl ThreadCounter { + /// Increments existing counter + pub fn increment(&self) -> Self { + ThreadCounter { + counter: self.counter + 1, + author_id: self.author_id.clone(), + } + } + + /// Creates new counter by author_id. Counter instantiated with 1. + pub fn new(author_id: ThreadAuthorId) -> Self { + ThreadCounter { + author_id, + counter: 1, + } + } +} + +#[cfg(test)] +mod tests { + use crate::types::ThreadCounter; + + #[test] + fn thread_counter_increment_works() { + let test = ThreadCounter { + author_id: 56, + counter: 56, + }; + let expected = ThreadCounter { + author_id: 56, + counter: 57, + }; + + assert_eq!(expected, test.increment()); + } + + #[test] + fn thread_counter_new_works() { + let expected = ThreadCounter { + author_id: 56, + counter: 1, + }; + + assert_eq!(expected, ThreadCounter::new(56)); + } +} diff --git a/runtime-modules/proposals/engine/Cargo.toml b/runtime-modules/proposals/engine/Cargo.toml new file mode 100644 index 0000000000..19f3027feb --- /dev/null +++ b/runtime-modules/proposals/engine/Cargo.toml @@ -0,0 +1,106 @@ +[package] +name = 'substrate-proposals-engine-module' +version = '2.0.0' +authors = ['Joystream contributors'] +edition = '2018' + +[features] +default = ['std'] +no_std = [] +std = [ + 'codec/std', + 'rstd/std', + 'srml-support/std', + 'primitives/std', + 'system/std', + 'timestamp/std', + 'serde', + 'stake/std', + 'balances/std', + 'sr-primitives/std', + 'membership/std', + 'common/std', + +] + + +[dependencies.num_enum] +default_features = false +version = "0.4.2" + +[dependencies.serde] +features = ['derive'] +optional = true +version = '1.0.101' + +[dependencies.codec] +default-features = false +features = ['derive'] +package = 'parity-scale-codec' +version = '1.0.0' + +[dependencies.primitives] +default_features = false +git = 'https://github.com/paritytech/substrate.git' +package = 'substrate-primitives' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' + +[dependencies.rstd] +default_features = false +git = 'https://github.com/paritytech/substrate.git' +package = 'sr-std' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' + +[dependencies.srml-support] +default_features = false +git = 'https://github.com/paritytech/substrate.git' +package = 'srml-support' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' + +[dependencies.system] +default_features = false +git = 'https://github.com/paritytech/substrate.git' +package = 'srml-system' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' + +[dependencies.timestamp] +default_features = false +git = 'https://github.com/paritytech/substrate.git' +package = 'srml-timestamp' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' + +[dependencies.balances] +package = 'srml-balances' +default-features = false +git = 'https://github.com/paritytech/substrate.git' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' + +[dependencies.sr-primitives] +default_features = false +git = 'https://github.com/paritytech/substrate.git' +package = 'sr-primitives' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' + +[dependencies.stake] +default_features = false +package = 'substrate-stake-module' +path = '../../stake' + +[dependencies.membership] +default_features = false +package = 'substrate-membership-module' +path = '../../membership' + +[dependencies.common] +default_features = false +package = 'substrate-common-module' +path = '../../common' + +[dev-dependencies] +mockall = "0.6.0" + +[dev-dependencies.runtime-io] +default_features = false +git = 'https://github.com/paritytech/substrate.git' +package = 'sr-io' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' diff --git a/runtime-modules/proposals/engine/src/lib.rs b/runtime-modules/proposals/engine/src/lib.rs new file mode 100644 index 0000000000..936e501ac2 --- /dev/null +++ b/runtime-modules/proposals/engine/src/lib.rs @@ -0,0 +1,827 @@ +//! # Proposals engine module +//! Proposals `engine` module for the Joystream platform. Version 2. +//! The main component of the proposals system. Provides methods and extrinsics to create and +//! vote for proposals, inspired by Parity **Democracy module**. +//! +//! ## Overview +//! Proposals `engine` module provides an abstract mechanism to work with proposals: creation, voting, +//! execution, canceling, etc. Proposal execution demands serialized _Dispatchable_ proposal code. +//! It could be any _Dispatchable_ + _Parameter_ type, but most likely, it would be serialized (via +//! Parity _codec_ crate) extrisic call. A proposal stage can be described by its [status](./enum.ProposalStatus.html). +//! +//! ## Proposal lifecycle +//! When a proposal passes [checks](./struct.Module.html#method.ensure_create_proposal_parameters_are_valid) +//! for its [parameters](./struct.ProposalParameters.html) - it can be [created](./struct.Module.html#method.create_proposal). +//! The newly created proposal has _Active_ status. The proposal can be voted on or canceled during its +//! _voting period_. Votes can be [different](./enum.VoteKind.html). When the proposal gets enough votes +//! to be slashed or approved or _voting period_ ends - the proposal becomes _Finalized_. If the proposal +//! got approved and _grace period_ passed - the `engine` module tries to execute the proposal. +//! The final [approved status](./enum.ApprovedProposalStatus.html) of the proposal defines +//! an overall proposal outcome. +//! +//! ### Notes +//! +//! - The proposal can be [vetoed](./struct.Module.html#method.veto_proposal) +//! anytime before the proposal execution by the _sudo_. +//! - When the proposal is created with some stake - refunding on proposal finalization with +//! different statuses should be accomplished from the external handler from the _stake module_ +//! (_StakingEventsHandler_). Such a handler should call +//! [refund_proposal_stake](./struct.Module.html#method.refund_proposal_stake) callback function. +//! - If the _council_ got reelected during the proposal _voting period_ the external handler calls +//! [reset_active_proposals](./trait.Module.html#method.reset_active_proposals) function and +//! all voting results get cleared. +//! +//! ### Important abstract types to be implemented +//! Proposals `engine` module has several abstractions to be implemented in order to work correctly. +//! - _VoterOriginValidator_ - ensure valid voter identity. Voters should have permissions to vote: +//! they should be council members. +//! - [VotersParameters](./trait.VotersParameters.html) - defines total voter number, which is +//! the council size +//! - _ProposerOriginValidator_ - ensure valid proposer identity. Proposers should have permissions +//! to create a proposal: they should be members of the Joystream. +//! - [StakeHandlerProvider](./trait.StakeHandlerProvider.html) - defines an interface for the staking. +//! +//! A full list of the abstractions can be found [here](./trait.Trait.html). +//! +//! ### Supported extrinsics +//! - [vote](./struct.Module.html#method.vote) - registers a vote for the proposal +//! - [cancel_proposal](./struct.Module.html#method.cancel_proposal) - cancels the proposal (can be canceled only by owner) +//! - [veto_proposal](./struct.Module.html#method.veto_proposal) - vetoes the proposal +//! +//! ### Public API +//! - [create_proposal](./struct.Module.html#method.create_proposal) - creates proposal using provided parameters +//! - [ensure_create_proposal_parameters_are_valid](./struct.Module.html#method.ensure_create_proposal_parameters_are_valid) - ensures that we can create the proposal +//! - [refund_proposal_stake](./struct.Module.html#method.refund_proposal_stake) - a callback for _StakingHandlerEvents_ +//! - [reset_active_proposals](./trait.Module.html#method.reset_active_proposals) - resets voting results for active proposals +//! +//! ## Usage +//! +//! ``` +//! use srml_support::{decl_module, dispatch::Result, print}; +//! use system::ensure_signed; +//! use codec::Encode; +//! use substrate_proposals_engine_module::{self as engine, ProposalParameters}; +//! +//! pub trait Trait: engine::Trait + membership::members::Trait {} +//! +//! decl_module! { +//! pub struct Module for enum Call where origin: T::Origin { +//! fn executable_proposal(origin) { +//! print("executed!"); +//! } +//! +//! pub fn create_spending_proposal( +//! origin, +//! proposer_id: T::MemberId, +//! ) -> Result { +//! let account_id = ensure_signed(origin)?; +//! let parameters = ProposalParameters::default(); +//! let title = b"Spending proposal".to_vec(); +//! let description = b"We need to spend some tokens to support the working group lead." +//! .to_vec(); +//! let encoded_proposal_code = >::executable_proposal().encode(); +//! +//! >::ensure_create_proposal_parameters_are_valid( +//! ¶meters, +//! &title, +//! &description, +//! None +//! )?; +//! >::create_proposal( +//! account_id, +//! proposer_id, +//! parameters, +//! title, +//! description, +//! None, +//! encoded_proposal_code +//! )?; +//! Ok(()) +//! } +//! } +//! } +//! # fn main() {} +//! ``` + +// Ensure we're `no_std` when compiling for Wasm. +#![cfg_attr(not(feature = "std"), no_std)] + +// Do not delete! Cannot be uncommented by default, because of Parity decl_module! issue. +//#![warn(missing_docs)] + +use types::FinalizedProposalData; +use types::ProposalStakeManager; +pub use types::{ + ActiveStake, ApprovedProposalStatus, FinalizationData, Proposal, ProposalDecisionStatus, + ProposalParameters, ProposalStatus, VotingResults, +}; +pub use types::{BalanceOf, CurrencyOf, NegativeImbalance}; +pub use types::{DefaultStakeHandlerProvider, StakeHandler, StakeHandlerProvider}; +pub use types::{ProposalCodeDecoder, ProposalExecutable}; +pub use types::{VoteKind, VotersParameters}; + +pub(crate) mod types; + +#[cfg(test)] +mod tests; + +use codec::Decode; +use rstd::prelude::*; +use sr_primitives::traits::{DispatchResult, Zero}; +use srml_support::traits::{Currency, Get}; +use srml_support::{ + decl_error, decl_event, decl_module, decl_storage, ensure, print, Parameter, StorageDoubleMap, +}; +use system::{ensure_root, RawOrigin}; + +use crate::types::ApprovedProposalData; +use common::origin_validator::ActorOriginValidator; +use srml_support::dispatch::Dispatchable; + +type MemberId = ::MemberId; + +/// Proposals engine trait. +pub trait Trait: + system::Trait + timestamp::Trait + stake::Trait + membership::members::Trait +{ + /// Engine event type. + type Event: From> + Into<::Event>; + + /// Validates proposer id and origin combination + type ProposerOriginValidator: ActorOriginValidator< + Self::Origin, + MemberId, + Self::AccountId, + >; + + /// Validates voter id and origin combination + type VoterOriginValidator: ActorOriginValidator, Self::AccountId>; + + /// Provides data for voting. Defines maximum voters count for the proposal. + type TotalVotersCounter: VotersParameters; + + /// Proposal Id type + type ProposalId: From + Parameter + Default + Copy; + + /// Provides stake logic implementation. Can be used to mock stake logic. + type StakeHandlerProvider: StakeHandlerProvider; + + /// The fee is applied when cancel the proposal. A fee would be slashed (burned). + type CancellationFee: Get>; + + /// The fee is applied when the proposal gets rejected. A fee would be slashed (burned). + type RejectionFee: Get>; + + /// Defines max allowed proposal title length. + type TitleMaxLength: Get; + + /// Defines max allowed proposal description length. + type DescriptionMaxLength: Get; + + /// Defines max simultaneous active proposals number. + type MaxActiveProposalLimit: Get; + + /// Proposals executable code. Can be instantiated by external module Call enum members. + type DispatchableCallCode: Parameter + Dispatchable + Default; +} + +decl_event!( + /// Proposals engine events + pub enum Event + where + ::ProposalId, + MemberId = MemberId, + ::BlockNumber, + ::AccountId, + ::StakeId, + { + /// Emits on proposal creation. + /// Params: + /// - Member id of a proposer. + /// - Id of a newly created proposal after it was saved in storage. + ProposalCreated(MemberId, ProposalId), + + /// Emits on proposal status change. + /// Params: + /// - Id of a updated proposal. + /// - New proposal status + ProposalStatusUpdated(ProposalId, ProposalStatus), + + /// Emits on voting for the proposal + /// Params: + /// - Voter - member id of a voter. + /// - Id of a proposal. + /// - Kind of vote. + Voted(MemberId, ProposalId, VoteKind), + } +); + +decl_error! { + /// Engine module predefined errors + pub enum Error { + /// Proposal cannot have an empty title" + EmptyTitleProvided, + + /// Proposal cannot have an empty body + EmptyDescriptionProvided, + + /// Title is too long + TitleIsTooLong, + + /// Description is too long + DescriptionIsTooLong, + + /// The proposal does not exist + ProposalNotFound, + + /// Proposal is finalized already + ProposalFinalized, + + /// The proposal have been already voted on + AlreadyVoted, + + /// Not an author + NotAuthor, + + /// Max active proposals number exceeded + MaxActiveProposalNumberExceeded, + + /// Stake cannot be empty with this proposal + EmptyStake, + + /// Stake should be empty for this proposal + StakeShouldBeEmpty, + + /// Stake differs from the proposal requirements + StakeDiffersFromRequired, + + /// Approval threshold cannot be zero + InvalidParameterApprovalThreshold, + + /// Slashing threshold cannot be zero + InvalidParameterSlashingThreshold, + + /// Require root origin in extrinsics + RequireRootOrigin, + } +} + +impl From for Error { + fn from(error: system::Error) -> Self { + match error { + system::Error::Other(msg) => Error::Other(msg), + system::Error::RequireRootOrigin => Error::RequireRootOrigin, + _ => Error::Other(error.into()), + } + } +} + +// Storage for the proposals engine module +decl_storage! { + pub trait Store for Module as ProposalEngine{ + /// Map proposal by its id. + pub Proposals get(fn proposals): map T::ProposalId => ProposalOf; + + /// Count of all proposals that have been created. + pub ProposalCount get(fn proposal_count): u32; + + /// Map proposal executable code by proposal id. + pub DispatchableCallCode get(fn proposal_codes): map T::ProposalId => Vec; + + /// Count of active proposals. + pub ActiveProposalCount get(fn active_proposal_count): u32; + + /// Ids of proposals that are open for voting (have not been finalized yet). + pub ActiveProposalIds get(fn active_proposal_ids): linked_map T::ProposalId=> (); + + /// Ids of proposals that were approved and theirs grace period was not expired. + pub PendingExecutionProposalIds get(fn pending_proposal_ids): linked_map T::ProposalId=> (); + + /// Double map for preventing duplicate votes. Should be cleaned after usage. + pub VoteExistsByProposalByVoter get(fn vote_by_proposal_by_voter): + double_map T::ProposalId, twox_256(MemberId) => VoteKind; + + /// Map proposal id by stake id. Required by StakingEventsHandler callback call + pub StakesProposals get(fn stakes_proposals): map T::StakeId => T::ProposalId; + } +} + +decl_module! { + /// 'Proposal engine' substrate module + pub struct Module for enum Call where origin: T::Origin { + /// Predefined errors + type Error = Error; + + /// Emits an event. Default substrate implementation. + fn deposit_event() = default; + + /// Exports const - the fee is applied when cancel the proposal. A fee would be slashed (burned). + const CancellationFee: BalanceOf = T::CancellationFee::get(); + + /// Exports const - the fee is applied when the proposal gets rejected. A fee would be slashed (burned). + const RejectionFee: BalanceOf = T::RejectionFee::get(); + + /// Exports const - max allowed proposal title length. + const TitleMaxLength: u32 = T::TitleMaxLength::get(); + + /// Exports const - max allowed proposal description length. + const DescriptionMaxLength: u32 = T::DescriptionMaxLength::get(); + + /// Exports const - max simultaneous active proposals number. + const MaxActiveProposalLimit: u32 = T::MaxActiveProposalLimit::get(); + + /// Vote extrinsic. Conditions: origin must allow votes. + pub fn vote(origin, voter_id: MemberId, proposal_id: T::ProposalId, vote: VoteKind) { + T::VoterOriginValidator::ensure_actor_origin( + origin, + voter_id, + )?; + + ensure!(>::exists(proposal_id), Error::ProposalNotFound); + let mut proposal = Self::proposals(proposal_id); + + ensure!(matches!(proposal.status, ProposalStatus::Active{..}), Error::ProposalFinalized); + + let did_not_vote_before = !>::exists( + proposal_id, + voter_id, + ); + + ensure!(did_not_vote_before, Error::AlreadyVoted); + + proposal.voting_results.add_vote(vote.clone()); + + // mutation + + >::insert(proposal_id, proposal); + >::insert( proposal_id, voter_id, vote.clone()); + Self::deposit_event(RawEvent::Voted(voter_id, proposal_id, vote)); + } + + /// Cancel a proposal by its original proposer. + pub fn cancel_proposal(origin, proposer_id: MemberId, proposal_id: T::ProposalId) { + T::ProposerOriginValidator::ensure_actor_origin( + origin, + proposer_id, + )?; + + ensure!(>::exists(proposal_id), Error::ProposalNotFound); + let proposal = Self::proposals(proposal_id); + + ensure!(proposer_id == proposal.proposer_id, Error::NotAuthor); + ensure!(matches!(proposal.status, ProposalStatus::Active{..}), Error::ProposalFinalized); + + // mutation + + Self::finalize_proposal(proposal_id, ProposalDecisionStatus::Canceled); + } + + /// Veto a proposal. Must be root. + pub fn veto_proposal(origin, proposal_id: T::ProposalId) { + ensure_root(origin)?; + + ensure!(>::exists(proposal_id), Error::ProposalNotFound); + let proposal = Self::proposals(proposal_id); + + // mutation + + if >::exists(proposal_id) { + Self::veto_pending_execution_proposal(proposal_id, proposal); + } else { + ensure!(matches!(proposal.status, ProposalStatus::Active{..}), Error::ProposalFinalized); + Self::finalize_proposal(proposal_id, ProposalDecisionStatus::Vetoed); + } + } + + /// Block finalization. Perform voting period check, vote result tally, approved proposals + /// grace period checks, and proposal execution. + fn on_finalize(_n: T::BlockNumber) { + let finalized_proposals = Self::get_finalized_proposals(); + + // mutation + + // Check vote results. Approved proposals with zero grace period will be + // transitioned to the PendingExecution status. + for proposal_data in finalized_proposals { + >::insert(proposal_data.proposal_id, proposal_data.proposal); + Self::finalize_proposal(proposal_data.proposal_id, proposal_data.status); + } + + let executable_proposals = + Self::get_approved_proposal_with_expired_grace_period(); + + // Execute approved proposals with expired grace period + for approved_proosal in executable_proposals { + Self::execute_proposal(approved_proosal); + } + } + } +} + +impl Module { + /// Create proposal. Requires 'proposal origin' membership. + pub fn create_proposal( + account_id: T::AccountId, + proposer_id: MemberId, + parameters: ProposalParameters>, + title: Vec, + description: Vec, + stake_balance: Option>, + encoded_dispatchable_call_code: Vec, + ) -> Result { + Self::ensure_create_proposal_parameters_are_valid( + ¶meters, + &title, + &description, + stake_balance, + )?; + + // checks passed + // mutation + + let next_proposal_count_value = Self::proposal_count() + 1; + let new_proposal_id = next_proposal_count_value; + let proposal_id = T::ProposalId::from(new_proposal_id); + + // Check stake_balance for value and create stake if value exists, else take None + // If create_stake() returns error - return error from extrinsic + let stake_id_result = stake_balance + .map(|stake_amount| { + ProposalStakeManager::::create_stake(stake_amount, account_id.clone()) + }) + .transpose()?; + + let mut stake_data = None; + if let Some(stake_id) = stake_id_result { + stake_data = Some(ActiveStake { + stake_id, + source_account_id: account_id, + }); + + >::insert(stake_id, proposal_id); + } + + let new_proposal = Proposal { + created_at: Self::current_block(), + parameters, + title, + description, + proposer_id, + status: ProposalStatus::Active(stake_data), + voting_results: VotingResults::default(), + }; + + >::insert(proposal_id, new_proposal); + >::insert(proposal_id, encoded_dispatchable_call_code); + >::insert(proposal_id, ()); + ProposalCount::put(next_proposal_count_value); + Self::increase_active_proposal_counter(); + + Self::deposit_event(RawEvent::ProposalCreated(proposer_id, proposal_id)); + + Ok(proposal_id) + } + + /// Performs all checks for the proposal creation: + /// - title, body lengths + /// - max active proposal + /// - provided parameters: approval_threshold_percentage and slashing_threshold_percentage > 0 + /// - provided stake balance and parameters.required_stake are valid + pub fn ensure_create_proposal_parameters_are_valid( + parameters: &ProposalParameters>, + title: &[u8], + description: &[u8], + stake_balance: Option>, + ) -> DispatchResult { + ensure!(!title.is_empty(), Error::EmptyTitleProvided); + ensure!( + title.len() as u32 <= T::TitleMaxLength::get(), + Error::TitleIsTooLong + ); + + ensure!(!description.is_empty(), Error::EmptyDescriptionProvided); + ensure!( + description.len() as u32 <= T::DescriptionMaxLength::get(), + Error::DescriptionIsTooLong + ); + + ensure!( + (Self::active_proposal_count()) < T::MaxActiveProposalLimit::get(), + Error::MaxActiveProposalNumberExceeded + ); + + ensure!( + parameters.approval_threshold_percentage > 0, + Error::InvalidParameterApprovalThreshold + ); + + ensure!( + parameters.slashing_threshold_percentage > 0, + Error::InvalidParameterSlashingThreshold + ); + + // check stake parameters + if let Some(required_stake) = parameters.required_stake { + if let Some(staked_balance) = stake_balance { + ensure!( + required_stake == staked_balance, + Error::StakeDiffersFromRequired + ); + } else { + return Err(Error::EmptyStake); + } + } + + if stake_balance.is_some() && parameters.required_stake.is_none() { + return Err(Error::StakeShouldBeEmpty); + } + + Ok(()) + } + + /// Callback from StakingEventsHandler. Refunds unstaked imbalance back to the source account. + /// There can be a lot of invariant breaks in the scope of this proposal. + /// Such situations are handled by adding error messages to the log. + pub fn refund_proposal_stake(stake_id: T::StakeId, imbalance: NegativeImbalance) { + if >::exists(stake_id) { + let proposal_id = Self::stakes_proposals(stake_id); + + if >::exists(proposal_id) { + let proposal = Self::proposals(proposal_id); + + if let ProposalStatus::Active(active_stake_result) = proposal.status { + if let Some(active_stake) = active_stake_result { + let refunding_result = CurrencyOf::::resolve_into_existing( + &active_stake.source_account_id, + imbalance, + ); + + if refunding_result.is_err() { + print("Broken invariant: cannot refund"); + } + } + } else { + print("Broken invariant: proposal status is not Active"); + } + } else { + print("Broken invariant: proposal doesn't exist"); + } + } else { + print("Broken invariant: stake doesn't exist"); + } + } + + /// Resets voting results for active proposals. + /// Possible application includes new council elections. + pub fn reset_active_proposals() { + >::enumerate().for_each(|(proposal_id, _)| { + >::mutate(proposal_id, |proposal| { + proposal.reset_proposal(); + >::remove_prefix(&proposal_id); + }); + }); + } +} + +impl Module { + // Wrapper-function over system::block_number() + fn current_block() -> T::BlockNumber { + >::block_number() + } + + // Enumerates through active proposals. Tally Voting results. + // Returns proposals with finalized status and id + fn get_finalized_proposals() -> Vec> { + // Enumerate active proposals id and gather finalization data. + // Skip proposals with unfinished voting. + >::enumerate() + .filter_map(|(proposal_id, _)| { + // load current proposal + let proposal = Self::proposals(proposal_id); + + // Calculates votes, takes in account voting period expiration. + // If voting process is in progress, then decision status is None. + let decision_status = proposal.define_proposal_decision_status( + T::TotalVotersCounter::total_voters_count(), + Self::current_block(), + ); + + // map to FinalizedProposalData if decision for the proposal is made or return None + decision_status.map(|status| FinalizedProposalData { + proposal_id, + proposal, + status, + finalized_at: Self::current_block(), + }) + }) + .collect() // compose output vector + } + + // Veto approved proposal during its grace period. Saves a new proposal status and removes + // proposal id from the 'PendingExecutionProposalIds' + fn veto_pending_execution_proposal(proposal_id: T::ProposalId, proposal: ProposalOf) { + >::remove(proposal_id); + + let vetoed_proposal_status = ProposalStatus::finalized( + ProposalDecisionStatus::Vetoed, + None, + None, + Self::current_block(), + ); + + >::insert( + proposal_id, + Proposal { + status: vetoed_proposal_status, + ..proposal + }, + ); + } + + // Executes approved proposal code + fn execute_proposal(approved_proposal: ApprovedProposal) { + let proposal_code = Self::proposal_codes(approved_proposal.proposal_id); + + let proposal_code_result = T::DispatchableCallCode::decode(&mut &proposal_code[..]); + + let approved_proposal_status = match proposal_code_result { + Ok(proposal_code) => { + if let Err(error) = proposal_code.dispatch(T::Origin::from(RawOrigin::Root)) { + ApprovedProposalStatus::failed_execution( + error.into().message.unwrap_or("Dispatch error"), + ) + } else { + ApprovedProposalStatus::Executed + } + } + Err(error) => ApprovedProposalStatus::failed_execution(error.what()), + }; + + let proposal_execution_status = approved_proposal + .finalisation_status_data + .create_approved_proposal_status(approved_proposal_status); + + let mut proposal = approved_proposal.proposal; + proposal.status = proposal_execution_status.clone(); + >::insert(approved_proposal.proposal_id, proposal); + + Self::deposit_event(RawEvent::ProposalStatusUpdated( + approved_proposal.proposal_id, + proposal_execution_status, + )); + + >::remove(&approved_proposal.proposal_id); + } + + // Performs all actions on proposal finalization: + // - clean active proposal cache + // - update proposal status fields (status, finalized_at) + // - add to pending execution proposal cache if approved + // - slash and unstake proposal stake if stake exists + // - decrease active proposal counter + // - fire an event + // It prints an error message in case of an attempt to finalize the non-active proposal. + fn finalize_proposal(proposal_id: T::ProposalId, decision_status: ProposalDecisionStatus) { + Self::decrease_active_proposal_counter(); + >::remove(&proposal_id.clone()); + + let mut proposal = Self::proposals(proposal_id); + + if let ProposalStatus::Active(active_stake) = proposal.status.clone() { + if let ProposalDecisionStatus::Approved { .. } = decision_status { + >::insert(proposal_id, ()); + } + + // deal with stakes if necessary + let slash_balance = + Self::calculate_slash_balance(&decision_status, &proposal.parameters); + let slash_and_unstake_result = + Self::slash_and_unstake(active_stake.clone(), slash_balance); + + // create finalized proposal status with error if any + let new_proposal_status = ProposalStatus::finalized( + decision_status, + slash_and_unstake_result.err(), + active_stake, + Self::current_block(), + ); + + proposal.status = new_proposal_status.clone(); + >::insert(proposal_id, proposal); + + Self::deposit_event(RawEvent::ProposalStatusUpdated( + proposal_id, + new_proposal_status, + )); + } else { + print("Broken invariant: proposal cannot be non-active during the finalisation"); + } + } + + // Slashes the stake and perform unstake only in case of existing stake + fn slash_and_unstake( + current_stake_data: Option>, + slash_balance: BalanceOf, + ) -> Result<(), &'static str> { + // only if stake exists + if let Some(stake_data) = current_stake_data { + if !slash_balance.is_zero() { + ProposalStakeManager::::slash(stake_data.stake_id, slash_balance)?; + } + + ProposalStakeManager::::remove_stake(stake_data.stake_id)?; + } + + Ok(()) + } + + // Calculates required slash based on finalization ProposalDecisionStatus and proposal parameters. + // Method visibility allows testing. + pub(crate) fn calculate_slash_balance( + decision_status: &ProposalDecisionStatus, + proposal_parameters: &ProposalParameters>, + ) -> types::BalanceOf { + match decision_status { + ProposalDecisionStatus::Rejected | ProposalDecisionStatus::Expired => { + T::RejectionFee::get() + } + ProposalDecisionStatus::Approved { .. } | ProposalDecisionStatus::Vetoed => { + BalanceOf::::zero() + } + ProposalDecisionStatus::Canceled => T::CancellationFee::get(), + ProposalDecisionStatus::Slashed => proposal_parameters + .required_stake + .clone() + .unwrap_or_else(BalanceOf::::zero), // stake if set or zero + } + } + + // Enumerates approved proposals and checks their grace period expiration + fn get_approved_proposal_with_expired_grace_period() -> Vec> { + >::enumerate() + .filter_map(|(proposal_id, _)| { + let proposal = Self::proposals(proposal_id); + + if proposal.is_grace_period_expired(Self::current_block()) { + // this should be true, because it was tested inside is_grace_period_expired() + if let ProposalStatus::Finalized(finalisation_data) = proposal.status.clone() { + Some(ApprovedProposalData { + proposal_id, + proposal, + finalisation_status_data: finalisation_data, + }) + } else { + None + } + } else { + None + } + }) + .collect() + } + + // Increases active proposal counter. + fn increase_active_proposal_counter() { + let next_active_proposal_count_value = Self::active_proposal_count() + 1; + ActiveProposalCount::put(next_active_proposal_count_value); + } + + // Decreases active proposal counter down to zero. Decreasing below zero has no effect. + fn decrease_active_proposal_counter() { + let current_active_proposal_counter = Self::active_proposal_count(); + + if current_active_proposal_counter > 0 { + let next_active_proposal_count_value = current_active_proposal_counter - 1; + ActiveProposalCount::put(next_active_proposal_count_value); + }; + } +} + +// Simplification of the 'FinalizedProposalData' type +type FinalizedProposal = FinalizedProposalData< + ::ProposalId, + ::BlockNumber, + MemberId, + types::BalanceOf, + ::StakeId, + ::AccountId, +>; + +// Simplification of the 'ApprovedProposalData' type +type ApprovedProposal = ApprovedProposalData< + ::ProposalId, + ::BlockNumber, + MemberId, + types::BalanceOf, + ::StakeId, + ::AccountId, +>; + +// Simplification of the 'Proposal' type +type ProposalOf = Proposal< + ::BlockNumber, + MemberId, + types::BalanceOf, + ::StakeId, + ::AccountId, +>; diff --git a/runtime-modules/proposals/engine/src/tests/mock/balance_manager.rs b/runtime-modules/proposals/engine/src/tests/mock/balance_manager.rs new file mode 100644 index 0000000000..6c3cef6be1 --- /dev/null +++ b/runtime-modules/proposals/engine/src/tests/mock/balance_manager.rs @@ -0,0 +1,33 @@ +#![cfg(test)] + +pub use sr_primitives::traits::Zero; +use srml_support::traits::{Currency, Imbalance}; + +use super::*; + +/// StakingEventsHandler implementation for the stake::Trait. Restores balances after the unstaking +/// and slashes balances if necessary. +pub struct BalanceManagerStakingEventsHandler; +impl stake::StakingEventsHandler for BalanceManagerStakingEventsHandler { + fn unstaked( + _id: &u64, + _unstaked_amount: stake::BalanceOf, + imbalance: stake::NegativeImbalance, + ) -> stake::NegativeImbalance { + let default_account_id = 1; + + ::Currency::resolve_creating(&default_account_id, imbalance); + + stake::NegativeImbalance::::zero() + } + + fn slashed( + _id: &u64, + _slash_id: Option<::SlashId>, + _slashed_amount: stake::BalanceOf, + _remaining_stake: stake::BalanceOf, + imbalance: stake::NegativeImbalance, + ) -> stake::NegativeImbalance { + imbalance + } +} diff --git a/runtime-modules/proposals/engine/src/tests/mock/mod.rs b/runtime-modules/proposals/engine/src/tests/mock/mod.rs new file mode 100644 index 0000000000..5dd0ac9c60 --- /dev/null +++ b/runtime-modules/proposals/engine/src/tests/mock/mod.rs @@ -0,0 +1,186 @@ +//! Mock runtime for the module testing. +//! +//! Submodules: +//! - stakes: contains support for mocking external 'stake' module +//! - balance_restorator: restores balances after unstaking +//! - proposals: provides types for proposal execution tests +//! + +#![cfg(test)] +pub use primitives::{Blake2Hasher, H256}; +pub use sr_primitives::{ + testing::{Digest, DigestItem, Header, UintAuthorityId}, + traits::{BlakeTwo256, Convert, IdentityLookup, OnFinalize, Zero}, + weights::Weight, + BuildStorage, DispatchError, Perbill, +}; +use srml_support::{impl_outer_event, impl_outer_origin, parameter_types}; +pub use system; + +mod balance_manager; +pub(crate) mod proposals; +mod stakes; + +use balance_manager::*; +pub use proposals::*; +pub use stakes::*; + +// Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Test; + +impl_outer_origin! { + pub enum Origin for Test {} +} + +mod engine { + pub use crate::Event; +} + +mod membership_mod { + pub use membership::members::Event; +} + +impl_outer_event! { + pub enum TestEvent for Test { + balances, + engine, + membership_mod, + } +} + +parameter_types! { + pub const ExistentialDeposit: u32 = 0; + pub const TransferFee: u32 = 0; + pub const CreationFee: u32 = 0; +} + +impl balances::Trait for Test { + /// The type for recording an account's balance. + type Balance = u64; + /// What to do if an account's free balance gets zeroed. + type OnFreeBalanceZero = (); + /// What to do if a new account is created. + type OnNewAccount = (); + + type TransferPayment = (); + + type DustRemoval = (); + type Event = TestEvent; + type ExistentialDeposit = ExistentialDeposit; + type TransferFee = TransferFee; + type CreationFee = CreationFee; +} + +impl common::currency::GovernanceCurrency for Test { + type Currency = balances::Module; +} + +impl proposals::Trait for Test {} + +impl stake::Trait for Test { + type Currency = Balances; + type StakePoolId = StakePoolId; + type StakingEventsHandler = BalanceManagerStakingEventsHandler; + type StakeId = u64; + type SlashId = u64; +} + +parameter_types! { + pub const CancellationFee: u64 = 5; + pub const RejectionFee: u64 = 3; + pub const TitleMaxLength: u32 = 100; + pub const DescriptionMaxLength: u32 = 10000; + pub const MaxActiveProposalLimit: u32 = 100; +} + +impl membership::members::Trait for Test { + type Event = TestEvent; + type MemberId = u64; + type PaidTermId = u64; + type SubscriptionId = u64; + type ActorId = u64; + type InitialMembersBalance = (); +} + +impl crate::Trait for Test { + type Event = TestEvent; + type ProposerOriginValidator = (); + type VoterOriginValidator = (); + type TotalVotersCounter = (); + type ProposalId = u32; + type StakeHandlerProvider = stakes::TestStakeHandlerProvider; + type CancellationFee = CancellationFee; + type RejectionFee = RejectionFee; + type TitleMaxLength = TitleMaxLength; + type DescriptionMaxLength = DescriptionMaxLength; + type MaxActiveProposalLimit = MaxActiveProposalLimit; + type DispatchableCallCode = proposals::Call; +} + +impl Default for proposals::Call { + fn default() -> Self { + panic!("shouldn't call default for Call"); + } +} + +impl common::origin_validator::ActorOriginValidator for () { + fn ensure_actor_origin(origin: Origin, _account_id: u64) -> Result { + let signed_account_id = system::ensure_signed(origin)?; + + Ok(signed_account_id) + } +} + +// If changing count is required, we can upgrade the implementation as shown here: +// https://substrate.dev/recipes/3-entrees/testing/externalities.html +impl crate::VotersParameters for () { + fn total_voters_count() -> u32 { + 4 + } +} + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const MaximumBlockWeight: u32 = 1024; + pub const MaximumBlockLength: u32 = 2 * 1024; + pub const AvailableBlockRatio: Perbill = Perbill::one(); + pub const MinimumPeriod: u64 = 5; + pub const StakePoolId: [u8; 8] = *b"joystake"; +} + +impl system::Trait for Test { + type Origin = Origin; + type Call = (); + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type Event = TestEvent; + type BlockHashCount = BlockHashCount; + type MaximumBlockWeight = MaximumBlockWeight; + type MaximumBlockLength = MaximumBlockLength; + type AvailableBlockRatio = AvailableBlockRatio; + type Version = (); +} + +impl timestamp::Trait for Test { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; +} + +pub fn initial_test_ext() -> runtime_io::TestExternalities { + let t = system::GenesisConfig::default() + .build_storage::() + .unwrap(); + + t.into() +} + +pub type ProposalsEngine = crate::Module; +pub type System = system::Module; +pub type Balances = balances::Module; diff --git a/runtime-modules/proposals/engine/src/tests/mock/proposals.rs b/runtime-modules/proposals/engine/src/tests/mock/proposals.rs new file mode 100644 index 0000000000..b8b8cc6675 --- /dev/null +++ b/runtime-modules/proposals/engine/src/tests/mock/proposals.rs @@ -0,0 +1,18 @@ +//! Contains executable proposal extrinsic mocks + +use rstd::prelude::*; +use rstd::vec::Vec; +use srml_support::decl_module; +pub trait Trait: system::Trait {} + +decl_module! { + pub struct Module for enum Call where origin: T::Origin { + /// Working extrinsic test + pub fn dummy_proposal(_origin, _title: Vec, _description: Vec) {} + + /// Broken extrinsic test + pub fn faulty_proposal(_origin, _title: Vec, _description: Vec,) { + Err("ExecutionFailed")? + } + } +} diff --git a/runtime-modules/proposals/engine/src/tests/mock/stakes.rs b/runtime-modules/proposals/engine/src/tests/mock/stakes.rs new file mode 100644 index 0000000000..188c69c9e7 --- /dev/null +++ b/runtime-modules/proposals/engine/src/tests/mock/stakes.rs @@ -0,0 +1,66 @@ +#![cfg(test)] + +use rstd::marker::PhantomData; +use std::cell::RefCell; +use std::panic; +use std::rc::Rc; + +use super::Test; + +// Intercepts panic method +// Returns: whether panic occurred +pub(crate) fn panics(could_panic_func: F) -> bool { + { + let default_hook = panic::take_hook(); + panic::set_hook(Box::new(|info| { + println!("{}", info); + })); + + // intercept panic + let result = panic::catch_unwind(|| could_panic_func()); + + //restore default behaviour + panic::set_hook(default_hook); + + result.is_err() + } +} + +// Test StakeHandlerProvider implementation based on local thread static variables +pub struct TestStakeHandlerProvider; +impl crate::StakeHandlerProvider for TestStakeHandlerProvider { + /// Returns StakeHandler. Mock entry point for stake module. + fn stakes() -> Rc> { + THREAD_LOCAL_STAKE_HANDLER.with(|f| f.borrow().clone()) + } +} + +// 1. RefCell - thread_local! mutation pattern +// 2. Rc - ability to have multiple references +thread_local! { + pub static THREAD_LOCAL_STAKE_HANDLER: + RefCell>> = RefCell::new(Rc::new(crate::types::DefaultStakeHandler{marker: PhantomData::})); +} + +// Sets stake handler implementation. Mockall framework integration. +pub(crate) fn set_stake_handler_impl(mock: Rc>) { + THREAD_LOCAL_STAKE_HANDLER.with(|f| { + *f.borrow_mut() = mock.clone(); + }); +} + +// Tests mock expectation and restores default behaviour +pub(crate) fn test_expectation_and_clear_mock() { + set_stake_handler_impl(Rc::new(crate::types::DefaultStakeHandler { + marker: PhantomData::, + })); +} + +// Intercepts panic in provided function, test mock expectation and restores default behaviour +pub(crate) fn handle_mock(func: F) { + let panicked = panics(func); + + test_expectation_and_clear_mock(); + + assert!(!panicked); +} diff --git a/runtime-modules/proposals/engine/src/tests/mod.rs b/runtime-modules/proposals/engine/src/tests/mod.rs new file mode 100644 index 0000000000..5cdc3ec0e0 --- /dev/null +++ b/runtime-modules/proposals/engine/src/tests/mod.rs @@ -0,0 +1,1581 @@ +pub(crate) mod mock; + +use crate::*; +use mock::*; + +use codec::Encode; +use rstd::rc::Rc; +use sr_primitives::traits::{DispatchResult, OnFinalize, OnInitialize}; +use srml_support::{StorageDoubleMap, StorageMap, StorageValue}; +use system::RawOrigin; +use system::{EventRecord, Phase}; + +use srml_support::traits::Currency; + +pub(crate) fn increase_total_balance_issuance_using_account_id(account_id: u64, balance: u64) { + let initial_balance = Balances::total_issuance(); + { + let _ = ::Currency::deposit_creating(&account_id, balance); + } + assert_eq!(Balances::total_issuance(), initial_balance + balance); +} + +struct ProposalParametersFixture { + parameters: ProposalParameters, +} + +impl ProposalParametersFixture { + fn with_required_stake(&self, required_stake: BalanceOf) -> Self { + ProposalParametersFixture { + parameters: ProposalParameters { + required_stake: Some(required_stake), + ..self.parameters + }, + } + } + fn with_grace_period(&self, grace_period: u64) -> Self { + ProposalParametersFixture { + parameters: ProposalParameters { + grace_period, + ..self.parameters + }, + } + } + + fn params(&self) -> ProposalParameters { + self.parameters.clone() + } +} + +impl Default for ProposalParametersFixture { + fn default() -> Self { + ProposalParametersFixture { + parameters: ProposalParameters { + voting_period: 3, + approval_quorum_percentage: 60, + approval_threshold_percentage: 60, + slashing_quorum_percentage: 60, + slashing_threshold_percentage: 60, + grace_period: 0, + required_stake: None, + }, + } + } +} + +#[derive(Clone)] +struct DummyProposalFixture { + parameters: ProposalParameters, + account_id: u64, + proposer_id: u64, + proposal_code: Vec, + title: Vec, + description: Vec, + stake_balance: Option>, +} + +impl Default for DummyProposalFixture { + fn default() -> Self { + let title = b"title".to_vec(); + let description = b"description".to_vec(); + let dummy_proposal = + mock::proposals::Call::::dummy_proposal(title.clone(), description.clone()); + + DummyProposalFixture { + parameters: ProposalParameters { + voting_period: 3, + approval_quorum_percentage: 60, + approval_threshold_percentage: 60, + slashing_quorum_percentage: 60, + slashing_threshold_percentage: 60, + grace_period: 0, + required_stake: None, + }, + account_id: 1, + proposer_id: 1, + proposal_code: dummy_proposal.encode(), + title, + description, + stake_balance: None, + } + } +} + +impl DummyProposalFixture { + fn with_title_and_body(self, title: Vec, description: Vec) -> Self { + DummyProposalFixture { + title, + description, + ..self + } + } + + fn with_parameters(self, parameters: ProposalParameters) -> Self { + DummyProposalFixture { parameters, ..self } + } + + fn with_account_id(self, account_id: u64) -> Self { + DummyProposalFixture { account_id, ..self } + } + + fn with_stake(self, stake_balance: BalanceOf) -> Self { + DummyProposalFixture { + stake_balance: Some(stake_balance), + ..self + } + } + + fn with_proposal_code(self, proposal_code: Vec) -> Self { + DummyProposalFixture { + proposal_code, + ..self + } + } + + fn create_proposal_and_assert(self, result: Result) -> Option { + let proposal_id_result = ProposalsEngine::create_proposal( + self.account_id, + self.proposer_id, + self.parameters, + self.title, + self.description, + self.stake_balance, + self.proposal_code, + ); + assert_eq!(proposal_id_result, result); + + proposal_id_result.ok() + } +} + +struct CancelProposalFixture { + origin: RawOrigin, + proposal_id: u32, + proposer_id: u64, +} + +impl CancelProposalFixture { + fn new(proposal_id: u32) -> Self { + CancelProposalFixture { + proposal_id, + origin: RawOrigin::Signed(1), + proposer_id: 1, + } + } + + fn with_origin(self, origin: RawOrigin) -> Self { + CancelProposalFixture { origin, ..self } + } + + fn with_proposer(self, proposer_id: u64) -> Self { + CancelProposalFixture { + proposer_id, + ..self + } + } + + fn cancel_and_assert(self, expected_result: DispatchResult) { + assert_eq!( + ProposalsEngine::cancel_proposal( + self.origin.into(), + self.proposer_id, + self.proposal_id + ), + expected_result + ); + } +} +struct VetoProposalFixture { + origin: RawOrigin, + proposal_id: u32, +} + +impl VetoProposalFixture { + fn new(proposal_id: u32) -> Self { + VetoProposalFixture { + proposal_id, + origin: RawOrigin::Root, + } + } + + fn with_origin(self, origin: RawOrigin) -> Self { + VetoProposalFixture { origin, ..self } + } + + fn veto_and_assert(self, expected_result: DispatchResult) { + assert_eq!( + ProposalsEngine::veto_proposal(self.origin.into(), self.proposal_id,), + expected_result + ); + } +} + +struct VoteGenerator { + proposal_id: u32, + current_account_id: u64, + current_voter_id: u64, + pub auto_increment_voter_id: bool, +} + +impl VoteGenerator { + fn new(proposal_id: u32) -> Self { + VoteGenerator { + proposal_id, + current_voter_id: 0, + current_account_id: 0, + auto_increment_voter_id: true, + } + } + fn vote_and_assert_ok(&mut self, vote_kind: VoteKind) { + self.vote_and_assert(vote_kind, Ok(())); + } + + fn vote_and_assert(&mut self, vote_kind: VoteKind, expected_result: DispatchResult) { + assert_eq!(self.vote(vote_kind.clone()), expected_result); + } + + fn vote(&mut self, vote_kind: VoteKind) -> DispatchResult { + if self.auto_increment_voter_id { + self.current_account_id += 1; + self.current_voter_id += 1; + } + + ProposalsEngine::vote( + system::RawOrigin::Signed(self.current_account_id).into(), + self.current_voter_id, + self.proposal_id, + vote_kind, + ) + } +} + +struct EventFixture; +impl EventFixture { + fn assert_events(expected_raw_events: Vec>) { + let expected_events = expected_raw_events + .iter() + .map(|ev| EventRecord { + phase: Phase::ApplyExtrinsic(0), + event: TestEvent::engine(ev.clone()), + topics: vec![], + }) + .collect::>>(); + + assert_eq!(System::events(), expected_events); + } +} + +// Recommendation from Parity on testing on_finalize +// https://substrate.dev/docs/en/next/development/module/tests +fn run_to_block(n: u64) { + while System::block_number() < n { + >::on_finalize(System::block_number()); + >::on_finalize(System::block_number()); + System::set_block_number(System::block_number() + 1); + >::on_initialize(System::block_number()); + >::on_initialize(System::block_number()); + } +} + +fn run_to_block_and_finalize(n: u64) { + run_to_block(n); + >::on_finalize(n); +} + +#[test] +fn create_dummy_proposal_succeeds() { + initial_test_ext().execute_with(|| { + let dummy_proposal = DummyProposalFixture::default(); + + dummy_proposal.create_proposal_and_assert(Ok(1)); + }); +} + +#[test] +fn vote_succeeds() { + initial_test_ext().execute_with(|| { + let dummy_proposal = DummyProposalFixture::default(); + let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); + + let mut vote_generator = VoteGenerator::new(proposal_id); + vote_generator.vote_and_assert_ok(VoteKind::Approve); + }); +} + +#[test] +fn vote_fails_with_insufficient_rights() { + initial_test_ext().execute_with(|| { + assert_eq!( + ProposalsEngine::vote(system::RawOrigin::None.into(), 1, 1, VoteKind::Approve), + Err(Error::Other("RequireSignedOrigin")) + ); + }); +} + +#[test] +fn proposal_execution_succeeds() { + initial_test_ext().execute_with(|| { + let parameters_fixture = ProposalParametersFixture::default(); + let dummy_proposal = + DummyProposalFixture::default().with_parameters(parameters_fixture.params()); + let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); + + // internal active proposal counter check + assert_eq!(::get(), 1); + + let mut vote_generator = VoteGenerator::new(proposal_id); + vote_generator.vote_and_assert_ok(VoteKind::Approve); + vote_generator.vote_and_assert_ok(VoteKind::Approve); + vote_generator.vote_and_assert_ok(VoteKind::Approve); + vote_generator.vote_and_assert_ok(VoteKind::Approve); + + run_to_block_and_finalize(1); + + let proposal = >::get(proposal_id); + + assert_eq!( + proposal, + Proposal { + parameters: parameters_fixture.params(), + proposer_id: 1, + created_at: 1, + status: ProposalStatus::approved(ApprovedProposalStatus::Executed, 1), + title: b"title".to_vec(), + description: b"description".to_vec(), + voting_results: VotingResults { + abstentions: 0, + approvals: 4, + rejections: 0, + slashes: 0, + }, + } + ); + + // internal active proposal counter check + assert_eq!(::get(), 0); + }); +} + +#[test] +fn proposal_execution_failed() { + initial_test_ext().execute_with(|| { + let parameters_fixture = ProposalParametersFixture::default(); + + let faulty_proposal = mock::proposals::Call::::faulty_proposal( + b"title".to_vec(), + b"description".to_vec(), + ); + + let dummy_proposal = DummyProposalFixture::default() + .with_parameters(parameters_fixture.params()) + .with_proposal_code(faulty_proposal.encode()); + + let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); + + let mut vote_generator = VoteGenerator::new(proposal_id); + vote_generator.vote_and_assert_ok(VoteKind::Approve); + vote_generator.vote_and_assert_ok(VoteKind::Approve); + vote_generator.vote_and_assert_ok(VoteKind::Approve); + vote_generator.vote_and_assert_ok(VoteKind::Approve); + + run_to_block_and_finalize(2); + + let proposal = >::get(proposal_id); + + assert_eq!( + proposal, + Proposal { + parameters: parameters_fixture.params(), + proposer_id: 1, + created_at: 1, + status: ProposalStatus::approved( + ApprovedProposalStatus::failed_execution("ExecutionFailed"), + 1 + ), + title: b"title".to_vec(), + description: b"description".to_vec(), + voting_results: VotingResults { + abstentions: 0, + approvals: 4, + rejections: 0, + slashes: 0, + }, + } + ) + }); +} + +#[test] +fn voting_results_calculation_succeeds() { + initial_test_ext().execute_with(|| { + let parameters = ProposalParameters { + voting_period: 3, + approval_quorum_percentage: 50, + approval_threshold_percentage: 50, + slashing_quorum_percentage: 60, + slashing_threshold_percentage: 60, + grace_period: 0, + required_stake: None, + }; + let dummy_proposal = DummyProposalFixture::default().with_parameters(parameters); + let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); + + let mut vote_generator = VoteGenerator::new(proposal_id); + vote_generator.vote_and_assert_ok(VoteKind::Approve); + vote_generator.vote_and_assert_ok(VoteKind::Approve); + vote_generator.vote_and_assert_ok(VoteKind::Reject); + vote_generator.vote_and_assert_ok(VoteKind::Abstain); + + run_to_block_and_finalize(2); + + let proposal = >::get(proposal_id); + + assert_eq!( + proposal.voting_results, + VotingResults { + abstentions: 1, + approvals: 2, + rejections: 1, + slashes: 0, + } + ) + }); +} + +#[test] +fn rejected_voting_results_and_remove_proposal_id_from_active_succeeds() { + initial_test_ext().execute_with(|| { + // internal active proposal counter check + assert_eq!(::get(), 0); + + let dummy_proposal = DummyProposalFixture::default(); + let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); + + let mut vote_generator = VoteGenerator::new(proposal_id); + vote_generator.vote_and_assert_ok(VoteKind::Reject); + vote_generator.vote_and_assert_ok(VoteKind::Reject); + vote_generator.vote_and_assert_ok(VoteKind::Abstain); + vote_generator.vote_and_assert_ok(VoteKind::Abstain); + + assert!(>::exists(proposal_id)); + + // internal active proposal counter check + assert_eq!(::get(), 1); + + run_to_block_and_finalize(2); + + let proposal = >::get(proposal_id); + + assert_eq!( + proposal.voting_results, + VotingResults { + abstentions: 2, + approvals: 0, + rejections: 2, + slashes: 0, + } + ); + + assert_eq!( + proposal.status, + ProposalStatus::finalized_successfully(ProposalDecisionStatus::Rejected, 1), + ); + assert!(!>::exists(proposal_id)); + + // internal active proposal counter check + assert_eq!(::get(), 0); + }); +} + +#[test] +fn create_proposal_fails_with_invalid_body_or_title() { + initial_test_ext().execute_with(|| { + let mut dummy_proposal = + DummyProposalFixture::default().with_title_and_body(Vec::new(), b"body".to_vec()); + dummy_proposal.create_proposal_and_assert(Err(Error::EmptyTitleProvided.into())); + + dummy_proposal = + DummyProposalFixture::default().with_title_and_body(b"title".to_vec(), Vec::new()); + dummy_proposal.create_proposal_and_assert(Err(Error::EmptyDescriptionProvided.into())); + + let too_long_title = vec![0; 200]; + dummy_proposal = + DummyProposalFixture::default().with_title_and_body(too_long_title, b"body".to_vec()); + dummy_proposal.create_proposal_and_assert(Err(Error::TitleIsTooLong.into())); + + let too_long_body = vec![0; 11000]; + dummy_proposal = + DummyProposalFixture::default().with_title_and_body(b"title".to_vec(), too_long_body); + dummy_proposal.create_proposal_and_assert(Err(Error::DescriptionIsTooLong.into())); + }); +} + +#[test] +fn vote_fails_with_expired_voting_period() { + initial_test_ext().execute_with(|| { + let dummy_proposal = DummyProposalFixture::default(); + let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); + + run_to_block_and_finalize(6); + + let mut vote_generator = VoteGenerator::new(proposal_id); + vote_generator.vote_and_assert(VoteKind::Approve, Err(Error::ProposalFinalized)); + }); +} + +#[test] +fn vote_fails_with_not_active_proposal() { + initial_test_ext().execute_with(|| { + let dummy_proposal = DummyProposalFixture::default(); + let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); + + let mut vote_generator = VoteGenerator::new(proposal_id); + vote_generator.vote_and_assert_ok(VoteKind::Reject); + vote_generator.vote_and_assert_ok(VoteKind::Reject); + vote_generator.vote_and_assert_ok(VoteKind::Abstain); + vote_generator.vote_and_assert_ok(VoteKind::Abstain); + + run_to_block_and_finalize(2); + + let mut vote_generator_to_fail = VoteGenerator::new(proposal_id); + vote_generator_to_fail.vote_and_assert(VoteKind::Approve, Err(Error::ProposalFinalized)); + }); +} + +#[test] +fn vote_fails_with_absent_proposal() { + initial_test_ext().execute_with(|| { + let mut vote_generator = VoteGenerator::new(2); + vote_generator.vote_and_assert(VoteKind::Approve, Err(Error::ProposalNotFound)); + }); +} + +#[test] +fn vote_fails_on_double_voting() { + initial_test_ext().execute_with(|| { + let dummy_proposal = DummyProposalFixture::default(); + let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); + + let mut vote_generator = VoteGenerator::new(proposal_id); + vote_generator.auto_increment_voter_id = false; + + vote_generator.vote_and_assert_ok(VoteKind::Approve); + vote_generator.vote_and_assert(VoteKind::Approve, Err(Error::AlreadyVoted)); + }); +} + +#[test] +fn cancel_proposal_succeeds() { + initial_test_ext().execute_with(|| { + let parameters_fixture = ProposalParametersFixture::default(); + let dummy_proposal = + DummyProposalFixture::default().with_parameters(parameters_fixture.params()); + let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); + + // internal active proposal counter check + assert_eq!(::get(), 1); + + let cancel_proposal = CancelProposalFixture::new(proposal_id); + cancel_proposal.cancel_and_assert(Ok(())); + + // internal active proposal counter check + assert_eq!(::get(), 0); + + let proposal = >::get(proposal_id); + + assert_eq!( + proposal, + Proposal { + parameters: parameters_fixture.params(), + proposer_id: 1, + created_at: 1, + status: ProposalStatus::finalized_successfully(ProposalDecisionStatus::Canceled, 1), + title: b"title".to_vec(), + description: b"description".to_vec(), + voting_results: VotingResults::default(), + } + ) + }); +} + +#[test] +fn cancel_proposal_fails_with_not_active_proposal() { + initial_test_ext().execute_with(|| { + let dummy_proposal = DummyProposalFixture::default(); + let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); + + run_to_block_and_finalize(6); + + let cancel_proposal = CancelProposalFixture::new(proposal_id); + cancel_proposal.cancel_and_assert(Err(Error::ProposalFinalized)); + }); +} + +#[test] +fn cancel_proposal_fails_with_not_existing_proposal() { + initial_test_ext().execute_with(|| { + let cancel_proposal = CancelProposalFixture::new(2); + cancel_proposal.cancel_and_assert(Err(Error::ProposalNotFound)); + }); +} + +#[test] +fn cancel_proposal_fails_with_insufficient_rights() { + initial_test_ext().execute_with(|| { + let dummy_proposal = DummyProposalFixture::default(); + let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); + + let cancel_proposal = CancelProposalFixture::new(proposal_id) + .with_origin(RawOrigin::Signed(2)) + .with_proposer(2); + cancel_proposal.cancel_and_assert(Err(Error::NotAuthor)); + }); +} + +#[test] +fn veto_proposal_succeeds() { + initial_test_ext().execute_with(|| { + // internal active proposal counter check + assert_eq!(::get(), 0); + + let parameters_fixture = ProposalParametersFixture::default(); + let dummy_proposal = + DummyProposalFixture::default().with_parameters(parameters_fixture.params()); + let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); + + // internal active proposal counter check + assert_eq!(::get(), 1); + + let veto_proposal = VetoProposalFixture::new(proposal_id); + veto_proposal.veto_and_assert(Ok(())); + + let proposal = >::get(proposal_id); + + assert_eq!( + proposal, + Proposal { + parameters: parameters_fixture.params(), + proposer_id: 1, + created_at: 1, + status: ProposalStatus::finalized_successfully(ProposalDecisionStatus::Vetoed, 1), + title: b"title".to_vec(), + description: b"description".to_vec(), + voting_results: VotingResults::default(), + } + ); + + // internal active proposal counter check + assert_eq!(::get(), 0); + }); +} + +#[test] +fn veto_proposal_fails_with_not_active_proposal() { + initial_test_ext().execute_with(|| { + let dummy_proposal = DummyProposalFixture::default(); + let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); + + run_to_block_and_finalize(6); + + let veto_proposal = VetoProposalFixture::new(proposal_id); + veto_proposal.veto_and_assert(Err(Error::ProposalFinalized)); + }); +} + +#[test] +fn veto_proposal_fails_with_not_existing_proposal() { + initial_test_ext().execute_with(|| { + let veto_proposal = VetoProposalFixture::new(2); + veto_proposal.veto_and_assert(Err(Error::ProposalNotFound)); + }); +} + +#[test] +fn veto_proposal_fails_with_insufficient_rights() { + initial_test_ext().execute_with(|| { + let dummy_proposal = DummyProposalFixture::default(); + let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); + + let veto_proposal = VetoProposalFixture::new(proposal_id).with_origin(RawOrigin::Signed(2)); + veto_proposal.veto_and_assert(Err(Error::RequireRootOrigin)); + }); +} + +#[test] +fn create_proposal_event_emitted() { + initial_test_ext().execute_with(|| { + let dummy_proposal = DummyProposalFixture::default(); + dummy_proposal.create_proposal_and_assert(Ok(1)); + + EventFixture::assert_events(vec![RawEvent::ProposalCreated(1, 1)]); + }); +} + +#[test] +fn veto_proposal_event_emitted() { + initial_test_ext().execute_with(|| { + let dummy_proposal = DummyProposalFixture::default(); + let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); + + let veto_proposal = VetoProposalFixture::new(proposal_id); + veto_proposal.veto_and_assert(Ok(())); + + EventFixture::assert_events(vec![ + RawEvent::ProposalCreated(1, 1), + RawEvent::ProposalStatusUpdated( + 1, + ProposalStatus::finalized_successfully(ProposalDecisionStatus::Vetoed, 1), + ), + ]); + }); +} + +#[test] +fn cancel_proposal_event_emitted() { + initial_test_ext().execute_with(|| { + let dummy_proposal = DummyProposalFixture::default(); + let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); + + let cancel_proposal = CancelProposalFixture::new(proposal_id); + cancel_proposal.cancel_and_assert(Ok(())); + + EventFixture::assert_events(vec![ + RawEvent::ProposalCreated(1, 1), + RawEvent::ProposalStatusUpdated( + 1, + ProposalStatus::Finalized(FinalizationData { + proposal_status: ProposalDecisionStatus::Canceled, + encoded_unstaking_error_due_to_broken_runtime: None, + stake_data_after_unstaking_error: None, + finalized_at: 1, + }), + ), + ]); + }); +} + +#[test] +fn vote_proposal_event_emitted() { + initial_test_ext().execute_with(|| { + let dummy_proposal = DummyProposalFixture::default(); + let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); + + let mut vote_generator = VoteGenerator::new(proposal_id); + vote_generator.vote_and_assert_ok(VoteKind::Approve); + + EventFixture::assert_events(vec![ + RawEvent::ProposalCreated(1, 1), + RawEvent::Voted(1, 1, VoteKind::Approve), + ]); + }); +} + +#[test] +fn create_proposal_and_expire_it() { + initial_test_ext().execute_with(|| { + let parameters_fixture = ProposalParametersFixture::default(); + let dummy_proposal = + DummyProposalFixture::default().with_parameters(parameters_fixture.params()); + let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); + + run_to_block_and_finalize(8); + + let proposal = >::get(proposal_id); + + assert_eq!( + proposal, + Proposal { + parameters: parameters_fixture.params(), + proposer_id: 1, + created_at: 1, + status: ProposalStatus::finalized_successfully(ProposalDecisionStatus::Expired, 4), + title: b"title".to_vec(), + description: b"description".to_vec(), + voting_results: VotingResults::default(), + } + ) + }); +} + +#[test] +fn proposal_execution_postponed_because_of_grace_period() { + initial_test_ext().execute_with(|| { + let parameters_fixture = ProposalParametersFixture::default().with_grace_period(2); + let dummy_proposal = + DummyProposalFixture::default().with_parameters(parameters_fixture.params()); + + let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); + + let mut vote_generator = VoteGenerator::new(proposal_id); + vote_generator.vote_and_assert_ok(VoteKind::Approve); + vote_generator.vote_and_assert_ok(VoteKind::Approve); + vote_generator.vote_and_assert_ok(VoteKind::Approve); + vote_generator.vote_and_assert_ok(VoteKind::Approve); + + run_to_block_and_finalize(1); + run_to_block_and_finalize(2); + + // check internal cache for proposal_id presense + assert!(>::enumerate() + .find(|(x, _)| *x == proposal_id) + .is_some()); + + let proposal = >::get(proposal_id); + + assert_eq!( + proposal, + Proposal { + parameters: parameters_fixture.params(), + proposer_id: 1, + created_at: 1, + status: ProposalStatus::approved(ApprovedProposalStatus::PendingExecution, 1), + title: b"title".to_vec(), + description: b"description".to_vec(), + voting_results: VotingResults { + abstentions: 0, + approvals: 4, + rejections: 0, + slashes: 0, + }, + } + ); + }); +} + +#[test] +fn proposal_execution_vetoed_successfully_during_the_grace_period() { + initial_test_ext().execute_with(|| { + let parameters_fixture = ProposalParametersFixture::default().with_grace_period(2); + let dummy_proposal = + DummyProposalFixture::default().with_parameters(parameters_fixture.params()); + + let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); + + let mut vote_generator = VoteGenerator::new(proposal_id); + vote_generator.vote_and_assert_ok(VoteKind::Approve); + vote_generator.vote_and_assert_ok(VoteKind::Approve); + vote_generator.vote_and_assert_ok(VoteKind::Approve); + vote_generator.vote_and_assert_ok(VoteKind::Approve); + + run_to_block_and_finalize(1); + run_to_block_and_finalize(2); + + // check internal cache for proposal_id presense + assert!(>::enumerate() + .find(|(x, _)| *x == proposal_id) + .is_some()); + + let proposal = >::get(proposal_id); + + assert_eq!( + proposal, + Proposal { + parameters: parameters_fixture.params(), + proposer_id: 1, + created_at: 1, + status: ProposalStatus::approved(ApprovedProposalStatus::PendingExecution, 1), + title: b"title".to_vec(), + description: b"description".to_vec(), + voting_results: VotingResults { + abstentions: 0, + approvals: 4, + rejections: 0, + slashes: 0, + }, + } + ); + + let veto_proposal = VetoProposalFixture::new(proposal_id); + veto_proposal.veto_and_assert(Ok(())); + + let proposal = >::get(proposal_id); + + assert_eq!( + proposal, + Proposal { + parameters: parameters_fixture.params(), + proposer_id: 1, + created_at: 1, + status: ProposalStatus::finalized_successfully(ProposalDecisionStatus::Vetoed, 2), + title: b"title".to_vec(), + description: b"description".to_vec(), + voting_results: VotingResults { + abstentions: 0, + approvals: 4, + rejections: 0, + slashes: 0, + }, + } + ); + + // check internal cache for proposal_id presense + assert!(>::enumerate() + .find(|(x, _)| *x == proposal_id) + .is_none()); + }); +} + +#[test] +fn proposal_execution_succeeds_after_the_grace_period() { + initial_test_ext().execute_with(|| { + let parameters_fixture = ProposalParametersFixture::default().with_grace_period(1); + let dummy_proposal = + DummyProposalFixture::default().with_parameters(parameters_fixture.params()); + let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); + + let mut vote_generator = VoteGenerator::new(proposal_id); + vote_generator.vote_and_assert_ok(VoteKind::Approve); + vote_generator.vote_and_assert_ok(VoteKind::Approve); + vote_generator.vote_and_assert_ok(VoteKind::Approve); + vote_generator.vote_and_assert_ok(VoteKind::Approve); + + run_to_block_and_finalize(1); + + // check internal cache for proposal_id presence + assert!(>::enumerate() + .find(|(x, _)| *x == proposal_id) + .is_some()); + + let mut proposal = >::get(proposal_id); + + let mut expected_proposal = Proposal { + parameters: parameters_fixture.params(), + proposer_id: 1, + created_at: 1, + status: ProposalStatus::approved(ApprovedProposalStatus::PendingExecution, 1), + title: b"title".to_vec(), + description: b"description".to_vec(), + voting_results: VotingResults { + abstentions: 0, + approvals: 4, + rejections: 0, + slashes: 0, + }, + }; + + assert_eq!(proposal, expected_proposal); + + run_to_block_and_finalize(2); + + proposal = >::get(proposal_id); + + expected_proposal.status = ProposalStatus::approved(ApprovedProposalStatus::Executed, 1); + + assert_eq!(proposal, expected_proposal); + + // check internal cache for proposal_id absense + assert!(>::enumerate() + .find(|(x, _)| *x == proposal_id) + .is_none()); + }); +} + +#[test] +fn create_proposal_fails_on_exceeding_max_active_proposals_count() { + initial_test_ext().execute_with(|| { + for idx in 1..101 { + let dummy_proposal = DummyProposalFixture::default(); + dummy_proposal.create_proposal_and_assert(Ok(idx)); + // internal active proposal counter check + assert_eq!(::get(), idx); + } + + let dummy_proposal = DummyProposalFixture::default(); + dummy_proposal + .create_proposal_and_assert(Err(Error::MaxActiveProposalNumberExceeded.into())); + // internal active proposal counter check + assert_eq!(::get(), 100); + }); +} + +#[test] +fn voting_internal_cache_exists_after_proposal_finalization() { + initial_test_ext().execute_with(|| { + let dummy_proposal = DummyProposalFixture::default(); + dummy_proposal.create_proposal_and_assert(Ok(1)); + + // last created proposal id equals current proposal count + let proposal_id = ::get(); + + let mut vote_generator = VoteGenerator::new(proposal_id); + vote_generator.vote_and_assert_ok(VoteKind::Reject); + vote_generator.vote_and_assert_ok(VoteKind::Reject); + vote_generator.vote_and_assert_ok(VoteKind::Abstain); + vote_generator.vote_and_assert_ok(VoteKind::Abstain); + + // cache exists + assert!(>::exists( + proposal_id, + 1 + )); + + run_to_block_and_finalize(2); + + // cache still exists and is not cleared + assert!(>::exists( + proposal_id, + 1 + )); + }); +} + +#[test] +fn create_dummy_proposal_succeeds_with_stake() { + initial_test_ext().execute_with(|| { + let account_id = 1; + + let required_stake = 200; + let parameters_fixture = + ProposalParametersFixture::default().with_required_stake(required_stake); + + let dummy_proposal = DummyProposalFixture::default() + .with_parameters(parameters_fixture.params()) + .with_account_id(account_id) + .with_stake(200); + + let _imbalance = ::Currency::deposit_creating(&account_id, 500); + + let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); + + let proposal = >::get(proposal_id); + + assert_eq!( + proposal, + Proposal { + parameters: parameters_fixture.params(), + proposer_id: 1, + created_at: 1, + status: ProposalStatus::Active(Some(ActiveStake { + stake_id: 0, // valid stake_id + source_account_id: 1 + })), + title: b"title".to_vec(), + description: b"description".to_vec(), + voting_results: VotingResults::default(), + } + ) + }); +} + +#[test] +fn create_dummy_proposal_fail_with_stake_on_empty_account() { + initial_test_ext().execute_with(|| { + let account_id = 1; + + let required_stake = 200; + let parameters_fixture = + ProposalParametersFixture::default().with_required_stake(required_stake); + let dummy_proposal = DummyProposalFixture::default() + .with_parameters(parameters_fixture.params()) + .with_account_id(account_id) + .with_stake(required_stake); + + dummy_proposal + .create_proposal_and_assert(Err(Error::Other("too few free funds in account"))); + }); +} + +#[test] +fn create_proposal_fais_with_invalid_stake_parameters() { + initial_test_ext().execute_with(|| { + let parameters_fixture = ProposalParametersFixture::default(); + + let mut dummy_proposal = DummyProposalFixture::default() + .with_parameters(parameters_fixture.params()) + .with_stake(200); + + dummy_proposal.create_proposal_and_assert(Err(Error::StakeShouldBeEmpty.into())); + + let parameters_fixture_stake_200 = parameters_fixture.with_required_stake(200); + dummy_proposal = + DummyProposalFixture::default().with_parameters(parameters_fixture_stake_200.params()); + + dummy_proposal.create_proposal_and_assert(Err(Error::EmptyStake.into())); + + let parameters_fixture_stake_300 = parameters_fixture.with_required_stake(300); + dummy_proposal = DummyProposalFixture::default() + .with_parameters(parameters_fixture_stake_300.params()) + .with_stake(200); + + dummy_proposal.create_proposal_and_assert(Err(Error::StakeDiffersFromRequired.into())); + }); +} + +#[test] +fn finalize_expired_proposal_and_check_stake_removing_with_balance_checks_succeeds() { + initial_test_ext().execute_with(|| { + let account_id = 1; + + let stake_amount = 200; + let parameters = ProposalParameters { + voting_period: 3, + approval_quorum_percentage: 50, + approval_threshold_percentage: 60, + slashing_quorum_percentage: 60, + slashing_threshold_percentage: 60, + grace_period: 5, + required_stake: Some(stake_amount), + }; + let dummy_proposal = DummyProposalFixture::default() + .with_parameters(parameters) + .with_account_id(account_id) + .with_stake(stake_amount); + + let account_balance = 500; + let _imbalance = + ::Currency::deposit_creating(&account_id, account_balance); + + assert_eq!( + ::Currency::total_balance(&account_id), + account_balance + ); + + let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); + assert_eq!( + ::Currency::total_balance(&account_id), + account_balance - stake_amount + ); + + let mut proposal = >::get(proposal_id); + + let mut expected_proposal = Proposal { + parameters, + proposer_id: 1, + created_at: 1, + status: ProposalStatus::Active(Some(ActiveStake { + stake_id: 0, + source_account_id: 1, + })), + title: b"title".to_vec(), + description: b"description".to_vec(), + voting_results: VotingResults::default(), + }; + + assert_eq!(proposal, expected_proposal); + + run_to_block_and_finalize(5); + + proposal = >::get(proposal_id); + + expected_proposal.status = ProposalStatus::Finalized(FinalizationData { + proposal_status: ProposalDecisionStatus::Expired, + finalized_at: 4, + encoded_unstaking_error_due_to_broken_runtime: None, + stake_data_after_unstaking_error: None, + }); + + assert_eq!(proposal, expected_proposal); + + let rejection_fee = RejectionFee::get(); + assert_eq!( + ::Currency::total_balance(&account_id), + account_balance - rejection_fee + ); + }); +} + +#[test] +fn proposal_cancellation_with_slashes_with_balance_checks_succeeds() { + initial_test_ext().execute_with(|| { + let account_id = 1; + + let stake_amount = 200; + let parameters = ProposalParameters { + voting_period: 3, + approval_quorum_percentage: 50, + approval_threshold_percentage: 60, + slashing_quorum_percentage: 60, + slashing_threshold_percentage: 60, + grace_period: 5, + required_stake: Some(stake_amount), + }; + let dummy_proposal = DummyProposalFixture::default() + .with_parameters(parameters) + .with_account_id(account_id.clone()) + .with_stake(stake_amount); + + let account_balance = 500; + let _imbalance = + ::Currency::deposit_creating(&account_id, account_balance); + + assert_eq!( + ::Currency::total_balance(&account_id), + account_balance + ); + + let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); + assert_eq!( + ::Currency::total_balance(&account_id), + account_balance - stake_amount + ); + + let mut proposal = >::get(proposal_id); + + let mut expected_proposal = Proposal { + parameters, + proposer_id: 1, + created_at: 1, + status: ProposalStatus::Active(Some(ActiveStake { + stake_id: 0, + source_account_id: 1, + })), + title: b"title".to_vec(), + description: b"description".to_vec(), + voting_results: VotingResults::default(), + }; + + assert_eq!(proposal, expected_proposal); + + let cancel_proposal_fixture = CancelProposalFixture::new(proposal_id); + + cancel_proposal_fixture.cancel_and_assert(Ok(())); + + proposal = >::get(proposal_id); + + expected_proposal.status = ProposalStatus::Finalized(FinalizationData { + proposal_status: ProposalDecisionStatus::Canceled, + finalized_at: 1, + encoded_unstaking_error_due_to_broken_runtime: None, + stake_data_after_unstaking_error: None, + }); + + assert_eq!(proposal, expected_proposal); + + let cancellation_fee = CancellationFee::get(); + assert_eq!( + ::Currency::total_balance(&account_id), + account_balance - cancellation_fee + ); + }); +} + +#[test] +fn finalize_proposal_using_stake_mocks_succeeds() { + handle_mock(|| { + initial_test_ext().execute_with(|| { + let mock = { + let mut mock = crate::types::MockStakeHandler::::new(); + mock.expect_create_stake().times(1).returning(|| Ok(1)); + + mock.expect_make_stake_imbalance() + .times(1) + .returning(|_, _| Ok(crate::types::NegativeImbalance::::new(200))); + + mock.expect_stake().times(1).returning(|_, _| Ok(())); + + mock.expect_remove_stake().times(1).returning(|_| Ok(())); + + mock.expect_unstake().times(1).returning(|_| Ok(())); + + mock.expect_slash().times(1).returning(|_, _| Ok(())); + + Rc::new(mock) + }; + set_stake_handler_impl(mock.clone()); + + let account_id = 1; + + let stake_amount = 200; + let parameters_fixture = + ProposalParametersFixture::default().with_required_stake(stake_amount); + let dummy_proposal = DummyProposalFixture::default() + .with_parameters(parameters_fixture.params()) + .with_account_id(account_id) + .with_stake(stake_amount); + + let _proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); + + run_to_block_and_finalize(5); + }); + }); +} + +#[test] +fn proposal_slashing_succeeds() { + initial_test_ext().execute_with(|| { + let dummy_proposal = DummyProposalFixture::default(); + let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); + + let mut vote_generator = VoteGenerator::new(proposal_id); + vote_generator.vote_and_assert_ok(VoteKind::Reject); + vote_generator.vote_and_assert_ok(VoteKind::Slash); + vote_generator.vote_and_assert_ok(VoteKind::Slash); + vote_generator.vote_and_assert_ok(VoteKind::Slash); + + assert!(>::exists(proposal_id)); + + run_to_block_and_finalize(2); + + let proposal = >::get(proposal_id); + + assert_eq!( + proposal.voting_results, + VotingResults { + abstentions: 0, + approvals: 0, + rejections: 1, + slashes: 3, + } + ); + + assert_eq!( + proposal.status, + ProposalStatus::Finalized(FinalizationData { + proposal_status: ProposalDecisionStatus::Slashed, + encoded_unstaking_error_due_to_broken_runtime: None, + finalized_at: 1, + stake_data_after_unstaking_error: None, + }), + ); + assert!(!>::exists(proposal_id)); + }); +} + +#[test] +fn finalize_proposal_using_stake_mocks_failed() { + handle_mock(|| { + initial_test_ext().execute_with(|| { + let mock = { + let mut mock = crate::types::MockStakeHandler::::new(); + mock.expect_create_stake().times(1).returning(|| Ok(1)); + + mock.expect_remove_stake() + .times(1) + .returning(|_| Err("Cannot remove stake")); + + mock.expect_make_stake_imbalance() + .times(1) + .returning(|_, _| Ok(crate::types::NegativeImbalance::::new(200))); + + mock.expect_stake().times(1).returning(|_, _| Ok(())); + + mock.expect_unstake().times(1).returning(|_| Ok(())); + + mock.expect_slash().times(1).returning(|_, _| Ok(())); + + Rc::new(mock) + }; + set_stake_handler_impl(mock.clone()); + + let account_id = 1; + + let stake_amount = 200; + let parameters_fixture = + ProposalParametersFixture::default().with_required_stake(stake_amount); + let dummy_proposal = DummyProposalFixture::default() + .with_parameters(parameters_fixture.params()) + .with_account_id(account_id) + .with_stake(stake_amount); + + let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); + + run_to_block_and_finalize(5); + + let proposal = >::get(proposal_id); + assert_eq!( + proposal, + Proposal { + parameters: parameters_fixture.params(), + proposer_id: 1, + created_at: 1, + status: ProposalStatus::finalized( + ProposalDecisionStatus::Expired, + Some("Cannot remove stake"), + Some(ActiveStake { + stake_id: 1, + source_account_id: 1 + }), + 4, + ), + title: b"title".to_vec(), + description: b"description".to_vec(), + voting_results: VotingResults::default(), + } + ); + }); + }); +} + +#[test] +fn create_proposal_fails_with_invalid_threshold_parameters() { + initial_test_ext().execute_with(|| { + let mut parameters = ProposalParameters { + voting_period: 3, + approval_quorum_percentage: 50, + approval_threshold_percentage: 0, + slashing_quorum_percentage: 60, + slashing_threshold_percentage: 60, + grace_period: 5, + required_stake: None, + }; + + let mut dummy_proposal = DummyProposalFixture::default().with_parameters(parameters); + + dummy_proposal + .create_proposal_and_assert(Err(Error::InvalidParameterApprovalThreshold.into())); + + parameters.approval_threshold_percentage = 60; + parameters.slashing_threshold_percentage = 0; + dummy_proposal = DummyProposalFixture::default().with_parameters(parameters); + + dummy_proposal + .create_proposal_and_assert(Err(Error::InvalidParameterSlashingThreshold.into())); + }); +} + +#[test] +fn proposal_reset_succeeds() { + initial_test_ext().execute_with(|| { + let dummy_proposal = DummyProposalFixture::default(); + let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); + + let mut vote_generator = VoteGenerator::new(proposal_id); + vote_generator.vote_and_assert_ok(VoteKind::Reject); + vote_generator.vote_and_assert_ok(VoteKind::Abstain); + vote_generator.vote_and_assert_ok(VoteKind::Slash); + + assert!(>::exists(proposal_id)); + assert_eq!( + >::get(&proposal_id, &2), + VoteKind::Abstain + ); + + run_to_block_and_finalize(2); + + let proposal = >::get(proposal_id); + + assert_eq!( + proposal.voting_results, + VotingResults { + abstentions: 1, + approvals: 0, + rejections: 1, + slashes: 1, + } + ); + + ProposalsEngine::reset_active_proposals(); + + let updated_proposal = >::get(proposal_id); + + assert_eq!( + updated_proposal.voting_results, + VotingResults { + abstentions: 0, + approvals: 0, + rejections: 0, + slashes: 0, + } + ); + + // whole double map prefix was removed (should return default value) + assert_eq!( + >::get(&proposal_id, &2), + VoteKind::default() + ); + }); +} + +#[test] +fn proposal_counters_are_valid() { + initial_test_ext().execute_with(|| { + let mut dummy_proposal = DummyProposalFixture::default(); + let _ = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); + + dummy_proposal = DummyProposalFixture::default(); + let _ = dummy_proposal.create_proposal_and_assert(Ok(2)).unwrap(); + + dummy_proposal = DummyProposalFixture::default(); + let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(3)).unwrap(); + + assert_eq!(ActiveProposalCount::get(), 3); + assert_eq!(ProposalCount::get(), 3); + + let cancel_proposal_fixture = CancelProposalFixture::new(proposal_id); + cancel_proposal_fixture.cancel_and_assert(Ok(())); + + assert_eq!(ActiveProposalCount::get(), 2); + assert_eq!(ProposalCount::get(), 3); + }); +} + +#[test] +fn proposal_stake_cache_is_valid() { + initial_test_ext().execute_with(|| { + increase_total_balance_issuance_using_account_id(1, 50000); + + let stake = 250u32; + let parameters = ProposalParametersFixture::default().with_required_stake(stake.into()); + let dummy_proposal = DummyProposalFixture::default() + .with_parameters(parameters.params()) + .with_stake(stake as u64); + + let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); + let expected_stake_id = 0; + assert_eq!( + >::get(&expected_stake_id), + proposal_id + ); + }); +} + +#[test] +fn slash_balance_is_calculated_correctly() { + initial_test_ext().execute_with(|| { + let vetoed_slash_balance = ProposalsEngine::calculate_slash_balance( + &ProposalDecisionStatus::Vetoed, + &ProposalParametersFixture::default().params(), + ); + + assert_eq!(vetoed_slash_balance, 0); + + let approved_slash_balance = ProposalsEngine::calculate_slash_balance( + &ProposalDecisionStatus::Approved(ApprovedProposalStatus::Executed), + &ProposalParametersFixture::default().params(), + ); + + assert_eq!(approved_slash_balance, 0); + + let rejection_fee = ::RejectionFee::get(); + + let rejected_slash_balance = ProposalsEngine::calculate_slash_balance( + &ProposalDecisionStatus::Rejected, + &ProposalParametersFixture::default().params(), + ); + + assert_eq!(rejected_slash_balance, rejection_fee); + + let expired_slash_balance = ProposalsEngine::calculate_slash_balance( + &ProposalDecisionStatus::Expired, + &ProposalParametersFixture::default().params(), + ); + + assert_eq!(expired_slash_balance, rejection_fee); + + let cancellation_fee = ::CancellationFee::get(); + + let cancellation_slash_balance = ProposalsEngine::calculate_slash_balance( + &ProposalDecisionStatus::Canceled, + &ProposalParametersFixture::default().params(), + ); + + assert_eq!(cancellation_slash_balance, cancellation_fee); + + let slash_balance_with_no_stake = ProposalsEngine::calculate_slash_balance( + &ProposalDecisionStatus::Slashed, + &ProposalParametersFixture::default().params(), + ); + + assert_eq!(slash_balance_with_no_stake, 0); + + let stake = 256; + let slash_balance_with_stake = ProposalsEngine::calculate_slash_balance( + &ProposalDecisionStatus::Slashed, + &ProposalParametersFixture::default() + .with_required_stake(stake) + .params(), + ); + + assert_eq!(slash_balance_with_stake, stake); + }); +} diff --git a/runtime-modules/proposals/engine/src/types/mod.rs b/runtime-modules/proposals/engine/src/types/mod.rs new file mode 100644 index 0000000000..d6ad1a8c73 --- /dev/null +++ b/runtime-modules/proposals/engine/src/types/mod.rs @@ -0,0 +1,793 @@ +//! Proposals types module for the Joystream platform. Version 2. +//! Provides types for the proposal engine. + +#![warn(missing_docs)] + +use codec::{Decode, Encode}; +use rstd::cmp::PartialOrd; +use rstd::ops::Add; +use rstd::prelude::*; + +#[cfg(feature = "std")] +use serde::{Deserialize, Serialize}; +use sr_primitives::Perbill; +use srml_support::dispatch; +use srml_support::traits::Currency; + +mod proposal_statuses; +mod stakes; + +pub use proposal_statuses::{ + ApprovedProposalStatus, FinalizationData, ProposalDecisionStatus, ProposalStatus, +}; +pub(crate) use stakes::ProposalStakeManager; +pub use stakes::{DefaultStakeHandlerProvider, StakeHandler, StakeHandlerProvider}; + +#[cfg(test)] +pub(crate) use stakes::DefaultStakeHandler; + +#[cfg(test)] +pub(crate) use stakes::MockStakeHandler; + +/// Vote kind for the proposal. Sum of all votes defines proposal status. +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)] +pub enum VoteKind { + /// Pass, an alternative or a ranking, for binary, multiple choice + /// and ranked choice propositions, respectively. + Approve, + + /// Against proposal. + Reject, + + /// Reject proposal and slash it stake. + Slash, + + /// Signals presence, but unwillingness to cast judgment on substance of vote. + Abstain, +} + +impl Default for VoteKind { + fn default() -> Self { + VoteKind::Reject + } +} + +/// Proposal parameters required to manage proposal risk. +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +#[derive(Encode, Decode, Default, Clone, Copy, PartialEq, Eq, Debug)] +pub struct ProposalParameters { + /// During this period, votes can be accepted + pub voting_period: BlockNumber, + + /// A pause before execution of the approved proposal. Zero means approved proposal would be + /// executed immediately. + pub grace_period: BlockNumber, + + /// Quorum percentage of approving voters required to pass the proposal. + pub approval_quorum_percentage: u32, + + /// Approval votes percentage threshold to pass the proposal. + pub approval_threshold_percentage: u32, + + /// Quorum percentage of voters required to slash the proposal. + pub slashing_quorum_percentage: u32, + + /// Slashing votes percentage threshold to slash the proposal. + pub slashing_threshold_percentage: u32, + + /// Proposal stake + pub required_stake: Option, +} + +/// Contains current voting results +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)] +pub struct VotingResults { + /// 'Abstain' votes counter + pub abstentions: u32, + + /// 'Approve' votes counter + pub approvals: u32, + + /// 'Reject' votes counter + pub rejections: u32, + + /// 'Slash' votes counter + pub slashes: u32, +} + +impl VotingResults { + /// Add vote to the related counter + pub fn add_vote(&mut self, vote: VoteKind) { + match vote { + VoteKind::Abstain => self.abstentions += 1, + VoteKind::Approve => self.approvals += 1, + VoteKind::Reject => self.rejections += 1, + VoteKind::Slash => self.slashes += 1, + } + } + + /// Calculates number of votes so far + pub fn votes_number(&self) -> u32 { + self.abstentions + self.approvals + self.rejections + self.slashes + } +} + +/// Contains created stake id and source account for the stake balance +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +#[derive(Encode, Decode, Default, Clone, Copy, PartialEq, Eq, Debug)] +pub struct ActiveStake { + /// Created stake id for the proposal + pub stake_id: StakeId, + + /// Source account of the stake balance. Refund if any will be provided using this account + pub source_account_id: AccountId, +} + +/// 'Proposal' contains information necessary for the proposal system functioning. +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)] +pub struct Proposal { + /// Proposals parameter, characterize different proposal types. + pub parameters: ProposalParameters, + + /// Identifier of member proposing. + pub proposer_id: ProposerId, + + /// Proposal description + pub title: Vec, + + /// Proposal body + pub description: Vec, + + /// When it was created. + pub created_at: BlockNumber, + + /// Current proposal status + pub status: ProposalStatus, + + /// Curring voting result for the proposal + pub voting_results: VotingResults, +} + +impl + Proposal +where + BlockNumber: Add + PartialOrd + Copy, + StakeId: Clone, + AccountId: Clone, +{ + /// Returns whether voting period expired by now + pub fn is_voting_period_expired(&self, now: BlockNumber) -> bool { + now >= self.created_at + self.parameters.voting_period + } + + /// Returns whether grace period expired by now. + /// Grace period can be expired only if proposal is finalized with Approved status. + /// Returns false otherwise. + pub fn is_grace_period_expired(&self, now: BlockNumber) -> bool { + if let ProposalStatus::Finalized(finalized_status) = self.status.clone() { + if let ProposalDecisionStatus::Approved(_) = finalized_status.proposal_status { + return now >= finalized_status.finalized_at + self.parameters.grace_period; + } + } + + false + } + + /// Determines the finalized proposal status using voting results tally for current proposal. + /// Calculates votes, takes in account voting period expiration. + /// If voting process is in progress, then decision status is None. + /// Parameters: current time, total voters number involved (council size). + /// Returns the proposal finalized status if any. + pub fn define_proposal_decision_status( + &self, + total_voters_count: u32, + now: BlockNumber, + ) -> Option { + let proposal_status_resolution = ProposalStatusResolution { + proposal: self, + approvals: self.voting_results.approvals, + slashes: self.voting_results.slashes, + now, + votes_count: self.voting_results.votes_number(), + total_voters_count, + }; + + if proposal_status_resolution.is_approval_quorum_reached() + && proposal_status_resolution.is_approval_threshold_reached() + { + Some(ProposalDecisionStatus::Approved( + ApprovedProposalStatus::PendingExecution, + )) + } else if proposal_status_resolution.is_slashing_quorum_reached() + && proposal_status_resolution.is_slashing_threshold_reached() + { + Some(ProposalDecisionStatus::Slashed) + } else if proposal_status_resolution.is_expired() { + Some(ProposalDecisionStatus::Expired) + } else if proposal_status_resolution.is_voting_completed() { + Some(ProposalDecisionStatus::Rejected) + } else { + None + } + } + + /// Reset the proposal in Active status. Proposal with other status won't be changed. + /// Reset proposal operation clears voting results. + pub fn reset_proposal(&mut self) { + if let ProposalStatus::Active(_) = self.status.clone() { + self.voting_results = VotingResults::default(); + } + } +} + +/// Provides data for the voting. +pub trait VotersParameters { + /// Defines maximum voters count for the proposal + fn total_voters_count() -> u32; +} + +// Calculates quorum, votes threshold, expiration status +struct ProposalStatusResolution<'a, BlockNumber, ProposerId, Balance, StakeId, AccountId> { + proposal: &'a Proposal, + now: BlockNumber, + votes_count: u32, + total_voters_count: u32, + approvals: u32, + slashes: u32, +} + +impl<'a, BlockNumber, ProposerId, Balance, StakeId, AccountId> + ProposalStatusResolution<'a, BlockNumber, ProposerId, Balance, StakeId, AccountId> +where + BlockNumber: Add + PartialOrd + Copy, + StakeId: Clone, + AccountId: Clone, +{ + // Proposal has been expired and quorum not reached. + pub fn is_expired(&self) -> bool { + self.proposal.is_voting_period_expired(self.now) + } + + // Approval quorum reached for the proposal. Compares predefined parameter with actual + // votes sum divided by total possible votes count. + pub fn is_approval_quorum_reached(&self) -> bool { + let actual_votes_fraction = + Perbill::from_rational_approximation(self.votes_count, self.total_voters_count); + let approval_quorum_fraction = + Perbill::from_percent(self.proposal.parameters.approval_quorum_percentage); + + actual_votes_fraction.deconstruct() >= approval_quorum_fraction.deconstruct() + } + + // Slashing quorum reached for the proposal. Compares predefined parameter with actual + // votes sum divided by total possible votes count. + pub fn is_slashing_quorum_reached(&self) -> bool { + let actual_votes_fraction = + Perbill::from_rational_approximation(self.votes_count, self.total_voters_count); + let slashing_quorum_fraction = + Perbill::from_percent(self.proposal.parameters.slashing_quorum_percentage); + + actual_votes_fraction.deconstruct() >= slashing_quorum_fraction.deconstruct() + } + + // Approval threshold reached for the proposal. Compares predefined parameter with 'approve' + // votes sum divided by actual votes count. + pub fn is_approval_threshold_reached(&self) -> bool { + let approval_votes_fraction = + Perbill::from_rational_approximation(self.approvals, self.votes_count); + let required_threshold_fraction = + Perbill::from_percent(self.proposal.parameters.approval_threshold_percentage); + + approval_votes_fraction.deconstruct() >= required_threshold_fraction.deconstruct() + } + + // Slashing threshold reached for the proposal. Compares predefined parameter with 'approve' + // votes sum divided by actual votes count. + pub fn is_slashing_threshold_reached(&self) -> bool { + let slashing_votes_fraction = + Perbill::from_rational_approximation(self.slashes, self.votes_count); + let required_threshold_fraction = + Perbill::from_percent(self.proposal.parameters.slashing_threshold_percentage); + + slashing_votes_fraction.deconstruct() >= required_threshold_fraction.deconstruct() + } + + // All voters had voted + pub fn is_voting_completed(&self) -> bool { + self.votes_count == self.total_voters_count + } +} + +/// Proposal executable code wrapper +pub trait ProposalExecutable { + /// Executes proposal code + fn execute(&self) -> dispatch::Result; +} + +/// Proposal code binary converter +pub trait ProposalCodeDecoder { + /// Converts proposal code binary to executable representation + fn decode_proposal( + proposal_type: u32, + proposal_code: Vec, + ) -> Result, &'static str>; +} + +/// Balance alias +pub type BalanceOf = + <::Currency as Currency<::AccountId>>::Balance; + +/// Balance alias for staking +pub type NegativeImbalance = + <::Currency as Currency<::AccountId>>::NegativeImbalance; + +/// Balance type of runtime +pub type CurrencyOf = ::Currency; + +/// Data container for the finalized proposal results +pub(crate) struct FinalizedProposalData< + ProposalId, + BlockNumber, + ProposerId, + Balance, + StakeId, + AccountId, +> { + /// Proposal id + pub proposal_id: ProposalId, + + /// Proposal to be finalized + pub proposal: Proposal, + + /// Proposal finalization status + pub status: ProposalDecisionStatus, + + /// Proposal finalization block number + pub finalized_at: BlockNumber, +} + +/// Data container for the approved proposal results +pub(crate) struct ApprovedProposalData< + ProposalId, + BlockNumber, + ProposerId, + Balance, + StakeId, + AccountId, +> { + /// Proposal id + pub proposal_id: ProposalId, + + /// Proposal to be finalized + pub proposal: Proposal, + + /// Proposal finalisation status data + pub finalisation_status_data: FinalizationData, +} + +#[cfg(test)] +mod tests { + use crate::types::ProposalStatusResolution; + use crate::*; + + // Alias introduced for simplicity of changing Proposal exact types. + type ProposalObject = Proposal; + + #[test] + fn proposal_voting_period_expired() { + let mut proposal = ProposalObject::default(); + + proposal.created_at = 1; + proposal.parameters.voting_period = 3; + + assert!(proposal.is_voting_period_expired(4)); + } + + #[test] + fn proposal_voting_period_not_expired() { + let mut proposal = ProposalObject::default(); + + proposal.created_at = 1; + proposal.parameters.voting_period = 3; + + assert!(!proposal.is_voting_period_expired(3)); + } + + #[test] + fn proposal_grace_period_expired() { + let mut proposal = ProposalObject::default(); + + proposal.parameters.grace_period = 3; + proposal.status = ProposalStatus::finalized_successfully( + ProposalDecisionStatus::Approved(ApprovedProposalStatus::PendingExecution), + 0, + ); + + assert!(proposal.is_grace_period_expired(4)); + } + + #[test] + fn proposal_grace_period_auto_expired() { + let mut proposal = ProposalObject::default(); + + proposal.parameters.grace_period = 0; + proposal.status = ProposalStatus::finalized_successfully( + ProposalDecisionStatus::Approved(ApprovedProposalStatus::PendingExecution), + 0, + ); + + assert!(proposal.is_grace_period_expired(1)); + } + + #[test] + fn proposal_grace_period_not_expired() { + let mut proposal = ProposalObject::default(); + + proposal.parameters.grace_period = 3; + + assert!(!proposal.is_grace_period_expired(3)); + } + + #[test] + fn proposal_grace_period_not_expired_because_of_not_approved_proposal() { + let mut proposal = ProposalObject::default(); + + proposal.parameters.grace_period = 3; + + assert!(!proposal.is_grace_period_expired(3)); + } + + #[test] + fn define_proposal_decision_status_returns_expired() { + let mut proposal = ProposalObject::default(); + let now = 5; + proposal.created_at = 1; + proposal.parameters.voting_period = 3; + proposal.parameters.approval_quorum_percentage = 80; + proposal.parameters.approval_threshold_percentage = 40; + proposal.parameters.slashing_quorum_percentage = 50; + proposal.parameters.slashing_threshold_percentage = 50; + + proposal.voting_results.add_vote(VoteKind::Reject); + proposal.voting_results.add_vote(VoteKind::Approve); + proposal.voting_results.add_vote(VoteKind::Approve); + + assert_eq!( + proposal.voting_results, + VotingResults { + abstentions: 0, + approvals: 2, + rejections: 1, + slashes: 0, + } + ); + + let expected_proposal_status = proposal.define_proposal_decision_status(5, now); + assert_eq!( + expected_proposal_status, + Some(ProposalDecisionStatus::Expired) + ); + } + + #[test] + fn define_proposal_decision_status_returns_approved() { + let now = 2; + let mut proposal = ProposalObject::default(); + proposal.created_at = 1; + proposal.parameters.voting_period = 3; + proposal.parameters.approval_quorum_percentage = 60; + proposal.parameters.slashing_quorum_percentage = 50; + proposal.parameters.slashing_threshold_percentage = 50; + + proposal.voting_results.add_vote(VoteKind::Reject); + proposal.voting_results.add_vote(VoteKind::Approve); + proposal.voting_results.add_vote(VoteKind::Approve); + proposal.voting_results.add_vote(VoteKind::Approve); + + assert_eq!( + proposal.voting_results, + VotingResults { + abstentions: 0, + approvals: 3, + rejections: 1, + slashes: 0, + } + ); + + let expected_proposal_status = proposal.define_proposal_decision_status(5, now); + assert_eq!( + expected_proposal_status, + Some(ProposalDecisionStatus::Approved( + ApprovedProposalStatus::PendingExecution + )) + ); + } + + #[test] + fn define_proposal_decision_status_returns_rejected() { + let mut proposal = ProposalObject::default(); + let now = 2; + + proposal.created_at = 1; + proposal.parameters.voting_period = 3; + proposal.parameters.approval_quorum_percentage = 50; + proposal.parameters.approval_threshold_percentage = 51; + proposal.parameters.slashing_quorum_percentage = 50; + proposal.parameters.slashing_threshold_percentage = 50; + + proposal.voting_results.add_vote(VoteKind::Reject); + proposal.voting_results.add_vote(VoteKind::Reject); + proposal.voting_results.add_vote(VoteKind::Abstain); + proposal.voting_results.add_vote(VoteKind::Approve); + + assert_eq!( + proposal.voting_results, + VotingResults { + abstentions: 1, + approvals: 1, + rejections: 2, + slashes: 0, + } + ); + + let expected_proposal_status = proposal.define_proposal_decision_status(4, now); + assert_eq!( + expected_proposal_status, + Some(ProposalDecisionStatus::Rejected) + ); + } + + #[test] + fn define_proposal_decision_status_returns_slashed() { + let mut proposal = ProposalObject::default(); + let now = 2; + + proposal.created_at = 1; + proposal.parameters.voting_period = 3; + proposal.parameters.approval_quorum_percentage = 50; + proposal.parameters.approval_threshold_percentage = 50; + proposal.parameters.slashing_quorum_percentage = 50; + proposal.parameters.slashing_threshold_percentage = 50; + + proposal.voting_results.add_vote(VoteKind::Slash); + proposal.voting_results.add_vote(VoteKind::Reject); + proposal.voting_results.add_vote(VoteKind::Abstain); + proposal.voting_results.add_vote(VoteKind::Slash); + + assert_eq!( + proposal.voting_results, + VotingResults { + abstentions: 1, + approvals: 0, + rejections: 1, + slashes: 2, + } + ); + + let expected_proposal_status = proposal.define_proposal_decision_status(4, now); + assert_eq!( + expected_proposal_status, + Some(ProposalDecisionStatus::Slashed) + ); + } + + #[test] + fn define_proposal_decision_status_returns_none() { + let mut proposal = ProposalObject::default(); + let now = 2; + + proposal.created_at = 1; + proposal.parameters.voting_period = 3; + proposal.parameters.approval_quorum_percentage = 60; + proposal.parameters.slashing_quorum_percentage = 50; + + proposal.voting_results.add_vote(VoteKind::Abstain); + assert_eq!( + proposal.voting_results, + VotingResults { + abstentions: 1, + approvals: 0, + rejections: 0, + slashes: 0, + } + ); + + let expected_proposal_status = proposal.define_proposal_decision_status(5, now); + assert_eq!(expected_proposal_status, None); + } + + #[test] + fn define_proposal_decision_status_returns_approved_before_slashing_before_rejection() { + let mut proposal = ProposalObject::default(); + let now = 2; + + proposal.created_at = 1; + proposal.parameters.voting_period = 3; + proposal.parameters.approval_quorum_percentage = 50; + proposal.parameters.approval_threshold_percentage = 30; + proposal.parameters.slashing_quorum_percentage = 50; + proposal.parameters.slashing_threshold_percentage = 30; + + proposal.voting_results.add_vote(VoteKind::Approve); + proposal.voting_results.add_vote(VoteKind::Approve); + proposal.voting_results.add_vote(VoteKind::Reject); + proposal.voting_results.add_vote(VoteKind::Reject); + proposal.voting_results.add_vote(VoteKind::Slash); + proposal.voting_results.add_vote(VoteKind::Slash); + + assert_eq!( + proposal.voting_results, + VotingResults { + abstentions: 0, + approvals: 2, + rejections: 2, + slashes: 2, + } + ); + + let expected_proposal_status = proposal.define_proposal_decision_status(6, now); + + assert_eq!( + expected_proposal_status, + Some(ProposalDecisionStatus::Approved( + ApprovedProposalStatus::PendingExecution + )) + ); + } + + #[test] + fn define_proposal_decision_status_returns_slashed_before_rejection() { + let mut proposal = ProposalObject::default(); + let now = 2; + + proposal.created_at = 1; + proposal.parameters.voting_period = 3; + proposal.parameters.approval_quorum_percentage = 50; + proposal.parameters.approval_threshold_percentage = 30; + proposal.parameters.slashing_quorum_percentage = 50; + proposal.parameters.slashing_threshold_percentage = 30; + + proposal.voting_results.add_vote(VoteKind::Abstain); + proposal.voting_results.add_vote(VoteKind::Approve); + proposal.voting_results.add_vote(VoteKind::Reject); + proposal.voting_results.add_vote(VoteKind::Reject); + proposal.voting_results.add_vote(VoteKind::Slash); + proposal.voting_results.add_vote(VoteKind::Slash); + + assert_eq!( + proposal.voting_results, + VotingResults { + abstentions: 1, + approvals: 1, + rejections: 2, + slashes: 2, + } + ); + + let expected_proposal_status = proposal.define_proposal_decision_status(6, now); + + assert_eq!( + expected_proposal_status, + Some(ProposalDecisionStatus::Slashed) + ); + } + + #[test] + fn proposal_status_resolution_approval_quorum_works_correctly() { + let no_approval_quorum_proposal: Proposal = Proposal { + parameters: ProposalParameters { + approval_quorum_percentage: 63, + slashing_threshold_percentage: 63, + ..ProposalParameters::default() + }, + ..Proposal::default() + }; + let no_approval_proposal_status_resolution = ProposalStatusResolution { + proposal: &no_approval_quorum_proposal, + now: 20, + votes_count: 314, + total_voters_count: 500, + approvals: 3, + slashes: 3, + }; + + assert!(!no_approval_proposal_status_resolution.is_approval_quorum_reached()); + + let approval_quorum_proposal_status_resolution = ProposalStatusResolution { + votes_count: 315, + ..no_approval_proposal_status_resolution + }; + + assert!(approval_quorum_proposal_status_resolution.is_approval_quorum_reached()); + } + + #[test] + fn proposal_status_resolution_slashing_quorum_works_correctly() { + let no_slashing_quorum_proposal: Proposal = Proposal { + parameters: ProposalParameters { + approval_quorum_percentage: 63, + slashing_quorum_percentage: 63, + ..ProposalParameters::default() + }, + ..Proposal::default() + }; + let no_slashing_proposal_status_resolution = ProposalStatusResolution { + proposal: &no_slashing_quorum_proposal, + now: 20, + votes_count: 314, + total_voters_count: 500, + approvals: 3, + slashes: 3, + }; + + assert!(!no_slashing_proposal_status_resolution.is_slashing_quorum_reached()); + + let slashing_quorum_proposal_status_resolution = ProposalStatusResolution { + votes_count: 315, + ..no_slashing_proposal_status_resolution + }; + + assert!(slashing_quorum_proposal_status_resolution.is_slashing_quorum_reached()); + } + + #[test] + fn proposal_status_resolution_approval_threshold_works_correctly() { + let no_approval_threshold_proposal: Proposal = Proposal { + parameters: ProposalParameters { + slashing_threshold_percentage: 63, + approval_threshold_percentage: 63, + ..ProposalParameters::default() + }, + ..Proposal::default() + }; + let no_approval_proposal_status_resolution = ProposalStatusResolution { + proposal: &no_approval_threshold_proposal, + now: 20, + votes_count: 500, + total_voters_count: 600, + approvals: 314, + slashes: 3, + }; + + assert!(!no_approval_proposal_status_resolution.is_approval_threshold_reached()); + + let approval_threshold_proposal_status_resolution = ProposalStatusResolution { + approvals: 315, + ..no_approval_proposal_status_resolution + }; + + assert!(approval_threshold_proposal_status_resolution.is_approval_threshold_reached()); + } + + #[test] + fn proposal_status_resolution_slashing_threshold_works_correctly() { + let no_slashing_threshold_proposal: Proposal = Proposal { + parameters: ProposalParameters { + slashing_threshold_percentage: 63, + approval_threshold_percentage: 63, + ..ProposalParameters::default() + }, + ..Proposal::default() + }; + let no_slashing_proposal_status_resolution = ProposalStatusResolution { + proposal: &no_slashing_threshold_proposal, + now: 20, + votes_count: 500, + total_voters_count: 600, + approvals: 3, + slashes: 314, + }; + + assert!(!no_slashing_proposal_status_resolution.is_slashing_threshold_reached()); + + let slashing_threshold_proposal_status_resolution = ProposalStatusResolution { + slashes: 315, + ..no_slashing_proposal_status_resolution + }; + + assert!(slashing_threshold_proposal_status_resolution.is_slashing_threshold_reached()); + } +} diff --git a/runtime-modules/proposals/engine/src/types/proposal_statuses.rs b/runtime-modules/proposals/engine/src/types/proposal_statuses.rs new file mode 100644 index 0000000000..4deb8b647d --- /dev/null +++ b/runtime-modules/proposals/engine/src/types/proposal_statuses.rs @@ -0,0 +1,197 @@ +#![warn(missing_docs)] + +use codec::{Decode, Encode}; +use rstd::prelude::*; + +use crate::ActiveStake; +#[cfg(feature = "std")] +use serde::{Deserialize, Serialize}; + +/// Current status of the proposal +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)] +pub enum ProposalStatus { + /// A new proposal status that is available for voting (with optional stake data). + Active(Option>), + + /// The proposal decision was made. + Finalized(FinalizationData), +} + +impl Default for ProposalStatus { + fn default() -> Self { + ProposalStatus::Active(None) + } +} + +impl ProposalStatus { + /// Creates finalized proposal status with provided ProposalDecisionStatus + pub fn finalized_successfully( + decision_status: ProposalDecisionStatus, + now: BlockNumber, + ) -> ProposalStatus { + Self::finalized(decision_status, None, None, now) + } + + /// Creates finalized proposal status with provided ProposalDecisionStatus and error + pub fn finalized( + decision_status: ProposalDecisionStatus, + encoded_unstaking_error_due_to_broken_runtime: Option<&str>, + active_stake: Option>, + now: BlockNumber, + ) -> ProposalStatus { + // drop the stake information if there were no errors on unstaking + let actual_stake = if encoded_unstaking_error_due_to_broken_runtime.is_some() { + active_stake + } else { + None + }; + ProposalStatus::Finalized(FinalizationData { + proposal_status: decision_status, + encoded_unstaking_error_due_to_broken_runtime: + encoded_unstaking_error_due_to_broken_runtime.map(|err| err.as_bytes().to_vec()), + finalized_at: now, + stake_data_after_unstaking_error: actual_stake, + }) + } + + /// Creates finalized and approved proposal status with provided ApprovedProposalStatus + pub fn approved( + approved_status: ApprovedProposalStatus, + now: BlockNumber, + ) -> ProposalStatus { + ProposalStatus::Finalized(FinalizationData { + proposal_status: ProposalDecisionStatus::Approved(approved_status), + encoded_unstaking_error_due_to_broken_runtime: None, + finalized_at: now, + stake_data_after_unstaking_error: None, + }) + } +} + +/// Final proposal status and potential error. +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)] +pub struct FinalizationData { + /// Final proposal status + pub proposal_status: ProposalDecisionStatus, + + /// Proposal finalization block number + pub finalized_at: BlockNumber, + + /// Error occured during the proposal finalization - unstaking failed in the stake module + pub encoded_unstaking_error_due_to_broken_runtime: Option>, + + /// Stake data for the proposal, filled if the unstaking wasn't successful + pub stake_data_after_unstaking_error: Option>, +} + +impl FinalizationData { + /// FinalizationData helper, creates ApprovedProposalStatus + pub fn create_approved_proposal_status( + self, + approved_status: ApprovedProposalStatus, + ) -> ProposalStatus { + ProposalStatus::Finalized(FinalizationData { + proposal_status: ProposalDecisionStatus::Approved(approved_status), + ..self + }) + } +} + +/// Status of the approved proposal. Defines execution stages. +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)] +pub enum ApprovedProposalStatus { + /// A proposal was approved and grace period is in effect + PendingExecution, + + /// Proposal was successfully executed + Executed, + + /// Proposal was executed and failed with an error + ExecutionFailed { + /// Error message + error: Vec, + }, +} + +impl ApprovedProposalStatus { + /// ApprovedProposalStatus helper, creates ExecutionFailed approved proposal status + pub fn failed_execution(err: &str) -> ApprovedProposalStatus { + ApprovedProposalStatus::ExecutionFailed { + error: err.as_bytes().to_vec(), + } + } +} + +/// Status for the proposal with finalized decision +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)] +pub enum ProposalDecisionStatus { + /// Proposal was withdrawn by its proposer. + Canceled, + + /// Proposal was vetoed by root. + Vetoed, + + /// A proposal was rejected + Rejected, + + /// A proposal was rejected ans its stake should be slashed + Slashed, + + /// Not enough votes and voting period expired. + Expired, + + /// To clear the quorum requirement, the percentage of council members with revealed votes + /// must be no less than the quorum value for the given proposal type. + Approved(ApprovedProposalStatus), +} + +#[cfg(test)] +mod tests { + use crate::{ + ActiveStake, ApprovedProposalStatus, FinalizationData, ProposalDecisionStatus, + ProposalStatus, + }; + + #[test] + fn approved_proposal_status_helper_succeeds() { + let msg = "error"; + + assert_eq!( + ApprovedProposalStatus::failed_execution(&msg), + ApprovedProposalStatus::ExecutionFailed { + error: msg.as_bytes().to_vec() + } + ); + } + + #[test] + fn finalized_proposal_status_helper_succeeds() { + let msg = "error"; + let block_number = 20; + let stake = ActiveStake { + stake_id: 50, + source_account_id: 2, + }; + + let proposal_status = ProposalStatus::finalized( + ProposalDecisionStatus::Slashed, + Some(msg), + Some(stake), + block_number, + ); + + assert_eq!( + ProposalStatus::Finalized(FinalizationData { + proposal_status: ProposalDecisionStatus::Slashed, + finalized_at: block_number, + encoded_unstaking_error_due_to_broken_runtime: Some(msg.as_bytes().to_vec()), + stake_data_after_unstaking_error: Some(stake) + }), + proposal_status + ); + } +} diff --git a/runtime-modules/proposals/engine/src/types/stakes.rs b/runtime-modules/proposals/engine/src/types/stakes.rs new file mode 100644 index 0000000000..88a378981b --- /dev/null +++ b/runtime-modules/proposals/engine/src/types/stakes.rs @@ -0,0 +1,247 @@ +#![warn(missing_docs)] + +use super::{BalanceOf, CurrencyOf, NegativeImbalance}; +use crate::Trait; +use rstd::convert::From; +use rstd::marker::PhantomData; +use rstd::rc::Rc; +use srml_support::traits::{Currency, ExistenceRequirement, WithdrawReasons}; + +// Mocking dependencies for testing +#[cfg(test)] +use mockall::predicate::*; +#[cfg(test)] +use mockall::*; + +/// Returns registered stake handler. This is scaffolds for the mocking of the stake module. +pub trait StakeHandlerProvider { + /// Returns stake logic handler + fn stakes() -> Rc>; +} + +/// Default implementation of the stake module logic provider. Returns actual implementation +/// dependent on the stake module. +pub struct DefaultStakeHandlerProvider; +impl StakeHandlerProvider for DefaultStakeHandlerProvider { + /// Returns stake logic handler + fn stakes() -> Rc> { + Rc::new(DefaultStakeHandler { + marker: PhantomData::::default(), + }) + } +} + +/// Stake logic handler. +#[cfg_attr(test, automock)] // attributes creates mocks in testing environment +pub trait StakeHandler { + /// Creates a stake. Returns created stake id or an error. + fn create_stake(&self) -> Result; + + /// Stake the imbalance + fn stake( + &self, + stake_id: &T::StakeId, + stake_imbalance: NegativeImbalance, + ) -> Result<(), &'static str>; + + /// Removes stake + fn remove_stake(&self, stake_id: T::StakeId) -> Result<(), &'static str>; + + /// Execute unstaking + fn unstake(&self, stake_id: T::StakeId) -> Result<(), &'static str>; + + /// Slash balance from the existing stake + fn slash(&self, stake_id: T::StakeId, slash_balance: BalanceOf) -> Result<(), &'static str>; + + /// Withdraw some balance from the source account and create stake imbalance + fn make_stake_imbalance( + &self, + balance: BalanceOf, + source_account_id: &T::AccountId, + ) -> Result, &'static str>; +} + +/// Default implementation of the stake logic. Uses actual stake module. +/// 'marker' responsible for the 'Trait' binding. +pub(crate) struct DefaultStakeHandler { + pub marker: PhantomData, +} + +impl StakeHandler for DefaultStakeHandler { + /// Creates a stake. Returns created stake id or an error. + fn create_stake(&self) -> Result<::StakeId, &'static str> { + Ok(stake::Module::::create_stake()) + } + + /// Stake the imbalance + fn stake( + &self, + stake_id: &::StakeId, + stake_imbalance: NegativeImbalance, + ) -> Result<(), &'static str> { + stake::Module::::stake(&stake_id, stake_imbalance).map_err(WrappedError)?; + + Ok(()) + } + + /// Removes stake + fn remove_stake(&self, stake_id: ::StakeId) -> Result<(), &'static str> { + stake::Module::::remove_stake(&stake_id).map_err(WrappedError)?; + + Ok(()) + } + + /// Execute unstaking + fn unstake(&self, stake_id: ::StakeId) -> Result<(), &'static str> { + stake::Module::::initiate_unstaking(&stake_id, None).map_err(WrappedError)?; + + Ok(()) + } + + /// Slash balance from the existing stake + fn slash( + &self, + stake_id: ::StakeId, + slash_balance: BalanceOf, + ) -> Result<(), &'static str> { + let _ignored_successful_result = + stake::Module::::slash_immediate(&stake_id, slash_balance, false) + .map_err(WrappedError)?; + + Ok(()) + } + + /// Withdraw some balance from the source account and create stake imbalance + fn make_stake_imbalance( + &self, + balance: BalanceOf, + source_account_id: &T::AccountId, + ) -> Result, &'static str> { + CurrencyOf::::withdraw( + source_account_id, + balance, + WithdrawReasons::all(), + ExistenceRequirement::AllowDeath, + ) + } +} + +/// Proposal implementation of the stake logic. +/// 'marker' responsible for the 'Trait' binding. +pub(crate) struct ProposalStakeManager { + pub marker: PhantomData, +} + +impl ProposalStakeManager { + /// Creates a stake using stake balance and source account. + /// Returns created stake id or an error. + pub fn create_stake( + stake_balance: BalanceOf, + source_account_id: T::AccountId, + ) -> Result { + let stake_id = T::StakeHandlerProvider::stakes().create_stake()?; + + let stake_imbalance = T::StakeHandlerProvider::stakes() + .make_stake_imbalance(stake_balance, &source_account_id)?; + + T::StakeHandlerProvider::stakes().stake(&stake_id, stake_imbalance)?; + + Ok(stake_id) + } + + /// Execute unstaking and removes the stake + pub fn remove_stake(stake_id: T::StakeId) -> Result<(), &'static str> { + T::StakeHandlerProvider::stakes().unstake(stake_id)?; + + T::StakeHandlerProvider::stakes().remove_stake(stake_id)?; + + Ok(()) + } + + /// Slash balance from the existing stake + pub fn slash(stake_id: T::StakeId, slash_balance: BalanceOf) -> Result<(), &'static str> { + T::StakeHandlerProvider::stakes().slash(stake_id, slash_balance) + } +} + +// 'New type' pattern for the error +// https://doc.rust-lang.org/book/ch19-03-advanced-traits.html#using-the-newtype-pattern-to-implement-external-traits-on-external-types +struct WrappedError(E); + +// error conversion for the Wrapped StakeActionError with the inner InitiateUnstakingError +impl From>> for &str { + fn from(wrapper: WrappedError>) -> Self { + { + match wrapper.0 { + stake::StakeActionError::StakeNotFound => "StakeNotFound", + stake::StakeActionError::Error(err) => match err { + stake::InitiateUnstakingError::UnstakingPeriodShouldBeGreaterThanZero => { + "UnstakingPeriodShouldBeGreaterThanZero" + } + stake::InitiateUnstakingError::UnstakingError(e) => match e { + stake::UnstakingError::NotStaked => "NotStaked", + stake::UnstakingError::AlreadyUnstaking => "AlreadyUnstaking", + stake::UnstakingError::CannotUnstakeWhileSlashesOngoing => { + "CannotUnstakeWhileSlashesOngoing" + } + }, + }, + } + } + } +} + +// error conversion for the Wrapped StakeActionError with the inner StakingError +impl From>> for &str { + fn from(wrapper: WrappedError>) -> Self { + { + match wrapper.0 { + stake::StakeActionError::StakeNotFound => "StakeNotFound", + stake::StakeActionError::Error(err) => match err { + stake::StakingError::CannotStakeZero => "CannotStakeZero", + stake::StakingError::CannotStakeLessThanMinimumBalance => { + "CannotStakeLessThanMinimumBalance" + } + stake::StakingError::AlreadyStaked => "AlreadyStaked", + }, + } + } + } +} + +// error conversion for the Wrapped StakeActionError with the inner InitiateSlashingError +impl From>> for &str { + fn from(wrapper: WrappedError>) -> Self { + { + match wrapper.0 { + stake::StakeActionError::StakeNotFound => "StakeNotFound", + stake::StakeActionError::Error(err) => match err { + stake::InitiateSlashingError::NotStaked => "NotStaked", + stake::InitiateSlashingError::SlashPeriodShouldBeGreaterThanZero => { + "SlashPeriodShouldBeGreaterThanZero" + } + stake::InitiateSlashingError::SlashAmountShouldBeGreaterThanZero => { + "SlashAmountShouldBeGreaterThanZero" + } + }, + } + } + } +} + +// error conversion for the Wrapped StakeActionError with the inner ImmediateSlashingError +impl From>> for &str { + fn from(wrapper: WrappedError>) -> Self { + { + match wrapper.0 { + stake::StakeActionError::StakeNotFound => "StakeNotFound", + stake::StakeActionError::Error(err) => match err { + stake::ImmediateSlashingError::NotStaked => "NotStaked", + stake::ImmediateSlashingError::SlashAmountShouldBeGreaterThanZero => { + "SlashAmountShouldBeGreaterThanZero" + } + }, + } + } + } +} diff --git a/runtime-modules/recurring-reward/src/lib.rs b/runtime-modules/recurring-reward/src/lib.rs index 400b3b7b54..df39fb0d09 100755 --- a/runtime-modules/recurring-reward/src/lib.rs +++ b/runtime-modules/recurring-reward/src/lib.rs @@ -1,3 +1,10 @@ +// Clippy linter warning. TODO: remove after the Constaninople release +#![allow(clippy::type_complexity)] +// disable it because of possible frontend API break + +// Clippy linter warning. TODO: refactor the Option> +#![allow(clippy::option_option)] // disable it because of possible API break + // Ensure we're `no_std` when compiling for Wasm. #![cfg_attr(not(feature = "std"), no_std)] use rstd::prelude::*; @@ -7,7 +14,6 @@ use runtime_primitives::traits::{MaybeSerialize, Member, One, SimpleArithmetic, use srml_support::{decl_module, decl_storage, ensure, Parameter}; use minting::{self, BalanceOf}; -use system; mod mock; mod tests; diff --git a/runtime-modules/roles/Cargo.toml b/runtime-modules/roles/Cargo.toml index 78f024c802..4b31781d2d 100644 --- a/runtime-modules/roles/Cargo.toml +++ b/runtime-modules/roles/Cargo.toml @@ -1,6 +1,6 @@ [package] name = 'substrate-roles-module' -version = '1.0.0' +version = '1.0.1' authors = ['Joystream contributors'] edition = '2018' diff --git a/runtime-modules/roles/src/actors.rs b/runtime-modules/roles/src/actors.rs index c702726168..d4aa0c321b 100644 --- a/runtime-modules/roles/src/actors.rs +++ b/runtime-modules/roles/src/actors.rs @@ -1,3 +1,7 @@ +// Clippy linter warning +#![allow(clippy::redundant_closure_call)] // disable it because of the substrate lib design + // example: pub Parameters get(parameters) build(|config: &GenesisConfig| {..} + use codec::{Decode, Encode}; use common::currency::{BalanceOf, GovernanceCurrency}; use rstd::prelude::*; @@ -8,10 +12,14 @@ use srml_support::traits::{ use srml_support::{decl_event, decl_module, decl_storage, ensure}; use system::{self, ensure_root, ensure_signed}; +#[cfg(feature = "std")] +use serde::{Deserialize, Serialize}; + pub use membership::members::Role; const STAKING_ID: LockIdentifier = *b"role_stk"; +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode, Copy, Clone, Eq, PartialEq, Debug)] pub struct RoleParameters { // minium balance required to stake to enter a role @@ -59,7 +67,7 @@ impl, BlockNumber: From> Default for RoleParameters Module { fn remove_actor_from_service(actor_account: T::AccountId, role: Role, member_id: MemberId) { let accounts: Vec = Self::account_ids_by_role(role) .into_iter() - .filter(|account| !(*account == actor_account)) + .filter(|account| *account != actor_account) .collect(); >::insert(role, accounts); let accounts: Vec = Self::account_ids_by_member_id(&member_id) .into_iter() - .filter(|account| !(*account == actor_account)) + .filter(|account| *account != actor_account) .collect(); >::insert(&member_id, accounts); let accounts: Vec = Self::actor_account_ids() .into_iter() - .filter(|account| !(*account == actor_account)) + .filter(|account| *account != actor_account) .collect(); >::put(accounts); @@ -258,7 +266,7 @@ decl_module! { if let Some(params) = Self::parameters(role) { if !(now % params.reward_period == T::BlockNumber::zero()) { continue } let accounts = Self::account_ids_by_role(role); - for actor in accounts.into_iter().map(|account| Self::actor_by_account_id(account)) { + for actor in accounts.into_iter().map(Self::actor_by_account_id) { if let Some(actor) = actor { if now > actor.joined_at + params.reward_period { // reward can top up balance if it is below minimum stake requirement @@ -377,7 +385,7 @@ decl_module! { pub fn set_role_parameters(origin, role: Role, params: RoleParameters, T::BlockNumber>) { ensure_root(origin)?; - let new_stake = params.min_stake.clone(); + let new_stake = params.min_stake; >::insert(role, params); // Update locks for all actors in the role. The lock for each account is already until max_value // It doesn't affect actors which are unbonding, they should have already been removed from AccountIdsByRole diff --git a/runtime-modules/roles/src/traits.rs b/runtime-modules/roles/src/traits.rs index 86e2e45790..876a5d9e2c 100644 --- a/runtime-modules/roles/src/traits.rs +++ b/runtime-modules/roles/src/traits.rs @@ -1,5 +1,4 @@ use crate::actors; -use system; // Roles pub trait Roles { diff --git a/runtime-modules/service-discovery/src/discovery.rs b/runtime-modules/service-discovery/src/discovery.rs index 9c662e782f..7a4a86f9cd 100644 --- a/runtime-modules/service-discovery/src/discovery.rs +++ b/runtime-modules/service-discovery/src/discovery.rs @@ -100,7 +100,7 @@ decl_module! { expires_at: >::block_number() + ttl, }); - Self::deposit_event(RawEvent::AccountInfoUpdated(sender.clone(), id.clone())); + Self::deposit_event(RawEvent::AccountInfoUpdated(sender, id)); } pub fn unset_ipns_id(origin) { diff --git a/runtime-modules/stake/src/lib.rs b/runtime-modules/stake/src/lib.rs index 4c3e38f6dd..572175ef3c 100755 --- a/runtime-modules/stake/src/lib.rs +++ b/runtime-modules/stake/src/lib.rs @@ -12,7 +12,6 @@ use srml_support::traits::{Currency, ExistenceRequirement, Get, Imbalance, Withd use srml_support::{decl_module, decl_storage, ensure, Parameter}; use rstd::collections::btree_map::BTreeMap; -use system; mod errors; pub use errors::*; @@ -378,9 +377,9 @@ where Ok((stake_to_reduce, staked_state.staked_amount)) } - _ => return Err(DecreasingStakeError::CannotDecreaseStakeWhileUnstaking), + _ => Err(DecreasingStakeError::CannotDecreaseStakeWhileUnstaking), }, - _ => return Err(DecreasingStakeError::NotStaked), + _ => Err(DecreasingStakeError::NotStaked), } } @@ -463,11 +462,9 @@ where ); // pause Unstaking if unstaking is active - match staked_state.staked_status { - StakedStatus::Unstaking(ref mut unstaking_state) => { - unstaking_state.is_active = false; - } - _ => (), + if let StakedStatus::Unstaking(ref mut unstaking_state) = staked_state.staked_status + { + unstaking_state.is_active = false; } Ok(slash_id) @@ -523,11 +520,10 @@ where // unpause unstaking on last ongoing slash cancelled if staked_state.ongoing_slashes.is_empty() { - match staked_state.staked_status { - StakedStatus::Unstaking(ref mut unstaking_state) => { - unstaking_state.is_active = true; - } - _ => (), + if let StakedStatus::Unstaking(ref mut unstaking_state) = + staked_state.staked_status + { + unstaking_state.is_active = true; } } diff --git a/runtime-modules/stake/src/tests.rs b/runtime-modules/stake/src/tests.rs index aba815e838..daf23d7c49 100644 --- a/runtime-modules/stake/src/tests.rs +++ b/runtime-modules/stake/src/tests.rs @@ -321,7 +321,7 @@ fn decreasing_stake() { Stake { created: 0, staking_status: StakingStatus::Staked(StakedState { - staked_amount: staked_amount, + staked_amount, ongoing_slashes: BTreeMap::new(), next_slash_id: 0, staked_status: StakedStatus::Normal, @@ -379,7 +379,7 @@ fn initiating_pausing_resuming_cancelling_slashes() { Stake { created: System::block_number(), staking_status: StakingStatus::Staked(StakedState { - staked_amount: staked_amount, + staked_amount, ongoing_slashes: BTreeMap::new(), next_slash_id: 0, staked_status: StakedStatus::Unstaking(UnstakingState { @@ -413,7 +413,7 @@ fn initiating_pausing_resuming_cancelling_slashes() { Stake { created: System::block_number(), staking_status: StakingStatus::Staked(StakedState { - staked_amount: staked_amount, + staked_amount, ongoing_slashes: expected_ongoing_slashes.clone(), next_slash_id: slash_id + 1, staked_status: StakedStatus::Unstaking(UnstakingState { @@ -449,7 +449,7 @@ fn initiating_pausing_resuming_cancelling_slashes() { Stake { created: System::block_number(), staking_status: StakingStatus::Staked(StakedState { - staked_amount: staked_amount, + staked_amount, ongoing_slashes: expected_ongoing_slashes.clone(), next_slash_id: slash_id + 1, staked_status: StakedStatus::Unstaking(UnstakingState { @@ -485,7 +485,7 @@ fn initiating_pausing_resuming_cancelling_slashes() { Stake { created: System::block_number(), staking_status: StakingStatus::Staked(StakedState { - staked_amount: staked_amount, + staked_amount, ongoing_slashes: expected_ongoing_slashes.clone(), next_slash_id: slash_id + 1, staked_status: StakedStatus::Unstaking(UnstakingState { @@ -512,7 +512,7 @@ fn initiating_pausing_resuming_cancelling_slashes() { Stake { created: System::block_number(), staking_status: StakingStatus::Staked(StakedState { - staked_amount: staked_amount, + staked_amount, ongoing_slashes: BTreeMap::new(), next_slash_id: slash_id + 1, staked_status: StakedStatus::Unstaking(UnstakingState { @@ -545,7 +545,7 @@ fn initiating_pausing_resuming_cancelling_slashes() { Stake { created: System::block_number(), staking_status: StakingStatus::Staked(StakedState { - staked_amount: staked_amount, + staked_amount, ongoing_slashes: expected_ongoing_slashes.clone(), next_slash_id: slash_id + 1, staked_status: StakedStatus::Unstaking(UnstakingState { diff --git a/runtime-modules/storage/src/data_directory.rs b/runtime-modules/storage/src/data_directory.rs index a015eb2c55..ebdad8d004 100644 --- a/runtime-modules/storage/src/data_directory.rs +++ b/runtime-modules/storage/src/data_directory.rs @@ -145,9 +145,9 @@ decl_module! { size, added_at: Self::current_block_and_time(), owner: who.clone(), - liaison: liaison, + liaison, liaison_judgement: LiaisonJudgement::Pending, - ipfs_content_id: ipfs_content_id.clone(), + ipfs_content_id, }; >::insert(&content_id, data); @@ -157,14 +157,14 @@ decl_module! { // The LiaisonJudgement can be updated, but only by the liaison. fn accept_content(origin, content_id: T::ContentId) { let who = ensure_signed(origin)?; - Self::update_content_judgement(&who, content_id.clone(), LiaisonJudgement::Accepted)?; + Self::update_content_judgement(&who, content_id, LiaisonJudgement::Accepted)?; >::mutate(|ids| ids.push(content_id)); Self::deposit_event(RawEvent::ContentAccepted(content_id, who)); } fn reject_content(origin, content_id: T::ContentId) { let who = ensure_signed(origin)?; - Self::update_content_judgement(&who, content_id.clone(), LiaisonJudgement::Rejected)?; + Self::update_content_judgement(&who, content_id, LiaisonJudgement::Rejected)?; Self::deposit_event(RawEvent::ContentRejected(content_id, who)); } @@ -198,11 +198,11 @@ decl_module! { impl ContentIdExists for Module { fn has_content(content_id: &T::ContentId) -> bool { - Self::data_object_by_content_id(content_id.clone()).is_some() + Self::data_object_by_content_id(*content_id).is_some() } fn get_data_object(content_id: &T::ContentId) -> Result, &'static str> { - match Self::data_object_by_content_id(content_id.clone()) { + match Self::data_object_by_content_id(*content_id) { Some(data) => Ok(data), None => Err(MSG_CID_NOT_FOUND), } diff --git a/runtime-modules/storage/src/data_object_storage_registry.rs b/runtime-modules/storage/src/data_object_storage_registry.rs index 18593478a6..81d0453a86 100644 --- a/runtime-modules/storage/src/data_object_storage_registry.rs +++ b/runtime-modules/storage/src/data_object_storage_registry.rs @@ -1,3 +1,7 @@ +// Clippy linter requirement +#![allow(clippy::redundant_closure_call)] // disable it because of the substrate lib design + // example: pub NextRelationshipId get(next_relationship_id) build(|config: &GenesisConfig| + use crate::data_directory::Trait as DDTrait; use crate::traits::{ContentHasStorage, ContentIdExists}; use codec::{Codec, Decode, Encode}; @@ -97,27 +101,27 @@ impl ContentHasStorage for Module { // TODO deprecated fn has_storage_provider(which: &T::ContentId) -> bool { let dosr_list = Self::relationships_by_content_id(which); - return dosr_list.iter().any(|&dosr_id| { + dosr_list.iter().any(|&dosr_id| { let res = Self::relationships(dosr_id); if res.is_none() { return false; } let dosr = res.unwrap(); dosr.ready - }); + }) } // TODO deprecated fn is_ready_at_storage_provider(which: &T::ContentId, provider: &T::AccountId) -> bool { let dosr_list = Self::relationships_by_content_id(which); - return dosr_list.iter().any(|&dosr_id| { + dosr_list.iter().any(|&dosr_id| { let res = Self::relationships(dosr_id); if res.is_none() { return false; } let dosr = res.unwrap(); dosr.storage_provider == *provider && dosr.ready - }); + }) } } @@ -138,7 +142,7 @@ decl_module! { // Create new ID, data. let new_id = Self::next_relationship_id(); let dosr: DataObjectStorageRelationship = DataObjectStorageRelationship { - content_id: cid.clone(), + content_id: cid, storage_provider: who.clone(), ready: false, }; @@ -148,9 +152,9 @@ decl_module! { // Also add the DOSR to the list of DOSRs for the CID. Uniqueness is guaranteed // by the map, so we can just append the new_id to the list. - let mut dosr_list = Self::relationships_by_content_id(cid.clone()); + let mut dosr_list = Self::relationships_by_content_id(cid); dosr_list.push(new_id); - >::insert(cid.clone(), dosr_list); + >::insert(cid, dosr_list); // Emit event Self::deposit_event(RawEvent::DataObjectStorageRelationshipAdded(new_id, cid, who)); diff --git a/runtime-modules/storage/src/data_object_type_registry.rs b/runtime-modules/storage/src/data_object_type_registry.rs index 498d968e6e..642ceb4b5b 100644 --- a/runtime-modules/storage/src/data_object_type_registry.rs +++ b/runtime-modules/storage/src/data_object_type_registry.rs @@ -1,3 +1,7 @@ +// Clippy linter requirement +#![allow(clippy::redundant_closure_call)] // disable it because of the substrate lib design + // example: NextDataObjectTypeId get(next_data_object_type_id) build(|config: &GenesisConfig| + use crate::traits; use codec::{Codec, Decode, Encode}; use rstd::prelude::*; @@ -150,7 +154,7 @@ decl_module! { impl Module { fn ensure_data_object_type(id: T::DataObjectTypeId) -> Result { - return Self::data_object_types(&id).ok_or(MSG_DO_TYPE_NOT_FOUND); + Self::data_object_types(&id).ok_or(MSG_DO_TYPE_NOT_FOUND) } } diff --git a/runtime-modules/token-minting/src/lib.rs b/runtime-modules/token-minting/src/lib.rs index b84237708e..1604762b02 100755 --- a/runtime-modules/token-minting/src/lib.rs +++ b/runtime-modules/token-minting/src/lib.rs @@ -1,3 +1,8 @@ +// Clippy linter warning +#![allow(clippy::type_complexity)] +// disable it because of possible frontend API break +// TODO: remove post-Constaninople + // Ensure we're `no_std` when compiling for Wasm. #![cfg_attr(not(feature = "std"), no_std)] @@ -15,8 +20,6 @@ mod tests; pub use mint::*; -use system; - pub trait Trait: system::Trait { /// The currency to mint. type Currency: Currency; @@ -73,6 +76,24 @@ impl From for TransferError { } } +impl From for &'static str { + fn from(err: GeneralError) -> &'static str { + match err { + GeneralError::MintNotFound => "MintNotFound", + GeneralError::NextAdjustmentInPast => "NextAdjustmentInPast", + } + } +} + +impl From for &'static str { + fn from(err: TransferError) -> &'static str { + match err { + TransferError::MintNotFound => "MintNotFound", + TransferError::NotEnoughCapacity => "NotEnoughCapacity", + } + } +} + #[derive(Encode, Decode, Copy, Clone, Debug, Eq, PartialEq)] pub enum Adjustment { // First adjustment will be after AdjustOnInterval.block_interval @@ -120,14 +141,13 @@ impl Module { // 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 - ); - } - _ => (), + if let Adjustment::IntervalAfterFirstAdjustmentAbsolute(_, first_adjustment_in) = + adjustment + { + ensure!( + first_adjustment_in > now, + GeneralError::NextAdjustmentInPast + ); } } diff --git a/runtime-modules/token-minting/src/mint.rs b/runtime-modules/token-minting/src/mint.rs index 9e9443a512..9ec438a1bd 100644 --- a/runtime-modules/token-minting/src/mint.rs +++ b/runtime-modules/token-minting/src/mint.rs @@ -63,7 +63,7 @@ where capacity: initial_capacity, created_at: now, total_minted: Zero::zero(), - next_adjustment: next_adjustment, + next_adjustment, } } diff --git a/runtime-modules/versioned-store-permissions/src/lib.rs b/runtime-modules/versioned-store-permissions/src/lib.rs index 643f4e072f..69ba3d50f2 100755 --- a/runtime-modules/versioned-store-permissions/src/lib.rs +++ b/runtime-modules/versioned-store-permissions/src/lib.rs @@ -6,7 +6,6 @@ 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}; diff --git a/runtime-modules/versioned-store/src/example.rs b/runtime-modules/versioned-store/src/example.rs index a488213886..032d5ff4ab 100644 --- a/runtime-modules/versioned-store/src/example.rs +++ b/runtime-modules/versioned-store/src/example.rs @@ -516,7 +516,7 @@ impl PropHelper { fn next_value(&mut self, value: PropertyValue) -> ClassPropertyValue { let value = ClassPropertyValue { in_class_index: self.prop_idx, - value: value, + value, }; self.prop_idx += 1; value diff --git a/runtime-modules/versioned-store/src/lib.rs b/runtime-modules/versioned-store/src/lib.rs index 0b660d3fe3..310750080e 100755 --- a/runtime-modules/versioned-store/src/lib.rs +++ b/runtime-modules/versioned-store/src/lib.rs @@ -18,7 +18,6 @@ use codec::{Decode, Encode}; use rstd::collections::btree_set::BTreeSet; use rstd::prelude::*; use srml_support::{decl_event, decl_module, decl_storage, dispatch, ensure}; -use system; mod example; mod mock; @@ -645,7 +644,7 @@ impl Module { Self::ensure_prop_value_matches_its_type(value.clone(), prop.clone())?; Self::ensure_valid_internal_prop(value.clone(), prop.clone())?; Self::validate_max_len_if_text_prop(value.clone(), prop.clone())?; - Self::validate_max_len_if_vec_prop(value.clone(), prop.clone())?; + Self::validate_max_len_if_vec_prop(value, prop)?; Ok(()) } @@ -674,7 +673,7 @@ impl Module { vec.len() <= max_len as usize } - fn validate_vec_len_ref(vec: &Vec, max_len: u16) -> bool { + fn validate_vec_len_ref(vec: &[T], max_len: u16) -> bool { vec.len() <= max_len as usize } @@ -702,7 +701,7 @@ impl Module { Self::ensure_known_class_id(class_id)?; if validate_vec_len_ref(&vec, vec_max_len) { for entity_id in vec.iter() { - Self::ensure_known_entity_id(entity_id.clone())?; + Self::ensure_known_entity_id(*entity_id)?; let entity = Self::entity_by_id(entity_id); ensure!(entity.class_id == class_id, ERROR_INTERNAL_RPOP_DOES_NOT_MATCH_ITS_CLASS); } @@ -775,7 +774,7 @@ impl Module { } } - pub fn ensure_property_name_is_valid(text: &Vec) -> dispatch::Result { + pub fn ensure_property_name_is_valid(text: &[u8]) -> dispatch::Result { PropertyNameConstraint::get().ensure_valid( text.len(), ERROR_PROPERTY_NAME_TOO_SHORT, @@ -783,7 +782,7 @@ impl Module { ) } - pub fn ensure_property_description_is_valid(text: &Vec) -> dispatch::Result { + pub fn ensure_property_description_is_valid(text: &[u8]) -> dispatch::Result { PropertyDescriptionConstraint::get().ensure_valid( text.len(), ERROR_PROPERTY_DESCRIPTION_TOO_SHORT, @@ -791,7 +790,7 @@ impl Module { ) } - pub fn ensure_class_name_is_valid(text: &Vec) -> dispatch::Result { + pub fn ensure_class_name_is_valid(text: &[u8]) -> dispatch::Result { ClassNameConstraint::get().ensure_valid( text.len(), ERROR_CLASS_NAME_TOO_SHORT, @@ -799,7 +798,7 @@ impl Module { ) } - pub fn ensure_class_description_is_valid(text: &Vec) -> dispatch::Result { + pub fn ensure_class_description_is_valid(text: &[u8]) -> dispatch::Result { ClassDescriptionConstraint::get().ensure_valid( text.len(), ERROR_CLASS_DESCRIPTION_TOO_SHORT, diff --git a/runtime-modules/versioned-store/src/mock.rs b/runtime-modules/versioned-store/src/mock.rs index b749453a0a..5bd79e3691 100644 --- a/runtime-modules/versioned-store/src/mock.rs +++ b/runtime-modules/versioned-store/src/mock.rs @@ -148,7 +148,7 @@ pub fn bool_prop_value() -> ClassPropertyValue { pub fn prop_value(index: u16, value: PropertyValue) -> ClassPropertyValue { ClassPropertyValue { in_class_index: index, - value: value, + value, } } diff --git a/runtime/CHANGELOG.md b/runtime/CHANGELOG.md index 72fbf612df..ad48410a12 100644 --- a/runtime/CHANGELOG.md +++ b/runtime/CHANGELOG.md @@ -1,5 +1,6 @@ -### Version 6.9.1 -... +### Version 6.13.0 - (Constantinople) runtime upgrade - May 20th 2020 +- New proposal system that strengthens the governance structure of the platform +- Adjusted inflation curve to better reflect a new realistic economic system for the platform ### Version 6.8.0 (Rome release) - March 9th 2020 - New versioned and permissioned content mangement system that powers a new media experience. @@ -35,5 +36,3 @@ - Council Runtime upgrade proposal - Simple PoS validator staking - Memo (account status message) - -Used in genesis block for first testnet (Sparta) diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 25c158357d..aa41d86893 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -5,7 +5,7 @@ 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.1' +version = '6.13.0' [features] default = ['std'] @@ -59,6 +59,9 @@ std = [ 'roles/std', 'service_discovery/std', 'storage/std', + 'proposals_engine/std', + 'proposals_discussion/std', + 'proposals_codex/std', ] # [dependencies] @@ -353,3 +356,21 @@ default_features = false package = 'substrate-storage-module' path = '../runtime-modules/storage' version = '1.0.0' + +[dependencies.proposals_engine] +default_features = false +package = 'substrate-proposals-engine-module' +path = '../runtime-modules/proposals/engine' +version = '2.0.0' + +[dependencies.proposals_discussion] +default_features = false +package = 'substrate-proposals-discussion-module' +path = '../runtime-modules/proposals/discussion' +version = '2.0.0' + +[dependencies.proposals_codex] +default_features = false +package = 'substrate-proposals-codex-module' +path = '../runtime-modules/proposals/codex' +version = '2.0.0' \ No newline at end of file diff --git a/runtime/src/integration/mod.rs b/runtime/src/integration/mod.rs new file mode 100644 index 0000000000..8d18108be0 --- /dev/null +++ b/runtime/src/integration/mod.rs @@ -0,0 +1 @@ +pub mod proposals; diff --git a/runtime/src/integration/proposals/council_elected_handler.rs b/runtime/src/integration/proposals/council_elected_handler.rs new file mode 100644 index 0000000000..24a5e5f360 --- /dev/null +++ b/runtime/src/integration/proposals/council_elected_handler.rs @@ -0,0 +1,14 @@ +#![warn(missing_docs)] + +use crate::Runtime; +use governance::election::CouncilElected; + +/// 'Council elected' event handler. Should be applied to the 'election' substrate module. +/// CouncilEvent is handled by resetting active proposals. +pub struct CouncilElectedHandler; + +impl CouncilElected for CouncilElectedHandler { + fn council_elected(_new_council: Elected, _term: Term) { + >::reset_active_proposals(); + } +} diff --git a/runtime/src/integration/proposals/council_origin_validator.rs b/runtime/src/integration/proposals/council_origin_validator.rs new file mode 100644 index 0000000000..cd53f83f30 --- /dev/null +++ b/runtime/src/integration/proposals/council_origin_validator.rs @@ -0,0 +1,208 @@ +#![warn(missing_docs)] + +use rstd::marker::PhantomData; + +use common::origin_validator::ActorOriginValidator; +use proposals_engine::VotersParameters; + +use super::{MemberId, MembershipOriginValidator}; + +/// Handles work with the council. +/// Provides implementations for ActorOriginValidator and VotersParameters. +pub struct CouncilManager { + marker: PhantomData, +} + +impl + ActorOriginValidator<::Origin, MemberId, ::AccountId> + for CouncilManager +{ + /// Check for valid combination of origin and actor_id. Actor_id should be valid member_id of + /// the membership module + fn ensure_actor_origin( + origin: ::Origin, + actor_id: MemberId, + ) -> Result<::AccountId, &'static str> { + let account_id = >::ensure_actor_origin(origin, actor_id)?; + + if >::is_councilor(&account_id) { + return Ok(account_id); + } + + Err("Council validation failed: account id doesn't belong to a council member") + } +} + +impl VotersParameters for CouncilManager { + /// Implement total_voters_count() as council size + fn total_voters_count() -> u32 { + >::active_council().len() as u32 + } +} + +#[cfg(test)] +mod tests { + use super::CouncilManager; + use crate::Runtime; + use common::origin_validator::ActorOriginValidator; + use membership::members::UserInfo; + use proposals_engine::VotersParameters; + use sr_primitives::AccountId32; + use system::RawOrigin; + + type Council = governance::council::Module; + + fn initial_test_ext() -> runtime_io::TestExternalities { + let t = system::GenesisConfig::default() + .build_storage::() + .unwrap(); + + t.into() + } + + type Membership = membership::members::Module; + + #[test] + fn council_origin_validator_fails_with_unregistered_member() { + initial_test_ext().execute_with(|| { + let origin = RawOrigin::Signed(AccountId32::default()); + let member_id = 1; + let error = "Membership validation failed: cannot find a profile for a member"; + + let validation_result = + CouncilManager::::ensure_actor_origin(origin.into(), member_id); + + assert_eq!(validation_result, Err(error)); + }); + } + + #[test] + fn council_origin_validator_succeeds() { + initial_test_ext().execute_with(|| { + let councilor1 = AccountId32::default(); + let councilor2: [u8; 32] = [2; 32]; + let councilor3: [u8; 32] = [3; 32]; + + assert!(Council::set_council( + system::RawOrigin::Root.into(), + vec![councilor1, councilor2.into(), councilor3.into()] + ) + .is_ok()); + + let account_id = AccountId32::default(); + let origin = RawOrigin::Signed(account_id.clone()); + let authority_account_id = AccountId32::default(); + Membership::set_screening_authority( + RawOrigin::Root.into(), + authority_account_id.clone(), + ) + .unwrap(); + + Membership::add_screened_member( + RawOrigin::Signed(authority_account_id).into(), + account_id.clone(), + UserInfo { + handle: Some(b"handle".to_vec()), + avatar_uri: None, + about: None, + }, + ) + .unwrap(); + let member_id = 0; // newly created member_id + + let validation_result = + CouncilManager::::ensure_actor_origin(origin.into(), member_id); + + assert_eq!(validation_result, Ok(account_id)); + }); + } + + #[test] + fn council_origin_validator_fails_with_incompatible_account_id_and_member_id() { + initial_test_ext().execute_with(|| { + let account_id = AccountId32::default(); + let error = + "Membership validation failed: given account doesn't match with profile accounts"; + let authority_account_id = AccountId32::default(); + Membership::set_screening_authority( + RawOrigin::Root.into(), + authority_account_id.clone(), + ) + .unwrap(); + + Membership::add_screened_member( + RawOrigin::Signed(authority_account_id).into(), + account_id.clone(), + UserInfo { + handle: Some(b"handle".to_vec()), + avatar_uri: None, + about: None, + }, + ) + .unwrap(); + let member_id = 0; // newly created member_id + + let invalid_account_id: [u8; 32] = [2; 32]; + let validation_result = CouncilManager::::ensure_actor_origin( + RawOrigin::Signed(invalid_account_id.into()).into(), + member_id, + ); + + assert_eq!(validation_result, Err(error)); + }); + } + + #[test] + fn council_origin_validator_fails_with_not_council_account_id() { + initial_test_ext().execute_with(|| { + let account_id = AccountId32::default(); + let origin = RawOrigin::Signed(account_id.clone()); + let error = "Council validation failed: account id doesn't belong to a council member"; + let authority_account_id = AccountId32::default(); + Membership::set_screening_authority( + RawOrigin::Root.into(), + authority_account_id.clone(), + ) + .unwrap(); + + Membership::add_screened_member( + RawOrigin::Signed(authority_account_id).into(), + account_id, + UserInfo { + handle: Some(b"handle".to_vec()), + avatar_uri: None, + about: None, + }, + ) + .unwrap(); + let member_id = 0; // newly created member_id + + let validation_result = + CouncilManager::::ensure_actor_origin(origin.into(), member_id); + + assert_eq!(validation_result, Err(error)); + }); + } + + #[test] + fn council_size_calculation_aka_total_voters_count_succeeds() { + initial_test_ext().execute_with(|| { + let councilor1 = AccountId32::default(); + let councilor2: [u8; 32] = [2; 32]; + let councilor3: [u8; 32] = [3; 32]; + let councilor4: [u8; 32] = [4; 32]; + assert!(Council::set_council( + system::RawOrigin::Root.into(), + vec![ + councilor1, + councilor2.into(), + councilor3.into(), + councilor4.into() + ] + ) + .is_ok()); + + assert_eq!(CouncilManager::::total_voters_count(), 4) + }); + } +} diff --git a/runtime/src/integration/proposals/membership_origin_validator.rs b/runtime/src/integration/proposals/membership_origin_validator.rs new file mode 100644 index 0000000000..abe0a0a8be --- /dev/null +++ b/runtime/src/integration/proposals/membership_origin_validator.rs @@ -0,0 +1,143 @@ +#![warn(missing_docs)] + +use rstd::marker::PhantomData; + +use common::origin_validator::ActorOriginValidator; +use system::ensure_signed; + +/// Member of the Joystream organization +pub type MemberId = ::MemberId; + +/// Default membership actor origin validator. +pub struct MembershipOriginValidator { + marker: PhantomData, +} + +impl + ActorOriginValidator<::Origin, MemberId, ::AccountId> + for MembershipOriginValidator +{ + /// Check for valid combination of origin and actor_id. Actor_id should be valid member_id of + /// the membership module + fn ensure_actor_origin( + origin: ::Origin, + actor_id: MemberId, + ) -> Result<::AccountId, &'static str> { + // check valid signed account_id + let account_id = ensure_signed(origin)?; + + // check whether actor_id belongs to the registered member + let profile_result = >::ensure_profile(actor_id); + + if let Ok(profile) = profile_result { + // whether the account_id belongs to the actor + if profile.controller_account == account_id { + return Ok(account_id); + } else { + return Err("Membership validation failed: given account doesn't match with profile accounts"); + } + } + + Err("Membership validation failed: cannot find a profile for a member") + } +} + +#[cfg(test)] +mod tests { + use super::MembershipOriginValidator; + use crate::Runtime; + use common::origin_validator::ActorOriginValidator; + use membership::members::UserInfo; + use sr_primitives::AccountId32; + use system::RawOrigin; + + type Membership = crate::members::Module; + + fn initial_test_ext() -> runtime_io::TestExternalities { + let t = system::GenesisConfig::default() + .build_storage::() + .unwrap(); + + t.into() + } + + #[test] + fn membership_origin_validator_fails_with_unregistered_member() { + initial_test_ext().execute_with(|| { + let origin = RawOrigin::Signed(AccountId32::default()); + let member_id = 1; + let error = "Membership validation failed: cannot find a profile for a member"; + + let validation_result = + MembershipOriginValidator::::ensure_actor_origin(origin.into(), member_id); + + assert_eq!(validation_result, Err(error)); + }); + } + + #[test] + fn membership_origin_validator_succeeds() { + initial_test_ext().execute_with(|| { + let account_id = AccountId32::default(); + let origin = RawOrigin::Signed(account_id.clone()); + let authority_account_id = AccountId32::default(); + Membership::set_screening_authority( + RawOrigin::Root.into(), + authority_account_id.clone(), + ) + .unwrap(); + + Membership::add_screened_member( + RawOrigin::Signed(authority_account_id).into(), + account_id.clone(), + UserInfo { + handle: Some(b"handle".to_vec()), + avatar_uri: None, + about: None, + }, + ) + .unwrap(); + let member_id = 0; // newly created member_id + + let validation_result = + MembershipOriginValidator::::ensure_actor_origin(origin.into(), member_id); + + assert_eq!(validation_result, Ok(account_id)); + }); + } + + #[test] + fn membership_origin_validator_fails_with_incompatible_account_id_and_member_id() { + initial_test_ext().execute_with(|| { + let account_id = AccountId32::default(); + let error = + "Membership validation failed: given account doesn't match with profile accounts"; + let authority_account_id = AccountId32::default(); + Membership::set_screening_authority( + RawOrigin::Root.into(), + authority_account_id.clone(), + ) + .unwrap(); + + Membership::add_screened_member( + RawOrigin::Signed(authority_account_id).into(), + account_id, + UserInfo { + handle: Some(b"handle".to_vec()), + avatar_uri: None, + about: None, + }, + ) + .unwrap(); + let member_id = 0; // newly created member_id + + let invalid_account_id: [u8; 32] = [2; 32]; + let validation_result = MembershipOriginValidator::::ensure_actor_origin( + RawOrigin::Signed(invalid_account_id.into()).into(), + member_id, + ); + + assert_eq!(validation_result, Err(error)); + }); + } +} diff --git a/runtime/src/integration/proposals/mod.rs b/runtime/src/integration/proposals/mod.rs new file mode 100644 index 0000000000..47627fa1fb --- /dev/null +++ b/runtime/src/integration/proposals/mod.rs @@ -0,0 +1,13 @@ +#![warn(missing_docs)] + +mod council_elected_handler; +mod council_origin_validator; +mod membership_origin_validator; +mod proposal_encoder; +mod staking_events_handler; + +pub use council_elected_handler::CouncilElectedHandler; +pub use council_origin_validator::CouncilManager; +pub use membership_origin_validator::{MemberId, MembershipOriginValidator}; +pub use proposal_encoder::ExtrinsicProposalEncoder; +pub use staking_events_handler::StakingEventsHandler; diff --git a/runtime/src/integration/proposals/proposal_encoder.rs b/runtime/src/integration/proposals/proposal_encoder.rs new file mode 100644 index 0000000000..7069f28af9 --- /dev/null +++ b/runtime/src/integration/proposals/proposal_encoder.rs @@ -0,0 +1,51 @@ +use crate::{Call, Runtime}; +use proposals_codex::{ProposalDetails, ProposalDetailsOf, ProposalEncoder}; +use roles::actors::Role; + +use codec::Encode; +use rstd::vec::Vec; + +/// _ProposalEncoder_ implementation. It encodes extrinsics with proposal details parameters +/// using Runtime Call and parity codec. +pub struct ExtrinsicProposalEncoder; +impl ProposalEncoder for ExtrinsicProposalEncoder { + fn encode_proposal(proposal_details: ProposalDetailsOf) -> Vec { + match proposal_details { + ProposalDetails::Text(text) => { + Call::ProposalsCodex(proposals_codex::Call::execute_text_proposal(text)).encode() + } + ProposalDetails::SetElectionParameters(election_parameters) => Call::CouncilElection( + governance::election::Call::set_election_parameters(election_parameters), + ) + .encode(), + ProposalDetails::SetContentWorkingGroupMintCapacity(mint_balance) => { + Call::ContentWorkingGroup(content_working_group::Call::set_mint_capacity( + mint_balance, + )) + .encode() + } + ProposalDetails::Spending(balance, destination) => Call::Council( + governance::council::Call::spend_from_council_mint(balance, destination), + ) + .encode(), + ProposalDetails::SetLead(new_lead) => { + Call::ContentWorkingGroup(content_working_group::Call::replace_lead(new_lead)) + .encode() + } + ProposalDetails::EvictStorageProvider(actor_account) => { + Call::Actors(roles::actors::Call::remove_actor(actor_account)).encode() + } + ProposalDetails::SetValidatorCount(new_validator_count) => { + Call::Staking(staking::Call::set_validator_count(new_validator_count)).encode() + } + ProposalDetails::SetStorageRoleParameters(role_parameters) => Call::Actors( + roles::actors::Call::set_role_parameters(Role::StorageProvider, role_parameters), + ) + .encode(), + ProposalDetails::RuntimeUpgrade(wasm_code) => Call::ProposalsCodex( + proposals_codex::Call::execute_runtime_upgrade_proposal(wasm_code), + ) + .encode(), + } + } +} diff --git a/runtime/src/integration/proposals/staking_events_handler.rs b/runtime/src/integration/proposals/staking_events_handler.rs new file mode 100644 index 0000000000..8adbbb0843 --- /dev/null +++ b/runtime/src/integration/proposals/staking_events_handler.rs @@ -0,0 +1,49 @@ +#![warn(missing_docs)] + +use rstd::marker::PhantomData; +use srml_support::traits::{Currency, Imbalance}; +use srml_support::StorageMap; + +// Balance alias +type BalanceOf = + <::Currency as Currency<::AccountId>>::Balance; + +// Balance alias for staking +type NegativeImbalance = + <::Currency as Currency<::AccountId>>::NegativeImbalance; + +/// Proposal implementation of the staking event handler from the stake module. +/// 'marker' responsible for the 'Trait' binding. +pub struct StakingEventsHandler { + pub marker: PhantomData, +} + +impl stake::StakingEventsHandler + for StakingEventsHandler +{ + /// Unstake remaining sum back to the source_account_id + fn unstaked( + id: &::StakeId, + _unstaked_amount: BalanceOf, + remaining_imbalance: NegativeImbalance, + ) -> NegativeImbalance { + if >::exists(id) { + >::refund_proposal_stake(*id, remaining_imbalance); + + return >::zero(); // imbalance was consumed + } + + remaining_imbalance + } + + /// Empty handler for slashing + fn slashed( + _: &::StakeId, + _: Option<::SlashId>, + _: BalanceOf, + _: BalanceOf, + remaining_imbalance: NegativeImbalance, + ) -> NegativeImbalance { + remaining_imbalance + } +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index d8ed411c8c..4bae8353eb 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1,8 +1,14 @@ -//! The Substrate Node Template runtime. This can be compiled with `#[no_std]`, ready for Wasm. +//! The Joystream Substrate Node runtime. #![cfg_attr(not(feature = "std"), no_std)] // `construct_runtime!` does a lot of recursion and requires us to increase the limit to 256. #![recursion_limit = "256"] +// srml_staking_reward_curve::build! - substrate macro produces a warning. +// TODO: remove after post-Rome substrate upgrade +#![allow(array_into_iter)] + +// Runtime integration tests +mod test; // Make the WASM binary available. // This is required only by the node build. @@ -10,6 +16,8 @@ #[cfg(feature = "std")] include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); +mod integration; + use authority_discovery_primitives::{ AuthorityId as EncodedAuthorityId, Signature as EncodedSignature, }; @@ -51,6 +59,9 @@ pub use srml_support::{ pub use staking::StakerStatus; pub use timestamp::Call as TimestampCall; +use integration::proposals::{CouncilManager, ExtrinsicProposalEncoder, MembershipOriginValidator}; +pub use proposals_codex::ProposalsConfigParameters; + /// An index to a block. pub type BlockNumber = u32; @@ -83,6 +94,22 @@ pub type Moment = u64; /// Credential type pub type Credential = u64; +/// Represents a thread identifier for both Forum and Proposals Discussion +/// +/// Note: Both modules expose type names ThreadId and PostId (which are defined on their Trait) and +/// used in state storage and dispatchable method's argument types, +/// and are therefore part of the public API/metadata of the runtime. +/// In the currenlty version the polkadot-js/api that is used and is compatible with the runtime, +/// the type registry has flat namespace and its not possible +/// to register identically named types from different modules, separately. And so we MUST configure +/// the underlying types to be identicaly to avoid issues with encoding/decoding these types on the client side. +pub type ThreadId = u64; + +/// Represents a post identifier for both Forum and Proposals Discussion +/// +/// See the Note about ThreadId +pub type PostId = u64; + /// Opaque types. These are used by the CLI to instantiate machinery that don't need to know /// the specifics of the runtime. They can then be made to be agnostic over specific formats /// of data like extrinsics, allowing for them to continue syncing the network through upgrades @@ -115,7 +142,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("joystream-node"), impl_name: create_runtime_str!("joystream-node"), authoring_version: 6, - spec_version: 8, + spec_version: 13, impl_version: 0, apis: RUNTIME_API_VERSIONS, }; @@ -165,7 +192,7 @@ pub fn native_version() -> NativeVersion { parameter_types! { pub const BlockHashCount: BlockNumber = 250; - pub const MaximumBlockWeight: Weight = 1_000_000; + pub const MaximumBlockWeight: Weight = 1_000_000_000; pub const AvailableBlockRatio: Perbill = Perbill::from_percent(75); pub const MaximumBlockLength: u32 = 5 * 1024 * 1024; pub const Version: RuntimeVersion = VERSION; @@ -331,17 +358,17 @@ impl session::historical::Trait for Runtime { srml_staking_reward_curve::build! { const REWARD_CURVE: PiecewiseLinear<'static> = curve!( min_inflation: 0_025_000, - max_inflation: 0_100_000, - ideal_stake: 0_500_000, + max_inflation: 0_300_000, + ideal_stake: 0_300_000, falloff: 0_050_000, - max_piece_count: 40, + max_piece_count: 100, test_precision: 0_005_000, ); } parameter_types! { pub const SessionsPerEra: sr_staking_primitives::SessionIndex = 6; - pub const BondingDuration: staking::EraIndex = 24 * 28; + pub const BondingDuration: staking::EraIndex = 24; pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE; } @@ -396,20 +423,16 @@ impl finality_tracker::Trait for Runtime { } pub use forum; -use governance::{council, election, proposals}; +pub use governance::election_params::ElectionParameters; +use governance::{council, election}; use membership::members; use storage::{data_directory, data_object_storage_registry, data_object_type_registry}; pub use versioned_store; -use versioned_store_permissions; pub use content_working_group as content_wg; mod migration; -use hiring; -use minting; -use recurringrewards; use roles::actors; use service_discovery::discovery; -use stake; /// Alias for ContentId, used in various places. pub type ContentId = primitives::H256; @@ -472,7 +495,7 @@ impl versioned_store_permissions::CredentialChecker for ContentWorkingG } } - return false; + false } // Any Active Channel Owner credential if credential == AnyActiveChannelOwnerCredential::get() => { @@ -487,7 +510,7 @@ impl versioned_store_permissions::CredentialChecker for ContentWorkingG } } - return false; + false } // mapping to workging group principal id n if n >= PrincipalIdMappingStartsAtCredential::get() => { @@ -577,7 +600,10 @@ parameter_types! { impl stake::Trait for Runtime { type Currency = ::Currency; type StakePoolId = StakePoolId; - type StakingEventsHandler = ContentWorkingGroupStakingEventHandler; + type StakingEventsHandler = ( + ContentWorkingGroupStakingEventHandler, + crate::integration::proposals::StakingEventsHandler, + ); type StakeId = u64; type SlashId = u64; } @@ -657,13 +683,9 @@ impl common::currency::GovernanceCurrency for Runtime { type Currency = balances::Module; } -impl governance::proposals::Trait for Runtime { - type Event = Event; -} - impl governance::election::Trait for Runtime { type Event = Event; - type CouncilElected = (Council,); + type CouncilElected = (Council, integration::proposals::CouncilElectedHandler); } impl governance::council::Trait for Runtime { @@ -727,7 +749,7 @@ impl roles::traits::Roles for LookupRoles { .filter(|id| !>::is_account_info_expired(id)) .collect(); - if live_ids.len() == 0 { + if live_ids.is_empty() { Err("no staked account found") } else { let index = random_index(live_ids.len()); @@ -778,6 +800,8 @@ impl forum::ForumUserRegistry for ShimMembershipRegistry { impl forum::Trait for Runtime { type Event = Event; type MembershipRegistry = ShimMembershipRegistry; + type ThreadId = ThreadId; + type PostId = PostId; } impl migration::Trait for Runtime { @@ -801,12 +825,70 @@ impl discovery::Trait for Runtime { type Roles = LookupRoles; } +parameter_types! { + pub const ProposalCancellationFee: u64 = 10000; + pub const ProposalRejectionFee: u64 = 5000; + pub const ProposalTitleMaxLength: u32 = 40; + pub const ProposalDescriptionMaxLength: u32 = 3000; + pub const ProposalMaxActiveProposalLimit: u32 = 5; +} + +impl proposals_engine::Trait for Runtime { + type Event = Event; + type ProposerOriginValidator = MembershipOriginValidator; + type VoterOriginValidator = CouncilManager; + type TotalVotersCounter = CouncilManager; + type ProposalId = u32; + type StakeHandlerProvider = proposals_engine::DefaultStakeHandlerProvider; + type CancellationFee = ProposalCancellationFee; + type RejectionFee = ProposalRejectionFee; + type TitleMaxLength = ProposalTitleMaxLength; + type DescriptionMaxLength = ProposalDescriptionMaxLength; + type MaxActiveProposalLimit = ProposalMaxActiveProposalLimit; + type DispatchableCallCode = Call; +} +impl Default for Call { + fn default() -> Self { + panic!("shouldn't call default for Call"); + } +} + +parameter_types! { + pub const ProposalMaxPostEditionNumber: u32 = 0; // post update is disabled + pub const ProposalMaxThreadInARowNumber: u32 = 100_000; // will not be used + pub const ProposalThreadTitleLengthLimit: u32 = 40; + pub const ProposalPostLengthLimit: u32 = 1000; +} + +impl proposals_discussion::Trait for Runtime { + type Event = Event; + type PostAuthorOriginValidator = MembershipOriginValidator; + type ThreadId = ThreadId; + type PostId = PostId; + type MaxPostEditionNumber = ProposalMaxPostEditionNumber; + type ThreadTitleLengthLimit = ProposalThreadTitleLengthLimit; + type PostLengthLimit = ProposalPostLengthLimit; + type MaxThreadInARowNumber = ProposalMaxThreadInARowNumber; +} + +parameter_types! { + pub const TextProposalMaxLength: u32 = 5_000; + pub const RuntimeUpgradeWasmProposalMaxLength: u32 = 2_000_000; +} + +impl proposals_codex::Trait for Runtime { + type MembershipOriginValidator = MembershipOriginValidator; + type TextProposalMaxLength = TextProposalMaxLength; + type RuntimeUpgradeWasmProposalMaxLength = RuntimeUpgradeWasmProposalMaxLength; + type ProposalEncoder = ExtrinsicProposalEncoder; +} + construct_runtime!( - pub enum Runtime where - Block = Block, - NodeBlock = opaque::Block, - UncheckedExtrinsic = UncheckedExtrinsic - { + pub enum Runtime where + Block = Block, + NodeBlock = opaque::Block, + UncheckedExtrinsic = UncheckedExtrinsic + { // Substrate System: system::{Module, Call, Storage, Config, Event}, Babe: babe::{Module, Call, Storage, Config, Inherent(Timestamp)}, @@ -825,13 +907,12 @@ construct_runtime!( RandomnessCollectiveFlip: randomness_collective_flip::{Module, Call, Storage}, Sudo: sudo, // Joystream - Proposals: proposals::{Module, Call, Storage, Event, Config}, + Migration: migration::{Module, Call, Storage, Event, Config}, CouncilElection: election::{Module, Call, Storage, Event, Config}, Council: council::{Module, Call, Storage, Event, Config}, Memo: memo::{Module, Call, Storage, Event}, Members: members::{Module, Call, Storage, Event, Config}, Forum: forum::{Module, Call, Storage, Event, Config}, - Migration: migration::{Module, Call, Storage, Event}, Actors: actors::{Module, Call, Storage, Event, Config}, DataObjectTypeRegistry: data_object_type_registry::{Module, Call, Storage, Event, Config}, DataDirectory: data_directory::{Module, Call, Storage, Event}, @@ -844,7 +925,12 @@ construct_runtime!( RecurringRewards: recurringrewards::{Module, Call, Storage}, Hiring: hiring::{Module, Call, Storage}, ContentWorkingGroup: content_wg::{Module, Call, Storage, Event, Config}, - } + // --- Proposals + ProposalsEngine: proposals_engine::{Module, Call, Storage, Event}, + ProposalsDiscussion: proposals_discussion::{Module, Call, Storage, Event}, + ProposalsCodex: proposals_codex::{Module, Call, Storage, Error, Config}, + // --- + } ); /// The address format for describing accounts. diff --git a/runtime/src/migration.rs b/runtime/src/migration.rs index fc4bd421a9..7d9e76409a 100644 --- a/runtime/src/migration.rs +++ b/runtime/src/migration.rs @@ -1,50 +1,60 @@ -use crate::VERSION; -use sr_primitives::print; -use srml_support::{decl_event, decl_module, decl_storage}; -use sudo; -use system; +// Clippy linter warning +#![allow(clippy::redundant_closure_call)] // disable it because of the substrate lib design -// When preparing a new major runtime release version bump this value to match it and update -// the initialization code in runtime_initialization(). Because of the way substrate runs runtime code -// the runtime doesn't need to maintain any logic for old migrations. All knowledge about state of the chain and runtime -// prior to the new runtime taking over is implicit in the migration code implementation. If assumptions are incorrect -// behaviour is undefined. -const MIGRATION_FOR_SPEC_VERSION: u32 = 0; +use crate::VERSION; +use rstd::prelude::*; +use sr_primitives::{print, traits::Zero}; +use srml_support::{debug, decl_event, decl_module, decl_storage}; impl Module { - fn runtime_initialization() { - if VERSION.spec_version != MIGRATION_FOR_SPEC_VERSION { - return; - } + /// This method is called from on_initialize() when a runtime upgrade is detected. This + /// happens when the runtime spec version is found to be higher than the stored value. + /// Important to note this method should be carefully maintained, because it runs on every runtime + /// upgrade. + fn runtime_upgraded() { + print("Running runtime upgraded handler"); - print("running runtime initializers"); + // Add initialization of modules introduced in new runtime release. Typically this + // would be any new storage values that need an initial value which would not + // have been initialized with config() or build() chainspec construction mechanism. + // Other tasks like resetting values, migrating values etc. - // ... - // add initialization of other modules introduced in this runtime - // ... + // Runtime Upgrade Code for going from Rome to Constantinople - Self::deposit_event(RawEvent::Migrated( - >::block_number(), - VERSION.spec_version, - )); + // Create the Council mint. If it fails, we can't do anything about it here. + if let Err(err) = governance::council::Module::::create_new_council_mint( + minting::BalanceOf::::zero(), + ) { + debug::warn!( + "Failed to create a mint for council during migration: {:?}", + err + ); + } + + // Initialise the proposal system various periods + proposals_codex::Module::::set_default_config_values(); } } pub trait Trait: system::Trait - + storage::data_directory::Trait - + storage::data_object_storage_registry::Trait - + forum::Trait - + sudo::Trait + + governance::election::Trait + + content_working_group::Trait + + roles::actors::Trait + + proposals_codex::Trait { type Event: From> + Into<::Event>; } decl_storage! { trait Store for Module as Migration { - /// Records at what runtime spec version the store was initialized. This allows the runtime - /// to know when to run initialize code if it was installed as an update. - pub SpecVersion get(spec_version) build(|_| VERSION.spec_version) : Option; + /// Records at what runtime spec version the store was initialized. At genesis this will be + /// initialized to Some(VERSION.spec_version). It is an Option because the first time the module + /// was introduced was as a runtime upgrade and type was never changed. + /// When the runtime is upgraded the spec version be updated. + pub SpecVersion get(spec_version) build(|_config: &GenesisConfig| { + VERSION.spec_version + }) : Option; } } @@ -60,11 +70,16 @@ decl_module! { fn on_initialize(_now: T::BlockNumber) { if Self::spec_version().map_or(true, |spec_version| VERSION.spec_version > spec_version) { - // mark store version with current version of the runtime + // Mark store version with current version of the runtime SpecVersion::put(VERSION.spec_version); - // run migrations and store initializers - Self::runtime_initialization(); + // Run migrations and store initializers + Self::runtime_upgraded(); + + Self::deposit_event(RawEvent::Migrated( + >::block_number(), + VERSION.spec_version, + )); } } } diff --git a/runtime/src/test/mod.rs b/runtime/src/test/mod.rs new file mode 100644 index 0000000000..46c37939e8 --- /dev/null +++ b/runtime/src/test/mod.rs @@ -0,0 +1,5 @@ +//! The Joystream Substrate Node runtime integration tests. + +#![cfg(test)] + +mod proposals_integration; diff --git a/runtime/src/test/proposals_integration.rs b/runtime/src/test/proposals_integration.rs new file mode 100644 index 0000000000..7cba3adf0c --- /dev/null +++ b/runtime/src/test/proposals_integration.rs @@ -0,0 +1,734 @@ +//! Proposals integration tests - with stake, membership, governance modules. + +#![cfg(test)] + +use crate::{BlockNumber, ElectionParameters, ProposalCancellationFee, Runtime}; +use codec::Encode; +use governance::election::CouncilElected; +use membership::members; +use membership::role_types::Role; +use proposals_engine::{ + ActiveStake, ApprovedProposalStatus, BalanceOf, Error, FinalizationData, Proposal, + ProposalDecisionStatus, ProposalParameters, ProposalStatus, VoteKind, VotersParameters, + VotingResults, +}; +use roles::actors::RoleParameters; + +use sr_primitives::traits::{DispatchResult, OnFinalize, OnInitialize}; +use sr_primitives::AccountId32; +use srml_support::traits::Currency; +use srml_support::{StorageLinkedMap, StorageMap, StorageValue}; +use system::RawOrigin; + +use crate::CouncilManager; + +fn initial_test_ext() -> runtime_io::TestExternalities { + let t = system::GenesisConfig::default() + .build_storage::() + .unwrap(); + + t.into() +} + +type Balances = balances::Module; +type System = system::Module; +type Membership = membership::members::Module; +type ProposalsEngine = proposals_engine::Module; +type Council = governance::council::Module; +type Election = governance::election::Module; +type ProposalCodex = proposals_codex::Module; +type Mint = minting::Module; + +fn setup_members(count: u8) { + let authority_account_id = ::AccountId::default(); + Membership::set_screening_authority(RawOrigin::Root.into(), authority_account_id.clone()) + .unwrap(); + + for i in 0..count { + let account_id: [u8; 32] = [i; 32]; + Membership::add_screened_member( + RawOrigin::Signed(authority_account_id.clone().into()).into(), + account_id.clone().into(), + members::UserInfo { + handle: Some(account_id.to_vec()), + avatar_uri: None, + about: None, + }, + ) + .unwrap(); + } +} + +fn setup_council() { + let councilor0 = AccountId32::default(); + let councilor1: [u8; 32] = [1; 32]; + let councilor2: [u8; 32] = [2; 32]; + let councilor3: [u8; 32] = [3; 32]; + let councilor4: [u8; 32] = [4; 32]; + let councilor5: [u8; 32] = [5; 32]; + assert!(Council::set_council( + system::RawOrigin::Root.into(), + vec![ + councilor0, + councilor1.into(), + councilor2.into(), + councilor3.into(), + councilor4.into(), + councilor5.into() + ] + ) + .is_ok()); +} + +pub(crate) fn increase_total_balance_issuance_using_account_id( + account_id: AccountId32, + balance: u128, +) { + type Balances = balances::Module; + let initial_balance = Balances::total_issuance(); + { + let _ = ::Currency::deposit_creating(&account_id, balance); + } + assert_eq!(Balances::total_issuance(), initial_balance + balance); +} + +// Recommendation from Parity on testing on_finalize +// https://substrate.dev/docs/en/next/development/module/tests +fn run_to_block(n: BlockNumber) { + while System::block_number() < n { + >::on_finalize(System::block_number()); + >::on_finalize(System::block_number()); + System::set_block_number(System::block_number() + 1); + >::on_initialize(System::block_number()); + >::on_initialize(System::block_number()); + } +} + +struct VoteGenerator { + proposal_id: u32, + current_account_id: AccountId32, + current_account_id_seed: u8, + current_voter_id: u64, + pub auto_increment_voter_id: bool, +} + +impl VoteGenerator { + fn new(proposal_id: u32) -> Self { + VoteGenerator { + proposal_id, + current_voter_id: 0, + current_account_id_seed: 0, + current_account_id: AccountId32::default(), + auto_increment_voter_id: true, + } + } + fn vote_and_assert_ok(&mut self, vote_kind: VoteKind) { + self.vote_and_assert(vote_kind, Ok(())); + } + + fn vote_and_assert(&mut self, vote_kind: VoteKind, expected_result: DispatchResult) { + assert_eq!(self.vote(vote_kind.clone()), expected_result); + } + + fn vote(&mut self, vote_kind: VoteKind) -> DispatchResult { + if self.auto_increment_voter_id { + self.current_account_id_seed += 1; + self.current_voter_id += 1; + let account_id: [u8; 32] = [self.current_account_id_seed; 32]; + self.current_account_id = account_id.into(); + } + + ProposalsEngine::vote( + system::RawOrigin::Signed(self.current_account_id.clone()).into(), + self.current_voter_id, + self.proposal_id, + vote_kind, + ) + } +} + +#[derive(Clone)] +struct DummyProposalFixture { + parameters: ProposalParameters, + account_id: AccountId32, + proposer_id: u64, + proposal_code: Vec, + title: Vec, + description: Vec, + stake_balance: Option>, +} + +impl Default for DummyProposalFixture { + fn default() -> Self { + let title = b"title".to_vec(); + let description = b"description".to_vec(); + let dummy_proposal = + proposals_codex::Call::::execute_text_proposal(b"text".to_vec()); + + DummyProposalFixture { + parameters: ProposalParameters { + voting_period: 3, + approval_quorum_percentage: 60, + approval_threshold_percentage: 60, + slashing_quorum_percentage: 60, + slashing_threshold_percentage: 60, + grace_period: 0, + required_stake: None, + }, + account_id: ::AccountId::default(), + proposer_id: 0, + proposal_code: dummy_proposal.encode(), + title, + description, + stake_balance: None, + } + } +} + +impl DummyProposalFixture { + fn with_parameters(self, parameters: ProposalParameters) -> Self { + DummyProposalFixture { parameters, ..self } + } + + fn with_account_id(self, account_id: AccountId32) -> Self { + DummyProposalFixture { account_id, ..self } + } + + fn with_stake(self, stake_balance: BalanceOf) -> Self { + DummyProposalFixture { + stake_balance: Some(stake_balance), + ..self + } + } + + fn with_proposer(self, proposer_id: u64) -> Self { + DummyProposalFixture { + proposer_id, + ..self + } + } + + fn create_proposal_and_assert(self, result: Result) -> Option { + let proposal_id_result = ProposalsEngine::create_proposal( + self.account_id, + self.proposer_id, + self.parameters, + self.title, + self.description, + self.stake_balance, + self.proposal_code, + ); + assert_eq!(proposal_id_result, result); + + proposal_id_result.ok() + } +} + +struct CancelProposalFixture { + origin: RawOrigin, + proposal_id: u32, + proposer_id: u64, +} + +impl CancelProposalFixture { + fn new(proposal_id: u32) -> Self { + let account_id = ::AccountId::default(); + CancelProposalFixture { + proposal_id, + origin: RawOrigin::Signed(account_id), + proposer_id: 0, + } + } + + fn with_proposer(self, proposer_id: u64) -> Self { + CancelProposalFixture { + proposer_id, + ..self + } + } + + fn cancel_and_assert(self, expected_result: DispatchResult) { + assert_eq!( + ProposalsEngine::cancel_proposal( + self.origin.into(), + self.proposer_id, + self.proposal_id + ), + expected_result + ); + } +} + +/// Main purpose of this integration test: check balance of the member on proposal finalization (cancellation) +/// It tests StakingEventsHandler integration. Also, membership module is tested during the proposal creation (ActorOriginValidator). +#[test] +fn proposal_cancellation_with_slashes_with_balance_checks_succeeds() { + initial_test_ext().execute_with(|| { + let account_id = ::AccountId::default(); + + setup_members(2); + let member_id = 0; // newly created member_id + + let stake_amount = 20000u128; + let parameters = ProposalParameters { + voting_period: 3, + approval_quorum_percentage: 50, + approval_threshold_percentage: 60, + slashing_quorum_percentage: 60, + slashing_threshold_percentage: 60, + grace_period: 5, + required_stake: Some(stake_amount), + }; + let dummy_proposal = DummyProposalFixture::default() + .with_parameters(parameters) + .with_account_id(account_id.clone()) + .with_stake(stake_amount) + .with_proposer(member_id); + + let account_balance = 500000; + let _imbalance = + ::Currency::deposit_creating(&account_id, account_balance); + + assert_eq!( + ::Currency::total_balance(&account_id), + account_balance + ); + + let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); + assert_eq!( + ::Currency::total_balance(&account_id), + account_balance - stake_amount + ); + + let mut proposal = ProposalsEngine::proposals(proposal_id); + + let mut expected_proposal = Proposal { + parameters, + proposer_id: member_id, + created_at: 1, + status: ProposalStatus::Active(Some(ActiveStake { + stake_id: 0, + source_account_id: account_id.clone(), + })), + title: b"title".to_vec(), + description: b"description".to_vec(), + voting_results: VotingResults::default(), + }; + + assert_eq!(proposal, expected_proposal); + + let cancel_proposal_fixture = + CancelProposalFixture::new(proposal_id).with_proposer(member_id); + + cancel_proposal_fixture.cancel_and_assert(Ok(())); + + proposal = ProposalsEngine::proposals(proposal_id); + + expected_proposal.status = ProposalStatus::Finalized(FinalizationData { + proposal_status: ProposalDecisionStatus::Canceled, + finalized_at: 1, + encoded_unstaking_error_due_to_broken_runtime: None, + stake_data_after_unstaking_error: None, + }); + + assert_eq!(proposal, expected_proposal); + + let cancellation_fee = ProposalCancellationFee::get() as u128; + assert_eq!( + ::Currency::total_balance(&account_id), + account_balance - cancellation_fee + ); + }); +} + +#[test] +fn proposal_reset_succeeds() { + initial_test_ext().execute_with(|| { + setup_members(4); + setup_council(); + // create proposal + let dummy_proposal = DummyProposalFixture::default(); + let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); + + // create some votes + let mut vote_generator = VoteGenerator::new(proposal_id); + vote_generator.vote_and_assert_ok(VoteKind::Reject); + vote_generator.vote_and_assert_ok(VoteKind::Abstain); + vote_generator.vote_and_assert_ok(VoteKind::Slash); + + assert!(>::exists( + proposal_id + )); + + // check + let proposal = ProposalsEngine::proposals(proposal_id); + assert_eq!( + proposal.voting_results, + VotingResults { + abstentions: 1, + approvals: 0, + rejections: 1, + slashes: 1, + } + ); + + // Ensure council was elected + assert_eq!(CouncilManager::::total_voters_count(), 6); + + // Check proposals CouncilElected hook + // just trigger the election hook, we don't care about the parameters + ::CouncilElected::council_elected(Vec::new(), 10); + + let updated_proposal = ProposalsEngine::proposals(proposal_id); + + assert_eq!( + updated_proposal.voting_results, + VotingResults { + abstentions: 0, + approvals: 0, + rejections: 0, + slashes: 0, + } + ); + + // Check council CouncilElected hook. It should set current council. And we passed empty council. + assert_eq!(CouncilManager::::total_voters_count(), 0); + }); +} + +struct CodexProposalTestFixture +where + SuccessfulCall: Fn() -> DispatchResult, +{ + successful_call: SuccessfulCall, + member_id: u64, +} + +impl CodexProposalTestFixture +where + SuccessfulCall: Fn() -> DispatchResult, +{ + fn call_extrinsic_and_assert(&self) { + setup_members(15); + setup_council(); + + let account_id: [u8; 32] = [self.member_id as u8; 32]; + increase_total_balance_issuance_using_account_id(account_id.clone().into(), 500000); + + assert_eq!((self.successful_call)(), Ok(())); + + let proposal_id = 1; + + let mut vote_generator = VoteGenerator::new(proposal_id); + vote_generator.vote_and_assert_ok(VoteKind::Approve); + vote_generator.vote_and_assert_ok(VoteKind::Approve); + vote_generator.vote_and_assert_ok(VoteKind::Approve); + vote_generator.vote_and_assert_ok(VoteKind::Approve); + vote_generator.vote_and_assert_ok(VoteKind::Approve); + + run_to_block(2); + + let proposal = ProposalsEngine::proposals(proposal_id); + + assert_eq!( + proposal, + Proposal { + status: ProposalStatus::approved(ApprovedProposalStatus::Executed, 1), + title: b"title".to_vec(), + description: b"body".to_vec(), + voting_results: VotingResults { + abstentions: 0, + approvals: 5, + rejections: 0, + slashes: 0, + }, + ..proposal + } + ); + } +} + +#[test] +fn text_proposal_execution_succeeds() { + initial_test_ext().execute_with(|| { + let member_id = 10; + let account_id: [u8; 32] = [member_id; 32]; + + let codex_extrinsic_test_fixture = CodexProposalTestFixture { + member_id: member_id as u64, + successful_call: || { + ProposalCodex::create_text_proposal( + RawOrigin::Signed(account_id.into()).into(), + member_id as u64, + b"title".to_vec(), + b"body".to_vec(), + Some(>::from(25000u32)), + b"text".to_vec(), + ) + }, + }; + + codex_extrinsic_test_fixture.call_extrinsic_and_assert(); + }); +} + +#[test] +fn set_lead_proposal_execution_succeeds() { + initial_test_ext().execute_with(|| { + let member_id = 10; + let account_id: [u8; 32] = [member_id; 32]; + + let codex_extrinsic_test_fixture = CodexProposalTestFixture { + member_id: member_id as u64, + successful_call: || { + ProposalCodex::create_set_lead_proposal( + RawOrigin::Signed(account_id.clone().into()).into(), + member_id as u64, + b"title".to_vec(), + b"body".to_vec(), + Some(>::from(50000u32)), + Some((member_id as u64, account_id.into())), + ) + }, + }; + + assert!(content_working_group::Module::::ensure_lead_is_set().is_err()); + + codex_extrinsic_test_fixture.call_extrinsic_and_assert(); + + assert!(content_working_group::Module::::ensure_lead_is_set().is_ok()); + }); +} + +#[test] +fn spending_proposal_execution_succeeds() { + initial_test_ext().execute_with(|| { + let member_id = 10; + let account_id: [u8; 32] = [member_id; 32]; + let new_balance = >::from(5555u32); + + let target_account_id: [u8; 32] = [12; 32]; + + assert!(Council::set_council_mint_capacity(RawOrigin::Root.into(), new_balance).is_ok()); + + let codex_extrinsic_test_fixture = CodexProposalTestFixture { + member_id: member_id as u64, + successful_call: || { + ProposalCodex::create_spending_proposal( + RawOrigin::Signed(account_id.clone().into()).into(), + member_id as u64, + b"title".to_vec(), + b"body".to_vec(), + Some(>::from(25_000_u32)), + new_balance, + target_account_id.clone().into(), + ) + }, + }; + + assert_eq!( + Balances::free_balance::(target_account_id.clone().into()), + 0 + ); + + codex_extrinsic_test_fixture.call_extrinsic_and_assert(); + + assert_eq!( + Balances::free_balance::(target_account_id.into()), + new_balance + ); + }); +} + +#[test] +fn set_content_working_group_mint_capacity_execution_succeeds() { + initial_test_ext().execute_with(|| { + let member_id = 1; + let account_id: [u8; 32] = [member_id; 32]; + let new_balance = >::from(55u32); + + let mint_id = + Mint::add_mint(0, None).expect("Failed to create a mint for the content working group"); + >::put(mint_id); + + assert_eq!(Mint::get_mint_capacity(mint_id), Ok(0)); + + let codex_extrinsic_test_fixture = CodexProposalTestFixture { + member_id: member_id as u64, + successful_call: || { + ProposalCodex::create_set_content_working_group_mint_capacity_proposal( + RawOrigin::Signed(account_id.clone().into()).into(), + member_id as u64, + b"title".to_vec(), + b"body".to_vec(), + Some(>::from(50000u32)), + new_balance, + ) + }, + }; + + codex_extrinsic_test_fixture.call_extrinsic_and_assert(); + + assert_eq!(Mint::get_mint_capacity(mint_id), Ok(new_balance)); + }); +} + +#[test] +fn set_election_parameters_proposal_execution_succeeds() { + initial_test_ext().execute_with(|| { + let member_id = 1; + let account_id: [u8; 32] = [member_id; 32]; + + let election_parameters = ElectionParameters { + announcing_period: 14400, + voting_period: 14400, + revealing_period: 14400, + council_size: 4, + candidacy_limit: 25, + new_term_duration: 14400, + min_council_stake: 1, + min_voting_stake: 1, + }; + assert_eq!(Election::announcing_period(), 0); + + let codex_extrinsic_test_fixture = CodexProposalTestFixture { + member_id: member_id as u64, + successful_call: || { + ProposalCodex::create_set_election_parameters_proposal( + RawOrigin::Signed(account_id.clone().into()).into(), + member_id as u64, + b"title".to_vec(), + b"body".to_vec(), + Some(>::from(200_000_u32)), + election_parameters, + ) + }, + }; + codex_extrinsic_test_fixture.call_extrinsic_and_assert(); + + assert_eq!(Election::announcing_period(), 14400); + }); +} + +#[test] +fn evict_storage_provider_proposal_execution_succeeds() { + initial_test_ext().execute_with(|| { + let member_id = 1; + let account_id: [u8; 32] = [member_id; 32]; + + let target_member_id = 3; + let target_account: [u8; 32] = [target_member_id; 32]; + let target_account_id: AccountId32 = target_account.into(); + + >::insert( + Role::StorageProvider, + roles::actors::RoleParameters::default(), + ); + + >::insert( + Role::StorageProvider, + vec![target_account_id.clone()], + ); + + >::insert( + target_account_id.clone(), + roles::actors::Actor { + member_id: target_member_id as u64, + role: Role::StorageProvider, + account: target_account_id, + joined_at: 1, + }, + ); + + let codex_extrinsic_test_fixture = CodexProposalTestFixture { + member_id: member_id as u64, + successful_call: || { + ProposalCodex::create_evict_storage_provider_proposal( + RawOrigin::Signed(account_id.clone().into()).into(), + member_id as u64, + b"title".to_vec(), + b"body".to_vec(), + Some(>::from(25000u32)), + target_account.into(), + ) + }, + }; + codex_extrinsic_test_fixture.call_extrinsic_and_assert(); + + assert_eq!( + >::get(Role::StorageProvider), + Vec::new() + ); + }); +} + +#[test] +fn set_storage_role_parameters_proposal_execution_succeeds() { + initial_test_ext().execute_with(|| { + let member_id = 1; + let account_id: [u8; 32] = [member_id; 32]; + + let default_role_parameters = RoleParameters { + min_actors: 1, + ..RoleParameters::default() + }; + + >::insert( + Role::StorageProvider, + default_role_parameters.clone(), + ); + + let target_role_parameters = RoleParameters { + startup_grace_period: 700, + ..default_role_parameters + }; + + let codex_extrinsic_test_fixture = CodexProposalTestFixture { + member_id: member_id as u64, + successful_call: || { + ProposalCodex::create_set_storage_role_parameters_proposal( + RawOrigin::Signed(account_id.clone().into()).into(), + member_id as u64, + b"title".to_vec(), + b"body".to_vec(), + Some(>::from(100_000_u32)), + target_role_parameters.clone(), + ) + }, + }; + codex_extrinsic_test_fixture.call_extrinsic_and_assert(); + + assert_eq!( + >::get(Role::StorageProvider), + Some(target_role_parameters) + ); + }); +} + +#[test] +fn set_validator_count_proposal_execution_succeeds() { + initial_test_ext().execute_with(|| { + let member_id = 1; + let account_id: [u8; 32] = [member_id; 32]; + + let new_validator_count = 8; + assert_eq!(::get(), 0); + + let codex_extrinsic_test_fixture = CodexProposalTestFixture { + member_id: member_id as u64, + successful_call: || { + ProposalCodex::create_set_validator_count_proposal( + RawOrigin::Signed(account_id.clone().into()).into(), + member_id as u64, + b"title".to_vec(), + b"body".to_vec(), + Some(>::from(100_000_u32)), + new_validator_count, + ) + }, + }; + codex_extrinsic_test_fixture.call_extrinsic_and_assert(); + + assert_eq!(::get(), new_validator_count); + }); +} diff --git a/scripts/compute-runtime-blob-hash.sh b/scripts/compute-runtime-blob-hash.sh index c197fbce6d..0e711198fb 100755 --- a/scripts/compute-runtime-blob-hash.sh +++ b/scripts/compute-runtime-blob-hash.sh @@ -1,5 +1,8 @@ #!/usr/bin/env bash +# The script computes the b2sum of the wasm blob in a pre-built joystream/node image +# Assumes b2sum is already instally on the host machine. + # Create a non running container from joystream/node docker create --name temp-container-joystream-node joystream/node @@ -13,4 +16,5 @@ docker rm temp-container-joystream-node # ubuntu 17.0+ with: apt-get install coreutils; b2sum -l 256 joystream_runtime.wasm # TODO: add install of b2sum to setup.sh b2sum -l 256 joystream_runtime.wasm +b2sum -l 512 joystream_runtime.wasm rm joystream_runtime.wasm diff --git a/setup.sh b/setup.sh index 32157661a0..9260f9f155 100755 --- a/setup.sh +++ b/setup.sh @@ -7,8 +7,9 @@ set -e # - rustup - rust insaller # - rust compiler and toolchains # - skips installing substrate and subkey -curl https://getsubstrate.io -sSf | bash -s -- --fast \ - && rustup component add rustfmt +curl https://getsubstrate.io -sSf | bash -s -- --fast + +rustup component add rustfmt clippy # TODO: Install additional tools... -# - b2sum \ No newline at end of file +# - b2sum diff --git a/tests/network-tests/.env b/tests/network-tests/.env new file mode 100644 index 0000000000..00ee4bf8c5 --- /dev/null +++ b/tests/network-tests/.env @@ -0,0 +1,22 @@ +# Address of the Joystream node. +NODE_URL = ws://127.0.0.1:9944 +# Account which is expected to provide sufficient funds to test accounts. +SUDO_ACCOUNT_URI = //Alice +# Amount of members able to buy membership in membership creation test. +MEMBERSHIP_CREATION_N = 2 +# ID of the membership paid terms used in membership creation test. +MEMBERSHIP_PAID_TERMS = 0 +# Council stake amount for first K accounts in council election test. +COUNCIL_STAKE_GREATER_AMOUNT = 1500 +# Council stake amount for first the rest participants in council election test. +COUNCIL_STAKE_LESSER_AMOUNT = 1000 +# Number of members with greater stake in council election test. +COUNCIL_ELECTION_K = 2 +# Balance to spend using spending proposal +SPENDING_BALANCE = 1000 +# Minting capacity for content working group minting capacity test. +MINTING_CAPACITY = 100020 +# Stake amount for Rome runtime upgrade proposal +RUNTIME_UPGRADE_PROPOSAL_STAKE = 100000 +# Constantinople runtime path +RUNTIME_WASM_PATH = ../../target/release/wbuild/joystream-node-runtime/joystream_node_runtime.compact.wasm \ No newline at end of file diff --git a/tests/network-tests/.prettierrc b/tests/network-tests/.prettierrc new file mode 100644 index 0000000000..bb2de5c084 --- /dev/null +++ b/tests/network-tests/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "arrowParens": "avoid", + "useTabs": false, + "tabWidth": 2 +} diff --git a/tests/network-tests/LICENSE b/tests/network-tests/LICENSE new file mode 100644 index 0000000000..2fb2e74d8d --- /dev/null +++ b/tests/network-tests/LICENSE @@ -0,0 +1,675 @@ +### GNU GENERAL PUBLIC LICENSE + +Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + +### Preamble + +The GNU General Public License is a free, copyleft license for +software and other kinds of works. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom +to share and change all versions of a program--to make sure it remains +free software for all its users. We, the Free Software Foundation, use +the GNU General Public License for most of our software; it applies +also to any other work released this way by its authors. You can apply +it to your programs, too. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you +have certain responsibilities if you distribute copies of the +software, or if you modify it: responsibilities to respect the freedom +of others. + +For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + +Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + +Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the +manufacturer can do so. This is fundamentally incompatible with the +aim of protecting users' freedom to change the software. The +systematic pattern of such abuse occurs in the area of products for +individuals to use, which is precisely where it is most unacceptable. +Therefore, we have designed this version of the GPL to prohibit the +practice for those products. If such problems arise substantially in +other domains, we stand ready to extend this provision to those +domains in future versions of the GPL, as needed to protect the +freedom of users. + +Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish +to avoid the special danger that patents applied to a free program +could make it effectively proprietary. To prevent this, the GPL +assures that patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and +modification follow. + +### TERMS AND CONDITIONS + +#### 0. Definitions. + +"This License" refers to version 3 of the GNU General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds +of works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of +an exact copy. The resulting work is called a "modified version" of +the earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based +on the Program. + +To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user +through a computer network, with no transfer of a copy, is not +conveying. + +An interactive user interface displays "Appropriate Legal Notices" to +the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +#### 1. Source Code. + +The "source code" for a work means the preferred form of the work for +making modifications to it. "Object code" means any non-source form of +a work. + +A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can +regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same +work. + +#### 2. Basic Permissions. + +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, +without conditions so long as your license otherwise remains in force. +You may convey covered works to others for the sole purpose of having +them make modifications exclusively for you, or provide you with +facilities for running those works, provided that you comply with the +terms of this License in conveying all material for which you do not +control copyright. Those thus making or running the covered works for +you must do so exclusively on your behalf, under your direction and +control, on terms that prohibit them from making any copies of your +copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the +conditions stated below. Sublicensing is not allowed; section 10 makes +it unnecessary. + +#### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such +circumvention is effected by exercising rights under this License with +respect to the covered work, and you disclaim any intention to limit +operation or modification of the work as a means of enforcing, against +the work's users, your or third parties' legal rights to forbid +circumvention of technological measures. + +#### 4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +#### 5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these +conditions: + +- a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. +- b) The work must carry prominent notices stating that it is + released under this License and any conditions added under + section 7. This requirement modifies the requirement in section 4 + to "keep intact all notices". +- c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. +- d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +#### 6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms of +sections 4 and 5, provided that you also convey the machine-readable +Corresponding Source under the terms of this License, in one of these +ways: + +- a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. +- b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the Corresponding + Source from a network server at no charge. +- c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. +- d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. +- e) Convey the object code using peer-to-peer transmission, + provided you inform other peers where the object code and + Corresponding Source of the work are being offered to the general + public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, +family, or household purposes, or (2) anything designed or sold for +incorporation into a dwelling. In determining whether a product is a +consumer product, doubtful cases shall be resolved in favor of +coverage. For a particular product received by a particular user, +"normally used" refers to a typical or common use of that class of +product, regardless of the status of the particular user or of the way +in which the particular user actually uses, or expects or is expected +to use, the product. A product is a consumer product regardless of +whether the product has substantial commercial, industrial or +non-consumer uses, unless such uses represent the only significant +mode of use of the product. + +"Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to +install and execute modified versions of a covered work in that User +Product from a modified version of its Corresponding Source. The +information must suffice to ensure that the continued functioning of +the modified object code is in no case prevented or interfered with +solely because modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or +updates for a work that has been modified or installed by the +recipient, or for the User Product in which it has been modified or +installed. Access to a network may be denied when the modification +itself materially and adversely affects the operation of the network +or violates the rules and protocols for communication across the +network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +#### 7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders +of that material) supplement the terms of this License with terms: + +- a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or +- b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or +- c) Prohibiting misrepresentation of the origin of that material, + or requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or +- d) Limiting the use for publicity purposes of names of licensors + or authors of the material; or +- e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or +- f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions + of it) with contractual assumptions of liability to the recipient, + for any liability that these contractual assumptions directly + impose on those licensors and authors. + +All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; the +above requirements apply either way. + +#### 8. Termination. + +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your license +from a particular copyright holder is reinstated (a) provisionally, +unless and until the copyright holder explicitly and finally +terminates your license, and (b) permanently, if the copyright holder +fails to notify you of the violation by some reasonable means prior to +60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +#### 9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or run +a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +#### 10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +#### 11. Patents. + +A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims owned +or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is "discriminatory" if it does not include within the +scope of its coverage, prohibits the exercise of, or is conditioned on +the non-exercise of one or more of the rights that are specifically +granted under this License. You may not convey a covered work if you +are a party to an arrangement with a third party that is in the +business of distributing software, under which you make payment to the +third party based on the extent of your activity of conveying the +work, and under which the third party grants, to any of the parties +who would receive the covered work from you, a discriminatory patent +license (a) in connection with copies of the covered work conveyed by +you (or copies made from those copies), or (b) primarily for and in +connection with specific products or compilations that contain the +covered work, unless you entered into that arrangement, or that patent +license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +#### 12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under +this License and any other pertinent obligations, then as a +consequence you may not convey it at all. For example, if you agree to +terms that obligate you to collect a royalty for further conveying +from those to whom you convey the Program, the only way you could +satisfy both those terms and this License would be to refrain entirely +from conveying the Program. + +#### 13. Use with the GNU Affero General Public License. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + +#### 14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions +of the GNU General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in +detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies that a certain numbered version of the GNU General Public +License "or any later version" applies to it, you have the option of +following the terms and conditions either of that numbered version or +of any later version published by the Free Software Foundation. If the +Program does not specify a version number of the GNU General Public +License, you may choose any version ever published by the Free +Software Foundation. + +If the Program specifies that a proxy can decide which future versions +of the GNU General Public License can be used, that proxy's public +statement of acceptance of a version permanently authorizes you to +choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +#### 15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT +WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND +PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE +DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR +CORRECTION. + +#### 16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR +CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT +NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR +LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM +TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER +PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +#### 17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +### How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these +terms. + +To do so, attach the following notices to the program. It is safest to +attach them to the start of each source file to most effectively state +the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program 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 this program. If not, see . + +Also add information on how to contact you by electronic and paper +mail. + +If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands \`show w' and \`show c' should show the +appropriate parts of the General Public License. Of course, your +program's commands might be different; for a GUI interface, you would +use an "about box". + +You should also get your employer (if you work as a programmer) or +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. For more information on this, and how to apply and follow +the GNU GPL, see . + +The GNU General Public License does not permit incorporating your +program into proprietary programs. If your program is a subroutine +library, you may consider it more useful to permit linking proprietary +applications with the library. If this is what you want to do, use the +GNU Lesser General Public License instead of this License. But first, +please read . diff --git a/tests/network-tests/package.json b/tests/network-tests/package.json new file mode 100644 index 0000000000..7ddbae95c9 --- /dev/null +++ b/tests/network-tests/package.json @@ -0,0 +1,35 @@ +{ + "name": "joystream-testing", + "version": "0.1.0", + "license": "GPL-3.0-only", + "scripts": { + "build": "tsc --build tsconfig.json", + "test": "mocha -r ts-node/register src/tests/constantinople/*", + "test-migration": "mocha -r ts-node/register src/tests/rome/* && mocha -r ts-node/register src/tests/constantinople/*", + "lint": "tslint --project tsconfig.json", + "prettier": "prettier --write ./src" + }, + "dependencies": { + "@joystream/types": "", + "@rome/types@npm:@joystream/types": "^0.7.0", + "@polkadot/api": "^0.96.1", + "@polkadot/keyring": "^1.7.0-beta.5", + "@types/bn.js": "^4.11.5", + "bn.js": "^4.11.8", + "dotenv": "^8.2.0", + "fs": "^0.0.1-security", + "uuid": "^7.0.3" + }, + "devDependencies": { + "@polkadot/ts": "^0.3.14", + "@types/chai": "^4.2.11", + "@types/mocha": "^7.0.2", + "@types/uuid": "^7.0.2", + "chai": "^4.2.0", + "mocha": "^7.1.1", + "prettier": "2.0.2", + "ts-node": "^8.8.1", + "tslint": "^6.1.0", + "typescript": "^3.8.3" + } +} diff --git a/tests/network-tests/src/tests/constantinople/electingCouncilTest.ts b/tests/network-tests/src/tests/constantinople/electingCouncilTest.ts new file mode 100644 index 0000000000..5950ef7204 --- /dev/null +++ b/tests/network-tests/src/tests/constantinople/electingCouncilTest.ts @@ -0,0 +1,127 @@ +import { membershipTest } from './membershipCreationTest'; +import { KeyringPair } from '@polkadot/keyring/types'; +import { ApiWrapper } from './utils/apiWrapper'; +import { WsProvider, Keyring } from '@polkadot/api'; +import { initConfig } from './utils/config'; +import BN = require('bn.js'); +import { registerJoystreamTypes, Seat } from '@joystream/types'; +import { assert } from 'chai'; +import { v4 as uuid } from 'uuid'; +import { Utils } from './utils/utils'; + +export function councilTest(m1KeyPairs: KeyringPair[], m2KeyPairs: KeyringPair[]) { + initConfig(); + const keyring = new Keyring({ type: 'sr25519' }); + const nodeUrl: string = process.env.NODE_URL!; + const sudoUri: string = process.env.SUDO_ACCOUNT_URI!; + const K: number = +process.env.COUNCIL_ELECTION_K!; + const greaterStake: BN = new BN(+process.env.COUNCIL_STAKE_GREATER_AMOUNT!); + const lesserStake: BN = new BN(+process.env.COUNCIL_STAKE_LESSER_AMOUNT!); + const defaultTimeout: number = 120000; + let sudo: KeyringPair; + let apiWrapper: ApiWrapper; + + before(async function () { + this.timeout(defaultTimeout); + registerJoystreamTypes(); + const provider = new WsProvider(nodeUrl); + apiWrapper = await ApiWrapper.create(provider); + }); + + it('Electing a council test', async () => { + // Setup goes here because M keypairs are generated after before() function + sudo = keyring.addFromUri(sudoUri); + let now = await apiWrapper.getBestBlock(); + const applyForCouncilFee: BN = apiWrapper.estimateApplyForCouncilFee(greaterStake); + const voteForCouncilFee: BN = apiWrapper.estimateVoteForCouncilFee(sudo.address, sudo.address, greaterStake); + const salt: string[] = new Array(); + m1KeyPairs.forEach(() => { + salt.push(''.concat(uuid().replace(/-/g, ''))); + }); + const revealVoteFee: BN = apiWrapper.estimateRevealVoteFee(sudo.address, salt[0]); + + // Topping the balances + await apiWrapper.transferBalanceToAccounts(sudo, m2KeyPairs, applyForCouncilFee.add(greaterStake)); + await apiWrapper.transferBalanceToAccounts( + sudo, + m1KeyPairs, + voteForCouncilFee.add(revealVoteFee).add(greaterStake) + ); + + // First K members stake more + await apiWrapper.sudoStartAnnouncingPerion(sudo, now.addn(100)); + await apiWrapper.batchApplyForCouncilElection(m2KeyPairs.slice(0, K), greaterStake); + m2KeyPairs.slice(0, K).forEach(keyPair => + apiWrapper.getCouncilElectionStake(keyPair.address).then(stake => { + assert( + stake.eq(greaterStake), + `${keyPair.address} not applied correctrly for council election with stake ${stake} versus expected ${greaterStake}` + ); + }) + ); + + // Last members stake less + await apiWrapper.batchApplyForCouncilElection(m2KeyPairs.slice(K), lesserStake); + m2KeyPairs.slice(K).forEach(keyPair => + apiWrapper.getCouncilElectionStake(keyPair.address).then(stake => { + assert( + stake.eq(lesserStake), + `${keyPair.address} not applied correctrly for council election with stake ${stake} versus expected ${lesserStake}` + ); + }) + ); + + // Voting + await apiWrapper.sudoStartVotingPerion(sudo, now.addn(100)); + await apiWrapper.batchVoteForCouncilMember( + m1KeyPairs.slice(0, K), + m2KeyPairs.slice(0, K), + salt.slice(0, K), + lesserStake + ); + await apiWrapper.batchVoteForCouncilMember(m1KeyPairs.slice(K), m2KeyPairs.slice(K), salt.slice(K), greaterStake); + + // Revealing + await apiWrapper.sudoStartRevealingPerion(sudo, now.addn(100)); + await apiWrapper.batchRevealVote(m1KeyPairs.slice(0, K), m2KeyPairs.slice(0, K), salt.slice(0, K)); + await apiWrapper.batchRevealVote(m1KeyPairs.slice(K), m2KeyPairs.slice(K), salt.slice(K)); + now = await apiWrapper.getBestBlock(); + + // Resolving election + // 3 is to ensure the revealing block is in future + await apiWrapper.sudoStartRevealingPerion(sudo, now.addn(3)); + await Utils.wait(apiWrapper.getBlockDuration().muln(2.5).toNumber()); + const seats: Seat[] = await apiWrapper.getCouncil(); + + // Preparing collections to increase assertion readability + const m2addresses: string[] = m2KeyPairs.map(keyPair => keyPair.address); + const m1addresses: string[] = m1KeyPairs.map(keyPair => keyPair.address); + const members: string[] = seats.map(seat => seat.member.toString()); + const bakers: string[] = seats.reduce( + (array, seat) => array.concat(seat.backers.map(baker => baker.member.toString())), + new Array() + ); + + // Assertions + m2addresses.forEach(address => assert(members.includes(address), `Account ${address} is not in the council`)); + m1addresses.forEach(address => assert(bakers.includes(address), `Account ${address} is not in the voters`)); + seats.forEach(seat => + assert( + Utils.getTotalStake(seat).eq(greaterStake.add(lesserStake)), + `Member ${seat.member} has unexpected stake ${Utils.getTotalStake(seat)}` + ) + ); + }).timeout(defaultTimeout); + + after(() => { + apiWrapper.close(); + }); +} + +describe('Council integration tests', () => { + const m1KeyPairs: KeyringPair[] = new Array(); + const m2KeyPairs: KeyringPair[] = new Array(); + membershipTest(m1KeyPairs); + membershipTest(m2KeyPairs); + councilTest(m1KeyPairs, m2KeyPairs); +}); diff --git a/tests/network-tests/src/tests/constantinople/membershipCreationTest.ts b/tests/network-tests/src/tests/constantinople/membershipCreationTest.ts new file mode 100644 index 0000000000..9e13e53333 --- /dev/null +++ b/tests/network-tests/src/tests/constantinople/membershipCreationTest.ts @@ -0,0 +1,94 @@ +import { WsProvider } from '@polkadot/api'; +import { registerJoystreamTypes } from '@joystream/types'; +import { Keyring } from '@polkadot/keyring'; +import { assert } from 'chai'; +import { KeyringPair } from '@polkadot/keyring/types'; +import BN = require('bn.js'); +import { ApiWrapper } from './utils/apiWrapper'; +import { initConfig } from './utils/config'; +import { v4 as uuid } from 'uuid'; + +export function membershipTest(nKeyPairs: KeyringPair[]) { + initConfig(); + const keyring = new Keyring({ type: 'sr25519' }); + const N: number = +process.env.MEMBERSHIP_CREATION_N!; + const paidTerms: number = +process.env.MEMBERSHIP_PAID_TERMS!; + const nodeUrl: string = process.env.NODE_URL!; + const sudoUri: string = process.env.SUDO_ACCOUNT_URI!; + const defaultTimeout: number = 30000; + let apiWrapper: ApiWrapper; + let sudo: KeyringPair; + let aKeyPair: KeyringPair; + let membershipFee: BN; + let membershipTransactionFee: BN; + + before(async function () { + this.timeout(defaultTimeout); + registerJoystreamTypes(); + const provider = new WsProvider(nodeUrl); + apiWrapper = await ApiWrapper.create(provider); + sudo = keyring.addFromUri(sudoUri); + for (let i = 0; i < N; i++) { + nKeyPairs.push(keyring.addFromUri(i + uuid().substring(0, 8))); + } + aKeyPair = keyring.addFromUri(uuid().substring(0, 8)); + membershipFee = await apiWrapper.getMembershipFee(paidTerms); + membershipTransactionFee = apiWrapper.estimateBuyMembershipFee( + sudo, + paidTerms, + 'member_name_which_is_longer_than_expected' + ); + await apiWrapper.transferBalanceToAccounts(sudo, nKeyPairs, membershipTransactionFee.add(new BN(membershipFee))); + await apiWrapper.transferBalance(sudo, aKeyPair.address, membershipTransactionFee); + }); + + it('Buy membeship is accepted with sufficient funds', async () => { + await Promise.all( + nKeyPairs.map(async (keyPair, index) => { + await apiWrapper.buyMembership(keyPair, paidTerms, `new_member_${index}${keyPair.address.substring(0, 8)}`); + }) + ); + nKeyPairs.forEach((keyPair, index) => + apiWrapper + .getMemberIds(keyPair.address) + .then(membership => assert(membership.length > 0, `Account ${keyPair.address} is not a member`)) + ); + }).timeout(defaultTimeout); + + it('Account A can not buy the membership with insufficient funds', async () => { + await apiWrapper + .getBalance(aKeyPair.address) + .then(balance => + assert( + balance.toBn() < membershipFee.add(membershipTransactionFee), + 'Account A already have sufficient balance to purchase membership' + ) + ); + await apiWrapper.buyMembership(aKeyPair, paidTerms, `late_member_${aKeyPair.address.substring(0, 8)}`, true); + apiWrapper + .getMemberIds(aKeyPair.address) + .then(membership => assert(membership.length === 0, 'Account A is a member')); + }).timeout(defaultTimeout); + + it('Account A was able to buy the membership with sufficient funds', async () => { + await apiWrapper.transferBalance(sudo, aKeyPair.address, membershipFee.add(membershipTransactionFee)); + apiWrapper + .getBalance(aKeyPair.address) + .then(balance => + assert(balance.toBn() >= membershipFee, 'The account balance is insufficient to purchase membership') + ); + await apiWrapper.buyMembership(aKeyPair, paidTerms, `late_member_${aKeyPair.address.substring(0, 8)}`); + apiWrapper + .getMemberIds(aKeyPair.address) + .then(membership => assert(membership.length > 0, 'Account A is a not member')); + }).timeout(defaultTimeout); + + after(() => { + apiWrapper.close(); + }); +} + +describe.skip('Membership integration tests', () => { + const nKeyPairs: KeyringPair[] = new Array(); + membershipTest(nKeyPairs); +}); diff --git a/tests/network-tests/src/tests/constantinople/utils/apiWrapper.ts b/tests/network-tests/src/tests/constantinople/utils/apiWrapper.ts new file mode 100644 index 0000000000..2ce8fd680b --- /dev/null +++ b/tests/network-tests/src/tests/constantinople/utils/apiWrapper.ts @@ -0,0 +1,371 @@ +import { ApiPromise, WsProvider } from '@polkadot/api'; +import { Option, Vec, Bytes, u32 } from '@polkadot/types'; +import { Codec } from '@polkadot/types/types'; +import { KeyringPair } from '@polkadot/keyring/types'; +import { UserInfo, PaidMembershipTerms, MemberId } from '@joystream/types/lib/members'; +import { Seat, VoteKind } from '@joystream/types'; +import { Balance, EventRecord } from '@polkadot/types/interfaces'; +import BN = require('bn.js'); +import { SubmittableExtrinsic } from '@polkadot/api/types'; +import { Sender } from './sender'; +import { Utils } from './utils'; + +export class ApiWrapper { + private readonly api: ApiPromise; + private readonly sender: Sender; + + public static async create(provider: WsProvider): Promise { + const api = await ApiPromise.create({ provider }); + return new ApiWrapper(api); + } + + constructor(api: ApiPromise) { + this.api = api; + this.sender = new Sender(api); + } + + public close() { + this.api.disconnect(); + } + + public async buyMembership( + account: KeyringPair, + paidTermsId: number, + name: string, + expectFailure = false + ): Promise { + return this.sender.signAndSend( + this.api.tx.members.buyMembership(paidTermsId, new UserInfo({ handle: name, avatar_uri: '', about: '' })), + account, + expectFailure + ); + } + + public getMemberIds(address: string): Promise { + return this.api.query.members.memberIdsByControllerAccountId>(address); + } + + public getBalance(address: string): Promise { + return this.api.query.balances.freeBalance(address); + } + + public async transferBalance(from: KeyringPair, to: string, amount: BN): Promise { + return this.sender.signAndSend(this.api.tx.balances.transfer(to, amount), from); + } + + public getPaidMembershipTerms(paidTermsId: number): Promise> { + return this.api.query.members.paidMembershipTermsById>(paidTermsId); + } + + public getMembershipFee(paidTermsId: number): Promise { + return this.getPaidMembershipTerms(paidTermsId).then(terms => terms.unwrap().fee.toBn()); + } + + public async transferBalanceToAccounts(from: KeyringPair, to: KeyringPair[], amount: BN): Promise { + for (const keyPair of to) { + await this.transferBalance(from, keyPair.address, amount); + } + return; + } + + private getBaseTxFee(): BN { + return this.api.createType('BalanceOf', this.api.consts.transactionPayment.transactionBaseFee).toBn(); + } + + private estimateTxFee(tx: SubmittableExtrinsic<'promise'>): BN { + const baseFee: BN = this.getBaseTxFee(); + const byteFee: BN = this.api.createType('BalanceOf', this.api.consts.transactionPayment.transactionByteFee).toBn(); + return Utils.calcTxLength(tx).mul(byteFee).add(baseFee); + } + + public estimateBuyMembershipFee(account: KeyringPair, paidTermsId: number, name: string): BN { + return this.estimateTxFee( + this.api.tx.members.buyMembership(paidTermsId, new UserInfo({ handle: name, avatar_uri: '', about: '' })) + ); + } + + public estimateApplyForCouncilFee(amount: BN): BN { + return this.estimateTxFee(this.api.tx.councilElection.apply(amount)); + } + + public estimateVoteForCouncilFee(nominee: string, salt: string, stake: BN): BN { + const hashedVote: string = Utils.hashVote(nominee, salt); + return this.estimateTxFee(this.api.tx.councilElection.vote(hashedVote, stake)); + } + + public estimateRevealVoteFee(nominee: string, salt: string): BN { + const hashedVote: string = Utils.hashVote(nominee, salt); + return this.estimateTxFee(this.api.tx.councilElection.reveal(hashedVote, nominee, salt)); + } + + public estimateProposeRuntimeUpgradeFee(stake: BN, name: string, description: string, runtime: Bytes | string): BN { + return this.estimateTxFee( + this.api.tx.proposalsCodex.createRuntimeUpgradeProposal(stake, name, description, stake, runtime) + ); + } + + public estimateProposeTextFee(stake: BN, name: string, description: string, text: string): BN { + return this.estimateTxFee(this.api.tx.proposalsCodex.createTextProposal(stake, name, description, stake, text)); + } + + public estimateProposeSpendingFee( + title: string, + description: string, + stake: BN, + balance: BN, + destination: string + ): BN { + return this.estimateTxFee( + this.api.tx.proposalsCodex.createSpendingProposal(stake, title, description, stake, balance, destination) + ); + } + + public estimateProposeWorkingGroupMintCapacityFee(title: string, description: string, stake: BN, balance: BN): BN { + return this.estimateTxFee( + this.api.tx.proposalsCodex.createSetContentWorkingGroupMintCapacityProposal( + stake, + title, + description, + stake, + balance + ) + ); + } + + public estimateVoteForProposalFee(): BN { + return this.estimateTxFee(this.api.tx.proposalsEngine.vote(0, 0, 'Approve')); + } + + private applyForCouncilElection(account: KeyringPair, amount: BN): Promise { + return this.sender.signAndSend(this.api.tx.councilElection.apply(amount), account, false); + } + + public batchApplyForCouncilElection(accounts: KeyringPair[], amount: BN): Promise { + return Promise.all( + accounts.map(async keyPair => { + await this.applyForCouncilElection(keyPair, amount); + }) + ); + } + + public async getCouncilElectionStake(address: string): Promise { + // TODO alter then `applicantStake` type will be introduced + return this.api.query.councilElection.applicantStakes(address).then(stake => { + const parsed = JSON.parse(stake.toString()); + return new BN(parsed.new); + }); + } + + private voteForCouncilMember(account: KeyringPair, nominee: string, salt: string, stake: BN): Promise { + const hashedVote: string = Utils.hashVote(nominee, salt); + return this.sender.signAndSend(this.api.tx.councilElection.vote(hashedVote, stake), account, false); + } + + public batchVoteForCouncilMember( + accounts: KeyringPair[], + nominees: KeyringPair[], + salt: string[], + stake: BN + ): Promise { + return Promise.all( + accounts.map(async (keyPair, index) => { + await this.voteForCouncilMember(keyPair, nominees[index].address, salt[index], stake); + }) + ); + } + + private revealVote(account: KeyringPair, commitment: string, nominee: string, salt: string): Promise { + return this.sender.signAndSend(this.api.tx.councilElection.reveal(commitment, nominee, salt), account, false); + } + + public batchRevealVote(accounts: KeyringPair[], nominees: KeyringPair[], salt: string[]): Promise { + return Promise.all( + accounts.map(async (keyPair, index) => { + const commitment = Utils.hashVote(nominees[index].address, salt[index]); + await this.revealVote(keyPair, commitment, nominees[index].address, salt[index]); + }) + ); + } + + // TODO consider using configurable genesis instead + public sudoStartAnnouncingPerion(sudo: KeyringPair, endsAtBlock: BN): Promise { + return this.sender.signAndSend( + this.api.tx.sudo.sudo(this.api.tx.councilElection.setStageAnnouncing(endsAtBlock)), + sudo, + false + ); + } + + public sudoStartVotingPerion(sudo: KeyringPair, endsAtBlock: BN): Promise { + return this.sender.signAndSend( + this.api.tx.sudo.sudo(this.api.tx.councilElection.setStageVoting(endsAtBlock)), + sudo, + false + ); + } + + public sudoStartRevealingPerion(sudo: KeyringPair, endsAtBlock: BN): Promise { + return this.sender.signAndSend( + this.api.tx.sudo.sudo(this.api.tx.councilElection.setStageRevealing(endsAtBlock)), + sudo, + false + ); + } + + public getBestBlock(): Promise { + return this.api.derive.chain.bestNumber(); + } + + public getCouncil(): Promise { + return this.api.query.council.activeCouncil>().then(seats => { + return (seats as unknown) as Seat[]; + }); + } + + public getRuntime(): Promise { + return this.api.query.substrate.code(); + } + + public async proposeRuntime( + account: KeyringPair, + stake: BN, + name: string, + description: string, + runtime: Bytes | string + ): Promise { + const memberId: BN = (await this.getMemberIds(account.address))[0].toBn(); + return this.sender.signAndSend( + this.api.tx.proposalsCodex.createRuntimeUpgradeProposal(memberId, name, description, stake, runtime), + account, + false + ); + } + + public async proposeText( + account: KeyringPair, + stake: BN, + name: string, + description: string, + text: string + ): Promise { + const memberId: BN = (await this.getMemberIds(account.address))[0].toBn(); + return this.sender.signAndSend( + this.api.tx.proposalsCodex.createTextProposal(memberId, name, description, stake, text), + account, + false + ); + } + + public async proposeSpending( + account: KeyringPair, + title: string, + description: string, + stake: BN, + balance: BN, + destination: string + ): Promise { + const memberId: BN = (await this.getMemberIds(account.address))[0].toBn(); + return this.sender.signAndSend( + this.api.tx.proposalsCodex.createSpendingProposal(memberId, title, description, stake, balance, destination), + account, + false + ); + } + + public async proposeWorkingGroupMintCapacity( + account: KeyringPair, + title: string, + description: string, + stake: BN, + balance: BN + ): Promise { + const memberId: BN = (await this.getMemberIds(account.address))[0].toBn(); + return this.sender.signAndSend( + this.api.tx.proposalsCodex.createSetContentWorkingGroupMintCapacityProposal( + memberId, + title, + description, + stake, + balance + ), + account, + false + ); + } + + public approveProposal(account: KeyringPair, memberId: BN, proposal: BN): Promise { + return this.sender.signAndSend(this.api.tx.proposalsEngine.vote(memberId, proposal, 'Approve'), account, false); + } + + public batchApproveProposal(council: KeyringPair[], proposal: BN): Promise { + return Promise.all( + council.map(async keyPair => { + const memberId: BN = (await this.getMemberIds(keyPair.address))[0].toBn(); + await this.approveProposal(keyPair, memberId, proposal); + }) + ); + } + + public getBlockDuration(): BN { + return this.api.createType('Moment', this.api.consts.babe.expectedBlockTime).toBn(); + } + + public expectProposalCreated(): Promise { + return new Promise(async resolve => { + await this.api.query.system.events>(events => { + events.forEach(record => { + if (record.event.method && record.event.method.toString() === 'ProposalCreated') { + resolve(new BN(record.event.data[1].toString())); + } + }); + }); + }); + } + + public expectRuntimeUpgraded(): Promise { + return new Promise(async resolve => { + await this.api.query.system.events>(events => { + events.forEach(record => { + if (record.event.method.toString() === 'RuntimeUpdated') { + resolve(); + } + }); + }); + }); + } + + public expectProposalFinalized(): Promise { + return new Promise(async resolve => { + await this.api.query.system.events>(events => { + events.forEach(record => { + if ( + record.event.method && + record.event.method.toString() === 'ProposalStatusUpdated' && + record.event.data[1].toString().includes('Executed') + ) { + resolve(); + } + }); + }); + }); + } + + public getTotalIssuance(): Promise { + return this.api.query.balances.totalIssuance(); + } + + public async getProposal(id: BN) { + const proposal = await this.api.query.proposalsEngine.proposals(id); + return; + } + + public async getRequiredProposalStake(numerator: number, denominator: number): Promise { + const issuance: number = await (await this.getTotalIssuance()).toNumber(); + const stake = (issuance * numerator) / denominator; + return new BN(stake.toFixed(0)); + } + + public getProposalCount(): Promise { + return this.api.query.proposalsEngine.proposalCount(); + } +} diff --git a/tests/network-tests/src/tests/constantinople/utils/config.ts b/tests/network-tests/src/tests/constantinople/utils/config.ts new file mode 100644 index 0000000000..612aa752c5 --- /dev/null +++ b/tests/network-tests/src/tests/constantinople/utils/config.ts @@ -0,0 +1,5 @@ +import { config } from 'dotenv'; + +export function initConfig() { + config(); +} diff --git a/tests/network-tests/src/tests/constantinople/utils/sender.ts b/tests/network-tests/src/tests/constantinople/utils/sender.ts new file mode 100644 index 0000000000..b8e1c15499 --- /dev/null +++ b/tests/network-tests/src/tests/constantinople/utils/sender.ts @@ -0,0 +1,66 @@ +import BN = require('bn.js'); +import { ApiPromise } from '@polkadot/api'; +import { Index } from '@polkadot/types/interfaces'; +import { SubmittableExtrinsic } from '@polkadot/api/types'; +import { KeyringPair } from '@polkadot/keyring/types'; + +export class Sender { + private readonly api: ApiPromise; + private static nonceMap: Map = new Map(); + + constructor(api: ApiPromise) { + this.api = api; + } + + private async getNonce(address: string): Promise { + let oncahinNonce: BN = new BN(0); + if (!Sender.nonceMap.get(address)) { + oncahinNonce = await this.api.query.system.accountNonce(address); + } + let nonce: BN | undefined = Sender.nonceMap.get(address); + if (!nonce) { + nonce = oncahinNonce; + } + const nextNonce: BN = nonce.addn(1); + Sender.nonceMap.set(address, nextNonce); + return nonce; + } + + private clearNonce(address: string): void { + Sender.nonceMap.delete(address); + } + + public async signAndSend( + tx: SubmittableExtrinsic<'promise'>, + account: KeyringPair, + expectFailure = false + ): Promise { + return new Promise(async (resolve, reject) => { + const nonce: BN = await this.getNonce(account.address); + const signedTx = tx.sign(account, { nonce }); + await signedTx + .send(async result => { + if (result.status.isFinalized === true && result.events !== undefined) { + result.events.forEach(event => { + if (event.event.method === 'ExtrinsicFailed') { + if (expectFailure) { + resolve(); + } else { + reject(new Error('Extrinsic failed unexpectedly')); + } + } + }); + resolve(); + } + if (result.status.isFuture) { + console.log('nonce ' + nonce + ' for account ' + account.address + ' is in future'); + this.clearNonce(account.address); + reject(new Error('Extrinsic nonce is in future')); + } + }) + .catch(error => { + reject(error); + }); + }); + } +} diff --git a/tests/network-tests/src/tests/constantinople/utils/utils.ts b/tests/network-tests/src/tests/constantinople/utils/utils.ts new file mode 100644 index 0000000000..0f6cd79e65 --- /dev/null +++ b/tests/network-tests/src/tests/constantinople/utils/utils.ts @@ -0,0 +1,51 @@ +import { IExtrinsic } from '@polkadot/types/types'; +import { Bytes } from '@polkadot/types'; +import { compactToU8a, stringToU8a } from '@polkadot/util'; +import { blake2AsHex } from '@polkadot/util-crypto'; +import BN = require('bn.js'); +import fs = require('fs'); +import { decodeAddress } from '@polkadot/keyring'; +import { Seat } from '@joystream/types'; + +export class Utils { + private static LENGTH_ADDRESS = 32 + 1; // publicKey + prefix + private static LENGTH_ERA = 2; // assuming mortals + private static LENGTH_SIGNATURE = 64; // assuming ed25519 or sr25519 + private static LENGTH_VERSION = 1; // 0x80 & version + + public static calcTxLength = (extrinsic?: IExtrinsic | null, nonce?: BN): BN => { + return new BN( + Utils.LENGTH_VERSION + + Utils.LENGTH_ADDRESS + + Utils.LENGTH_SIGNATURE + + Utils.LENGTH_ERA + + compactToU8a(nonce || 0).length + + (extrinsic ? extrinsic.encodedLength : 0) + ); + }; + + /** hash(accountId + salt) */ + public static hashVote(accountId: string, salt: string): string { + const accountU8a = decodeAddress(accountId); + const saltU8a = stringToU8a(salt); + const voteU8a = new Uint8Array(accountU8a.length + saltU8a.length); + voteU8a.set(accountU8a); + voteU8a.set(saltU8a, accountU8a.length); + + const hash = blake2AsHex(voteU8a, 256); + // console.log('Vote hash:', hash, 'for', { accountId, salt }); + return hash; + } + + public static wait(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + public static getTotalStake(seat: Seat): BN { + return new BN(+seat.stake.toString() + seat.backers.reduce((a, baker) => a + +baker.stake.toString(), 0)); + } + + public static readRuntimeFromFile(path: string): string { + return '0x' + fs.readFileSync(path).toString('hex'); + } +} diff --git a/tests/network-tests/src/tests/rome/electingCouncilTest.ts b/tests/network-tests/src/tests/rome/electingCouncilTest.ts new file mode 100644 index 0000000000..e4d901f5e8 --- /dev/null +++ b/tests/network-tests/src/tests/rome/electingCouncilTest.ts @@ -0,0 +1,127 @@ +import { membershipTest } from './membershipCreationTest'; +import { KeyringPair } from '@polkadot/keyring/types'; +import { ApiWrapper } from './utils/apiWrapper'; +import { WsProvider, Keyring } from '@polkadot/api'; +import { initConfig } from './utils/config'; +import BN = require('bn.js'); +import { registerJoystreamTypes, Seat } from '@rome/types'; +import { assert } from 'chai'; +import { v4 as uuid } from 'uuid'; +import { Utils } from './utils/utils'; + +export function councilTest(m1KeyPairs: KeyringPair[], m2KeyPairs: KeyringPair[]) { + initConfig(); + const keyring = new Keyring({ type: 'sr25519' }); + const nodeUrl: string = process.env.NODE_URL!; + const sudoUri: string = process.env.SUDO_ACCOUNT_URI!; + const K: number = +process.env.COUNCIL_ELECTION_K!; + const greaterStake: BN = new BN(+process.env.COUNCIL_STAKE_GREATER_AMOUNT!); + const lesserStake: BN = new BN(+process.env.COUNCIL_STAKE_LESSER_AMOUNT!); + const defaultTimeout: number = 120000; + let sudo: KeyringPair; + let apiWrapper: ApiWrapper; + + before(async function () { + this.timeout(defaultTimeout); + registerJoystreamTypes(); + const provider = new WsProvider(nodeUrl); + apiWrapper = await ApiWrapper.create(provider); + }); + + it('Electing a council test', async () => { + // Setup goes here because M keypairs are generated after before() function + sudo = keyring.addFromUri(sudoUri); + let now = await apiWrapper.getBestBlock(); + const applyForCouncilFee: BN = apiWrapper.estimateApplyForCouncilFee(greaterStake); + const voteForCouncilFee: BN = apiWrapper.estimateVoteForCouncilFee(sudo.address, sudo.address, greaterStake); + const salt: string[] = new Array(); + m1KeyPairs.forEach(() => { + salt.push(''.concat(uuid().replace(/-/g, ''))); + }); + const revealVoteFee: BN = apiWrapper.estimateRevealVoteFee(sudo.address, salt[0]); + + // Topping the balances + await apiWrapper.transferBalanceToAccounts(sudo, m2KeyPairs, applyForCouncilFee.add(greaterStake)); + await apiWrapper.transferBalanceToAccounts( + sudo, + m1KeyPairs, + voteForCouncilFee.add(revealVoteFee).add(greaterStake) + ); + + // First K members stake more + await apiWrapper.sudoStartAnnouncingPerion(sudo, now.addn(100)); + await apiWrapper.batchApplyForCouncilElection(m2KeyPairs.slice(0, K), greaterStake); + m2KeyPairs.slice(0, K).forEach(keyPair => + apiWrapper.getCouncilElectionStake(keyPair.address).then(stake => { + assert( + stake.eq(greaterStake), + `${keyPair.address} not applied correctrly for council election with stake ${stake} versus expected ${greaterStake}` + ); + }) + ); + + // Last members stake less + await apiWrapper.batchApplyForCouncilElection(m2KeyPairs.slice(K), lesserStake); + m2KeyPairs.slice(K).forEach(keyPair => + apiWrapper.getCouncilElectionStake(keyPair.address).then(stake => { + assert( + stake.eq(lesserStake), + `${keyPair.address} not applied correctrly for council election with stake ${stake} versus expected ${lesserStake}` + ); + }) + ); + + // Voting + await apiWrapper.sudoStartVotingPerion(sudo, now.addn(100)); + await apiWrapper.batchVoteForCouncilMember( + m1KeyPairs.slice(0, K), + m2KeyPairs.slice(0, K), + salt.slice(0, K), + lesserStake + ); + await apiWrapper.batchVoteForCouncilMember(m1KeyPairs.slice(K), m2KeyPairs.slice(K), salt.slice(K), greaterStake); + + // Revealing + await apiWrapper.sudoStartRevealingPerion(sudo, now.addn(100)); + await apiWrapper.batchRevealVote(m1KeyPairs.slice(0, K), m2KeyPairs.slice(0, K), salt.slice(0, K)); + await apiWrapper.batchRevealVote(m1KeyPairs.slice(K), m2KeyPairs.slice(K), salt.slice(K)); + now = await apiWrapper.getBestBlock(); + + // Resolving election + // 3 is to ensure the revealing block is in future + await apiWrapper.sudoStartRevealingPerion(sudo, now.addn(3)); + await Utils.wait(apiWrapper.getBlockDuration().muln(2.5).toNumber()); + const seats: Seat[] = await apiWrapper.getCouncil(); + + // Preparing collections to increase assertion readability + const m2addresses: string[] = m2KeyPairs.map(keyPair => keyPair.address); + const m1addresses: string[] = m1KeyPairs.map(keyPair => keyPair.address); + const members: string[] = seats.map(seat => seat.member.toString()); + const bakers: string[] = seats.reduce( + (array, seat) => array.concat(seat.backers.map(baker => baker.member.toString())), + new Array() + ); + + // Assertions + m2addresses.forEach(address => assert(members.includes(address), `Account ${address} is not in the council`)); + m1addresses.forEach(address => assert(bakers.includes(address), `Account ${address} is not in the voters`)); + seats.forEach(seat => + assert( + Utils.getTotalStake(seat).eq(greaterStake.add(lesserStake)), + `Member ${seat.member} has unexpected stake ${Utils.getTotalStake(seat)}` + ) + ); + }).timeout(defaultTimeout); + + after(() => { + apiWrapper.close(); + }); +} + +describe.skip('Council integration tests', () => { + const m1KeyPairs: KeyringPair[] = new Array(); + const m2KeyPairs: KeyringPair[] = new Array(); + membershipTest(m1KeyPairs); + membershipTest(m2KeyPairs); + councilTest(m1KeyPairs, m2KeyPairs); +}); diff --git a/tests/network-tests/src/tests/rome/membershipCreationTest.ts b/tests/network-tests/src/tests/rome/membershipCreationTest.ts new file mode 100644 index 0000000000..b96e2b0418 --- /dev/null +++ b/tests/network-tests/src/tests/rome/membershipCreationTest.ts @@ -0,0 +1,94 @@ +import { WsProvider } from '@polkadot/api'; +import { registerJoystreamTypes } from '@rome/types'; +import { Keyring } from '@polkadot/keyring'; +import { assert } from 'chai'; +import { KeyringPair } from '@polkadot/keyring/types'; +import BN = require('bn.js'); +import { ApiWrapper } from './utils/apiWrapper'; +import { initConfig } from './utils/config'; +import { v4 as uuid } from 'uuid'; + +export function membershipTest(nKeyPairs: KeyringPair[]) { + initConfig(); + const keyring = new Keyring({ type: 'sr25519' }); + const N: number = +process.env.MEMBERSHIP_CREATION_N!; + const paidTerms: number = +process.env.MEMBERSHIP_PAID_TERMS!; + const nodeUrl: string = process.env.NODE_URL!; + const sudoUri: string = process.env.SUDO_ACCOUNT_URI!; + const defaultTimeout: number = 30000; + let apiWrapper: ApiWrapper; + let sudo: KeyringPair; + let aKeyPair: KeyringPair; + let membershipFee: BN; + let membershipTransactionFee: BN; + + before(async function () { + this.timeout(defaultTimeout); + registerJoystreamTypes(); + const provider = new WsProvider(nodeUrl); + apiWrapper = await ApiWrapper.create(provider); + sudo = keyring.addFromUri(sudoUri); + for (let i = 0; i < N; i++) { + nKeyPairs.push(keyring.addFromUri(i + uuid().substring(0, 8))); + } + aKeyPair = keyring.addFromUri(uuid().substring(0, 8)); + membershipFee = await apiWrapper.getMembershipFee(paidTerms); + membershipTransactionFee = apiWrapper.estimateBuyMembershipFee( + sudo, + paidTerms, + 'member_name_which_is_longer_than_expected' + ); + await apiWrapper.transferBalanceToAccounts(sudo, nKeyPairs, membershipTransactionFee.add(new BN(membershipFee))); + await apiWrapper.transferBalance(sudo, aKeyPair.address, membershipTransactionFee); + }); + + it('Buy membeship is accepted with sufficient funds', async () => { + await Promise.all( + nKeyPairs.map(async (keyPair, index) => { + await apiWrapper.buyMembership(keyPair, paidTerms, `new_member_${index}${keyPair.address.substring(0, 8)}`); + }) + ); + nKeyPairs.forEach((keyPair, index) => + apiWrapper + .getMemberIds(keyPair.address) + .then(membership => assert(membership.length > 0, `Account ${keyPair.address} is not a member`)) + ); + }).timeout(defaultTimeout); + + it('Account A can not buy the membership with insufficient funds', async () => { + await apiWrapper + .getBalance(aKeyPair.address) + .then(balance => + assert( + balance.toBn() < membershipFee.add(membershipTransactionFee), + 'Account A already have sufficient balance to purchase membership' + ) + ); + await apiWrapper.buyMembership(aKeyPair, paidTerms, `late_member_${aKeyPair.address.substring(0, 8)}`, true); + apiWrapper + .getMemberIds(aKeyPair.address) + .then(membership => assert(membership.length === 0, 'Account A is a member')); + }).timeout(defaultTimeout); + + it('Account A was able to buy the membership with sufficient funds', async () => { + await apiWrapper.transferBalance(sudo, aKeyPair.address, membershipFee.add(membershipTransactionFee)); + apiWrapper + .getBalance(aKeyPair.address) + .then(balance => + assert(balance.toBn() >= membershipFee, 'The account balance is insufficient to purchase membership') + ); + await apiWrapper.buyMembership(aKeyPair, paidTerms, `late_member_${aKeyPair.address.substring(0, 8)}`); + apiWrapper + .getMemberIds(aKeyPair.address) + .then(membership => assert(membership.length > 0, 'Account A is a not member')); + }).timeout(defaultTimeout); + + after(() => { + apiWrapper.close(); + }); +} + +describe.skip('Membership integration tests', () => { + const nKeyPairs: KeyringPair[] = new Array(); + membershipTest(nKeyPairs); +}); diff --git a/tests/network-tests/src/tests/rome/romeRuntimeUpgradeTest.ts b/tests/network-tests/src/tests/rome/romeRuntimeUpgradeTest.ts new file mode 100644 index 0000000000..1ed8c7c8af --- /dev/null +++ b/tests/network-tests/src/tests/rome/romeRuntimeUpgradeTest.ts @@ -0,0 +1,75 @@ +import { initConfig } from './utils/config'; +import { Keyring, WsProvider } from '@polkadot/api'; +import { KeyringPair } from '@polkadot/keyring/types'; +import { membershipTest } from './membershipCreationTest'; +import { councilTest } from './electingCouncilTest'; +import { registerJoystreamTypes } from '@rome/types'; +import { ApiWrapper } from './utils/apiWrapper'; +import BN = require('bn.js'); +import { Utils } from './utils/utils'; + +describe('Runtime upgrade integration tests', () => { + initConfig(); + const keyring = new Keyring({ type: 'sr25519' }); + const nodeUrl: string = process.env.NODE_URL!; + const sudoUri: string = process.env.SUDO_ACCOUNT_URI!; + const proposalStake: BN = new BN(+process.env.RUNTIME_UPGRADE_PROPOSAL_STAKE!); + const runtimePath: string = process.env.RUNTIME_WASM_PATH!; + const defaultTimeout: number = 180000; + + const m1KeyPairs: KeyringPair[] = new Array(); + const m2KeyPairs: KeyringPair[] = new Array(); + + let apiWrapper: ApiWrapper; + let sudo: KeyringPair; + let provider: WsProvider; + + before(async function () { + this.timeout(defaultTimeout); + registerJoystreamTypes(); + provider = new WsProvider(nodeUrl); + apiWrapper = await ApiWrapper.create(provider); + }); + + membershipTest(m1KeyPairs); + membershipTest(m2KeyPairs); + councilTest(m1KeyPairs, m2KeyPairs); + + it('Upgrading the runtime test', async () => { + // Setup + sudo = keyring.addFromUri(sudoUri); + const runtime: string = Utils.readRuntimeFromFile(runtimePath); + const description: string = 'runtime upgrade proposal which is used for API integration testing'; + const runtimeProposalFee: BN = apiWrapper.estimateProposeRuntimeUpgradeFee( + proposalStake, + description, + description, + runtime + ); + const runtimeVoteFee: BN = apiWrapper.estimateVoteForRuntimeProposalFee(); + + // Topping the balances + await apiWrapper.transferBalance(sudo, m1KeyPairs[0].address, runtimeProposalFee.add(proposalStake)); + await apiWrapper.transferBalanceToAccounts(sudo, m2KeyPairs, runtimeVoteFee); + + // Proposal creation + const proposalPromise = apiWrapper.expectProposalCreated(); + await apiWrapper.proposeRuntime( + m1KeyPairs[0], + proposalStake, + 'testing runtime', + 'runtime to test proposal functionality', + runtime + ); + const proposalNumber = await proposalPromise; + + // Approving runtime update proposal + const runtimePromise = apiWrapper.expectRuntimeUpgraded(); + await apiWrapper.batchApproveProposal(m2KeyPairs, proposalNumber); + await runtimePromise; + }).timeout(defaultTimeout); + + after(() => { + apiWrapper.close(); + }); +}); diff --git a/tests/network-tests/src/tests/rome/utils/apiWrapper.ts b/tests/network-tests/src/tests/rome/utils/apiWrapper.ts new file mode 100644 index 0000000000..983908fc7d --- /dev/null +++ b/tests/network-tests/src/tests/rome/utils/apiWrapper.ts @@ -0,0 +1,370 @@ +import { ApiPromise, WsProvider } from '@polkadot/api'; +import { Option, Vec, Bytes, u32 } from '@polkadot/types'; +import { Codec } from '@polkadot/types/types'; +import { KeyringPair } from '@polkadot/keyring/types'; +import { UserInfo, PaidMembershipTerms, MemberId } from '@rome/types/lib/members'; +import { Seat, VoteKind } from '@rome/types'; +import { Balance, EventRecord } from '@polkadot/types/interfaces'; +import BN = require('bn.js'); +import { SubmittableExtrinsic } from '@polkadot/api/types'; +import { Sender } from './sender'; +import { Utils } from './utils'; + +export class ApiWrapper { + private readonly api: ApiPromise; + private readonly sender: Sender; + + public static async create(provider: WsProvider): Promise { + const api = await ApiPromise.create({ provider }); + return new ApiWrapper(api); + } + + constructor(api: ApiPromise) { + this.api = api; + this.sender = new Sender(api); + } + + public close() { + this.api.disconnect(); + } + + public async buyMembership( + account: KeyringPair, + paidTermsId: number, + name: string, + expectFailure = false + ): Promise { + return this.sender.signAndSend( + this.api.tx.members.buyMembership(paidTermsId, new UserInfo({ handle: name, avatar_uri: '', about: '' })), + account, + expectFailure + ); + } + + public getMemberIds(address: string): Promise { + return this.api.query.members.memberIdsByControllerAccountId>(address); + } + + public getBalance(address: string): Promise { + return this.api.query.balances.freeBalance(address); + } + + public async transferBalance(from: KeyringPair, to: string, amount: BN): Promise { + return this.sender.signAndSend(this.api.tx.balances.transfer(to, amount), from); + } + + public getPaidMembershipTerms(paidTermsId: number): Promise> { + return this.api.query.members.paidMembershipTermsById>(paidTermsId); + } + + public getMembershipFee(paidTermsId: number): Promise { + return this.getPaidMembershipTerms(paidTermsId).then(terms => terms.unwrap().fee.toBn()); + } + + public async transferBalanceToAccounts(from: KeyringPair, to: KeyringPair[], amount: BN): Promise { + return Promise.all( + to.map(async keyPair => { + await this.transferBalance(from, keyPair.address, amount); + }) + ); + } + + private getBaseTxFee(): BN { + return this.api.createType('BalanceOf', this.api.consts.transactionPayment.transactionBaseFee).toBn(); + } + + private estimateTxFee(tx: SubmittableExtrinsic<'promise'>): BN { + const baseFee: BN = this.getBaseTxFee(); + const byteFee: BN = this.api.createType('BalanceOf', this.api.consts.transactionPayment.transactionByteFee).toBn(); + return Utils.calcTxLength(tx).mul(byteFee).add(baseFee); + } + + public estimateBuyMembershipFee(account: KeyringPair, paidTermsId: number, name: string): BN { + return this.estimateTxFee( + this.api.tx.members.buyMembership(paidTermsId, new UserInfo({ handle: name, avatar_uri: '', about: '' })) + ); + } + + public estimateApplyForCouncilFee(amount: BN): BN { + return this.estimateTxFee(this.api.tx.councilElection.apply(amount)); + } + + public estimateVoteForCouncilFee(nominee: string, salt: string, stake: BN): BN { + const hashedVote: string = Utils.hashVote(nominee, salt); + return this.estimateTxFee(this.api.tx.councilElection.vote(hashedVote, stake)); + } + + public estimateRevealVoteFee(nominee: string, salt: string): BN { + const hashedVote: string = Utils.hashVote(nominee, salt); + return this.estimateTxFee(this.api.tx.councilElection.reveal(hashedVote, nominee, salt)); + } + + public estimateProposeRuntimeUpgradeFee(stake: BN, name: string, description: string, runtime: string): BN { + return this.estimateTxFee(this.api.tx.proposals.createProposal(stake, name, description, runtime)); + } + + public estimateProposeTextFee(stake: BN, name: string, description: string, text: string): BN { + return this.estimateTxFee(this.api.tx.proposalsCodex.createTextProposal(stake, name, description, stake, text)); + } + + public estimateProposeSpendingFee( + title: string, + description: string, + stake: BN, + balance: BN, + destination: string + ): BN { + return this.estimateTxFee( + this.api.tx.proposalsCodex.createSpendingProposal(stake, title, description, stake, balance, destination) + ); + } + + public estimateProposeWorkingGroupMintCapacityFee(title: string, description: string, stake: BN, balance: BN): BN { + return this.estimateTxFee( + this.api.tx.proposalsCodex.createSetContentWorkingGroupMintCapacityProposal( + stake, + title, + description, + stake, + balance + ) + ); + } + + public estimateVoteForRuntimeProposalFee(): BN { + return this.estimateTxFee(this.api.tx.proposals.voteOnProposal(0, 'Approve')); + } + + public newEstimate(): BN { + return new BN(100); + } + + private applyForCouncilElection(account: KeyringPair, amount: BN): Promise { + return this.sender.signAndSend(this.api.tx.councilElection.apply(amount), account, false); + } + + public batchApplyForCouncilElection(accounts: KeyringPair[], amount: BN): Promise { + return Promise.all( + accounts.map(async keyPair => { + await this.applyForCouncilElection(keyPair, amount); + }) + ); + } + + public async getCouncilElectionStake(address: string): Promise { + // TODO alter then `applicantStake` type will be introduced + return this.api.query.councilElection.applicantStakes(address).then(stake => { + const parsed = JSON.parse(stake.toString()); + return new BN(parsed.new); + }); + } + + private voteForCouncilMember(account: KeyringPair, nominee: string, salt: string, stake: BN): Promise { + const hashedVote: string = Utils.hashVote(nominee, salt); + return this.sender.signAndSend(this.api.tx.councilElection.vote(hashedVote, stake), account, false); + } + + public batchVoteForCouncilMember( + accounts: KeyringPair[], + nominees: KeyringPair[], + salt: string[], + stake: BN + ): Promise { + return Promise.all( + accounts.map(async (keyPair, index) => { + await this.voteForCouncilMember(keyPair, nominees[index].address, salt[index], stake); + }) + ); + } + + private revealVote(account: KeyringPair, commitment: string, nominee: string, salt: string): Promise { + return this.sender.signAndSend(this.api.tx.councilElection.reveal(commitment, nominee, salt), account, false); + } + + public batchRevealVote(accounts: KeyringPair[], nominees: KeyringPair[], salt: string[]): Promise { + return Promise.all( + accounts.map(async (keyPair, index) => { + const commitment = Utils.hashVote(nominees[index].address, salt[index]); + await this.revealVote(keyPair, commitment, nominees[index].address, salt[index]); + }) + ); + } + + // TODO consider using configurable genesis instead + public sudoStartAnnouncingPerion(sudo: KeyringPair, endsAtBlock: BN): Promise { + return this.sender.signAndSend( + this.api.tx.sudo.sudo(this.api.tx.councilElection.setStageAnnouncing(endsAtBlock)), + sudo, + false + ); + } + + public sudoStartVotingPerion(sudo: KeyringPair, endsAtBlock: BN): Promise { + return this.sender.signAndSend( + this.api.tx.sudo.sudo(this.api.tx.councilElection.setStageVoting(endsAtBlock)), + sudo, + false + ); + } + + public sudoStartRevealingPerion(sudo: KeyringPair, endsAtBlock: BN): Promise { + return this.sender.signAndSend( + this.api.tx.sudo.sudo(this.api.tx.councilElection.setStageRevealing(endsAtBlock)), + sudo, + false + ); + } + + public getBestBlock(): Promise { + return this.api.derive.chain.bestNumber(); + } + + public getCouncil(): Promise { + return this.api.query.council.activeCouncil>().then(seats => { + return JSON.parse(seats.toString()); + }); + } + + public getRuntime(): Promise { + return this.api.query.substrate.code(); + } + + public proposeRuntime( + account: KeyringPair, + stake: BN, + name: string, + description: string, + runtime: string + ): Promise { + return this.sender.signAndSend( + this.api.tx.proposals.createProposal(stake, name, description, runtime), + account, + false + ); + } + + public async proposeText( + account: KeyringPair, + stake: BN, + name: string, + description: string, + text: string + ): Promise { + const memberId: BN = (await this.getMemberIds(account.address))[0].toBn(); + return this.sender.signAndSend( + this.api.tx.proposalsCodex.createTextProposal(memberId, name, description, stake, text), + account, + false + ); + } + + public async proposeSpending( + account: KeyringPair, + title: string, + description: string, + stake: BN, + balance: BN, + destination: string + ): Promise { + const memberId: BN = (await this.getMemberIds(account.address))[0].toBn(); + return this.sender.signAndSend( + this.api.tx.proposalsCodex.createSpendingProposal(memberId, title, description, stake, balance, destination), + account, + false + ); + } + + public async proposeWorkingGroupMintCapacity( + account: KeyringPair, + title: string, + description: string, + stake: BN, + balance: BN + ): Promise { + const memberId: BN = (await this.getMemberIds(account.address))[0].toBn(); + return this.sender.signAndSend( + this.api.tx.proposalsCodex.createSetContentWorkingGroupMintCapacityProposal( + memberId, + title, + description, + stake, + balance + ), + account, + false + ); + } + + public approveProposal(account: KeyringPair, proposal: BN): Promise { + return this.sender.signAndSend( + this.api.tx.proposals.voteOnProposal(proposal, new VoteKind('Approve')), + account, + false + ); + } + + public batchApproveProposal(council: KeyringPair[], proposal: BN): Promise { + return Promise.all( + council.map(async keyPair => { + await this.approveProposal(keyPair, proposal); + }) + ); + } + + public getBlockDuration(): BN { + return this.api.createType('Moment', this.api.consts.babe.expectedBlockTime).toBn(); + } + + public expectProposalCreated(): Promise { + return new Promise(async resolve => { + await this.api.query.system.events>(events => { + events.forEach(record => { + if (record.event.method.toString() === 'ProposalCreated') { + resolve(new BN(record.event.data[1].toString())); + } + }); + }); + }); + } + + public expectRuntimeUpgraded(): Promise { + return new Promise(async resolve => { + await this.api.query.system.events>(events => { + events.forEach(record => { + if (record.event.method.toString() === 'RuntimeUpdated') { + resolve(); + } + }); + }); + }); + } + + public expectProposalFinalized(): Promise { + return new Promise(async resolve => { + await this.api.query.system.events>(events => { + events.forEach(record => { + if ( + record.event.method.toString() === 'ProposalStatusUpdated' && + record.event.data[1].toString().includes('Finalized') + ) { + resolve(); + } + }); + }); + }); + } + + public getTotalIssuance(): Promise { + return this.api.query.balances.totalIssuance(); + } + + public async getRequiredProposalStake(numerator: number, denominator: number): Promise { + const issuance: number = await (await this.getTotalIssuance()).toNumber(); + const stake = (issuance * numerator) / denominator; + return new BN(stake.toFixed(0)); + } + + public getProposalCount(): Promise { + return this.api.query.proposalsEngine.proposalCount(); + } +} diff --git a/tests/network-tests/src/tests/rome/utils/config.ts b/tests/network-tests/src/tests/rome/utils/config.ts new file mode 100644 index 0000000000..612aa752c5 --- /dev/null +++ b/tests/network-tests/src/tests/rome/utils/config.ts @@ -0,0 +1,5 @@ +import { config } from 'dotenv'; + +export function initConfig() { + config(); +} diff --git a/tests/network-tests/src/tests/rome/utils/sender.ts b/tests/network-tests/src/tests/rome/utils/sender.ts new file mode 100644 index 0000000000..b8e1c15499 --- /dev/null +++ b/tests/network-tests/src/tests/rome/utils/sender.ts @@ -0,0 +1,66 @@ +import BN = require('bn.js'); +import { ApiPromise } from '@polkadot/api'; +import { Index } from '@polkadot/types/interfaces'; +import { SubmittableExtrinsic } from '@polkadot/api/types'; +import { KeyringPair } from '@polkadot/keyring/types'; + +export class Sender { + private readonly api: ApiPromise; + private static nonceMap: Map = new Map(); + + constructor(api: ApiPromise) { + this.api = api; + } + + private async getNonce(address: string): Promise { + let oncahinNonce: BN = new BN(0); + if (!Sender.nonceMap.get(address)) { + oncahinNonce = await this.api.query.system.accountNonce(address); + } + let nonce: BN | undefined = Sender.nonceMap.get(address); + if (!nonce) { + nonce = oncahinNonce; + } + const nextNonce: BN = nonce.addn(1); + Sender.nonceMap.set(address, nextNonce); + return nonce; + } + + private clearNonce(address: string): void { + Sender.nonceMap.delete(address); + } + + public async signAndSend( + tx: SubmittableExtrinsic<'promise'>, + account: KeyringPair, + expectFailure = false + ): Promise { + return new Promise(async (resolve, reject) => { + const nonce: BN = await this.getNonce(account.address); + const signedTx = tx.sign(account, { nonce }); + await signedTx + .send(async result => { + if (result.status.isFinalized === true && result.events !== undefined) { + result.events.forEach(event => { + if (event.event.method === 'ExtrinsicFailed') { + if (expectFailure) { + resolve(); + } else { + reject(new Error('Extrinsic failed unexpectedly')); + } + } + }); + resolve(); + } + if (result.status.isFuture) { + console.log('nonce ' + nonce + ' for account ' + account.address + ' is in future'); + this.clearNonce(account.address); + reject(new Error('Extrinsic nonce is in future')); + } + }) + .catch(error => { + reject(error); + }); + }); + } +} diff --git a/tests/network-tests/src/tests/rome/utils/utils.ts b/tests/network-tests/src/tests/rome/utils/utils.ts new file mode 100644 index 0000000000..1e646ad025 --- /dev/null +++ b/tests/network-tests/src/tests/rome/utils/utils.ts @@ -0,0 +1,50 @@ +import { IExtrinsic } from '@polkadot/types/types'; +import { compactToU8a, stringToU8a, u8aToHex } from '@polkadot/util'; +import { blake2AsHex } from '@polkadot/util-crypto'; +import BN = require('bn.js'); +import fs = require('fs'); +import { decodeAddress } from '@polkadot/keyring'; +import { Seat } from '@rome/types'; + +export class Utils { + private static LENGTH_ADDRESS = 32 + 1; // publicKey + prefix + private static LENGTH_ERA = 2; // assuming mortals + private static LENGTH_SIGNATURE = 64; // assuming ed25519 or sr25519 + private static LENGTH_VERSION = 1; // 0x80 & version + + public static calcTxLength = (extrinsic?: IExtrinsic | null, nonce?: BN): BN => { + return new BN( + Utils.LENGTH_VERSION + + Utils.LENGTH_ADDRESS + + Utils.LENGTH_SIGNATURE + + Utils.LENGTH_ERA + + compactToU8a(nonce || 0).length + + (extrinsic ? extrinsic.encodedLength : 0) + ); + }; + + /** hash(accountId + salt) */ + public static hashVote(accountId: string, salt: string): string { + const accountU8a = decodeAddress(accountId); + const saltU8a = stringToU8a(salt); + const voteU8a = new Uint8Array(accountU8a.length + saltU8a.length); + voteU8a.set(accountU8a); + voteU8a.set(saltU8a, accountU8a.length); + + const hash = blake2AsHex(voteU8a, 256); + // console.log('Vote hash:', hash, 'for', { accountId, salt }); + return hash; + } + + public static wait(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + public static getTotalStake(seat: Seat): BN { + return new BN(+seat.stake.toString() + seat.backers.reduce((a, baker) => a + +baker.stake.toString(), 0)); + } + + public static readRuntimeFromFile(path: string): string { + return u8aToHex(fs.readFileSync(path)); + } +} diff --git a/tests/network-tests/tsconfig.json b/tests/network-tests/tsconfig.json new file mode 100644 index 0000000000..d53a0276a4 --- /dev/null +++ b/tests/network-tests/tsconfig.json @@ -0,0 +1,66 @@ +{ + "compilerOptions": { + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "esnext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, + "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "dist" /* Redirect output structure to the directory. */, + "rootDir": "src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + } +} diff --git a/tests/network-tests/tslint.json b/tests/network-tests/tslint.json new file mode 100644 index 0000000000..84c9724809 --- /dev/null +++ b/tests/network-tests/tslint.json @@ -0,0 +1,8 @@ +{ + "extends": ["tslint:recommended"], + "rules": { + "interface-name": [true, "never-prefix"], + "max-line-length": [true, 140], + "no-console": false + } +} diff --git a/utils/chain-spec-builder/src/main.rs b/utils/chain-spec-builder/src/main.rs index 015160a7aa..d8faee6e8b 100644 --- a/utils/chain-spec-builder/src/main.rs +++ b/utils/chain-spec-builder/src/main.rs @@ -152,11 +152,11 @@ fn generate_chain_spec( None, // Default::default(), ); - chain_spec.to_json(false).map_err(|err| err.to_string()) + chain_spec.to_json(false).map_err(|err| err) } fn generate_authority_keys_and_store(seeds: &[String], keystore_path: &Path) -> Result<(), String> { - for (n, seed) in seeds.into_iter().enumerate() { + for (n, seed) in seeds.iter().enumerate() { let keystore = Keystore::open(keystore_path.join(format!("auth-{}", n)), None) .map_err(|err| err.to_string())?;