Browse Source

Merge branch 'nicaea' into membership_refactoring

# Conflicts:
#	Cargo.lock
#	runtime-modules/content-working-group/Cargo.toml
#	runtime-modules/proposals/discussion/src/lib.rs
#	runtime-modules/roles/src/actors.rs
#	runtime-modules/roles/src/lib.rs
#	runtime-modules/roles/src/mock.rs
#	runtime-modules/storage/src/data_directory.rs
#	runtime-modules/storage/src/tests/mock.rs
#	runtime-modules/working-group/Cargo.toml
#	runtime/src/integration/proposals/council_origin_validator.rs
#	runtime/src/integration/proposals/membership_origin_validator.rs
#	runtime/src/lib.rs
#	runtime/src/tests/proposals_integration/mod.rs
Shamil Gadelshin 4 năm trước cách đây
mục cha
commit
41b246fa0b
100 tập tin đã thay đổi với 4746 bổ sung327 xóa
  1. 7 9
      .editorconfig
  2. 5 0
      .eslintrc.js
  3. 39 0
      .github/workflows/joystream-cli.yml
  4. 75 0
      .github/workflows/pioneer.yml
  5. 9 2
      .gitignore
  6. 3 0
      .prettierrc.js
  7. 21 82
      .travis.yml
  8. 2 0
      .yarnclean
  9. 52 33
      Cargo.lock
  10. 1 1
      Cargo.toml
  11. 16 7
      README.md
  12. 0 11
      cli/.editorconfig
  13. 0 6
      cli/.eslintrc
  14. 10 0
      cli/.eslintrc.js
  15. 2 0
      cli/.prettierignore
  16. 14 6
      cli/package.json
  17. 351 25
      cli/src/Api.ts
  18. 1 0
      cli/src/ExitCodes.ts
  19. 258 3
      cli/src/Types.ts
  20. 32 12
      cli/src/base/AccountsCommandBase.ts
  21. 349 1
      cli/src/base/ApiCommandBase.ts
  22. 80 0
      cli/src/base/DefaultCommandBase.ts
  23. 22 6
      cli/src/base/StateAwareCommandBase.ts
  24. 190 0
      cli/src/base/WorkingGroupsCommandBase.ts
  25. 10 2
      cli/src/commands/account/choose.ts
  26. 11 61
      cli/src/commands/api/inspect.ts
  27. 1 1
      cli/src/commands/council/info.ts
  28. 40 0
      cli/src/commands/working-groups/application.ts
  29. 96 0
      cli/src/commands/working-groups/createOpening.ts
  30. 58 0
      cli/src/commands/working-groups/fillOpening.ts
  31. 78 0
      cli/src/commands/working-groups/opening.ts
  32. 22 0
      cli/src/commands/working-groups/openings.ts
  33. 38 0
      cli/src/commands/working-groups/overview.ts
  34. 46 0
      cli/src/commands/working-groups/startAcceptingApplications.ts
  35. 46 0
      cli/src/commands/working-groups/startReviewPeriod.ts
  36. 45 0
      cli/src/commands/working-groups/terminateApplication.ts
  37. 35 1
      cli/src/helpers/display.ts
  38. 2 1
      cli/tsconfig.json
  39. 5 0
      devops/.eslintrc.js
  40. 2 1
      devops/dockerfiles/node-and-runtime/Dockerfile
  41. 54 0
      devops/eslint-config/index.js
  42. 34 0
      devops/eslint-config/package.json
  43. 5 5
      devops/git-hooks/pre-push
  44. 8 0
      devops/prettier-config/index.js
  45. 22 0
      devops/prettier-config/package.json
  46. 1 1
      node/Cargo.toml
  47. 56 25
      node/src/chain_spec.rs
  48. 2 2
      node/src/forum_config/from_serialized.rs
  49. 1 1
      node/src/forum_config/mod.rs
  50. 1 1
      node/src/service.rs
  51. 46 21
      package.json
  52. 1 0
      pioneer/.123trigger
  53. 1 0
      pioneer/.babelrc.js
  54. 3 0
      pioneer/.codeclimate.yml
  55. 1 0
      pioneer/.dockerignore
  56. 10 0
      pioneer/.editorconfig
  57. 4 0
      pioneer/.eslintignore
  58. 27 0
      pioneer/.eslintrc.js
  59. 25 0
      pioneer/.gitignore
  60. 159 0
      pioneer/.gitlab-ci.yml
  61. 0 0
      pioneer/.npmignore
  62. 1 0
      pioneer/.nvmrc
  63. 1 0
      pioneer/.prettierignore
  64. 3 0
      pioneer/.storybook/addons.ts
  65. 19 0
      pioneer/.storybook/config.tsx
  66. 4 0
      pioneer/.storybook/style.css
  67. 81 0
      pioneer/.storybook/webpack.config.js
  68. 13 0
      pioneer/.travis.yml
  69. 19 0
      pioneer/BOUNTIES.md
  70. 124 0
      pioneer/CHANGELOG.md
  71. 45 0
      pioneer/CONTRIBUTING.md
  72. 26 0
      pioneer/Dockerfile
  73. 201 0
      pioneer/LICENSE
  74. 54 0
      pioneer/README.md
  75. 4 0
      pioneer/babel.config.js
  76. 33 0
      pioneer/deployment.extras.yml
  77. 60 0
      pioneer/deployment.template.yml
  78. 24 0
      pioneer/gh-pages-refresh.sh
  79. 89 0
      pioneer/i18next-scanner.config.js
  80. 72 0
      pioneer/img/pioneer_new.svg
  81. 17 0
      pioneer/jest.config.js
  82. 14 0
      pioneer/lerna.json
  83. 87 0
      pioneer/package.json
  84. 201 0
      pioneer/packages/app-123code/LICENSE
  85. 22 0
      pioneer/packages/app-123code/README.md
  86. 16 0
      pioneer/packages/app-123code/package.json
  87. 49 0
      pioneer/packages/app-123code/src/AccountSelector.tsx
  88. 28 0
      pioneer/packages/app-123code/src/Summary.tsx
  89. 65 0
      pioneer/packages/app-123code/src/SummaryBar.tsx
  90. 47 0
      pioneer/packages/app-123code/src/Transfer.tsx
  91. 38 0
      pioneer/packages/app-123code/src/index.tsx
  92. 7 0
      pioneer/packages/app-123code/src/translate.ts
  93. 201 0
      pioneer/packages/app-accounts/LICENSE
  94. 5 0
      pioneer/packages/app-accounts/README.md
  95. 23 0
      pioneer/packages/app-accounts/package.json
  96. 26 0
      pioneer/packages/app-accounts/scripts/vanitygen.js
  97. 227 0
      pioneer/packages/app-accounts/src/Account.tsx
  98. 105 0
      pioneer/packages/app-accounts/src/Banner.tsx
  99. 50 0
      pioneer/packages/app-accounts/src/MemoForm.tsx
  100. 110 0
      pioneer/packages/app-accounts/src/Overview.tsx

+ 7 - 9
.editorconfig

@@ -1,16 +1,14 @@
+# In case prettier plugin or eslint with autofix is not enabled in IDE
+# The fallback settings here should match with our prettierrc config
+# so we get consistency!
 root = true
+
 [*]
-indent_style=tab
-indent_size=tab
-tab_width=4
+indent_style=space
+indent_size=2
+tab_width=2
 end_of_line=lf
 charset=utf-8
 trim_trailing_whitespace=true
 max_line_length=120
 insert_final_newline=true
-
-[*.yml]
-indent_style=space
-indent_size=2
-tab_width=8
-end_of_line=lf

+ 5 - 0
.eslintrc.js

@@ -0,0 +1,5 @@
+module.exports = {
+    extends: [
+        '@joystream/eslint-config'
+    ]
+}

+ 39 - 0
.github/workflows/joystream-cli.yml

@@ -0,0 +1,39 @@
+name: joystream-cli
+on: [pull_request, push]
+
+jobs:
+  cli_build_ubuntu:
+    name: Ubuntu Build
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        node-version: [12.x]
+    steps:
+    - uses: actions/checkout@v1
+    - name: Use Node.js ${{ matrix.node-version }}
+      uses: actions/setup-node@v1
+      with:
+        node-version: ${{ matrix.node-version }}
+    - name: build
+      run: |
+        yarn install --frozen-lockfile
+        yarn madge --circular types/
+        yarn workspace joystream-cli build
+
+  cli_build_osx:
+    name: MacOS Build
+    runs-on: macos-latest
+    strategy:
+      matrix:
+        node-version: [12.x]
+    steps:
+    - uses: actions/checkout@v1
+    - name: Use Node.js ${{ matrix.node-version }}
+      uses: actions/setup-node@v1
+      with:
+        node-version: ${{ matrix.node-version }}
+    - name: build
+      run: |
+        yarn install --frozen-lockfile --network-timeout 120000
+        yarn madge --circular types/
+        yarn workspace joystream-cli build

+ 75 - 0
.github/workflows/pioneer.yml

@@ -0,0 +1,75 @@
+name: Pioneer
+on: [pull_request, push]
+
+jobs:
+  pioneer_build_ubuntu:
+    name: Ubuntu Build
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        node-version: [12.x]
+    steps:
+    - uses: actions/checkout@v1
+    - name: Use Node.js ${{ matrix.node-version }}
+      uses: actions/setup-node@v1
+      with:
+        node-version: ${{ matrix.node-version }}
+    - name: build
+      run: |
+        yarn install --frozen-lockfile
+        yarn madge --circular types/
+        yarn workspace pioneer build
+
+  pioneer_build_osx:
+    name: MacOS Build
+    runs-on: macos-latest
+    strategy:
+      matrix:
+        node-version: [12.x]
+    steps:
+    - uses: actions/checkout@v1
+    - name: Use Node.js ${{ matrix.node-version }}
+      uses: actions/setup-node@v1
+      with:
+        node-version: ${{ matrix.node-version }}
+    - name: build
+      run: |
+        yarn install --frozen-lockfile --network-timeout 120000
+        yarn madge --circular types/
+        yarn workspace pioneer build
+
+  pioneer_lint_ubuntu:
+    name: Ubuntu Linting
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        node-version: [12.x]
+    steps:
+    - uses: actions/checkout@v1
+    - name: Use Node.js ${{ matrix.node-version }}
+      uses: actions/setup-node@v1
+      with:
+        node-version: ${{ matrix.node-version }}
+    - name: lint
+      run: |
+        yarn install --frozen-lockfile
+        yarn madge --circular types/
+        yarn workspace pioneer lint
+
+  pioneer_lint_osx:
+    name: MacOS Linting
+    runs-on: macos-latest
+    strategy:
+      matrix:
+        node-version: [12.x]
+    steps:
+    - uses: actions/checkout@v1
+    - name: Use Node.js ${{ matrix.node-version }}
+      uses: actions/setup-node@v1
+      with:
+        node-version: ${{ matrix.node-version }}
+    - name: lint
+      run: |
+        yarn install --frozen-lockfile --network-timeout 120000
+        yarn madge --circular types/
+        yarn workspace pioneer lint

+ 9 - 2
.gitignore

@@ -13,6 +13,7 @@ joystream_runtime.wasm
 
 # Generated by yarn
 yarn*
+!yarn.lock
 
 # JetBrains IDEs
 .idea
@@ -21,7 +22,13 @@ yarn*
 .*.sw*
 
 # Visual Studio Code
-.vscode
+.vscode/
 
 # Compiled WASM code
-*.wasm
+*.wasm
+
+# Temporary files
+.tmp/
+
+# Istanbul report output
+**.nyc_output/

+ 3 - 0
.prettierrc.js

@@ -0,0 +1,3 @@
+module.exports = {
+  ...require('@joystream/prettier-config'),
+}

+ 21 - 82
.travis.yml

@@ -1,96 +1,35 @@
 language: rust
 
+# Caching of the runtime .wasm blob poses a problem.
+# See: https://github.com/Joystream/joystream/issues/466
+# Always starting with a clean slate is probably better, it allows us to ensure
+# the WASM runtime is always rebuilt. It also allows us to detect when certain upstream dependencies
+# sometimes break the build. When cache is enabled do not use the produced WASM build.
+# This also means the binary should not be used to produce the final chainspec file (because the same
+# one is embedded in the binary)
+cache: cargo
+
 rust:
-  - 1.43.0
+  - stable
 
 matrix:
   include:
     - os: linux
       env: TARGET=x86_64-unknown-linux-gnu
-    - os: linux
-      env: TARGET=arm-unknown-linux-gnueabihf
-      services: docker
-    - os: osx
-      env: TARGET=x86_64-apple-darwin
-    - os: linux
-      env: TARGET=wasm-blob
-      services: docker
 
-before_install:
+install:
+  - rustup install nightly-2020-05-23
+  - rustup target add wasm32-unknown-unknown --toolchain nightly-2020-05-23
+  # travis installs rust using rustup with the "minimal" profile so these tools are not installed by default
   - rustup component add rustfmt
-  - 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
-  - cargo test --verbose --all
 
-install:
-  - |
-    if [ "$TARGET" = "arm-unknown-linux-gnueabihf" ]
-    then
-      docker pull joystream/rust-raspberry
-    fi
+before_script:
+  - cargo fmt --all -- --check
 
 script:
-  - |
-    if [ "$TARGET" = "arm-unknown-linux-gnueabihf" ]
-    then
-      docker run -u root \
-        --volume ${TRAVIS_BUILD_DIR}:/home/cross/project \
-          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:
-  - |
-    if [ "$TARGET" = "wasm-blob" ]
-    then
-      export ASSET="joystream_runtime.wasm"
-    else
-      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
-
-deploy:
-  - provider: releases
-    api_key:
-      secure: FfxZGQexxAGT0Skbctl1FuqmEvNHejPDPtNG8Du1ACSGjS7Y+M6o/aPqE6HL158AmddOgndsIPR+HM7VfMDAUMkLTbOhv3nMpDBZu1h25vwk+jHOM65tm5LWUu/ROWBpaAQiG7NKrvtfkNfbNBSETsEbWBt/DPrhlIfSbgsXBFDiid7uRrCiwvDUJ097/EUOJ9OVUrk+O4ebSzfIfKPGPtRU2rQQ0eNX7yX3TCm3jbQm/kplkQNRL9mnAJNxtKuvuko4LqZ6jN4XLoLTHUMjO7E0r6wXVB4GVjA4HA214eLlQD6BhgTbWMDxKgWyuKzPG+2GLKyluSSn0RurSl8tYryXKxKxuN3H1FX9r23a8AzGtpRACJtIePC2YmPuQRSnz2Bw8jlSP2WPLJtXGD036J/wVMj6W9TROm7IBigiC7QlqAqCYNByOnoKyhRCgYyAJZb0Jpa3qWaFhA6b6gCGhyH85QCcrc0q6JAB3oqH8Wfm/K2HVzBobmKaSFu5DpwInNnUXnLWGVzhSt3oCq6ld773izReGdLJtLC2vaJ9rZVaVw29s9M662EEuAGgaVLO/sinZJFeIIaCF4i4zUXwXSLIdfKXGOR0ZibkyT2FS6qPGvl/lLN5IREzD7v/rV8htGMLmw4jpPLNskvRjCHX42ewRRYdMvZzQQOAvSlWcsw=
-    file: ${ASSET}
-    on:
-      tags: true
-      repo: Joystream/joystream
-    draft: true
-    overwrite: true
-    skip_cleanup: true
-  - provider: releases
-    api_key:
-      secure: FfxZGQexxAGT0Skbctl1FuqmEvNHejPDPtNG8Du1ACSGjS7Y+M6o/aPqE6HL158AmddOgndsIPR+HM7VfMDAUMkLTbOhv3nMpDBZu1h25vwk+jHOM65tm5LWUu/ROWBpaAQiG7NKrvtfkNfbNBSETsEbWBt/DPrhlIfSbgsXBFDiid7uRrCiwvDUJ097/EUOJ9OVUrk+O4ebSzfIfKPGPtRU2rQQ0eNX7yX3TCm3jbQm/kplkQNRL9mnAJNxtKuvuko4LqZ6jN4XLoLTHUMjO7E0r6wXVB4GVjA4HA214eLlQD6BhgTbWMDxKgWyuKzPG+2GLKyluSSn0RurSl8tYryXKxKxuN3H1FX9r23a8AzGtpRACJtIePC2YmPuQRSnz2Bw8jlSP2WPLJtXGD036J/wVMj6W9TROm7IBigiC7QlqAqCYNByOnoKyhRCgYyAJZb0Jpa3qWaFhA6b6gCGhyH85QCcrc0q6JAB3oqH8Wfm/K2HVzBobmKaSFu5DpwInNnUXnLWGVzhSt3oCq6ld773izReGdLJtLC2vaJ9rZVaVw29s9M662EEuAGgaVLO/sinZJFeIIaCF4i4zUXwXSLIdfKXGOR0ZibkyT2FS6qPGvl/lLN5IREzD7v/rV8htGMLmw4jpPLNskvRjCHX42ewRRYdMvZzQQOAvSlWcsw=
-    file: ${ASSET}
-    on:
-      branch: development
-      repo: Joystream/joystream
-    draft: true
-    prerelease: true
-    overwrite: true
-    skip_cleanup: true
+  # we set release as build type for all steps to benefit from already compiled packages in prior steps
+  - BUILD_DUMMY_WASM_BINARY=1 cargo clippy --release --target=${TARGET} -- -D warnings
+  - BUILD_DUMMY_WASM_BINARY=1 cargo test --release --verbose --all --target=${TARGET}
+  - TRIGGER_WASM_BUILD=1 WASM_BUILD_TOOLCHAIN=nightly-2020-05-23 cargo build --release --target=${TARGET} -p joystream-node
+  - ls -l ./target/${TARGET}/release/wbuild/joystream-node-runtime/

+ 2 - 0
.yarnclean

@@ -0,0 +1,2 @@
+@types/react-native
+@polkadot/ts/node_modules

+ 52 - 33
Cargo.lock

@@ -1569,7 +1569,7 @@ dependencies = [
 
 [[package]]
 name = "joystream-node"
-version = "2.2.0"
+version = "2.6.0"
 dependencies = [
  "ctrlc",
  "derive_more 0.14.1",
@@ -1614,7 +1614,7 @@ dependencies = [
 
 [[package]]
 name = "joystream-node-runtime"
-version = "6.13.0"
+version = "6.20.0"
 dependencies = [
  "parity-scale-codec",
  "safe-mix",
@@ -1660,7 +1660,6 @@ dependencies = [
  "substrate-proposals-discussion-module",
  "substrate-proposals-engine-module",
  "substrate-recurring-reward-module",
- "substrate-roles-module",
  "substrate-service-discovery-module",
  "substrate-session",
  "substrate-stake-module",
@@ -1669,6 +1668,7 @@ dependencies = [
  "substrate-versioned-store",
  "substrate-versioned-store-permissions-module",
  "substrate-wasm-builder-runner",
+ "substrate-working-group-module",
 ]
 
 [[package]]
@@ -4579,11 +4579,14 @@ dependencies = [
 
 [[package]]
 name = "substrate-common-module"
-version = "1.0.0"
+version = "1.2.0"
 dependencies = [
+ "parity-scale-codec",
+ "serde",
  "sr-primitives",
  "srml-support",
  "srml-system",
+ "srml-timestamp",
 ]
 
 [[package]]
@@ -4691,7 +4694,7 @@ dependencies = [
 
 [[package]]
 name = "substrate-content-working-group-module"
-version = "1.1.0"
+version = "1.0.1"
 dependencies = [
  "parity-scale-codec",
  "serde",
@@ -4708,7 +4711,6 @@ dependencies = [
  "substrate-membership-module",
  "substrate-primitives",
  "substrate-recurring-reward-module",
- "substrate-roles-module",
  "substrate-stake-module",
  "substrate-token-mint-module",
  "substrate-versioned-store",
@@ -4804,7 +4806,7 @@ dependencies = [
 
 [[package]]
 name = "substrate-forum-module"
-version = "1.1.1"
+version = "1.2.2"
 dependencies = [
  "hex-literal 0.1.4",
  "parity-scale-codec",
@@ -4819,6 +4821,7 @@ dependencies = [
  "srml-support-procedural",
  "srml-system",
  "srml-timestamp",
+ "substrate-common-module",
  "substrate-primitives",
 ]
 
@@ -4854,7 +4857,7 @@ dependencies = [
 
 [[package]]
 name = "substrate-hiring-module"
-version = "1.0.1"
+version = "1.0.2"
 dependencies = [
  "hex-literal 0.1.4",
  "mockall",
@@ -4914,7 +4917,7 @@ dependencies = [
 
 [[package]]
 name = "substrate-membership-module"
-version = "1.1.0"
+version = "1.0.1"
 dependencies = [
  "parity-scale-codec",
  "serde",
@@ -5103,7 +5106,7 @@ dependencies = [
 
 [[package]]
 name = "substrate-proposals-codex-module"
-version = "2.0.0"
+version = "2.1.0"
 dependencies = [
  "num_enum",
  "parity-scale-codec",
@@ -5127,11 +5130,11 @@ dependencies = [
  "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",
+ "substrate-working-group-module",
 ]
 
 [[package]]
@@ -5195,24 +5198,6 @@ dependencies = [
  "substrate-token-mint-module",
 ]
 
-[[package]]
-name = "substrate-roles-module"
-version = "1.1.0"
-dependencies = [
- "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-rpc"
 version = "2.0.0"
@@ -5336,17 +5321,25 @@ dependencies = [
 
 [[package]]
 name = "substrate-service-discovery-module"
-version = "1.0.0"
+version = "2.0.0"
 dependencies = [
  "parity-scale-codec",
  "serde",
  "sr-io",
  "sr-primitives",
  "sr-std",
+ "srml-balances",
  "srml-support",
  "srml-system",
+ "srml-timestamp",
+ "substrate-common-module",
+ "substrate-hiring-module",
+ "substrate-membership-module",
  "substrate-primitives",
- "substrate-roles-module",
+ "substrate-recurring-reward-module",
+ "substrate-stake-module",
+ "substrate-token-mint-module",
+ "substrate-working-group-module",
 ]
 
 [[package]]
@@ -5412,7 +5405,7 @@ dependencies = [
 
 [[package]]
 name = "substrate-storage-module"
-version = "1.0.0"
+version = "2.0.0"
 dependencies = [
  "parity-scale-codec",
  "serde",
@@ -5424,9 +5417,13 @@ dependencies = [
  "srml-system",
  "srml-timestamp",
  "substrate-common-module",
+ "substrate-hiring-module",
  "substrate-membership-module",
  "substrate-primitives",
- "substrate-roles-module",
+ "substrate-recurring-reward-module",
+ "substrate-stake-module",
+ "substrate-token-mint-module",
+ "substrate-working-group-module",
 ]
 
 [[package]]
@@ -5569,6 +5566,28 @@ dependencies = [
  "wasmi",
 ]
 
+[[package]]
+name = "substrate-working-group-module"
+version = "1.1.0"
+dependencies = [
+ "parity-scale-codec",
+ "serde",
+ "sr-io",
+ "sr-primitives",
+ "sr-std",
+ "srml-balances",
+ "srml-support",
+ "srml-system",
+ "srml-timestamp",
+ "substrate-common-module",
+ "substrate-hiring-module",
+ "substrate-membership-module",
+ "substrate-primitives",
+ "substrate-recurring-reward-module",
+ "substrate-stake-module",
+ "substrate-token-mint-module",
+]
+
 [[package]]
 name = "subtle"
 version = "1.0.0"

+ 1 - 1
Cargo.toml

@@ -12,13 +12,13 @@ members = [
 	"runtime-modules/membership",
 	"runtime-modules/memo",
 	"runtime-modules/recurring-reward",
-	"runtime-modules/roles",
 	"runtime-modules/service-discovery",
 	"runtime-modules/stake",
 	"runtime-modules/storage",
 	"runtime-modules/token-minting",
 	"runtime-modules/versioned-store",
 	"runtime-modules/versioned-store-permissions",
+	"runtime-modules/working-group",
 	"node",
 	"utils/chain-spec-builder/"
 ]

+ 16 - 7
README.md

@@ -52,7 +52,7 @@ cargo build --release
 Run the node and connect to the public testnet.
 
 ```bash
-cargo run --release -- --chain ./rome-tesnet.json
+cargo run --release -- --chain ./rome-testnet.json
 ```
 
 The `rome-testnet.json` chain file can be obtained from the [releases page](https://github.com/Joystream/joystream/releases/tag/v6.8.0)
@@ -68,7 +68,7 @@ cargo install joystream-node --path node/
 Now you can run
 
 ```bash
-joystream-node --chain rome-testnet.json
+joystream-node --chain ./rome-testnet.json
 ```
 
 ### Local development
@@ -85,14 +85,23 @@ This will build and run a fresh new local development chain purging existing one
 cargo test
 ```
 
-### API integration tests
+### Network tests
 
 ```bash
-./scripts/run-dev-chain.sh
+./scripts/run-test-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.
+Proposal grace periods should be set to 0, otherwise proposal network tests will fail.
+
+### Rome-Constantinople migration network test
+
+Ensure Rome node is up and running, and node URL is set using `NODE_URL` environment variable (default value is `localhost:9944`).
+
+```bash
+yarn test-migration
+```
 
 ## Joystream Runtime
 
@@ -140,15 +149,15 @@ cargo-fmt
 
 ## Contributing
 
-Please see our [contributing guidlines](https://github.com/Joystream/joystream#contribute) for details on our code of conduct, and the process for submitting pull requests to us.
+Please see our [contributing guidlines](./CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us.
 
 ## Authors
 
-See also the list of [CONTRIBUTORS](./CONTRIBUTORS) who participated in this project.
+See also the list of [CONTRIBUTORS](https://github.com/Joystream/joystream/graphs/contributors) who participated in this project.
 
 ## License
 
-This project is licensed under the GPLv3 License - see the [LICENSE](LICENSE) file for details
+This project is licensed under the GPLv3 License - see the [LICENSE](./LICENSE) file for details
 
 ## Acknowledgments
 

+ 0 - 11
cli/.editorconfig

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

+ 0 - 6
cli/.eslintrc

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

+ 10 - 0
cli/.eslintrc.js

@@ -0,0 +1,10 @@
+module.exports = {
+  extends: [
+    // The oclif rules have some code-style/formatting rules which may conflict with
+    // our prettier global settings. Disabling for now
+    // I suggest to only add essential rules absolutely required to make the cli work with oclif
+    // at the end of this file.
+    // "oclif",
+    // "oclif-typescript",
+  ],
+}

+ 2 - 0
cli/.prettierignore

@@ -0,0 +1,2 @@
+/lib/
+.nyc_output

+ 14 - 6
cli/package.json

@@ -8,7 +8,7 @@
   },
   "bugs": "https://github.com/Joystream/substrate-runtime-joystream/issues",
   "dependencies": {
-    "@joystream/types": "^0.6.0",
+    "@joystream/types": "^0.11.0",
     "@oclif/command": "^1.5.19",
     "@oclif/config": "^1.14.0",
     "@oclif/plugin-help": "^2.2.3",
@@ -21,11 +21,13 @@
     "moment": "^2.24.0",
     "proper-lockfile": "^4.1.1",
     "slug": "^2.1.1",
-    "tslib": "^1.11.1"
+    "tslib": "^1.11.1",
+    "ajv": "^6.11.0"
   },
   "devDependencies": {
     "@oclif/dev-cli": "^1.22.2",
     "@oclif/test": "^1.2.5",
+    "@polkadot/ts": "^0.1.56",
     "@types/chai": "^4.2.11",
     "@types/mocha": "^5.2.7",
     "@types/node": "^10.17.18",
@@ -37,8 +39,7 @@
     "mocha": "^5.2.0",
     "nyc": "^14.1.1",
     "ts-node": "^8.8.2",
-    "typescript": "^3.8.3",
-    "@polkadot/ts": "^0.1.56"
+    "typescript": "^3.8.3"
   },
   "engines": {
     "node": ">=8.0.0"
@@ -71,6 +72,9 @@
       },
       "api": {
         "description": "Inspect the substrate node api, perform lower-level api calls or change the current api provider uri"
+      },
+      "working-groups": {
+        "description": "Working group lead and worker actions"
       }
     }
   },
@@ -81,10 +85,14 @@
   },
   "scripts": {
     "postpack": "rm -f oclif.manifest.json",
-    "posttest": "eslint . --ext .ts --config .eslintrc",
+    "posttest": "yarn lint",
     "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"
+    "build": "tsc --build tsconfig.json",
+    "version": "oclif-dev readme && git add README.md",
+    "lint": "eslint ./src/ --quiet --ext .ts",
+    "checks": "yarn lint && tsc --noEmit --pretty && prettier ./ --check",
+    "format": "prettier ./ --write"
   },
   "types": "lib/index.d.ts"
 }

+ 351 - 25
cli/src/Api.ts

@@ -1,25 +1,59 @@
 import BN from 'bn.js';
-import { registerJoystreamTypes } from '@joystream/types';
+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 { Hash, Balance } from '@polkadot/types/interfaces';
 import { KeyringPair } from '@polkadot/keyring/types';
 import { Codec } from '@polkadot/types/types';
-import { AccountSummary, CouncilInfoObj, CouncilInfoTuple, createCouncilInfoObj } from './Types';
+import { Option, Vec } from '@polkadot/types';
+import { u32 } from '@polkadot/types/primitive';
+import {
+    AccountSummary,
+    CouncilInfoObj, CouncilInfoTuple, createCouncilInfoObj,
+    WorkingGroups,
+    GroupMember,
+    OpeningStatus,
+    GroupOpeningStage,
+    GroupOpening,
+    GroupApplication
+} from './Types';
 import { DerivedFees, DerivedBalances } from '@polkadot/api-derive/types';
 import { CLIError } from '@oclif/errors';
 import ExitCodes from './ExitCodes';
+import {
+    Worker, WorkerId,
+    RoleStakeProfile,
+    Opening as WGOpening,
+    Application as WGApplication
+} from '@joystream/types/working-group';
+import {
+    Opening,
+    Application,
+    OpeningStage,
+    ApplicationStageKeys,
+    ApplicationId,
+    OpeningId
+} from '@joystream/types/hiring';
+import { MemberId, Profile } from '@joystream/types/members';
+import { RewardRelationship, RewardRelationshipId } from '@joystream/types/recurring-rewards';
+import { Stake, StakeId } from '@joystream/types/stake';
+import { LinkageResult } from '@polkadot/types/codec/Linkage';
+import { Moment } from '@polkadot/types/interfaces';
 
 export const DEFAULT_API_URI = 'wss://rome-rpc-endpoint.joystream.org:9944/';
-export const TOKEN_SYMBOL = 'JOY';
+const DEFAULT_DECIMALS = new u32(12);
 
-// Api wrapper for handling most common api calls and allowing easy API implementation switch in the future
+// Mapping of working group to api module
+export const apiModuleByGroup: { [key in WorkingGroups]: string } = {
+    [WorkingGroups.StorageProviders]: 'storageWorkingGroup'
+};
 
+// 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) {
+    private constructor(originalApi: ApiPromise) {
         this._api = originalApi;
     }
 
@@ -28,11 +62,25 @@ export default class Api {
     }
 
     private static async initApi(apiUri: string = DEFAULT_API_URI): Promise<ApiPromise> {
-        formatBalance.setDefaults({ unit: TOKEN_SYMBOL });
-        const wsProvider:WsProvider = new WsProvider(apiUri);
+        const wsProvider: WsProvider = new WsProvider(apiUri);
         registerJoystreamTypes();
+        const api = await ApiPromise.create({ provider: wsProvider });
+
+        // Initializing some api params based on pioneer/packages/react-api/Api.tsx
+        const [properties] = await Promise.all([
+            api.rpc.system.properties()
+        ]);
+
+        const tokenSymbol = properties.tokenSymbol.unwrapOr('DEV').toString();
+        const tokenDecimals = properties.tokenDecimals.unwrapOr(DEFAULT_DECIMALS).toNumber();
 
-        return await ApiPromise.create({ provider: wsProvider });
+        // formatBlanace config
+        formatBalance.setDefaults({
+            decimals: tokenDecimals,
+            unit: tokenSymbol
+        });
+
+        return api;
     }
 
     static async create(apiUri: string = DEFAULT_API_URI): Promise<Api> {
@@ -56,7 +104,7 @@ export default class Api {
         return results;
     }
 
-    async getAccountsBalancesInfo(accountAddresses:string[]): Promise<DerivedBalances[]> {
+    async getAccountsBalancesInfo(accountAddresses: string[]): Promise<DerivedBalances[]> {
         let accountsBalances: DerivedBalances[] = await this._api.derive.balances.votingBalances(accountAddresses);
 
         return accountsBalances;
@@ -64,7 +112,7 @@ export default class Api {
 
     // Get on-chain data related to given account.
     // For now it's just account balances
-    async getAccountSummary(accountAddresses:string): Promise<AccountSummary> {
+    async getAccountSummary(accountAddresses: string): Promise<AccountSummary> {
         const balances: DerivedBalances = (await this.getAccountsBalancesInfo([accountAddresses]))[0];
         // TODO: Some more information can be fetched here in the future
 
@@ -73,21 +121,21 @@ export default class Api {
 
     async getCouncilInfo(): Promise<CouncilInfoObj> {
         const queries: { [P in keyof CouncilInfoObj]: QueryableStorageMultiArg<"promise"> } = {
-            activeCouncil:    this._api.query.council.activeCouncil,
-            termEndsAt:       this._api.query.council.termEndsAt,
-            autoStart:        this._api.query.councilElection.autoStart,
-            newTermDuration:  this._api.query.councilElection.newTermDuration,
-            candidacyLimit:   this._api.query.councilElection.candidacyLimit,
-            councilSize:      this._api.query.councilElection.councilSize,
-            minCouncilStake:  this._api.query.councilElection.minCouncilStake,
-            minVotingStake:   this._api.query.councilElection.minVotingStake,
+            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
+            votingPeriod: this._api.query.councilElection.votingPeriod,
+            revealingPeriod: this._api.query.councilElection.revealingPeriod,
+            round: this._api.query.councilElection.round,
+            stage: this._api.query.councilElection.stage
         }
-        const results: CouncilInfoTuple = <CouncilInfoTuple> await this.queryMultiOnce(Object.values(queries));
+        const results: CouncilInfoTuple = <CouncilInfoTuple>await this.queryMultiOnce(Object.values(queries));
 
         return createCouncilInfoObj(...results);
     }
@@ -96,7 +144,7 @@ export default class Api {
     async estimateFee(account: KeyringPair, recipientAddr: string, amount: BN): Promise<BN> {
         const transfer = this._api.tx.balances.transfer(recipientAddr, amount);
         const signature = account.sign(transfer.toU8a());
-        const transactionByteSize:BN = new BN(transfer.encodedLength + signature.length);
+        const transactionByteSize: BN = new BN(transfer.encodedLength + signature.length);
 
         const fees: DerivedFees = await this._api.derive.balances.fees();
 
@@ -111,4 +159,282 @@ export default class Api {
             .signAndSend(account);
         return txHash;
     }
+
+    // Working groups
+    // TODO: This is a lot of repeated logic from "/pioneer/joy-roles/src/transport.substrate.ts"
+    // (although simplified a little bit)
+    // Hopefully this will be refactored to "joystream-js" soon
+    protected singleLinkageResult<T extends Codec>(result: LinkageResult) {
+        return result[0] as T;
+    }
+
+    protected multiLinkageResult<K extends Codec, V extends Codec>(result: LinkageResult): [Vec<K>, Vec<V>] {
+        return [result[0] as Vec<K>, result[1] as Vec<V>];
+    }
+
+    protected async blockHash(height: number): Promise<string> {
+        const blockHash = await this._api.rpc.chain.getBlockHash(height);
+
+        return blockHash.toString();
+    }
+
+    protected async blockTimestamp(height: number): Promise<Date> {
+        const blockTime = (await this._api.query.timestamp.now.at(await this.blockHash(height))) as Moment;
+
+        return new Date(blockTime.toNumber());
+    }
+
+    protected workingGroupApiQuery(group: WorkingGroups) {
+        const module = apiModuleByGroup[group];
+        return this._api.query[module];
+    }
+
+    protected async memberProfileById(memberId: MemberId): Promise<Profile | null> {
+        const profile = await this._api.query.members.memberProfile(memberId) as Option<Profile>;
+
+        return profile.unwrapOr(null);
+    }
+
+    async groupLead(group: WorkingGroups): Promise<GroupMember | null> {
+        const optLeadId = (await this.workingGroupApiQuery(group).currentLead()) as Option<WorkerId>;
+
+        if (!optLeadId.isSome) {
+            return null;
+        }
+
+        const leadWorkerId = optLeadId.unwrap();
+        const leadWorker = this.singleLinkageResult<Worker>(
+            await this.workingGroupApiQuery(group).workerById(leadWorkerId) as LinkageResult
+        );
+
+        if (!leadWorker.is_active) {
+            return null;
+        }
+
+        return await this.groupMember(leadWorkerId, leadWorker);
+    }
+
+    protected async stakeValue(stakeId: StakeId): Promise<Balance> {
+        const stake = this.singleLinkageResult<Stake>(
+            await this._api.query.stake.stakes(stakeId) as LinkageResult
+        );
+        return stake.value;
+    }
+
+    protected async workerStake (stakeProfile: RoleStakeProfile): Promise<Balance> {
+        return this.stakeValue(stakeProfile.stake_id);
+    }
+
+    protected async workerTotalReward(relationshipId: RewardRelationshipId): Promise<Balance> {
+        const relationship = this.singleLinkageResult<RewardRelationship>(
+            await this._api.query.recurringRewards.rewardRelationships(relationshipId) as LinkageResult
+        );
+        return relationship.total_reward_received;
+    }
+
+    protected async groupMember(
+        id: WorkerId,
+        worker: Worker
+    ): Promise<GroupMember> {
+        const roleAccount = worker.role_account_id;
+        const memberId = worker.member_id;
+
+        const profile = await this.memberProfileById(memberId);
+
+        if (!profile) {
+            throw new Error(`Group member profile not found! (member id: ${memberId.toNumber()})`);
+        }
+
+        let stakeValue: Balance = this._api.createType("Balance", 0);
+        if (worker.role_stake_profile && worker.role_stake_profile.isSome) {
+            stakeValue = await this.workerStake(worker.role_stake_profile.unwrap());
+        }
+
+        let earnedValue: Balance = this._api.createType("Balance", 0);
+        if (worker.reward_relationship && worker.reward_relationship.isSome) {
+            earnedValue = await this.workerTotalReward(worker.reward_relationship.unwrap());
+        }
+
+        return ({
+            workerId: id,
+            roleAccount,
+            memberId,
+            profile,
+            stake: stakeValue,
+            earned: earnedValue
+        });
+    }
+
+    async groupMembers(group: WorkingGroups): Promise<GroupMember[]> {
+        const nextId = (await this.workingGroupApiQuery(group).nextWorkerId()) as WorkerId;
+
+        // This is chain specfic, but if next id is still 0, it means no curators have been added yet
+        if (nextId.eq(0)) {
+            return [];
+        }
+
+        const [workerIds, workers] = this.multiLinkageResult<WorkerId, Worker>(
+            (await this.workingGroupApiQuery(group).workerById()) as LinkageResult
+        );
+
+        let groupMembers: GroupMember[] = [];
+        for (let [index, worker] of Object.entries(workers.toArray())) {
+            const workerId = workerIds[parseInt(index)];
+            groupMembers.push(await this.groupMember(workerId, worker));
+        }
+
+        return groupMembers.reverse();
+    }
+
+    async openingsByGroup(group: WorkingGroups): Promise<GroupOpening[]> {
+        const openings: GroupOpening[] = [];
+        const nextId = (await this.workingGroupApiQuery(group).nextOpeningId()) as OpeningId;
+
+        // This is chain specfic, but if next id is still 0, it means no openings have been added yet
+        if (!nextId.eq(0)) {
+            const highestId = nextId.toNumber() - 1;
+            for (let i = highestId; i >= 0; i--) {
+                openings.push(await this.groupOpening(group, i));
+            }
+        }
+
+        return openings;
+    }
+
+    protected async hiringOpeningById(id: number | OpeningId): Promise<Opening> {
+        const result = await this._api.query.hiring.openingById(id) as LinkageResult;
+        return this.singleLinkageResult<Opening>(result);
+    }
+
+    protected async hiringApplicationById(id: number | ApplicationId): Promise<Application> {
+        const result = await this._api.query.hiring.applicationById(id) as LinkageResult;
+        return this.singleLinkageResult<Application>(result);
+    }
+
+    async wgApplicationById(group: WorkingGroups, wgApplicationId: number): Promise<WGApplication> {
+        const nextAppId = await this.workingGroupApiQuery(group).nextApplicationId() as ApplicationId;
+
+        if (wgApplicationId < 0 || wgApplicationId >= nextAppId.toNumber()) {
+            throw new CLIError('Invalid working group application ID!');
+        }
+
+        return this.singleLinkageResult<WGApplication>(
+            await this.workingGroupApiQuery(group).applicationById(wgApplicationId) as LinkageResult
+        );
+    }
+
+    protected async parseApplication(wgApplicationId: number, wgApplication: WGApplication): Promise<GroupApplication> {
+        const appId = wgApplication.application_id;
+        const application = await this.hiringApplicationById(appId);
+
+        const { active_role_staking_id: roleStakingId, active_application_staking_id: appStakingId } = application;
+
+        return {
+            wgApplicationId,
+            applicationId: appId.toNumber(),
+            member: await this.memberProfileById(wgApplication.member_id),
+            roleAccout: wgApplication.role_account_id,
+            stakes: {
+                application: appStakingId.isSome ? (await this.stakeValue(appStakingId.unwrap())).toNumber() : 0,
+                role: roleStakingId.isSome ? (await this.stakeValue(roleStakingId.unwrap())).toNumber() : 0
+            },
+            humanReadableText: application.human_readable_text.toString(),
+            stage: application.stage.type as ApplicationStageKeys
+        };
+    }
+
+    async groupApplication(group: WorkingGroups, wgApplicationId: number): Promise<GroupApplication> {
+        const wgApplication = await this.wgApplicationById(group, wgApplicationId);
+        return await this.parseApplication(wgApplicationId, wgApplication);
+    }
+
+    protected async groupOpeningApplications(group: WorkingGroups, wgOpeningId: number): Promise<GroupApplication[]> {
+        const applications: GroupApplication[] = [];
+
+        const nextAppId = await this.workingGroupApiQuery(group).nextApplicationId() as ApplicationId;
+        for (let i = 0; i < nextAppId.toNumber(); i++) {
+            const wgApplication = await this.wgApplicationById(group, i);
+            if (wgApplication.opening_id.toNumber() !== wgOpeningId) {
+                continue;
+            }
+            applications.push(await this.parseApplication(i, wgApplication));
+        }
+
+
+        return applications;
+    }
+
+    async groupOpening(group: WorkingGroups, wgOpeningId: number): Promise<GroupOpening> {
+        const nextId = ((await this.workingGroupApiQuery(group).nextOpeningId()) as OpeningId).toNumber();
+
+        if (wgOpeningId < 0 || wgOpeningId >= nextId) {
+            throw new CLIError('Invalid working group opening ID!');
+        }
+
+        const groupOpening = this.singleLinkageResult<WGOpening>(
+            await this.workingGroupApiQuery(group).openingById(wgOpeningId) as LinkageResult
+        );
+
+        const openingId = groupOpening.hiring_opening_id.toNumber();
+        const opening = await this.hiringOpeningById(openingId);
+        const applications = await this.groupOpeningApplications(group, wgOpeningId);
+        const stage = await this.parseOpeningStage(opening.stage);
+        const stakes = {
+            application: opening.application_staking_policy.unwrapOr(undefined),
+            role: opening.role_staking_policy.unwrapOr(undefined)
+        }
+
+        return ({
+            wgOpeningId,
+            openingId,
+            opening,
+            stage,
+            stakes,
+            applications
+        });
+    }
+
+    async parseOpeningStage(stage: OpeningStage): Promise<GroupOpeningStage> {
+        let
+            status: OpeningStatus | undefined,
+            stageBlock: number | undefined,
+            stageDate: Date | undefined;
+
+        if (stage.isOfType('WaitingToBegin')) {
+            const stageData = stage.asType('WaitingToBegin');
+            const currentBlockNumber = (await this._api.derive.chain.bestNumber()).toNumber();
+            const expectedBlockTime = (this._api.consts.babe.expectedBlockTime as Moment).toNumber();
+            status = OpeningStatus.WaitingToBegin;
+            stageBlock = stageData.begins_at_block.toNumber();
+            stageDate = new Date(Date.now() + (stageBlock - currentBlockNumber) * expectedBlockTime);
+        }
+
+        if (stage.isOfType('Active')) {
+            const stageData = stage.asType('Active');
+            const substage = stageData.stage;
+            if (substage.isOfType('AcceptingApplications')) {
+                status = OpeningStatus.AcceptingApplications;
+                stageBlock = substage.asType('AcceptingApplications').started_accepting_applicants_at_block.toNumber();
+            }
+            if (substage.isOfType('ReviewPeriod')) {
+                status = OpeningStatus.InReview;
+                stageBlock = substage.asType('ReviewPeriod').started_review_period_at_block.toNumber();
+            }
+            if (substage.isOfType('Deactivated')) {
+                status = substage.asType('Deactivated').cause.isOfType('Filled')
+                    ? OpeningStatus.Complete
+                    : OpeningStatus.Cancelled;
+                stageBlock = substage.asType('Deactivated').deactivated_at_block.toNumber();
+            }
+            if (stageBlock) {
+                stageDate = new Date(await this.blockTimestamp(stageBlock));
+            }
+        }
+
+        return {
+            status: status || OpeningStatus.Unknown,
+            block: stageBlock,
+            date: stageDate
+        };
+    }
 }

+ 1 - 0
cli/src/ExitCodes.ts

@@ -6,6 +6,7 @@ enum ExitCodes {
     InvalidFile = 402,
     NoAccountFound = 403,
     NoAccountSelected = 404,
+    AccessDenied = 405,
 
     UnexpectedException = 500,
     FsOperationFailed = 501,

+ 258 - 3
cli/src/Types.ts

@@ -1,9 +1,29 @@
 import BN from 'bn.js';
-import { ElectionStage, Seat } from '@joystream/types';
-import { Option } from '@polkadot/types';
-import { BlockNumber, Balance } from '@polkadot/types/interfaces';
+import { ElectionStage, Seat } from '@joystream/types/council';
+import { Option, Text } from '@polkadot/types';
+import { Constructor } from '@polkadot/types/types';
+import { Struct, Vec } from '@polkadot/types/codec';
+import { u32 } from '@polkadot/types/primitive';
+import { BlockNumber, Balance, AccountId } from '@polkadot/types/interfaces';
 import { DerivedBalances } from '@polkadot/api-derive/types';
 import { KeyringPair } from '@polkadot/keyring/types';
+import { WorkerId } from '@joystream/types/working-group';
+import { Profile, MemberId } from '@joystream/types/members';
+import {
+    GenericJoyStreamRoleSchema,
+    JobSpecifics,
+    ApplicationDetails,
+    QuestionSections,
+    QuestionSection,
+    QuestionsFields,
+    QuestionField,
+    EntryInMembershipModuke,
+    HiringProcess,
+    AdditionalRolehiringProcessDetails,
+    CreatorDetails
+} from '@joystream/types/hiring/schemas/role.schema.typings';
+import ajv from 'ajv';
+import { Opening, StakingPolicy, ApplicationStageKeys } from '@joystream/types/hiring';
 
 // KeyringPair type extended with mandatory "meta.name"
 // It's used for accounts/keys management within CLI.
@@ -61,3 +81,238 @@ export function createCouncilInfoObj(
 // Total balance:   100 JOY
 // Free calance:     50 JOY
 export type NameValueObj = { name: string, value: string };
+
+// Working groups related types
+export enum WorkingGroups {
+    StorageProviders = 'storageProviders'
+}
+
+// In contrast to Pioneer, currently only StorageProviders group is available in CLI
+export const AvailableGroups: readonly WorkingGroups[] = [
+  WorkingGroups.StorageProviders
+] as const;
+
+// Compound working group types
+export type GroupMember = {
+    workerId: WorkerId;
+    memberId: MemberId;
+    roleAccount: AccountId;
+    profile: Profile;
+    stake: Balance;
+    earned: Balance;
+}
+
+export type GroupApplication = {
+    wgApplicationId: number;
+    applicationId: number;
+    member: Profile | null;
+    roleAccout: AccountId;
+    stakes: {
+        application: number;
+        role: number;
+    },
+    humanReadableText: string;
+    stage: ApplicationStageKeys;
+}
+
+export enum OpeningStatus {
+    WaitingToBegin = 'WaitingToBegin',
+    AcceptingApplications = 'AcceptingApplications',
+    InReview = 'InReview',
+    Complete = 'Complete',
+    Cancelled = 'Cancelled',
+    Unknown = 'Unknown'
+}
+
+export type GroupOpeningStage = {
+    status: OpeningStatus;
+    block?: number;
+    date?: Date;
+}
+
+export type GroupOpeningStakes = {
+    application?: StakingPolicy;
+    role?: StakingPolicy;
+}
+
+export type GroupOpening = {
+    wgOpeningId: number;
+    openingId: number;
+    stage: GroupOpeningStage;
+    opening: Opening;
+    stakes: GroupOpeningStakes;
+    applications: GroupApplication[];
+}
+
+// Some helper structs for generating human_readable_text in working group opening extrinsic
+// Note those types are not part of the runtime etc., we just use them to simplify prompting for values
+// (since there exists functionality that handles that for substrate types like: Struct, Vec etc.)
+interface WithJSONable<T> {
+    toJSON: () => T;
+}
+export class HRTJobSpecificsStruct extends Struct implements WithJSONable<JobSpecifics> {
+    constructor (value?: JobSpecifics) {
+        super({
+          title: "Text",
+          description: "Text",
+        }, value);
+    }
+    get title(): string {
+        return (this.get('title') as Text).toString();
+    }
+    get description(): string {
+        return (this.get('description') as Text).toString();
+    }
+    toJSON(): JobSpecifics {
+        const { title, description } = this;
+        return { title, description };
+    }
+}
+export class HRTEntryInMembershipModukeStruct extends Struct implements WithJSONable<EntryInMembershipModuke> {
+    constructor (value?: EntryInMembershipModuke) {
+        super({
+          handle: "Text",
+        }, value);
+    }
+    get handle(): string {
+        return (this.get('handle') as Text).toString();
+    }
+    toJSON(): EntryInMembershipModuke {
+        const { handle } = this;
+        return { handle };
+    }
+}
+export class HRTCreatorDetailsStruct extends Struct implements WithJSONable<CreatorDetails> {
+    constructor (value?: CreatorDetails) {
+        super({
+          membership: HRTEntryInMembershipModukeStruct,
+        }, value);
+    }
+    get membership(): EntryInMembershipModuke {
+        return (this.get('membership') as HRTEntryInMembershipModukeStruct).toJSON();
+    }
+    toJSON(): CreatorDetails {
+        const { membership } = this;
+        return { membership };
+    }
+}
+export class HRTHiringProcessStruct extends Struct implements WithJSONable<HiringProcess> {
+    constructor (value?: HiringProcess) {
+        super({
+          details: "Vec<Text>",
+        }, value);
+    }
+    get details(): AdditionalRolehiringProcessDetails {
+        return (this.get('details') as Vec<Text>).toArray().map(v => v.toString());
+    }
+    toJSON(): HiringProcess {
+        const { details } = this;
+        return { details };
+    }
+}
+export class HRTQuestionFieldStruct extends Struct implements WithJSONable<QuestionField> {
+    constructor (value?: QuestionField) {
+        super({
+            title: "Text",
+            type: "Text"
+        }, value);
+    }
+    get title(): string {
+        return (this.get('title') as Text).toString();
+    }
+    get type(): string {
+        return (this.get('type') as Text).toString();
+    }
+    toJSON(): QuestionField {
+        const { title, type } = this;
+        return { title, type };
+    }
+}
+class HRTQuestionsFieldsVec extends Vec.with(HRTQuestionFieldStruct) implements WithJSONable<QuestionsFields> {
+    toJSON(): QuestionsFields {
+        return this.toArray().map(v => v.toJSON());
+    }
+}
+export class HRTQuestionSectionStruct extends Struct implements WithJSONable<QuestionSection> {
+    constructor (value?: QuestionSection) {
+        super({
+            title: "Text",
+            questions: HRTQuestionsFieldsVec
+        }, value);
+    }
+    get title(): string {
+        return (this.get('title') as Text).toString();
+    }
+    get questions(): QuestionsFields {
+        return (this.get('questions') as HRTQuestionsFieldsVec).toJSON();
+    }
+    toJSON(): QuestionSection {
+        const { title, questions } = this;
+        return { title, questions };
+    }
+}
+export class HRTQuestionSectionsVec extends Vec.with(HRTQuestionSectionStruct) implements WithJSONable<QuestionSections> {
+    toJSON(): QuestionSections {
+        return this.toArray().map(v => v.toJSON());
+    }
+};
+export class HRTApplicationDetailsStruct extends Struct implements WithJSONable<ApplicationDetails> {
+    constructor (value?: ApplicationDetails) {
+        super({
+            sections: HRTQuestionSectionsVec
+        }, value);
+    }
+    get sections(): QuestionSections {
+        return (this.get('sections') as HRTQuestionSectionsVec).toJSON();
+    }
+    toJSON(): ApplicationDetails {
+        const { sections } = this;
+        return { sections };
+    }
+}
+export class HRTStruct extends Struct implements WithJSONable<GenericJoyStreamRoleSchema> {
+    constructor (value?: GenericJoyStreamRoleSchema) {
+        super({
+            version: "u32",
+            headline: "Text",
+            job: HRTJobSpecificsStruct,
+            application: HRTApplicationDetailsStruct,
+            reward: "Text",
+            creator: HRTCreatorDetailsStruct,
+            process: HRTHiringProcessStruct
+        }, value);
+    }
+    get version(): number {
+        return (this.get('version') as u32).toNumber();
+    }
+    get headline(): string {
+        return (this.get('headline') as Text).toString();
+    }
+    get job(): JobSpecifics {
+        return (this.get('job') as HRTJobSpecificsStruct).toJSON();
+    }
+    get application(): ApplicationDetails {
+        return (this.get('application') as HRTApplicationDetailsStruct).toJSON();
+    }
+    get reward(): string {
+        return (this.get('reward') as Text).toString();
+    }
+    get creator(): CreatorDetails {
+        return (this.get('creator') as HRTCreatorDetailsStruct).toJSON();
+    }
+    get process(): HiringProcess {
+        return (this.get('process') as HRTHiringProcessStruct).toJSON();
+    }
+    toJSON(): GenericJoyStreamRoleSchema {
+        const { version, headline, job, application, reward, creator, process } = this;
+        return { version, headline, job, application, reward, creator, process };
+    }
+};
+
+// A mapping of argName to json struct and schemaValidator
+// It is used to map arguments of type "Bytes" that are in fact a json string
+// (and can be validated against a schema)
+export type JSONArgsMapping = { [argName: string]: {
+    struct: Constructor<Struct>,
+    schemaValidator: ajv.ValidateFunction
+} };

+ 32 - 12
cli/src/base/AccountsCommandBase.ts

@@ -11,26 +11,27 @@ import { NamedKeyringPair } from '../Types';
 import { DerivedBalances } from '@polkadot/api-derive/types';
 import { toFixedLength } from '../helpers/display';
 
-const ACCOUNTS_DIRNAME = '/accounts';
+const ACCOUNTS_DIRNAME = 'accounts';
+const SPECIAL_ACCOUNT_POSTFIX = '__DEV';
 
 /**
  * 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).
+ * { APP_DATA_PATH }/{ ACCOUNTS_DIRNAME } (ie. ~/.local/share/joystream-cli/accounts on Ubuntu)
+ * Where: APP_DATA_PATH is provided by StateAwareCommandBase 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);
+        return path.join(this.getAppDataPath(), ACCOUNTS_DIRNAME);
     }
 
-    getAccountFilePath(account: NamedKeyringPair): string {
-        return path.join(this.getAccountsDirPath(), this.generateAccountFilename(account));
+    getAccountFilePath(account: NamedKeyringPair, isSpecial: boolean = false): string {
+        return path.join(this.getAccountsDirPath(), this.generateAccountFilename(account, isSpecial));
     }
 
-    generateAccountFilename(account: NamedKeyringPair): string {
-        return `${ slug(account.meta.name, '_') }__${ account.address }.json`;
+    generateAccountFilename(account: NamedKeyringPair, isSpecial: boolean = false): string {
+        return `${ slug(account.meta.name, '_') }__${ account.address }${ isSpecial ? SPECIAL_ACCOUNT_POSTFIX : '' }.json`;
     }
 
     private initAccountsFs(): void {
@@ -39,14 +40,27 @@ export default abstract class AccountsCommandBase extends ApiCommandBase {
         }
     }
 
-    saveAccount(account: NamedKeyringPair, password: string): void {
+    saveAccount(account: NamedKeyringPair, password: string, isSpecial: boolean = false): void {
         try {
-            fs.writeFileSync(this.getAccountFilePath(account), JSON.stringify(account.toJson(password)));
+            const destPath = this.getAccountFilePath(account, isSpecial);
+            fs.writeFileSync(destPath, JSON.stringify(account.toJson(password)));
         } catch(e) {
             throw this.createDataWriteError();
         }
     }
 
+    // Add dev "Alice" and "Bob" accounts
+    initSpecialAccounts() {
+        const keyring = new Keyring({ type: 'sr25519' });
+        keyring.addFromUri('//Alice', { name: 'Alice' });
+        keyring.addFromUri('//Bob', { name: 'Bob' });
+        keyring.getPairs().forEach(pair => this.saveAccount(
+            { ...pair, meta: { name: pair.meta.name } },
+            '',
+            true
+        ));
+    }
+
     fetchAccountFromJsonFile(jsonBackupFilePath: string): NamedKeyringPair {
         if (!fs.existsSync(jsonBackupFilePath)) {
             throw new CLIError('Input file does not exist!', { exit: ExitCodes.FileNotFound });
@@ -91,7 +105,7 @@ export default abstract class AccountsCommandBase extends ApiCommandBase {
         }
     }
 
-    fetchAccounts(): NamedKeyringPair[] {
+    fetchAccounts(includeSpecial: boolean = false): NamedKeyringPair[] {
         let files: string[] = [];
         const accountDir = this.getAccountsDirPath();
         try {
@@ -104,6 +118,7 @@ export default abstract class AccountsCommandBase extends ApiCommandBase {
         return <NamedKeyringPair[]> files
             .map(fileName => {
                 const filePath = path.join(accountDir, fileName);
+                if (!includeSpecial && filePath.includes(SPECIAL_ACCOUNT_POSTFIX+'.')) return null;
                 return this.fetchAccountOrNullFromFile(filePath);
             })
             .filter(accObj => accObj !== null);
@@ -145,7 +160,11 @@ export default abstract class AccountsCommandBase extends ApiCommandBase {
     }
 
     async setSelectedAccount(account: NamedKeyringPair): Promise<void> {
-        await this.setPreservedState({ selectedAccountFilename: this.generateAccountFilename(account) });
+        const accountFilename = fs.existsSync(this.getAccountFilePath(account, true))
+            ? this.generateAccountFilename(account, true)
+            : this.generateAccountFilename(account);
+
+        await this.setPreservedState({ selectedAccountFilename: accountFilename });
     }
 
     async promptForPassword(message:string = 'Your account\'s password') {
@@ -210,6 +229,7 @@ export default abstract class AccountsCommandBase extends ApiCommandBase {
         await super.init();
         try {
             this.initAccountsFs();
+            this.initSpecialAccounts();
         } catch (e) {
             throw this.createDataDirInitError();
         }

+ 349 - 1
cli/src/base/ApiCommandBase.ts

@@ -2,7 +2,19 @@ import ExitCodes from '../ExitCodes';
 import { CLIError } from '@oclif/errors';
 import StateAwareCommandBase from './StateAwareCommandBase';
 import Api from '../Api';
-import { ApiPromise } from '@polkadot/api'
+import { JSONArgsMapping } from '../Types';
+import { getTypeDef, createType, Option, Tuple, Bytes } from '@polkadot/types';
+import { Codec, TypeDef, TypeDefInfo, Constructor } from '@polkadot/types/types';
+import { Vec, Struct, Enum } from '@polkadot/types/codec';
+import { ApiPromise } from '@polkadot/api';
+import { KeyringPair } from '@polkadot/keyring/types';
+import chalk from 'chalk';
+import { SubmittableResultImpl } from '@polkadot/api/types';
+import ajv from 'ajv';
+
+export type ApiMethodInputArg = Codec;
+
+class ExtrinsicFailedError extends Error { };
 
 /**
  * Abstract base class for commands that require access to the API.
@@ -25,4 +37,340 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
         const apiUri: string = this.getPreservedState().apiUri;
         this.api = await Api.create(apiUri);
     }
+
+    // This is needed to correctly handle some structs, enums etc.
+    // Where the main typeDef doesn't provide enough information
+    protected getRawTypeDef(type: string) {
+        const instance = createType(type as any);
+        return getTypeDef(instance.toRawType());
+    }
+
+    // Prettifier for type names which are actually JSON strings
+    protected prettifyJsonTypeName(json: string) {
+        const obj = JSON.parse(json) as { [key: string]: string };
+        return "{\n"+Object.keys(obj).map(prop => `  ${prop}${chalk.white(':'+obj[prop])}`).join("\n")+"\n}";
+    }
+
+    // Get param name based on TypeDef object
+    protected paramName(typeDef: TypeDef) {
+        return chalk.green(
+            typeDef.displayName ||
+            typeDef.name ||
+            (typeDef.type.startsWith('{') ? this.prettifyJsonTypeName(typeDef.type) : typeDef.type)
+        );
+    }
+
+    // Prompt for simple/plain value (provided as string) of given type
+    async promptForSimple(typeDef: TypeDef, defaultValue?: Codec): Promise<Codec> {
+        const providedValue = await this.simplePrompt({
+            message: `Provide value for ${ this.paramName(typeDef) }`,
+            type: 'input',
+            default: defaultValue?.toString()
+        });
+        return createType(typeDef.type as any, providedValue);
+    }
+
+    // Prompt for Option<Codec> value
+    async promptForOption(typeDef: TypeDef, defaultValue?: Option<Codec>): Promise<Option<Codec>> {
+        const subtype = <TypeDef> typeDef.sub; // We assume that Opion always has a single subtype
+        const confirmed = await this.simplePrompt({
+            message: `Do you want to provide the optional ${ this.paramName(typeDef) } parameter?`,
+            type: 'confirm',
+            default: defaultValue ? defaultValue.isSome : false,
+        });
+
+        if (confirmed) {
+            this.openIndentGroup();
+            const value = await this.promptForParam(subtype.type, subtype.name, defaultValue?.unwrapOr(undefined));
+            this.closeIndentGroup();
+            return new Option(subtype.type as any, value);
+        }
+
+        return new Option(subtype.type as any, null);
+    }
+
+    // Prompt for Tuple
+    // TODO: Not well tested yet
+    async promptForTuple(typeDef: TypeDef, defaultValue: Tuple): Promise<Tuple> {
+        console.log(chalk.grey(`Providing values for ${ this.paramName(typeDef) } tuple:`));
+
+        this.openIndentGroup();
+        const result: ApiMethodInputArg[] = [];
+        // We assume that for Tuple there is always at least 1 subtype (pethaps it's even always an array?)
+        const subtypes: TypeDef[] = Array.isArray(typeDef.sub) ? typeDef.sub! : [ typeDef.sub! ];
+
+        for (const [index, subtype] of Object.entries(subtypes)) {
+            const inputParam = await this.promptForParam(subtype.type, subtype.name, defaultValue[parseInt(index)]);
+            result.push(inputParam);
+        }
+        this.closeIndentGroup();
+
+        return new Tuple((subtypes.map(subtype => subtype.type)) as any, result);
+    }
+
+    // Prompt for Struct
+    async promptForStruct(typeDef: TypeDef, defaultValue?: Struct): Promise<ApiMethodInputArg> {
+        console.log(chalk.grey(`Providing values for ${ this.paramName(typeDef) } struct:`));
+
+        this.openIndentGroup();
+        const structType = typeDef.type;
+        const rawTypeDef = this.getRawTypeDef(structType);
+        // We assume struct typeDef always has array of typeDefs inside ".sub"
+        const structSubtypes = rawTypeDef.sub as TypeDef[];
+
+        const structValues: { [key: string]: ApiMethodInputArg } = {};
+        for (const subtype of structSubtypes) {
+            structValues[subtype.name!] =
+                await this.promptForParam(subtype.type, subtype.name, defaultValue && defaultValue.get(subtype.name!));
+        }
+        this.closeIndentGroup();
+
+        return createType(structType as any, structValues);
+    }
+
+    // Prompt for Vec
+    async promptForVec(typeDef: TypeDef, defaultValue?: Vec<Codec>): Promise<Vec<Codec>> {
+        console.log(chalk.grey(`Providing values for ${ this.paramName(typeDef) } vector:`));
+
+        this.openIndentGroup();
+        // We assume Vec always has one TypeDef as ".sub"
+        const subtype = typeDef.sub as TypeDef;
+        let entries: Codec[] = [];
+        let addAnother = false;
+        do {
+            addAnother = await this.simplePrompt({
+                message: `Do you want to add another entry to ${ this.paramName(typeDef) } vector (currently: ${entries.length})?`,
+                type: 'confirm',
+                default: defaultValue ? entries.length < defaultValue.length : false
+            });
+            const defaultEntryValue = defaultValue && defaultValue[entries.length];
+            if (addAnother) {
+                entries.push(await this.promptForParam(subtype.type, subtype.name, defaultEntryValue));
+            }
+        } while (addAnother);
+        this.closeIndentGroup();
+
+        return new Vec(subtype.type as any, entries);
+    }
+
+    // Prompt for Enum
+    async promptForEnum(typeDef: TypeDef, defaultValue?: Enum): Promise<Enum> {
+        const enumType = typeDef.type;
+        const rawTypeDef = this.getRawTypeDef(enumType);
+        // We assume enum always has array on TypeDefs inside ".sub"
+        const enumSubtypes = rawTypeDef.sub as TypeDef[];
+
+        const enumSubtypeName = await this.simplePrompt({
+            message: `Choose value for ${this.paramName(typeDef)}:`,
+            type: 'list',
+            choices: enumSubtypes.map(subtype => ({
+                name: subtype.name,
+                value: subtype.name
+            })),
+            default: defaultValue?.type
+        });
+
+        const enumSubtype = enumSubtypes.find(st => st.name === enumSubtypeName)!;
+
+        if (enumSubtype.type !== 'Null') {
+            return createType(
+                enumType as any,
+                { [enumSubtype.name!]: await this.promptForParam(enumSubtype.type, enumSubtype.name, defaultValue?.value) }
+            );
+        }
+
+        return createType(enumType as any, enumSubtype.name);
+    }
+
+    // Prompt for param based on "paramType" string (ie. Option<MemeberId>)
+    // TODO: This may not yet work for all possible types
+    async promptForParam(paramType: string, forcedName?: string, defaultValue?: ApiMethodInputArg): Promise<ApiMethodInputArg> {
+        const typeDef = getTypeDef(paramType);
+        const rawTypeDef = this.getRawTypeDef(paramType);
+
+        if (forcedName) {
+            typeDef.name = forcedName;
+        }
+
+        if (rawTypeDef.info === TypeDefInfo.Option) {
+            return await this.promptForOption(typeDef, defaultValue as Option<Codec>);
+        }
+        else if (rawTypeDef.info === TypeDefInfo.Tuple) {
+            return await this.promptForTuple(typeDef, defaultValue as Tuple);
+        }
+        else if (rawTypeDef.info === TypeDefInfo.Struct) {
+            return await this.promptForStruct(typeDef, defaultValue as Struct);
+        }
+        else if (rawTypeDef.info === TypeDefInfo.Enum) {
+            return await this.promptForEnum(typeDef, defaultValue as Enum);
+        }
+        else if (rawTypeDef.info === TypeDefInfo.Vec) {
+            return await this.promptForVec(typeDef, defaultValue as Vec<Codec>);
+        }
+        else {
+            return await this.promptForSimple(typeDef, defaultValue);
+        }
+    }
+
+    async promptForJsonBytes(
+        JsonStruct: Constructor<Struct>,
+        argName?: string,
+        defaultValue?: Bytes,
+        schemaValidator?: ajv.ValidateFunction
+    ) {
+        const rawType = (new JsonStruct()).toRawType();
+        const typeDef = getTypeDef(rawType);
+
+        const defaultStruct =
+            defaultValue &&
+            new JsonStruct(JSON.parse(Buffer.from(defaultValue.toHex().replace('0x', ''), 'hex').toString()));
+
+        if (argName) {
+            typeDef.name = argName;
+        }
+
+        let isValid: boolean = true, jsonText: string;
+        do {
+            const structVal = await this.promptForStruct(typeDef, defaultStruct);
+            jsonText = JSON.stringify(structVal.toJSON());
+            if (schemaValidator) {
+                isValid = Boolean(schemaValidator(JSON.parse(jsonText)));
+                if (!isValid) {
+                    this.log("\n");
+                    this.warn(
+                        "Schema validation failed with:\n"+
+                        schemaValidator.errors?.map(e => chalk.red(`${chalk.bold(e.dataPath)}: ${e.message}`)).join("\n")+
+                        "\nTry again..."
+                    )
+                    this.log("\n");
+                }
+            }
+        } while(!isValid);
+
+        return new Bytes('0x'+Buffer.from(jsonText, 'ascii').toString('hex'));
+    }
+
+    async promptForExtrinsicParams(
+        module: string,
+        method: string,
+        jsonArgs?: JSONArgsMapping,
+        defaultValues?: ApiMethodInputArg[]
+    ): Promise<ApiMethodInputArg[]> {
+        const extrinsicMethod = this.getOriginalApi().tx[module][method];
+        let values: ApiMethodInputArg[] = [];
+
+        this.openIndentGroup();
+        for (const [index, arg] of Object.entries(extrinsicMethod.meta.args.toArray())) {
+            const argName = arg.name.toString();
+            const argType = arg.type.toString();
+            const defaultValue = defaultValues && defaultValues[parseInt(index)];
+            if (jsonArgs && jsonArgs[argName]) {
+                const { struct, schemaValidator } = jsonArgs[argName];
+                values.push(await this.promptForJsonBytes(struct, argName, defaultValue as Bytes, schemaValidator));
+            }
+            else {
+                values.push(await this.promptForParam(argType, argName, defaultValue));
+            }
+        };
+        this.closeIndentGroup();
+
+        return values;
+    }
+
+    sendExtrinsic(account: KeyringPair, module: string, method: string, params: Codec[]) {
+        return new Promise((resolve, reject) => {
+            const extrinsicMethod = this.getOriginalApi().tx[module][method];
+            let unsubscribe: () => void;
+            extrinsicMethod(...params)
+                .signAndSend(account, {}, (result: SubmittableResultImpl) => {
+                    // Implementation loosely based on /pioneer/packages/react-signer/src/Modal.tsx
+                    if (!result || !result.status) {
+                        return;
+                    }
+
+                    if (result.status.isFinalized) {
+                      unsubscribe();
+                      result.events
+                        .filter(({ event: { section } }): boolean => section === 'system')
+                        .forEach(({ event: { method } }): void => {
+                          if (method === 'ExtrinsicFailed') {
+                            reject(new ExtrinsicFailedError('Extrinsic execution error!'));
+                          } else if (method === 'ExtrinsicSuccess') {
+                            resolve();
+                          }
+                        });
+                    } else if (result.isError) {
+                        reject(new ExtrinsicFailedError('Extrinsic execution error!'));
+                    }
+                })
+                .then(unsubFunc => unsubscribe = unsubFunc)
+                .catch(e => reject(new ExtrinsicFailedError(`Cannot send the extrinsic: ${e.message ? e.message : JSON.stringify(e)}`)));
+        });
+    }
+
+    async sendAndFollowExtrinsic(
+        account: KeyringPair,
+        module: string,
+        method: string,
+        params: Codec[],
+        warnOnly: boolean = false // If specified - only warning will be displayed (instead of error beeing thrown)
+    ) {
+        try {
+            this.log(chalk.white(`\nSending ${ module }.${ method } extrinsic...`));
+            await this.sendExtrinsic(account, module, method, params);
+            this.log(chalk.green(`Extrinsic successful!`));
+        } catch (e) {
+            if (e instanceof ExtrinsicFailedError && warnOnly) {
+                this.warn(`${ module }.${ method } extrinsic failed! ${ e.message }`);
+            }
+            else if (e instanceof ExtrinsicFailedError) {
+                throw new CLIError(`${ module }.${ method } extrinsic failed! ${ e.message }`, { exit: ExitCodes.ApiError });
+            }
+            else {
+                throw e;
+            }
+        }
+    }
+
+    async buildAndSendExtrinsic(
+        account: KeyringPair,
+        module: string,
+        method: string,
+        jsonArgs?: JSONArgsMapping, // Special JSON arguments (ie. human_readable_text of working group opening)
+        defaultValues?: ApiMethodInputArg[],
+        warnOnly: boolean = false // If specified - only warning will be displayed (instead of error beeing thrown)
+    ): Promise<ApiMethodInputArg[]> {
+        const params = await this.promptForExtrinsicParams(module, method, jsonArgs, defaultValues);
+        await this.sendAndFollowExtrinsic(account, module, method, params, warnOnly);
+
+        return params;
+    }
+
+    extrinsicArgsFromDraft(module: string, method: string, draftFilePath: string): ApiMethodInputArg[] {
+        let draftJSONObj, parsedArgs: ApiMethodInputArg[] = [];
+        const extrinsicMethod = this.getOriginalApi().tx[module][method];
+        try {
+            draftJSONObj = require(draftFilePath);
+        } catch(e) {
+            throw new CLIError(`Could not load draft from: ${draftFilePath}`, { exit: ExitCodes.InvalidFile });
+        }
+        if (
+            !draftJSONObj
+            || !Array.isArray(draftJSONObj)
+            || draftJSONObj.length !== extrinsicMethod.meta.args.length
+        ) {
+            throw new CLIError(`The draft file at ${draftFilePath} is invalid!`, { exit: ExitCodes.InvalidFile });
+        }
+        for (const [index, arg] of Object.entries(extrinsicMethod.meta.args.toArray())) {
+            const argName = arg.name.toString();
+            const argType = arg.type.toString();
+            try {
+                parsedArgs.push(createType(argType as any, draftJSONObj[parseInt(index)]));
+            } catch (e) {
+                throw new CLIError(`Couldn't parse ${argName} value from draft at ${draftFilePath}!`, { exit: ExitCodes.InvalidFile });
+            }
+        }
+
+        return parsedArgs;
+    }
 }

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

@@ -1,11 +1,91 @@
 import ExitCodes from '../ExitCodes';
 import Command from '@oclif/command';
+import inquirer, { DistinctQuestion } from 'inquirer';
+import chalk from 'chalk';
 
 /**
  * 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 {
+    protected indentGroupsOpened = 0;
+    protected jsonPrettyIdent = '';
+
+    openIndentGroup() {
+        console.group();
+        ++this.indentGroupsOpened;
+    }
+
+    closeIndentGroup() {
+        console.groupEnd();
+        --this.indentGroupsOpened;
+    }
+
+    async simplePrompt(question: DistinctQuestion) {
+        const { result } = await inquirer.prompt([{
+            ...question,
+            name: 'result',
+            // prefix = 2 spaces for each group - 1 (because 1 is always added by default)
+            prefix: Array.from(new Array(this.indentGroupsOpened)).map(() => '  ').join('').slice(1)
+        }]);
+
+        return result;
+    }
+
+    private jsonPrettyIndented(line:string) {
+        return `${this.jsonPrettyIdent}${ line }`;
+    }
+
+    private jsonPrettyOpen(char: '{' | '[') {
+        this.jsonPrettyIdent += '    ';
+        return chalk.gray(char)+"\n";
+    }
+
+    private jsonPrettyClose(char: '}' | ']') {
+        this.jsonPrettyIdent = this.jsonPrettyIdent.slice(0, -4);
+        return this.jsonPrettyIndented(chalk.gray(char));
+    }
+
+    private jsonPrettyKeyVal(key:string, val:any): string {
+        return this.jsonPrettyIndented(chalk.white(`${key}: ${this.jsonPrettyAny(val)}`));
+    }
+
+    private jsonPrettyObj(obj: { [key: string]: any }): string {
+        return this.jsonPrettyOpen('{')
+            + Object.keys(obj).map(k => this.jsonPrettyKeyVal(k, obj[k])).join(',\n') + "\n"
+            + this.jsonPrettyClose('}');
+    }
+
+    private jsonPrettyArr(arr: any[]): string {
+        return this.jsonPrettyOpen('[')
+            + arr.map(v => this.jsonPrettyIndented(this.jsonPrettyAny(v))).join(',\n') + "\n"
+            + this.jsonPrettyClose(']');
+    }
+
+    private jsonPrettyAny(val: any): string {
+        if (Array.isArray(val)) {
+            return this.jsonPrettyArr(val);
+        }
+        else if (typeof val === 'object' && val !== null) {
+            return this.jsonPrettyObj(val);
+        }
+        else if (typeof val === 'string') {
+            return chalk.green(`"${val}"`);
+        }
+
+        // Number, boolean etc.
+        return chalk.cyan(val);
+    }
+
+    jsonPrettyPrint(json: string) {
+        try {
+            const parsed = JSON.parse(json);
+            console.log(this.jsonPrettyAny(parsed));
+        } catch(e) {
+            console.log(this.jsonPrettyAny(json));
+        }
+    }
+
     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

+ 22 - 6
cli/src/base/StateAwareCommandBase.ts

@@ -5,6 +5,7 @@ import { CLIError } from '@oclif/errors';
 import { DEFAULT_API_URI } from '../Api';
 import lockFile from 'proper-lockfile';
 import DefaultCommandBase from './DefaultCommandBase';
+import os from 'os';
 
 // Type for the state object (which is preserved as json in the state file)
 type StateObject = {
@@ -18,7 +19,7 @@ const DEFAULT_STATE: StateObject = {
     apiUri: DEFAULT_API_URI
 }
 
-// State file path (relative to this.config.dataDir)
+// State file path (relative to getAppDataPath())
 const STATE_FILE = '/state.json';
 
 // Possible data directory access errors
@@ -31,13 +32,28 @@ enum DataDirErrorType {
 /**
  * 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 preserved state is kept in a json file inside the data directory.
  * 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 {
+    getAppDataPath(): string {
+        const systemAppDataPath =
+            process.env.APPDATA ||
+            (
+                process.platform === 'darwin'
+                    ? path.join(os.homedir(), '/Library/Application Support')
+                    : path.join(os.homedir(), '/.local/share')
+            );
+        const packageJson: { name?: string } = require('../../package.json');
+        if (!packageJson || !packageJson.name) {
+            throw new CLIError('Cannot get package name from package.json!');
+        }
+        return path.join(systemAppDataPath, packageJson.name);
+    }
+
     getStateFilePath(): string {
-        return path.join(this.config.dataDir, STATE_FILE);
+        return path.join(this.getAppDataPath(), STATE_FILE);
     }
 
     private createDataDirFsError(errorType: DataDirErrorType, specificPath: string = '') {
@@ -49,7 +65,7 @@ export default abstract class StateAwareCommandBase extends DefaultCommandBase {
 
         const errorMsg =
             `Unexpected error while trying to ${ actionStrs[errorType] } the data directory.`+
-            `(${ path.join(this.config.dataDir, specificPath) })! Permissions issue?`;
+            `(${ path.join(this.getAppDataPath(), specificPath) })! Permissions issue?`;
 
         return new CLIError(errorMsg, { exit: ExitCodes.FsOperationFailed });
     }
@@ -67,8 +83,8 @@ export default abstract class StateAwareCommandBase extends DefaultCommandBase {
     }
 
     private initStateFs(): void {
-        if (!fs.existsSync(this.config.dataDir)) {
-            fs.mkdirSync(this.config.dataDir);
+        if (!fs.existsSync(this.getAppDataPath())) {
+            fs.mkdirSync(this.getAppDataPath());
         }
         if (!fs.existsSync(this.getStateFilePath())) {
             fs.writeFileSync(this.getStateFilePath(), JSON.stringify(DEFAULT_STATE));

+ 190 - 0
cli/src/base/WorkingGroupsCommandBase.ts

@@ -0,0 +1,190 @@
+import ExitCodes from '../ExitCodes';
+import AccountsCommandBase from './AccountsCommandBase';
+import { flags } from '@oclif/command';
+import { WorkingGroups, AvailableGroups, NamedKeyringPair, GroupMember, GroupOpening } from '../Types';
+import { apiModuleByGroup } from '../Api';
+import { CLIError } from '@oclif/errors';
+import inquirer from 'inquirer';
+import { ApiMethodInputArg } from './ApiCommandBase';
+import fs from 'fs';
+import path from 'path';
+import _ from 'lodash';
+import { ApplicationStageKeys } from '@joystream/types/hiring';
+
+const DEFAULT_GROUP = WorkingGroups.StorageProviders;
+const DRAFTS_FOLDER = 'opening-drafts';
+
+/**
+ * Abstract base class for commands related to working groups
+ */
+export default abstract class WorkingGroupsCommandBase extends AccountsCommandBase {
+    group: WorkingGroups = DEFAULT_GROUP;
+
+    static flags = {
+        group: flags.string({
+            char: 'g',
+            description:
+                "The working group context in which the command should be executed\n" +
+                `Available values are: ${AvailableGroups.join(', ')}.`,
+            required: true,
+            default: DEFAULT_GROUP
+        }),
+    };
+
+    // Use when lead access is required in given command
+    async getRequiredLead(): Promise<GroupMember> {
+        let selectedAccount: NamedKeyringPair = await this.getRequiredSelectedAccount();
+        let lead = await this.getApi().groupLead(this.group);
+
+        if (!lead || lead.roleAccount.toString() !== selectedAccount.address) {
+            this.error('Lead access required for this command!', { exit: ExitCodes.AccessDenied });
+        }
+
+        return lead;
+    }
+
+    // Use when worker access is required in given command
+    async getRequiredWorker(): Promise<GroupMember> {
+        let selectedAccount: NamedKeyringPair = await this.getRequiredSelectedAccount();
+        let groupMembers = await this.getApi().groupMembers(this.group);
+        let groupMembersByAccount = groupMembers.filter(m => m.roleAccount.toString() === selectedAccount.address);
+
+        if (!groupMembersByAccount.length) {
+            this.error('Worker access required for this command!', { exit: ExitCodes.AccessDenied });
+        }
+        else if (groupMembersByAccount.length === 1) {
+            return groupMembersByAccount[0];
+        }
+        else {
+            return await this.promptForWorker(groupMembersByAccount);
+        }
+    }
+
+    async promptForWorker(groupMembers: GroupMember[]): Promise<GroupMember> {
+        const { choosenWorkerIndex } = await inquirer.prompt([{
+            name: 'chosenWorkerIndex',
+            message: 'Choose the worker to execute the command as',
+            type: 'list',
+            choices: groupMembers.map((groupMember, index) => ({
+                name: `Worker ID ${ groupMember.workerId.toString() }`,
+                value: index
+            }))
+        }]);
+
+        return groupMembers[choosenWorkerIndex];
+    }
+
+    async promptForApplicationsToAccept(opening: GroupOpening): Promise<number[]> {
+        const acceptableApplications = opening.applications.filter(a => a.stage === ApplicationStageKeys.Active);
+        const acceptedApplications = await this.simplePrompt({
+            message: 'Select succesful applicants',
+            type: 'checkbox',
+            choices: acceptableApplications.map(a => ({
+                name: ` ${a.wgApplicationId}: ${a.member?.handle.toString()}`,
+                value: a.wgApplicationId,
+            }))
+        });
+
+        return acceptedApplications;
+    }
+
+    async promptForNewOpeningDraftName() {
+        let
+            draftName: string = '',
+            fileExists: boolean = false,
+            overrideConfirmed: boolean = false;
+
+        do {
+            draftName = await this.simplePrompt({
+                type: 'input',
+                message: 'Provide the draft name',
+                validate: val => (typeof val === 'string' && val.length >= 1) || 'Draft name is required!'
+            });
+
+            fileExists = fs.existsSync(this.getOpeningDraftPath(draftName));
+            if (fileExists) {
+                overrideConfirmed = await this.simplePrompt({
+                    type: 'confirm',
+                    message: 'Such draft already exists. Do you wish to override it?',
+                    default: false
+                });
+            }
+        } while(fileExists && !overrideConfirmed);
+
+        return draftName;
+    }
+
+    async promptForOpeningDraft() {
+        let draftFiles: string[] = [];
+        try {
+            draftFiles = fs.readdirSync(this.getOpeingDraftsPath());
+        }
+        catch(e) {
+            throw this.createDataReadError(DRAFTS_FOLDER);
+        }
+        if (!draftFiles.length) {
+            throw new CLIError('No drafts available!', { exit: ExitCodes.FileNotFound });
+        }
+        const draftNames = draftFiles.map(fileName => _.startCase(fileName.replace('.json', '')));
+        const selectedDraftName = await this.simplePrompt({
+            message: 'Select a draft',
+            type: 'list',
+            choices: draftNames
+        });
+
+        return selectedDraftName;
+    }
+
+    loadOpeningDraftParams(draftName: string) {
+        const draftFilePath = this.getOpeningDraftPath(draftName);
+        const params = this.extrinsicArgsFromDraft(
+            apiModuleByGroup[this.group],
+            'addOpening',
+            draftFilePath
+        );
+
+        return params;
+    }
+
+    getOpeingDraftsPath() {
+        return path.join(this.getAppDataPath(), DRAFTS_FOLDER);
+    }
+
+    getOpeningDraftPath(draftName: string) {
+        return path.join(this.getOpeingDraftsPath(), _.snakeCase(draftName)+'.json');
+    }
+
+    saveOpeningDraft(draftName: string, params: ApiMethodInputArg[]) {
+        const paramsJson = JSON.stringify(
+            params.map(p => p.toJSON()),
+            null,
+            2
+        );
+
+        try {
+            fs.writeFileSync(this.getOpeningDraftPath(draftName), paramsJson);
+        } catch(e) {
+            throw this.createDataWriteError(DRAFTS_FOLDER);
+        }
+    }
+
+    private initOpeningDraftsDir(): void {
+        if (!fs.existsSync(this.getOpeingDraftsPath())) {
+            fs.mkdirSync(this.getOpeingDraftsPath());
+        }
+    }
+
+    async init() {
+        await super.init();
+        try {
+            this.initOpeningDraftsDir();
+        } catch (e) {
+            throw this.createDataDirInitError();
+        }
+        const { flags } = this.parse(this.constructor as typeof WorkingGroupsCommandBase);
+        if (!AvailableGroups.includes(flags.group as any)) {
+            throw new CLIError(`Invalid group! Available values are: ${AvailableGroups.join(', ')}`, { exit: ExitCodes.InvalidInput });
+        }
+        this.group = flags.group as WorkingGroups;
+    }
+}

+ 10 - 2
cli/src/commands/account/choose.ts

@@ -1,13 +1,21 @@
 import AccountsCommandBase from '../../base/AccountsCommandBase';
 import chalk from 'chalk';
 import ExitCodes from '../../ExitCodes';
-import { NamedKeyringPair } from '../../Types'
+import { NamedKeyringPair } from '../../Types';
+import { flags } from '@oclif/command';
 
 export default class AccountChoose extends AccountsCommandBase {
     static description = 'Choose default account to use in the CLI';
+    static flags = {
+        showSpecial: flags.boolean({
+            description: 'Whether to show special (DEV chain) accounts',
+            required: false
+        }),
+    };
 
     async run() {
-        const accounts: NamedKeyringPair[] = this.fetchAccounts();
+        const { showSpecial } = this.parse(AccountChoose).flags;
+        const accounts: NamedKeyringPair[] = this.fetchAccounts(showSpecial);
         const selectedAccount: NamedKeyringPair | null = this.getSelectedAccount();
 
         this.log(chalk.white(`Found ${ accounts.length } existing accounts...\n`));

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

@@ -2,14 +2,13 @@ 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 { Option } from '@polkadot/types';
+import { Codec } 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';
+import ApiCommandBase, { ApiMethodInputArg } from '../../base/ApiCommandBase';
 
 // Command flags type
 type ApiInspectFlags = {
@@ -30,12 +29,6 @@ const TYPES_AVAILABLE = [
 // It works as if we specified: type ApiType = 'query' | 'consts'...;
 type ApiType = typeof TYPES_AVAILABLE[number];
 
-// Format of the api input args (as they are specified in the CLI)
-type ApiMethodInputSimpleArg = string;
-// This recurring type allows the correct handling of nested types like:
-// ((Type1, Type2), Option<Type3>) etc.
-type ApiMethodInputArg = ApiMethodInputSimpleArg | ApiMethodInputArg[];
-
 export default class ApiInspect extends ApiCommandBase {
     static description =
         'Lists available node API modules/methods and/or their description(s), '+
@@ -154,62 +147,19 @@ export default class ApiInspect extends ApiCommandBase {
         return { apiType, apiModule, apiMethod };
     }
 
-    // Prompt for simple value (string)
-    async promptForSimple(typeName: string): Promise<string> {
-        const userInput = await inquirer.prompt([{
-            name: 'providedValue',
-            message: `Provide value for ${ typeName }`,
-            type: 'input'
-        } ])
-        return <string> userInput.providedValue;
-    }
-
-    // Prompt for optional value (returns undefined if user refused to provide)
-    async promptForOption(typeDef: TypeDef): Promise<ApiMethodInputArg | undefined> {
-        const userInput = await inquirer.prompt([{
-            name: 'confirmed',
-            message: `Do you want to provide the optional ${ typeDef.type } parameter?`,
-            type: 'confirm'
-        } ]);
-
-        if (userInput.confirmed) {
-            const subtype = <TypeDef> typeDef.sub; // We assume that Opion always has a single subtype
-            let value = await this.promptForParam(subtype.type);
-            return value;
-        }
-    }
-
-    // Prompt for tuple - returns array of values
-    async promptForTuple(typeDef: TypeDef): Promise<(ApiMethodInputArg)[]> {
-        let result: ApiMethodInputArg[] = [];
-
-        if (!typeDef.sub) return [ await this.promptForSimple(typeDef.type) ];
-
-        const subtypes: TypeDef[] = Array.isArray(typeDef.sub) ? typeDef.sub : [ typeDef.sub ];
-
-        for (let subtype of subtypes) {
-            let inputParam = await this.promptForParam(subtype.type);
-            if (inputParam !== undefined) result.push(inputParam);
-        }
-
-        return result;
-    }
-
-    // Prompt for param based on "paramType" string (ie. Option<MemeberId>)
-    async promptForParam(paramType: string): Promise<ApiMethodInputArg | undefined> {
-        const typeDef: TypeDef = getTypeDef(paramType);
-        if (typeDef.info === TypeDefInfo.Option) return await this.promptForOption(typeDef);
-        else if (typeDef.info === TypeDefInfo.Tuple) return await this.promptForTuple(typeDef);
-        else return await this.promptForSimple(typeDef.type);
-    }
-
     // Request values for params using array of param types (strings)
     async requestParamsValues(paramTypes: string[]): Promise<ApiMethodInputArg[]> {
         let result: ApiMethodInputArg[] = [];
         for (let [key, paramType] of Object.entries(paramTypes)) {
             this.log(chalk.bold.white(`Parameter no. ${ parseInt(key)+1 } (${ paramType }):`));
             let paramValue = await this.promptForParam(paramType);
-            if (paramValue !== undefined) result.push(paramValue);
+            if (paramValue instanceof Option && paramValue.isSome) {
+                result.push(paramValue.unwrap());
+            }
+            else if (!(paramValue instanceof Option)) {
+                result.push(paramValue);
+            }
+            // In case of empty option we MUST NOT add anything to the array (otherwise it causes some error)
         }
 
         return result;
@@ -227,7 +177,7 @@ export default class ApiInspect extends ApiCommandBase {
 
             if (apiType === 'query') {
                 // Api query - call with (or without) arguments
-                let args: ApiMethodInputArg[] = flags.callArgs ? flags.callArgs.split(',') : [];
+                let args: (string | 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:');

+ 1 - 1
cli/src/commands/council/info.ts

@@ -1,4 +1,4 @@
-import { ElectionStage } from '@joystream/types';
+import { ElectionStage } from '@joystream/types/council';
 import { formatNumber, formatBalance } from '@polkadot/util';
 import { BlockNumber } from '@polkadot/types/interfaces';
 import { CouncilInfoObj, NameValueObj } from '../../Types';

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

@@ -0,0 +1,40 @@
+import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
+import { displayCollapsedRow, displayHeader } from '../../helpers/display';
+import _ from 'lodash';
+import chalk from 'chalk';
+
+export default class WorkingGroupsApplication extends WorkingGroupsCommandBase {
+    static description = 'Shows an overview of given application by Working Group Application ID';
+    static args = [
+        {
+            name: 'wgApplicationId',
+            required: true,
+            description: 'Working Group Application ID'
+        },
+    ]
+    static flags = {
+        ...WorkingGroupsCommandBase.flags,
+    };
+
+    async run() {
+        const { args } = this.parse(WorkingGroupsApplication);
+
+        const application = await this.getApi().groupApplication(this.group, parseInt(args.wgApplicationId));
+
+        displayHeader('Human readable text');
+        this.jsonPrettyPrint(application.humanReadableText);
+
+        displayHeader(`Details`);
+        const applicationRow = {
+            'WG application ID': application.wgApplicationId,
+            'Application ID': application.applicationId,
+            'Member handle': application.member?.handle.toString() || chalk.red('NONE'),
+            'Role account': application.roleAccout.toString(),
+            'Stage': application.stage,
+            'Application stake': application.stakes.application,
+            'Role stake': application.stakes.role,
+            'Total stake': Object.values(application.stakes).reduce((a, b) => a + b)
+        };
+        displayCollapsedRow(applicationRow);
+    }
+}

+ 96 - 0
cli/src/commands/working-groups/createOpening.ts

@@ -0,0 +1,96 @@
+import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
+import { HRTStruct } from '../../Types';
+import chalk from 'chalk';
+import { flags } from '@oclif/command';
+import { ApiMethodInputArg } from '../../base/ApiCommandBase';
+import { schemaValidator } from '@joystream/types/hiring';
+import { apiModuleByGroup } from '../../Api';
+
+export default class WorkingGroupsCreateOpening extends WorkingGroupsCommandBase {
+    static description = 'Create working group opening (requires lead access)';
+    static flags = {
+        ...WorkingGroupsCommandBase.flags,
+        useDraft: flags.boolean({
+            char: 'd',
+            description:
+                "Whether to create the opening from existing draft.\n"+
+                "If provided without --draftName - the list of choices will be displayed."
+        }),
+        draftName: flags.string({
+            char: 'n',
+            description:
+                'Name of the draft to create the opening from.',
+            dependsOn: ['useDraft']
+        }),
+        createDraftOnly: flags.boolean({
+            char: 'c',
+            description:
+                'If provided - the extrinsic will not be executed. Use this flag if you only want to create a draft.'
+        }),
+        skipPrompts: flags.boolean({
+            char: 's',
+            description:
+                "Whether to skip all prompts when adding from draft (will use all default values)",
+            dependsOn: ['useDraft'],
+            exclusive: ['createDraftOnly']
+        })
+    };
+
+    async run() {
+        const account = await this.getRequiredSelectedAccount();
+        // lead-only gate
+        await this.getRequiredLead();
+
+        const { flags } = this.parse(WorkingGroupsCreateOpening);
+
+        let defaultValues: ApiMethodInputArg[] | undefined = undefined;
+        if (flags.useDraft) {
+            const draftName = flags.draftName || await this.promptForOpeningDraft();
+            defaultValues =  await this.loadOpeningDraftParams(draftName);
+        }
+
+        if (!flags.skipPrompts) {
+            const module = apiModuleByGroup[this.group];
+            const method = 'addOpening';
+            const jsonArgsMapping = { 'human_readable_text': { struct: HRTStruct, schemaValidator } };
+
+            let saveDraft = false, params: ApiMethodInputArg[];
+            if (flags.createDraftOnly) {
+                params = await this.promptForExtrinsicParams(module, method, jsonArgsMapping, defaultValues);
+                saveDraft = true;
+            }
+            else {
+                await this.requestAccountDecoding(account); // Prompt for password
+
+                params = await this.buildAndSendExtrinsic(
+                    account,
+                    module,
+                    method,
+                    jsonArgsMapping,
+                    defaultValues,
+                    true
+                );
+
+                this.log(chalk.green('Opening succesfully created!'));
+
+                saveDraft = await this.simplePrompt({
+                    message: 'Do you wish to save this opening as draft?',
+                    type: 'confirm'
+                });
+            }
+
+            if (saveDraft) {
+                const draftName = await this.promptForNewOpeningDraftName();
+                this.saveOpeningDraft(draftName, params);
+
+                this.log(chalk.green(`Opening draft ${ chalk.white(draftName) } succesfully saved!`));
+            }
+        }
+        else {
+            await this.requestAccountDecoding(account); // Prompt for password
+            this.log(chalk.white('Sending the extrinsic...'));
+            await this.sendExtrinsic(account, apiModuleByGroup[this.group], 'addOpening', defaultValues!);
+            this.log(chalk.green('Opening succesfully created!'));
+        }
+    }
+}

+ 58 - 0
cli/src/commands/working-groups/fillOpening.ts

@@ -0,0 +1,58 @@
+import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
+import _ from 'lodash';
+import { OpeningStatus } from '../../Types';
+import ExitCodes from '../../ExitCodes';
+import { apiModuleByGroup } from '../../Api';
+import { OpeningId } from '@joystream/types/hiring';
+import { ApplicationIdSet, RewardPolicy } from '@joystream/types/working-group';
+import chalk from 'chalk';
+
+export default class WorkingGroupsFillOpening extends WorkingGroupsCommandBase {
+    static description = 'Allows filling working group opening that\'s currently in review. Requires lead access.';
+    static args = [
+        {
+            name: 'wgOpeningId',
+            required: true,
+            description: 'Working Group Opening ID'
+        },
+    ]
+    static flags = {
+        ...WorkingGroupsCommandBase.flags,
+    };
+
+    async run() {
+        const { args } = this.parse(WorkingGroupsFillOpening);
+
+        const account = await this.getRequiredSelectedAccount();
+        // Lead-only gate
+        await this.getRequiredLead();
+
+        const opening = await this.getApi().groupOpening(this.group, parseInt(args.wgOpeningId));
+
+        if (opening.stage.status !== OpeningStatus.InReview) {
+            this.error('This opening is not in the Review stage!', { exit: ExitCodes.InvalidInput });
+        }
+
+        const applicationIds = await this.promptForApplicationsToAccept(opening);
+        const rewardPolicyOpt = await this.promptForParam(`Option<${RewardPolicy.name}>`, 'RewardPolicy');
+
+        await this.requestAccountDecoding(account);
+
+        await this.sendAndFollowExtrinsic(
+            account,
+            apiModuleByGroup[this.group],
+            'fillOpening',
+            [
+                new OpeningId(opening.wgOpeningId),
+                new ApplicationIdSet(applicationIds),
+                rewardPolicyOpt
+            ]
+        );
+
+        this.log(chalk.green(`Opening ${chalk.white(opening.wgOpeningId)} succesfully filled!`));
+        this.log(
+            chalk.green('Accepted working group application IDs: ') +
+            chalk.white(applicationIds.length ? applicationIds.join(chalk.green(', ')) : 'NONE')
+        );
+    }
+}

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

@@ -0,0 +1,78 @@
+import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
+import { displayTable, displayCollapsedRow, displayHeader } from '../../helpers/display';
+import _ from 'lodash';
+import { OpeningStatus, GroupOpeningStage, GroupOpeningStakes } from '../../Types';
+import { StakingAmountLimitModeKeys, StakingPolicy } from '@joystream/types/hiring';
+import { formatBalance } from '@polkadot/util';
+import chalk from 'chalk';
+
+export default class WorkingGroupsOpening extends WorkingGroupsCommandBase {
+    static description = 'Shows an overview of given working group opening by Working Group Opening ID';
+    static args = [
+        {
+            name: 'wgOpeningId',
+            required: true,
+            description: 'Working Group Opening ID'
+        },
+    ]
+    static flags = {
+        ...WorkingGroupsCommandBase.flags,
+    };
+
+    stageColumns(stage: GroupOpeningStage) {
+        const { status, date, block } = stage;
+        const statusTimeHeader = status === OpeningStatus.WaitingToBegin ? 'Starts at' : 'Last status change';
+        return {
+            'Stage': _.startCase(status),
+            [statusTimeHeader]: (date && block)
+                ? `~ ${date.toLocaleTimeString()} ${ date.toLocaleDateString()} (#${block})`
+                : (block && `#${block}` || '?')
+        };
+    }
+
+    formatStake(stake: StakingPolicy | undefined) {
+        if (!stake) return 'NONE';
+        const { amount, amount_mode } = stake;
+        return amount_mode.type === StakingAmountLimitModeKeys.AtLeast
+            ? `>= ${ formatBalance(amount) }`
+            : `== ${ formatBalance(amount) }`;
+    }
+
+    stakeColumns(stakes: GroupOpeningStakes) {
+        const { role, application } = stakes;
+        return {
+            'Application stake': this.formatStake(application),
+            'Role stake': this.formatStake(role),
+        }
+    }
+
+    async run() {
+        const { args } = this.parse(WorkingGroupsOpening);
+
+        const opening = await this.getApi().groupOpening(this.group, parseInt(args.wgOpeningId));
+
+        displayHeader('Human readable text');
+        this.jsonPrettyPrint(opening.opening.human_readable_text.toString());
+
+        displayHeader('Opening details');
+        const openingRow = {
+            'WG Opening ID': opening.wgOpeningId,
+            'Opening ID': opening.openingId,
+            ...this.stageColumns(opening.stage),
+            ...this.stakeColumns(opening.stakes)
+        };
+        displayCollapsedRow(openingRow);
+
+        displayHeader(`Applications (${opening.applications.length})`);
+        const applicationsRows = opening.applications.map(a => ({
+            'WG appl. ID': a.wgApplicationId,
+            'Appl. ID': a.applicationId,
+            'Member': a.member?.handle.toString() || chalk.red('NONE'),
+            'Stage': a.stage,
+            'Appl. stake': a.stakes.application,
+            'Role stake': a.stakes.role,
+            'Total stake': Object.values(a.stakes).reduce((a, b) => a + b)
+        }));
+        displayTable(applicationsRows, 5);
+    }
+  }

+ 22 - 0
cli/src/commands/working-groups/openings.ts

@@ -0,0 +1,22 @@
+import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
+import { displayTable } from '../../helpers/display';
+import _ from 'lodash';
+
+export default class WorkingGroupsOpenings extends WorkingGroupsCommandBase {
+    static description = 'Shows an overview of given working group openings';
+    static flags = {
+        ...WorkingGroupsCommandBase.flags,
+    };
+
+    async run() {
+        const openings = await this.getApi().openingsByGroup(this.group);
+
+        const openingsRows = openings.map(o => ({
+            'WG Opening ID': o.wgOpeningId,
+            'Opening ID': o.openingId,
+            'Stage': `${_.startCase(o.stage.status)}${o.stage.block ? ` (#${o.stage.block})` : ''}`,
+            'Applications': o.applications.length
+        }));
+        displayTable(openingsRows, 5);
+    }
+}

+ 38 - 0
cli/src/commands/working-groups/overview.ts

@@ -0,0 +1,38 @@
+import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
+import { displayHeader, displayNameValueTable, displayTable } from '../../helpers/display';
+import { formatBalance } from '@polkadot/util';
+import chalk from 'chalk';
+
+export default class WorkingGroupsOverview extends WorkingGroupsCommandBase {
+    static description = 'Shows an overview of given working group (current lead and workers)';
+    static flags = {
+        ...WorkingGroupsCommandBase.flags,
+    };
+
+    async run() {
+        const lead = await this.getApi().groupLead(this.group);
+        const members = await this.getApi().groupMembers(this.group);
+
+        displayHeader('Group lead');
+        if (lead) {
+            displayNameValueTable([
+                { name: 'Member id:', value: lead.memberId.toString() },
+                { name: 'Member handle:', value: lead.profile.handle.toString() },
+                { name: 'Role account:', value: lead.roleAccount.toString() },
+            ]);
+        }
+        else {
+            this.log(chalk.yellow('No lead assigned!'));
+        }
+
+        displayHeader('Members');
+        const membersRows = members.map(m => ({
+            'Worker id': m.workerId.toString(),
+            'Member id': m.memberId.toString(),
+            'Member handle': m.profile.handle.toString(),
+            'Stake': formatBalance(m.stake),
+            'Earned': formatBalance(m.earned)
+        }));
+        displayTable(membersRows, 5);
+    }
+  }

+ 46 - 0
cli/src/commands/working-groups/startAcceptingApplications.ts

@@ -0,0 +1,46 @@
+import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
+import _ from 'lodash';
+import { OpeningStatus } from '../../Types';
+import ExitCodes from '../../ExitCodes';
+import { apiModuleByGroup } from '../../Api';
+import { OpeningId } from '@joystream/types/hiring';
+import chalk from 'chalk';
+
+export default class WorkingGroupsStartAcceptingApplications extends WorkingGroupsCommandBase {
+    static description = 'Changes the status of pending opening to "Accepting applications". Requires lead access.';
+    static args = [
+        {
+            name: 'wgOpeningId',
+            required: true,
+            description: 'Working Group Opening ID'
+        },
+    ]
+    static flags = {
+        ...WorkingGroupsCommandBase.flags,
+    };
+
+    async run() {
+        const { args } = this.parse(WorkingGroupsStartAcceptingApplications);
+
+        const account = await this.getRequiredSelectedAccount();
+        // Lead-only gate
+        await this.getRequiredLead();
+
+        const opening = await this.getApi().groupOpening(this.group, parseInt(args.wgOpeningId));
+
+        if (opening.stage.status !== OpeningStatus.WaitingToBegin) {
+            this.error('This opening is not in "Waiting To Begin" stage!', { exit: ExitCodes.InvalidInput });
+        }
+
+        await this.requestAccountDecoding(account);
+
+        await this.sendAndFollowExtrinsic(
+            account,
+            apiModuleByGroup[this.group],
+            'acceptApplications',
+            [ new OpeningId(opening.wgOpeningId) ]
+        );
+
+        this.log(chalk.green(`Opening ${chalk.white(opening.wgOpeningId)} status changed to: ${ chalk.white('Accepting Applications') }`));
+    }
+}

+ 46 - 0
cli/src/commands/working-groups/startReviewPeriod.ts

@@ -0,0 +1,46 @@
+import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
+import _ from 'lodash';
+import { OpeningStatus } from '../../Types';
+import ExitCodes from '../../ExitCodes';
+import { apiModuleByGroup } from '../../Api';
+import { OpeningId } from '@joystream/types/hiring';
+import chalk from 'chalk';
+
+export default class WorkingGroupsStartReviewPeriod extends WorkingGroupsCommandBase {
+    static description = 'Changes the status of active opening to "In review". Requires lead access.';
+    static args = [
+        {
+            name: 'wgOpeningId',
+            required: true,
+            description: 'Working Group Opening ID'
+        },
+    ]
+    static flags = {
+        ...WorkingGroupsCommandBase.flags,
+    };
+
+    async run() {
+        const { args } = this.parse(WorkingGroupsStartReviewPeriod);
+
+        const account = await this.getRequiredSelectedAccount();
+        // Lead-only gate
+        await this.getRequiredLead();
+
+        const opening = await this.getApi().groupOpening(this.group, parseInt(args.wgOpeningId));
+
+        if (opening.stage.status !== OpeningStatus.AcceptingApplications) {
+            this.error('This opening is not in "Accepting Applications" stage!', { exit: ExitCodes.InvalidInput });
+        }
+
+        await this.requestAccountDecoding(account);
+
+        await this.sendAndFollowExtrinsic(
+            account,
+            apiModuleByGroup[this.group],
+            'beginApplicantReview',
+            [ new OpeningId(opening.wgOpeningId) ]
+        );
+
+        this.log(chalk.green(`Opening ${chalk.white(opening.wgOpeningId)} status changed to: ${ chalk.white('In Review') }`));
+    }
+}

+ 45 - 0
cli/src/commands/working-groups/terminateApplication.ts

@@ -0,0 +1,45 @@
+import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
+import _ from 'lodash';
+import ExitCodes from '../../ExitCodes';
+import { apiModuleByGroup } from '../../Api';
+import { ApplicationStageKeys, ApplicationId } from '@joystream/types/hiring';
+import chalk from 'chalk';
+
+export default class WorkingGroupsTerminateApplication extends WorkingGroupsCommandBase {
+    static description = 'Terminates given working group application. Requires lead access.';
+    static args = [
+        {
+            name: 'wgApplicationId',
+            required: true,
+            description: 'Working Group Application ID'
+        },
+    ]
+    static flags = {
+        ...WorkingGroupsCommandBase.flags,
+    };
+
+    async run() {
+        const { args } = this.parse(WorkingGroupsTerminateApplication);
+
+        const account = await this.getRequiredSelectedAccount();
+        // Lead-only gate
+        await this.getRequiredLead();
+
+        const application = await this.getApi().groupApplication(this.group, parseInt(args.wgApplicationId));
+
+        if (application.stage !== ApplicationStageKeys.Active) {
+            this.error('This application is not active!', { exit: ExitCodes.InvalidInput });
+        }
+
+        await this.requestAccountDecoding(account);
+
+        await this.sendAndFollowExtrinsic(
+            account,
+            apiModuleByGroup[this.group],
+            'terminateApplication',
+            [new ApplicationId(application.wgApplicationId)]
+        );
+
+        this.log(chalk.green(`Application ${chalk.white(application.wgApplicationId)} has been succesfully terminated!`));
+    }
+}

+ 35 - 1
cli/src/helpers/display.ts

@@ -1,4 +1,4 @@
-import { cli } from 'cli-ux';
+import { cli, Table } from 'cli-ux';
 import chalk from 'chalk';
 import { NameValueObj } from '../Types';
 
@@ -23,6 +23,40 @@ export function displayNameValueTable(rows: NameValueObj[]) {
     );
 }
 
+export function displayCollapsedRow(row: { [k: string]: string | number }) {
+    const collapsedRow: NameValueObj[] = Object.keys(row).map(name => ({
+        name,
+        value: typeof row[name] === 'string' ? row[name] as string : row[name].toString()
+    }));
+
+    displayNameValueTable(collapsedRow);
+}
+
+export function displayCollapsedTable(rows: { [k: string]: string | number }[]) {
+    for (const row of rows) displayCollapsedRow(row);
+}
+
+export function displayTable(rows: { [k: string]: string | number }[], cellHorizontalPadding = 0) {
+    if (!rows.length) {
+        return;
+    }
+    const maxLength = (columnName: string) => rows.reduce(
+        (maxLength, row) => {
+            const val = row[columnName];
+            const valLength = typeof val === 'string' ? val.length : val.toString().length;
+            return Math.max(maxLength, valLength);
+        },
+        columnName.length
+    )
+    const columnDef = (columnName: string) => ({
+        get: (row: typeof rows[number])  => chalk.white(`${row[columnName]}`),
+        minWidth: maxLength(columnName) + cellHorizontalPadding
+    });
+    let columns: Table.table.Columns<{ [k: string]: string }> = {};
+    Object.keys(rows[0]).forEach(columnName => columns[columnName] = columnDef(columnName))
+    cli.table(rows, columns);
+}
+
 export function toFixedLength(text: string, length: number, spacesOnLeft = false): string {
     if (text.length > length && length > 3) {
         return text.slice(0, length-3) + '...';

+ 2 - 1
cli/tsconfig.json

@@ -7,7 +7,8 @@
     "rootDir": "src",
     "strict": true,
     "target": "es2017",
-    "esModuleInterop": true
+    "esModuleInterop": true,
+	"types" : [ "node" ]
   },
   "include": [
     "src/**/*"

+ 5 - 0
devops/.eslintrc.js

@@ -0,0 +1,5 @@
+module.exports = {
+  env: {
+    node: true,
+  },
+}

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

@@ -3,7 +3,8 @@ LABEL description="Compiles all workspace artifacts"
 WORKDIR /joystream
 COPY . /joystream
 
-RUN cargo build --release
+# Build joystream-node and its dependencies - runtime
+RUN cargo build --release -p joystream-node
 
 FROM debian:stretch
 LABEL description="Joystream node"

+ 54 - 0
devops/eslint-config/index.js

@@ -0,0 +1,54 @@
+// This config is used globally at the root of the repo, so it should be as thin
+// as possible with rules that we absolutely require across all projects.
+module.exports = {
+  env: {
+    es6: true,
+  },
+  globals: {
+    Atomics: 'readonly',
+    SharedArrayBuffer: 'readonly',
+  },
+  // We are relying on version that comes with @polkadot/dev
+  // Newest version is breaking pioneer!
+  parser: '@typescript-eslint/parser',
+  parserOptions: {
+    ecmaFeatures: {
+      jsx: true,
+    },
+    ecmaVersion: 2019,
+    sourceType: 'module',
+  },
+  extends: [
+    'standard',
+    'eslint:recommended',
+    'plugin:@typescript-eslint/recommended',
+    'plugin:react/recommended',
+    // this is only in newer versions of eslint-plugin-react-hooks
+    // 'plugin:react-hooks/recommended',
+    'plugin:prettier/recommended',
+    'prettier/@typescript-eslint',
+    'prettier/react',
+    'prettier/standard',
+  ],
+  settings: {
+    react: {
+      version: 'detect',
+    },
+  },
+  rules: {
+    // drop these when using newer versions of eslint-plugin-react-hooks
+    'react-hooks/rules-of-hooks': 'error',
+    'react-hooks/exhaustive-deps': 'warn',
+    // only cli projects should really have this rule, web apps
+    // should prefer using 'debug' package at least to allow control of
+    // output verbosity if logging to console.
+    'no-console': 'off',
+  },
+  plugins: [
+    'standard',
+    '@typescript-eslint',
+    'react',
+    'react-hooks',
+    'prettier',
+  ],
+}

+ 34 - 0
devops/eslint-config/package.json

@@ -0,0 +1,34 @@
+{
+  "name": "@joystream/eslint-config",
+  "version": "1.0.0",
+  "description": "joystream eslint shared config",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/joystream/joystream.git"
+  },
+  "author": "Joystream contributors",
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/joystream/joystream/issues"
+  },
+  "homepage": "https://github.com/joystream/joystream#readme",
+  "peerDependencies": {
+    "eslint": ">= 5"
+  },
+  "dependencies": {
+    "@typescript-eslint/parser": "^2.34.0",
+    "eslint-config-prettier": "^6.11.0",
+    "eslint-plugin-prettier": "^3.1.3",
+    "eslint-plugin-react": "^7.16.0",
+    "eslint-plugin-react-hooks": "^2.3.0",
+    "eslint-config-standard": "^14.1.1",
+    "eslint-plugin-standard": "^4.0.1",
+    "eslint-plugin-promise": "^4.2.1",
+    "eslint-plugin-import": "^2.22.0",
+    "eslint-plugin-node": "^11.1.0"
+  }
+}

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

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

+ 8 - 0
devops/prettier-config/index.js

@@ -0,0 +1,8 @@
+module.exports = {
+  singleQuote: true,
+  arrowParens: 'always',
+  useTabs: false,
+  tabWidth: 2,
+  semi: false,
+  trailingComma: 'es5',
+}

+ 22 - 0
devops/prettier-config/package.json

@@ -0,0 +1,22 @@
+{
+  "name": "@joystream/prettier-config",
+  "version": "1.0.0",
+  "description": "joystream prettier shared config",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/joystream/joystream.git"
+  },
+  "author": "Joystream contributors",
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/joystream/joystream/issues"
+  },
+  "homepage": "https://github.com/joystream/joystream#readme",
+  "peerDependencies": {
+    "prettier": ">= 2"
+  }
+}

+ 1 - 1
node/Cargo.toml

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

+ 56 - 25
node/src/chain_spec.rs

@@ -15,17 +15,18 @@
 // along with Joystream node.  If not, see <http://www.gnu.org/licenses/>.
 
 // Clippy linter warning.
-#![allow(clippy::identity_op)] // disable it because we use such syntax for a code readability
-                               // Example:  voting_period: 1 * DAY
+// Disable it because we use such syntax for a code readability.
+// Example:  voting_period: 1 * DAY
+#![allow(clippy::identity_op)]
 
 use node_runtime::{
-    versioned_store::InputValidationLengthConstraint as VsInputValidation, ActorsConfig,
+    versioned_store::InputValidationLengthConstraint as VsInputValidation,
     AuthorityDiscoveryConfig, BabeConfig, Balance, BalancesConfig, ContentWorkingGroupConfig,
     CouncilConfig, CouncilElectionConfig, DataObjectStorageRegistryConfig,
     DataObjectTypeRegistryConfig, ElectionParameters, GrandpaConfig, ImOnlineConfig, IndicesConfig,
     MembersConfig, MigrationConfig, Perbill, ProposalsCodexConfig, SessionConfig, SessionKeys,
-    Signature, StakerStatus, StakingConfig, SudoConfig, SystemConfig, VersionedStoreConfig, DAYS,
-    WASM_BINARY,
+    Signature, StakerStatus, StakingConfig, StorageWorkingGroupConfig, SudoConfig, SystemConfig,
+    VersionedStoreConfig, DAYS, WASM_BINARY,
 };
 pub use node_runtime::{AccountId, GenesisConfig};
 use primitives::{sr25519, Pair, Public};
@@ -41,6 +42,8 @@ type AccountPublic = <Signature as Verify>::Signer;
 /// Specialized `ChainSpec`. This is a specialization of the general Substrate ChainSpec type.
 pub type ChainSpec = substrate_service::ChainSpec<GenesisConfig>;
 
+use node_runtime::common::constraints::InputValidationLengthConstraint;
+
 /// The chain specification option. This is expected to come in from the CLI and
 /// is little more than one of a number of alternatives which can easily be converted
 /// from a string (`--chain=...`) into a `ChainSpec`.
@@ -186,6 +189,7 @@ pub fn testnet_genesis(
 
     // default codex proposals config parameters
     let cpcp = node_runtime::ProposalsConfigParameters::default();
+    let default_text_constraint = node_runtime::working_group::default_text_constraint();
 
     GenesisConfig {
         system: Some(SystemConfig {
@@ -253,7 +257,7 @@ pub fn testnet_genesis(
         }),
         membership: Some(MembersConfig {
             default_paid_membership_fee: 100u128,
-            members: crate::members_config::initial_members(),
+            members: vec![],
         }),
         forum: Some(crate::forum_config::from_serialized::create(
             endowed_accounts[0].clone(),
@@ -264,9 +268,12 @@ pub fn testnet_genesis(
         data_object_storage_registry: Some(DataObjectStorageRegistryConfig {
             first_relationship_id: 1,
         }),
-        actors: Some(ActorsConfig {
-            enable_storage_role: true,
-            request_life_time: 300,
+        working_group_Instance2: Some(StorageWorkingGroupConfig {
+            phantom: Default::default(),
+            storage_working_group_mint_capacity: 0,
+            opening_human_readable_text_constraint: default_text_constraint,
+            worker_application_human_readable_text_constraint: default_text_constraint,
+            worker_exit_rationale_text_constraint: default_text_constraint,
         }),
         versioned_store: Some(VersionedStoreConfig {
             class_by_id: vec![],
@@ -293,14 +300,14 @@ pub fn testnet_genesis(
             next_principal_id: 0,
             channel_creation_enabled: true, // there is no extrinsic to change it so enabling at genesis
             unstaker_by_stake_id: vec![],
-            channel_handle_constraint: crate::forum_config::new_validation(5, 20),
-            channel_description_constraint: crate::forum_config::new_validation(1, 1024),
-            opening_human_readable_text: crate::forum_config::new_validation(1, 2048),
-            curator_application_human_readable_text: crate::forum_config::new_validation(1, 2048),
-            curator_exit_rationale_text: crate::forum_config::new_validation(1, 2048),
-            channel_avatar_constraint: crate::forum_config::new_validation(5, 1024),
-            channel_banner_constraint: crate::forum_config::new_validation(5, 1024),
-            channel_title_constraint: crate::forum_config::new_validation(5, 1024),
+            channel_handle_constraint: InputValidationLengthConstraint::new(5, 20),
+            channel_description_constraint: InputValidationLengthConstraint::new(1, 1024),
+            opening_human_readable_text: InputValidationLengthConstraint::new(1, 2048),
+            curator_application_human_readable_text: InputValidationLengthConstraint::new(1, 2048),
+            curator_exit_rationale_text: InputValidationLengthConstraint::new(1, 2048),
+            channel_avatar_constraint: InputValidationLengthConstraint::new(5, 1024),
+            channel_banner_constraint: InputValidationLengthConstraint::new(5, 1024),
+            channel_title_constraint: InputValidationLengthConstraint::new(5, 1024),
         }),
         migration: Some(MigrationConfig {}),
         proposals_codex: Some(ProposalsCodexConfig {
@@ -324,14 +331,38 @@ pub fn testnet_genesis(
             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,
+            add_working_group_opening_proposal_voting_period: cpcp
+                .add_working_group_opening_proposal_voting_period,
+            add_working_group_opening_proposal_grace_period: cpcp
+                .add_working_group_opening_proposal_grace_period,
+            begin_review_working_group_leader_applications_proposal_voting_period: cpcp
+                .begin_review_working_group_leader_applications_proposal_voting_period,
+            begin_review_working_group_leader_applications_proposal_grace_period: cpcp
+                .begin_review_working_group_leader_applications_proposal_grace_period,
+            fill_working_group_leader_opening_proposal_voting_period: cpcp
+                .fill_working_group_leader_opening_proposal_voting_period,
+            fill_working_group_leader_opening_proposal_grace_period: cpcp
+                .fill_working_group_leader_opening_proposal_grace_period,
+            set_working_group_mint_capacity_proposal_voting_period: cpcp
+                .set_content_working_group_mint_capacity_proposal_voting_period,
+            set_working_group_mint_capacity_proposal_grace_period: cpcp
+                .set_content_working_group_mint_capacity_proposal_grace_period,
+            decrease_working_group_leader_stake_proposal_voting_period: cpcp
+                .decrease_working_group_leader_stake_proposal_voting_period,
+            decrease_working_group_leader_stake_proposal_grace_period: cpcp
+                .decrease_working_group_leader_stake_proposal_grace_period,
+            slash_working_group_leader_stake_proposal_voting_period: cpcp
+                .slash_working_group_leader_stake_proposal_voting_period,
+            slash_working_group_leader_stake_proposal_grace_period: cpcp
+                .slash_working_group_leader_stake_proposal_grace_period,
+            set_working_group_leader_reward_proposal_voting_period: cpcp
+                .set_working_group_leader_reward_proposal_voting_period,
+            set_working_group_leader_reward_proposal_grace_period: cpcp
+                .set_working_group_leader_reward_proposal_grace_period,
+            terminate_working_group_leader_role_proposal_voting_period: cpcp
+                .terminate_working_group_leader_role_proposal_voting_period,
+            terminate_working_group_leader_role_proposal_grace_period: cpcp
+                .terminate_working_group_leader_role_proposal_grace_period,
         }),
     }
 }

+ 2 - 2
node/src/forum_config/from_serialized.rs

@@ -19,7 +19,7 @@ struct ForumData {
 }
 
 fn parse_forum_json() -> Result<ForumData> {
-    let data = include_str!("../../res/forum_data_acropolis_serialized.json");
+    let data = include_str!("../../res/forum_data_empty.json");
     serde_json::from_str(data)
 }
 
@@ -40,12 +40,12 @@ pub fn create(forum_sudo: AccountId) -> ForumConfig {
         next_category_id,
         next_thread_id,
         next_post_id,
-        forum_sudo,
         category_title_constraint: new_validation(10, 90),
         category_description_constraint: new_validation(10, 490),
         thread_title_constraint: new_validation(10, 90),
         post_text_constraint: new_validation(10, 990),
         thread_moderation_rationale_constraint: new_validation(10, 290),
         post_moderation_rationale_constraint: new_validation(10, 290),
+        forum_sudo,
     }
 }

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

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

+ 1 - 1
node/src/service.rs

@@ -224,7 +224,7 @@ macro_rules! new_full {
 			(true, false) => {
 				// start the full GRANDPA voter
 				let grandpa_config = grandpa::GrandpaParams {
-					config: config,
+					config,
 					link: grandpa_link,
 					network: service.network(),
 					inherent_data_providers: inherent_data_providers.clone(),

+ 46 - 21
package.json

@@ -1,21 +1,46 @@
-{
-	"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"
-	  }
-	}
-}
+{
+  "private": true,
+  "name": "joystream",
+  "version": "1.0.0",
+  "license": "GPL-3.0-only",
+  "scripts": {
+    "test": "yarn && yarn workspaces run test",
+    "test-migration": "yarn && yarn workspaces run test-migration",
+    "postinstall": "yarn workspace @joystream/types build",
+    "cargo-checks": "devops/git-hooks/pre-commit && devops/git-hooks/pre-push",
+    "cargo-build": "scripts/cargo-build.sh",
+    "lint": "yarn workspaces run lint"
+  },
+  "workspaces": [
+    "tests/network-tests",
+    "cli",
+    "types",
+    "pioneer",
+    "pioneer/packages/*",
+    "storage-node",
+    "storage-node/packages/*",
+    "devops/eslint-config",
+    "devops/prettier-config"
+  ],
+  "resolutions": {
+    "@polkadot/api": "^0.96.1",
+    "@polkadot/api-contract": "^0.96.1",
+    "@polkadot/keyring": "^1.7.0-beta.5",
+    "@polkadot/types": "^0.96.1",
+    "@polkadot/util": "^1.7.0-beta.5",
+    "@polkadot/util-crypto": "^1.7.0-beta.5",
+    "babel-core": "^7.0.0-bridge.0",
+    "typescript": "^3.7.2"
+  },
+  "devDependencies": {
+    "husky": "^4.2.5",
+    "prettier": "2.0.2",
+    "eslint": "^5.16.0"
+  },
+  "husky": {
+    "hooks": {
+      "pre-commit": "devops/git-hooks/pre-commit",
+      "pre-push": "devops/git-hooks/pre-push"
+    }
+  }
+}

+ 1 - 0
pioneer/.123trigger

@@ -0,0 +1 @@
+5

+ 1 - 0
pioneer/.babelrc.js

@@ -0,0 +1 @@
+module.exports = require('./babel.config.js');

+ 3 - 0
pioneer/.codeclimate.yml

@@ -0,0 +1,3 @@
+exclude_patterns:
+- "**/*.spec.js"
+- "**/*.spec.ts"

+ 1 - 0
pioneer/.dockerignore

@@ -0,0 +1 @@
+node_modules

+ 10 - 0
pioneer/.editorconfig

@@ -0,0 +1,10 @@
+root = true
+[*]
+indent_style=space
+indent_size=2
+tab_width=2
+end_of_line=lf
+charset=utf-8
+trim_trailing_whitespace=true
+max_line_length=120
+insert_final_newline=true

+ 4 - 0
pioneer/.eslintignore

@@ -0,0 +1,4 @@
+**/build/*
+**/coverage/*
+**/node_modules/*
+i18next-scanner.config.js

+ 27 - 0
pioneer/.eslintrc.js

@@ -0,0 +1,27 @@
+// At some point don't depend on @polkadot rules and use @joystream/eslint-config
+const base = require('@polkadot/dev-react/config/eslint');
+
+// add override for any (a metric ton of them, initial conversion)
+module.exports = {
+  ...base,
+  parserOptions: {
+    ...base.parserOptions,
+    project: [
+      './tsconfig.json'
+    ]
+  },
+  rules: {
+    ...base.rules,
+    '@typescript-eslint/no-explicit-any': 'off',
+    '@typescript-eslint/camelcase': 'off',
+    'react/prop-types': 'off',
+    'new-cap': 'off',
+    '@typescript-eslint/interface-name-prefix': 'off',
+    '@typescript-eslint/ban-ts-comment': 'error',
+    // why only required in VSCode!?!? is eslint plugin not working like eslint commandline?
+    // Or are we having to add this because of new versions of eslint-config-* ?
+    'no-console': 'off',
+  },
+  // isolate pioneer from monorepo eslint rules
+  root: true
+};

+ 25 - 0
pioneer/.gitignore

@@ -0,0 +1,25 @@
+build/
+coverage/
+node_modules/
+tmp/
+.idea/
+.vscode/
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+.npmrc
+cc-test-reporter
+package-lock.json
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+!patches/**
+.idea/
+
+# Built Joystream types:
+packages/joy-types/lib/
+
+# Storybook
+storybook-static/

+ 159 - 0
pioneer/.gitlab-ci.yml

@@ -0,0 +1,159 @@
+image: roffe/kubectl:latest
+variables:
+  CI_REGISTRY: parity.azurecr.io
+  CI_REGISTRY_USER: parity
+  AUTO_DEVOPS_DOMAIN: poc-3.polkadot.io
+
+.kubernetes: &kubernetes
+  tags:
+    - kubernetes
+
+stages:
+  - dockerize
+  - test
+  - review
+  - staging
+  - production
+  - cleanup
+
+before_script:
+  - export DOCKER_IMAGE=$CI_REGISTRY/$CI_PROJECT_PATH_SLUG
+  - export DOCKER_TAG=$CI_COMMIT_REF_SLUG-$VERSION
+  - export DOCKER_IMAGE_FULL_NAME=$DOCKER_IMAGE:$DOCKER_TAG
+
+dockerize:
+  stage: dockerize
+  environment:
+    name: infrastructure_build
+  tags:
+    - kubernetes-parity-build
+  image: docker:git
+  services:
+    - docker:dind
+  variables:
+    DOCKER_DRIVER: overlay2
+    DOCKER_HOST: tcp://localhost:2375
+  script:
+    - echo $DOCKER_IMAGE
+    - echo $DOCKER_TAG
+    - echo $DOCKER_IMAGE_FULL_NAME
+    - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
+    - docker build -t "$DOCKER_IMAGE_FULL_NAME" .
+    - docker push "$DOCKER_IMAGE_FULL_NAME"
+  only:
+    - master
+
+review:
+  stage: review
+  <<: *kubernetes
+  script:
+    - setup_kubernetes
+    - deploy
+  environment:
+    name: review/$CI_COMMIT_REF_NAME
+    url: https://$CI_ENVIRONMENT_SLUG.$AUTO_DEVOPS_DOMAIN
+    on_stop: stop_review
+  only:
+    refs:
+      - branches
+    kubernetes: active
+  except:
+    - master
+
+stop_review:
+  stage: cleanup
+  <<: *kubernetes
+  variables:
+    GIT_STRATEGY: none
+  script:
+    - setup_kubernetes
+    - delete
+  environment:
+    name: review/$CI_COMMIT_REF_NAME
+    action: stop
+  when: manual
+  allow_failure: true
+  only:
+    refs:
+      - branches
+    kubernetes: active
+  except:
+    - master
+
+staging:
+  stage: staging
+  <<: *kubernetes
+  script:
+    - setup_kubernetes
+    - deploy
+  environment:
+    name: staging
+    url: https://staging.$AUTO_DEVOPS_DOMAIN
+  only:
+    refs:
+      - master
+    kubernetes: active
+
+production:
+  stage: production
+  <<: *kubernetes
+  script:
+    - setup_kubernetes
+    - deploy
+  environment:
+    name: production
+    url: https://$AUTO_DEVOPS_DOMAIN
+  when: manual
+  only:
+    refs:
+      - master
+    kubernetes: active
+
+# ---------------------------------------------------------------------------
+.auto_devops: &auto_devops |
+  # Auto DevOps variables and functions
+  [[ "$TRACE" ]] && set -x
+  export DOCKER_IMAGE=$CI_REGISTRY/$CI_PROJECT_PATH_SLUG
+  export DOCKER_TAG=$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHA
+  export DOCKER_IMAGE_FULL_NAME=$DOCKER_IMAGE:$DOCKER_TAG
+
+  export AUTODEVOPS_HOST=$(echo $CI_ENVIRONMENT_URL | awk -F/ '{print $3}')
+
+  function build() {
+    if [[ -n "$CI_REGISTRY_USER" ]]; then
+      echo "Logging to GitLab Container Registry with CI credentials..."
+      docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
+      echo ""
+    fi
+
+    echo "Building Dockerfile-based application..."
+    docker build -t "$DOCKER_IMAGE_FULL_NAME" .
+
+    echo "Pushing to GitLab Container Registry..."
+    docker push "$DOCKER_IMAGE_FULL_NAME"
+    echo ""
+  }
+
+  function setup_kubernetes() {
+    kubectl describe namespace "$KUBE_NAMESPACE" || kubectl create namespace "$KUBE_NAMESPACE"
+    kubectl create secret -n "$KUBE_NAMESPACE" \
+      docker-registry gitlab-registry \
+      --docker-server="$CI_REGISTRY" \
+      --docker-username="$CI_REGISTRY_USER" \
+      --docker-password="$CI_REGISTRY_PASSWORD" \
+      --docker-email="$GITLAB_USER_EMAIL" \
+      -o yaml --dry-run | kubectl replace -n "$KUBE_NAMESPACE" --force -f -
+  }
+
+  function deploy() {
+    cat ./deployment.template.yml | envsubst | kubectl apply -n "$KUBE_NAMESPACE" -f -
+  }
+
+  function delete() {
+    kubectl -n "$KUBE_NAMESPACE" delete "deploy/$CI_ENVIRONMENT_SLUG-backend"
+    kubectl -n "$KUBE_NAMESPACE" delete "svc/$CI_ENVIRONMENT_SLUG-service"
+    kubectl -n "$KUBE_NAMESPACE" delete "ing/$CI_ENVIRONMENT_SLUG-ingress"
+  }
+
+before_script:
+  - *auto_devops

+ 0 - 0
pioneer/.npmignore


+ 1 - 0
pioneer/.nvmrc

@@ -0,0 +1 @@
+10.13.0

+ 1 - 0
pioneer/.prettierignore

@@ -0,0 +1 @@
+**

+ 3 - 0
pioneer/.storybook/addons.ts

@@ -0,0 +1,3 @@
+import '@storybook/addon-knobs/register';
+import '@storybook/addon-actions/register';
+import '@storybook/addon-storysource/register';

+ 19 - 0
pioneer/.storybook/config.tsx

@@ -0,0 +1,19 @@
+import React from 'react'
+import { configure, addDecorator } from '@storybook/react';
+import '@storybook/addon-console';
+import StoryRouter from 'storybook-react-router';
+
+import GlobalStyle from '@polkadot/react-components/styles';
+import 'semantic-ui-css/semantic.min.css'
+import './style.css'
+
+addDecorator(StoryRouter());
+
+addDecorator(story => (
+  <div className='StorybookRoot'>
+    <GlobalStyle />
+    {story()}
+  </div>
+));
+
+configure(require.context('../packages', true, /\.stories\.tsx?$/), module)

+ 4 - 0
pioneer/.storybook/style.css

@@ -0,0 +1,4 @@
+.StorybookRoot {
+  background-color: #fafafa;
+  padding: 1rem 5rem;
+}

+ 81 - 0
pioneer/.storybook/webpack.config.js

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

+ 13 - 0
pioneer/.travis.yml

@@ -0,0 +1,13 @@
+language: node_js
+node_js:
+  - "12"
+cache:
+  yarn: true
+  directories:
+    - node_modules
+before_install:
+  - curl -o- -L https://yarnpkg.com/install.sh | bash
+  - export PATH=$HOME/.yarn/bin:$PATH
+script:
+  - yarn
+  - yarn build

+ 19 - 0
pioneer/BOUNTIES.md

@@ -0,0 +1,19 @@
+# Bounties
+
+From time-to-time we will add bounties for features.
+
+These are generously provided by the [Web3 Foundation](https://web3.foundation/) and as such employees of Parity or those of the W3F are ineligible for them. (This includes people being by either Party for development or community work, related or un-related to polkadot-js).
+
+The idea is that these bounties should be left open to community participation, so only if there is no outside interest for a specific issue, should those directly or indirectly paid by the W3F for work, attempt to close an issue. (in which case it will be "un-bountied")
+
+Current bounties are tracked by the [!bounty](https://github.com/polkadot-js/apps/labels/%21bounty) label.
+
+## Process
+
+Once listed, the normal [Gitcoin](https://gitcoin.co/) process kicks in. This means application, work and payment is managed by this tool. The values for bounties are determined by the size estimation done by the team.
+
+## Some small requests
+
+Please don't start work on an issue until you have been approved via the gitcoin interface. We generally love enthusiasm and code in the repo, however short-cutting the process does create some issues for the management of the bounties. We certainly don't want to be playing favorites if 2 PRs for the same issue are created at the same time. And in cases where somebody else has been approved and an unapproved PR comes in... well, it gets really murky.
+
+When making changes, please do not force push in your PRs, especially not after a review has been started. We will clone your repo and work from that, doing a simple `pull` on a force-pushed branch ends up being, well, less than simple. We squash merge all PRs, so you do not clutter up the history by using stock-standard pushes to your branch.

+ 124 - 0
pioneer/CHANGELOG.md

@@ -0,0 +1,124 @@
+# 0.36.1
+
+- Api 0.95.1, Util 1.6.1, Extension 0.13.1
+- Support latest contracts ABI (via API), incl. rework of contracts UI
+- Support for Kusama CC2
+- Support for Edgeware mainnet
+- Experimental Ledger support
+- Display forks on explorer (limited to Babe)
+- Change settings to have Save as well as Save & Reload (depending on changes made)
+- Updates to struct & enum rendering (as per extrinsic app)
+- Backup, Password change & Delete don't show for built-in dev accounts
+- Add commissions to the staking overview
+- UI theme update
+- A large number of components refactored for React functional components
+- Allow dismiss of all notifications (via bounty)
+- Migrate all buttons to have icons (via bounty)
+- Proposal submission via modal (via bounty)
+- i18n string extraction (via bounty)
+- adjust signature validity (via bounty)
+- Make the network selection clickable on network name (via bounty)
+- ... and a number of cleanups all around
+
+# 0.35.1
+
+- Api 0.91.1, Util 1.2.1, Extension 0.10.1
+- Support for accounts added via Qr (for instance, the Parity Signer)
+- Support for accounts tied to specific chains (instead of just available to all)
+- GenericAsset app transfers
+- Support for Edgeware with default types
+- Display received heartbeats for validators
+- Allow optional params (really as optional) in RPC toolbox
+- Add Polkascan for Kusama
+- Fix account derivation with `///password`
+- Lots of component & maintainability cleanups
+
+# 0.34.1
+
+- Kusama support
+- Full support for Substrate 2.x & Polkadot 0.5.0 networks
+- Lots of UI updated to support both Substrate 1.x & 2.x chains
+- Add of claims app for Kusama (and Polkadot)
+- Basic Council, Parachains & Treasury apps
+- Moved ui-* packages to react-*
+
+# 0.33.1
+
+- Allow for externally injected accounts (i.e. via extension, polkadot-js & SingleSource)
+- Links to extrnisics & addresses on Polkascan
+- Rework Account & Address layouts with cards
+- Transfer can happen from any point (via Transfer modal)
+- Use new api.derive functions
+- Introduce multi support (most via api.derive.*)
+- Update all account and address modals
+- Add seconding of proposals
+- Staking updates, including un-bonding & withdrawals
+- Update explorer with global query on hash/blocks
+- Add filters on the staking page
+- Vanitygen now supports sr25519 as well
+- Fixes for importing of old JSON
+- Latest @polkadot/util & @polkadot/api
+- A large number of optimizations and smaller fixes
+
+# 0.32.1
+
+- Support for Substrate 1.0 release & metadata v4
+- @polkadot/api 0.77.1
+
+# 0.31.1
+
+- Cleanups, fixes and features around the poc-4 staking module
+- Number of UI enhancements
+
+# 0.30.1
+
+- Staking page indicator for offline nodes (count & block)
+- Rework page tabs and content layouts
+- Cleanup of all UI summary headers
+- Emberic Elem support (replaces Dried Danta)
+
+# 0.29.1
+
+- @polkadot/util & @polkadot/api 0.75.1
+
+# 0.28.1
+
+- Support for substrate 1.0-rc
+
+# 0.27.1
+
+- Bring in new staking & nominating functions
+- Swap default keyring accounts (on creation) to sr25519
+- New faster crypto algorithms
+- Misc. bug fixes all around
+
+# 0.26.1
+
+- Swap keyring to HDKD derivation, mnemonic keys are now not backwards compatible with those created earlier. (Defaults are still for ed25519)
+- Swap crypto to new WASM-backed version (and remove libsodium dependency)
+- UI to allow for derived keys for ed25519 and sr25519
+- New mobile-friendly sidebar
+- Fix issues with nominating (old non-bonds interface)
+
+# 0.25.1
+
+- Swap to publishing -beta.x on merge (non-breaking testing)
+
+ # 0.24.1
+
+ Storage now handles Option type properly
+
+ # 0.23.1
+
+ JavaScript console introduced
+
+# 0.22.1
+
+- Use new Compact<Index> transaction format - this requires the latest binaries from either Polkadot or Substrate
+
+# 0.21.1
+
+- PoC-3 support with latest Substrate master & Polkadot master
+- Add support for Charred Cherry (Substrate) and Alexander (Polkadot) testnets
+- Too many changes to mention, master now only supports latest PoC-3 iteration
+- Use https://poc-2.polkadot.io if access is required to PoC-2 era networks

+ 45 - 0
pioneer/CONTRIBUTING.md

@@ -0,0 +1,45 @@
+# Contributing
+
+## What?
+
+Individuals making significant and valuable contributions are given commit-access to a project to contribute as they see fit.
+A project is more like an open wiki than a standard guarded open source project.
+
+## Rules
+
+There are a few basic ground-rules for contributors (including the maintainer(s) of the project):
+
+1. **No `--force` pushes** or modifying the Git history in any way. If you need to rebase, ensure you do it in your own repo.
+2. **Non-master branches**, prefixed with a short name moniker (e.g. `<initials>-<feature>`) must be used for ongoing work.
+3. **All modifications** must be made in a **pull-request** to solicit feedback from other contributors.
+4. A pull-request *must not be merged until CI* has finished successfully.
+
+#### Merging pull requests once CI is successful:
+- A pull request with no large change to logic that is an urgent fix may be merged after a non-author contributor has reviewed it well.
+- No PR should be merged until all reviews' comments are addressed.
+
+#### Reviewing pull requests:
+When reviewing a pull request, the end-goal is to suggest useful changes to the author. Reviews should finish with approval unless there are issues that would result in:
+
+- Buggy behaviour.
+- Undue maintenance burden.
+- Breaking with house coding style.
+- Pessimisation (i.e. reduction of speed as measured in the projects benchmarks).
+- Feature reduction (i.e. it removes some aspect of functionality that a significant minority of users rely on).
+- Uselessness (i.e. it does not strictly add a feature or fix a known issue).
+
+#### Reviews may not be used as an effective veto for a PR because:
+- There exists a somewhat cleaner/better/faster way of accomplishing the same feature/fix.
+- It does not fit well with some other contributors' longer-term vision for the project.
+
+## Releases
+
+Declaring formal releases remains the prerogative of the project maintainer(s).
+
+## Changes to this arrangement
+
+This is an experiment and feedback is welcome! This document may also be subject to pull-requests or changes by contributors where you believe you have something valuable to add or change.
+
+## Heritage
+
+These contributing guidelines are modified from the "OPEN Open Source Project" guidelines for the Level project: [https://github.com/Level/community/blob/master/CONTRIBUTING.md](https://github.com/Level/community/blob/master/CONTRIBUTING.md)

+ 26 - 0
pioneer/Dockerfile

@@ -0,0 +1,26 @@
+FROM ubuntu:18.04 as builder
+
+# Install any needed packages
+RUN apt-get update && apt-get install -y curl git gnupg
+
+# install nodejs
+RUN curl -sL https://deb.nodesource.com/setup_10.x | bash -
+RUN apt-get install -y nodejs
+
+WORKDIR /app
+RUN git clone https://github.com/polkadot-js/apps
+
+WORKDIR /app/apps
+RUN npm install yarn -g
+RUN yarn
+RUN NODE_ENV=production yarn build
+
+FROM ubuntu:18.04
+
+RUN apt-get update && apt-get -y install nginx
+
+COPY --from=builder /app/apps/packages/apps/build /var/www/html
+
+EXPOSE 80
+
+CMD ["nginx", "-g", "daemon off;"]

+ 201 - 0
pioneer/LICENSE

@@ -0,0 +1,201 @@
+                              Apache License
+                        Version 2.0, January 2004
+                    http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+  "License" shall mean the terms and conditions for use, reproduction,
+  and distribution as defined by Sections 1 through 9 of this document.
+
+  "Licensor" shall mean the copyright owner or entity authorized by
+  the copyright owner that is granting the License.
+
+  "Legal Entity" shall mean the union of the acting entity and all
+  other entities that control, are controlled by, or are under common
+  control with that entity. For the purposes of this definition,
+  "control" means (i) the power, direct or indirect, to cause the
+  direction or management of such entity, whether by contract or
+  otherwise, or (ii) ownership of fifty percent (50%) or more of the
+  outstanding shares, or (iii) beneficial ownership of such entity.
+
+  "You" (or "Your") shall mean an individual or Legal Entity
+  exercising permissions granted by this License.
+
+  "Source" form shall mean the preferred form for making modifications,
+  including but not limited to software source code, documentation
+  source, and configuration files.
+
+  "Object" form shall mean any form resulting from mechanical
+  transformation or translation of a Source form, including but
+  not limited to compiled object code, generated documentation,
+  and conversions to other media types.
+
+  "Work" shall mean the work of authorship, whether in Source or
+  Object form, made available under the License, as indicated by a
+  copyright notice that is included in or attached to the work
+  (an example is provided in the Appendix below).
+
+  "Derivative Works" shall mean any work, whether in Source or Object
+  form, that is based on (or derived from) the Work and for which the
+  editorial revisions, annotations, elaborations, or other modifications
+  represent, as a whole, an original work of authorship. For the purposes
+  of this License, Derivative Works shall not include works that remain
+  separable from, or merely link (or bind by name) to the interfaces of,
+  the Work and Derivative Works thereof.
+
+  "Contribution" shall mean any work of authorship, including
+  the original version of the Work and any modifications or additions
+  to that Work or Derivative Works thereof, that is intentionally
+  submitted to Licensor for inclusion in the Work by the copyright owner
+  or by an individual or Legal Entity authorized to submit on behalf of
+  the copyright owner. For the purposes of this definition, "submitted"
+  means any form of electronic, verbal, or written communication sent
+  to the Licensor or its representatives, including but not limited to
+  communication on electronic mailing lists, source code control systems,
+  and issue tracking systems that are managed by, or on behalf of, the
+  Licensor for the purpose of discussing and improving the Work, but
+  excluding communication that is conspicuously marked or otherwise
+  designated in writing by the copyright owner as "Not a Contribution."
+
+  "Contributor" shall mean Licensor and any individual or Legal Entity
+  on behalf of whom a Contribution has been received by Licensor and
+  subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+  this License, each Contributor hereby grants to You a perpetual,
+  worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+  copyright license to reproduce, prepare Derivative Works of,
+  publicly display, publicly perform, sublicense, and distribute the
+  Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+  this License, each Contributor hereby grants to You a perpetual,
+  worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+  (except as stated in this section) patent license to make, have made,
+  use, offer to sell, sell, import, and otherwise transfer the Work,
+  where such license applies only to those patent claims licensable
+  by such Contributor that are necessarily infringed by their
+  Contribution(s) alone or by combination of their Contribution(s)
+  with the Work to which such Contribution(s) was submitted. If You
+  institute patent litigation against any entity (including a
+  cross-claim or counterclaim in a lawsuit) alleging that the Work
+  or a Contribution incorporated within the Work constitutes direct
+  or contributory patent infringement, then any patent licenses
+  granted to You under this License for that Work shall terminate
+  as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+  Work or Derivative Works thereof in any medium, with or without
+  modifications, and in Source or Object form, provided that You
+  meet the following conditions:
+
+  (a) You must give any other recipients of the Work or
+      Derivative Works a copy of this License; and
+
+  (b) You must cause any modified files to carry prominent notices
+      stating that You changed the files; and
+
+  (c) You must retain, in the Source form of any Derivative Works
+      that You distribute, all copyright, patent, trademark, and
+      attribution notices from the Source form of the Work,
+      excluding those notices that do not pertain to any part of
+      the Derivative Works; and
+
+  (d) If the Work includes a "NOTICE" text file as part of its
+      distribution, then any Derivative Works that You distribute must
+      include a readable copy of the attribution notices contained
+      within such NOTICE file, excluding those notices that do not
+      pertain to any part of the Derivative Works, in at least one
+      of the following places: within a NOTICE text file distributed
+      as part of the Derivative Works; within the Source form or
+      documentation, if provided along with the Derivative Works; or,
+      within a display generated by the Derivative Works, if and
+      wherever such third-party notices normally appear. The contents
+      of the NOTICE file are for informational purposes only and
+      do not modify the License. You may add Your own attribution
+      notices within Derivative Works that You distribute, alongside
+      or as an addendum to the NOTICE text from the Work, provided
+      that such additional attribution notices cannot be construed
+      as modifying the License.
+
+  You may add Your own copyright statement to Your modifications and
+  may provide additional or different license terms and conditions
+  for use, reproduction, or distribution of Your modifications, or
+  for any such Derivative Works as a whole, provided Your use,
+  reproduction, and distribution of the Work otherwise complies with
+  the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+  any Contribution intentionally submitted for inclusion in the Work
+  by You to the Licensor shall be under the terms and conditions of
+  this License, without any additional terms or conditions.
+  Notwithstanding the above, nothing herein shall supersede or modify
+  the terms of any separate license agreement you may have executed
+  with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+  names, trademarks, service marks, or product names of the Licensor,
+  except as required for reasonable and customary use in describing the
+  origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+  agreed to in writing, Licensor provides the Work (and each
+  Contributor provides its Contributions) on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+  implied, including, without limitation, any warranties or conditions
+  of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+  PARTICULAR PURPOSE. You are solely responsible for determining the
+  appropriateness of using or redistributing the Work and assume any
+  risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+  whether in tort (including negligence), contract, or otherwise,
+  unless required by applicable law (such as deliberate and grossly
+  negligent acts) or agreed to in writing, shall any Contributor be
+  liable to You for damages, including any direct, indirect, special,
+  incidental, or consequential damages of any character arising as a
+  result of this License or out of the use or inability to use the
+  Work (including but not limited to damages for loss of goodwill,
+  work stoppage, computer failure or malfunction, or any and all
+  other commercial damages or losses), even if such Contributor
+  has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+  the Work or Derivative Works thereof, You may choose to offer,
+  and charge a fee for, acceptance of support, warranty, indemnity,
+  or other liability obligations and/or rights consistent with this
+  License. However, in accepting such obligations, You may act only
+  on Your own behalf and on Your sole responsibility, not on behalf
+  of any other Contributor, and only if You agree to indemnify,
+  defend, and hold each Contributor harmless for any liability
+  incurred by, or claims asserted against, such Contributor by reason
+  of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+  To apply the Apache License to your work, attach the following
+  boilerplate notice, with the fields enclosed by brackets "[]"
+  replaced with your own identifying information. (Don't include
+  the brackets!)  The text should be enclosed in the appropriate
+  comment syntax for the file format. We also recommend that a
+  file or class name and description of purpose be included on the
+  same "printed page" as the copyright notice for easier
+  identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.

+ 54 - 0
pioneer/README.md

@@ -0,0 +1,54 @@
+<p align="center"><img src="img/pioneer_new.svg"></p>
+
+![Content Directory](https://user-images.githubusercontent.com/4144334/67765742-bbfab280-fa44-11e9-8b13-494b1bfb6014.jpeg)
+
+A Portal into the Joystream network. Provides a view and interaction layer from a browser.
+
+This can be accessed as a hosted application via [https://testnet.joystream.org](https://testnet.joystream.org).
+
+## overview
+
+The repo is split into a number of packages, each representing an application. These are -
+
+- [apps](packages/apps/) This is the main entry point. It handles the selection sidebar and routing to the specific application being displayed.
+- [app-accounts](packages/app-accounts/) A basic account management app.
+- [app-address-book](packages/app-address-book/) A basic address management app.
+- [app-explorer](packages/app-explorer/) A simple block explorer. It only shows the most recent blocks, updating as they become available.
+- [app-extrinsics](packages/app-extrinsics/) Submission of extrinsics to a node.
+- [app-js](packages/app-js/) An online code editor with [@polkadot-js/api](https://github.com/polkadot-js/api/tree/master/packages/api) access to the currently connected node.
+- [app-settings](packages/app-settings/) A basic settings management app, allowing choice of language, node to connect to, and theme
+- [app-staking](packages/app-staking/) A basic staking management app, allowing staking and nominations.
+- [app-storage](packages/app-storage/) A simple node storage query application. Multiple queries can be queued and updates as new values become available.
+- [app-toolbox](packages/app-toolbox/) Submission of raw data to RPC endpoints and utility hashing functions.
+- [app-transfer](packages/app-transfer/) A basic account management app, allowing transfer of Units/DOTs between accounts.
+
+In addition the following libraries are also included in the repo. These are to be moved to the [@polkadot/ui](https://github.com/polkadot-js/ui/) repository once it reaches a base level of stability and usability. (At this point with the framework being tested on the apps above, it makes development easier having it close)
+
+- [react-components](packages/react-components/) A reactive (using RxJS) application framework with a number of useful shared components.
+- [react-signer](packages/react-signer/) Signer implementation for apps.
+- [react-query](packages/react-query) Base components that use the RxJS Observable APIs
+
+## development
+
+Contributions are welcome!
+
+To start off, this repo (along with others in the [@polkadot](https://github.com/polkadot-js/) family) uses yarn workspaces to organise the code. As such, after cloning dependencies _should_ be installed via `yarn`, not via npm, the latter will result in broken dependencies.
+
+To get started -
+
+1. Clone the repo locally, via `git clone https://github.com/joystream/apps <optional local path>`
+2. Ensure that you have a recent LTS version of Node.js, for development purposes [Node >=10.13.0](https://nodejs.org/en/) is recommended.
+3. Ensure that you have a recent version of Yarn, for development purposes [Yarn >=1.10.1](https://yarnpkg.com/docs/install) is required.
+4. Install the dependencies by running `yarn`
+5. Ready! Now you can launch the UI (assuming you have a local Polkadot Node running), via `yarn run start`
+6. Access the UI via [http://localhost:3000](http://localhost:3000)
+
+### Storybook
+
+There is a [StoryBook](https://storybook.js.org) implementation, the UI for which can be started with `yarn storybook` and then accessed in a browser via http://localhost:3001 (and the server will open a new browser tab by default when it starts).
+
+Story code can be placed anywhere in the `packages` directory, and will be detected as long as the file name ends in `.stories.tsx. Stories should be defined in the [Component Story Format (CSF)](https://storybook.js.org/docs/formats/component-story-format) for consistency.
+
+There are several StoryBook addons available, the most useful of which is [Knobs](https://www.npmjs.com/package/@storybook/addon-knobs), which allows props to be altered in real time.
+
+Note that currently StoryBook only allows for stateless components; it has no connection to polkadot.js or any Substrate node. This means that existing components, which are often tightly coupled with the Polkadot API, cannot be used in storybook.

+ 4 - 0
pioneer/babel.config.js

@@ -0,0 +1,4 @@
+module.exports = {
+  extends: '@polkadot/dev-react/config/babel',
+  sourceType: 'unambiguous'
+};

+ 33 - 0
pioneer/deployment.extras.yml

@@ -0,0 +1,33 @@
+apiVersion: extensions/v1beta1
+kind: Ingress
+metadata:
+  name: production-ingress-substrate-ui
+  namespace: poc3-122
+  annotations:
+    kubernetes.io/ingress.class: traefik
+    traefik.frontend.entryPoints: "https,http"
+spec:
+  rules:
+  - host: substrate-ui.parity.io
+    http:
+      paths:
+      - backend:
+          serviceName: production-service
+          servicePort: 80
+---
+apiVersion: extensions/v1beta1
+kind: Ingress
+metadata:
+  name: production-ingress-substrate-ui-light
+  namespace: poc3-122
+  annotations:
+    kubernetes.io/ingress.class: traefik
+    traefik.frontend.entryPoints: "https,http"
+spec:
+  rules:
+  - host: substrate-ui-light.parity.io
+    http:
+      paths:
+      - backend:
+          serviceName: production-service
+          servicePort: 80

+ 60 - 0
pioneer/deployment.template.yml

@@ -0,0 +1,60 @@
+---
+apiVersion: v1
+data:
+# AZURE_DOCKER_REGISTRY_CONFIG is base64 of this:
+# {"auths":{"parity.azurecr.io":{"username":"parity","password":"<password>","email":"admin@parity.io","auth":"<base64 of user+passwoed>"}}}
+  .dockerconfigjson: $AZURE_DOCKER_REGISTRY_CONFIG
+kind: Secret
+metadata:
+  name: azure-docker-registry-key
+type: kubernetes.io/dockerconfigjson
+---
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+  name: $CI_ENVIRONMENT_SLUG-backend
+spec:
+  replicas: $REPLICAS
+  template:
+    metadata:
+      labels:
+        app: $CI_ENVIRONMENT_SLUG
+        component: backend
+    spec:
+      containers:
+        - name: $CI_ENVIRONMENT_SLUG-backend
+          image: $DOCKER_IMAGE_FULL_NAME
+          imagePullPolicy: Always
+          ports:
+          - containerPort: 80
+      imagePullSecrets:
+        - name: azure-docker-registry-key
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: $CI_ENVIRONMENT_SLUG-service
+spec:
+  selector:
+    app: $CI_ENVIRONMENT_SLUG
+  ports:
+    - name: http
+      port: 80
+      targetPort: 80
+      protocol: TCP
+---
+apiVersion: extensions/v1beta1
+kind: Ingress
+metadata:
+  name: $CI_ENVIRONMENT_SLUG-ingress
+  annotations:
+    kubernetes.io/ingress.class: traefik
+    traefik.frontend.entryPoints: "https,http"
+spec:
+  rules:
+  - host: $AUTODEVOPS_HOST
+    http:
+      paths:
+      - backend:
+          serviceName: $CI_ENVIRONMENT_SLUG-service
+          servicePort: 80

+ 24 - 0
pioneer/gh-pages-refresh.sh

@@ -0,0 +1,24 @@
+#!/bin/bash
+
+exit 0
+
+# checkout latest
+git fetch
+git checkout gh-pages
+git pull
+git checkout --orphan gh-pages-temp
+
+# cleanup
+rm -rf node_modules
+rm -rf coverage
+rm -rf packages
+rm -rf test
+
+# add
+git add -A
+git commit -am "refresh history"
+
+# danger, force new
+git branch -D gh-pages
+git branch -m gh-pages
+git push -f origin gh-pages

+ 89 - 0
pioneer/i18next-scanner.config.js

@@ -0,0 +1,89 @@
+const fs = require('fs');
+const path = require('path');
+const typescript = require('typescript');
+
+module.exports = {
+  input: [
+    'packages/*/src/**/*.{ts,tsx}',
+    // Use ! to filter out files or directories
+    '!packages/*/src/**/*.spec.{ts,tsx}',
+    '!packages/*/src/i18n/**',
+    '!**/node_modules/**'
+  ],
+  output: './',
+  options: {
+    debug: true,
+    func: {
+      list: ['t', 'i18next.t', 'i18n.t'],
+      extensions: ['.tsx']
+    },
+    trans: {
+      component: 'Trans'
+    },
+    lngs: ['en'],
+    defaultLng: 'en',
+    ns: [
+      'app-123code',
+      'app-accounts',
+      'app-address-book',
+      'app-claims',
+      'app-contracts',
+      'app-council',
+      'app-dashboard',
+      'app-democracy',
+      'app-explorer',
+      'app-extrinsics',
+      'app-generic-asset',
+      'app-js',
+      'app-parachains',
+      'app-settings',
+      'app-staking',
+      'app-storage',
+      'app-sudo',
+      'app-toolbox',
+      'app-transfer',
+      'app-treasury',
+      'apps',
+      'apps-routing',
+      'react-api',
+      'react-components',
+      'react-params',
+      'react-query',
+      'react-signer',
+      'ui'
+    ],
+    defaultNs: 'ui',
+    resource: {
+      loadPath: 'packages/apps/public/locales/{{lng}}/{{ns}}.json',
+      savePath: 'packages/apps/public/locales/{{lng}}/{{ns}}.json',
+      jsonIndent: 2,
+      lineEnding: '\n'
+    },
+    nsSeparator: false, // namespace separator
+    keySeparator: false // key separator
+  },
+  transform: function transform (file, enc, done) {
+    const { ext } = path.parse(file.path);
+
+    if (ext === '.tsx') {
+      const content = fs.readFileSync(file.path, enc);
+
+      const { outputText } = typescript.transpileModule(content, {
+        compilerOptions: {
+          target: 'es2018'
+        },
+        fileName: path.basename(file.path)
+      });
+
+      const parserHandler = (key, options) => {
+        options.defaultValue = key;
+        options.ns = /packages\/(.*?)\/src/g.exec(file.path)[1];
+        this.parser.set(key, options);
+      };
+
+      this.parser.parseFuncFromString(outputText, parserHandler);
+    }
+
+    done();
+  }
+};

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 72 - 0
pioneer/img/pioneer_new.svg


+ 17 - 0
pioneer/jest.config.js

@@ -0,0 +1,17 @@
+/* eslint-disable @typescript-eslint/no-var-requires */
+const config = require('@polkadot/dev-react/config/jest');
+const findPackages = require('./scripts/findPackages');
+
+const internalModules = findPackages().reduce((modules, { dir, name }) => {
+  modules[`${name}(.*)$`] = `<rootDir>/packages/${dir}/src/$1`;
+
+  return modules;
+}, {});
+
+module.exports = Object.assign({}, config, {
+  moduleNameMapper: {
+    ...internalModules,
+    '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 'empty/object',
+    '\\.(css|less)$': 'empty/object'
+  }
+});

+ 14 - 0
pioneer/lerna.json

@@ -0,0 +1,14 @@
+{
+  "lerna": "2.11.0",
+  "npmClient": "yarn",
+  "useWorkspaces": true,
+  "command": {
+    "publish": {
+      "allowBranch": "master"
+    }
+  },
+  "packages": [
+    "packages/*"
+  ],
+  "version": "0.37.0-beta.63"
+}

+ 87 - 0
pioneer/package.json

@@ -0,0 +1,87 @@
+{
+  "version": "0.37.0-beta.63",
+  "private": true,
+  "engines": {
+    "node": ">=10.13.0",
+    "yarn": "^1.10.1"
+  },
+  "homepage": ".",
+  "name": "pioneer",
+  "scripts": {
+    "analyze": "yarn run build && cd packages/apps && yarn run source-map-explorer build/main.*.js",
+    "build": "yarn run build:code && yarn run build:i18n",
+    "build:code": "NODE_ENV=production polkadot-dev-build-ts",
+    "build:i18n": "i18next-scanner --config i18next-scanner.config.js",
+    "docs": "echo \"skipping docs\"",
+    "clean": "polkadot-dev-clean-build",
+    "clean:i18n": "rm -rf packages/apps/public/locales/en && mkdir -p packages/apps/public/locales/en",
+    "lint": "eslint --ext .js,.jsx,.ts,.tsx . && tsc --noEmit --pretty",
+    "lint-only-errors": "eslint --quiet --ext .js,.jsx,.ts,.tsx . && tsc --noEmit --pretty",
+    "lint-autofix": "eslint --fix --ext .js,.jsx,.ts,.tsx . && tsc --noEmit --pretty",
+    "postinstall": "polkadot-dev-yarn-only",
+    "test": "echo \"skipping tests\"",
+    "vanitygen": "node packages/app-accounts/scripts/vanitygen.js",
+    "start": "cd packages/apps && webpack --config webpack.config.js",
+    "generate-schemas": "json2ts -i packages/joy-types/src/schemas/role.schema.json -o packages/joy-types/src/schemas/role.schema.ts",
+    "build-storybook": "build-storybook -c .storybook",
+    "storybook": "start-storybook -s ./packages/apps/public -p 3001"
+  },
+  "devDependencies": {
+    "@babel/core": "^7.7.0",
+    "@babel/runtime": "^7.7.1",
+    "@babel/cli": "^7.7.4",
+    "@polkadot/dev-react": "^0.32.0-beta.13",
+    "@polkadot/ts": "^0.1.84",
+    "@polkadot/dev": "^0.32.0-beta.15",
+    "@storybook/addon-knobs": "^5.2.5",
+    "@storybook/addon-storysource": "^5.2.5",
+    "@types/jest": "^24.0.22",
+    "@types/react-router-dom": "^5.1.4",
+    "@types/yup": "^0.26.36",
+    "autoprefixer": "^9.7.1",
+    "empty": "^0.10.1",
+    "html-loader": "^0.5.5",
+    "i18next-scanner": "^2.10.3",
+    "json-schema-to-typescript": "^7.1.0",
+    "markdown-loader": "^5.1.0",
+    "postcss": "^7.0.21",
+    "postcss-clean": "^1.1.0",
+    "postcss-flexbugs-fixes": "^4.1.0",
+    "postcss-import": "^12.0.0",
+    "postcss-loader": "^3.0.0",
+    "postcss-nested": "^4.2.1",
+    "postcss-sass": "^0.4.1",
+    "postcss-simple-vars": "^5.0.0",
+    "precss": "^4.0.0",
+    "source-map-explorer": "^2.0.1",
+    "storybook-react-router": "^1.0.8",
+    "ts-jest": "^24.1.0",
+    "tsconfig-paths-webpack-plugin": "^3.2.0",
+    "webpack": "^4.33.0",
+    "typescript": "3.7.2",
+    "cpx": "^1.5.0",
+    "eslint-config-semistandard": "^15.0.0",
+    "eslint-config-standard": "^14.1.1",
+    "eslint-plugin-import": "^2.20.2",
+    "eslint-plugin-node": "^11.1.0",
+    "eslint-plugin-promise": "^4.2.1",
+    "eslint-plugin-standard": "^4.0.1"
+  },
+  "dependencies": {
+    "@polkadot/ui-settings": "^0.47.0-beta.3",
+    "@storybook/addon-actions": "^5.2.5",
+    "@storybook/addon-console": "^1.2.1",
+    "@storybook/react": "^5.2.5",
+    "@types/lodash": "^4.14.138",
+    "@types/marked": "^0.7.0",
+    "ajv": "^6.10.2",
+    "css-loader": "^3.2.0",
+    "less": "^3.10.3",
+    "less-loader": "^5.0.0",
+    "lodash": "^4.17.15",
+    "node-sass": "^4.13.0",
+    "sass-loader": "^8.0.0",
+    "style-loader": "^1.0.0",
+    "@joystream/types": "./types"
+  }
+}

+ 201 - 0
pioneer/packages/app-123code/LICENSE

@@ -0,0 +1,201 @@
+                              Apache License
+                        Version 2.0, January 2004
+                    http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+  "License" shall mean the terms and conditions for use, reproduction,
+  and distribution as defined by Sections 1 through 9 of this document.
+
+  "Licensor" shall mean the copyright owner or entity authorized by
+  the copyright owner that is granting the License.
+
+  "Legal Entity" shall mean the union of the acting entity and all
+  other entities that control, are controlled by, or are under common
+  control with that entity. For the purposes of this definition,
+  "control" means (i) the power, direct or indirect, to cause the
+  direction or management of such entity, whether by contract or
+  otherwise, or (ii) ownership of fifty percent (50%) or more of the
+  outstanding shares, or (iii) beneficial ownership of such entity.
+
+  "You" (or "Your") shall mean an individual or Legal Entity
+  exercising permissions granted by this License.
+
+  "Source" form shall mean the preferred form for making modifications,
+  including but not limited to software source code, documentation
+  source, and configuration files.
+
+  "Object" form shall mean any form resulting from mechanical
+  transformation or translation of a Source form, including but
+  not limited to compiled object code, generated documentation,
+  and conversions to other media types.
+
+  "Work" shall mean the work of authorship, whether in Source or
+  Object form, made available under the License, as indicated by a
+  copyright notice that is included in or attached to the work
+  (an example is provided in the Appendix below).
+
+  "Derivative Works" shall mean any work, whether in Source or Object
+  form, that is based on (or derived from) the Work and for which the
+  editorial revisions, annotations, elaborations, or other modifications
+  represent, as a whole, an original work of authorship. For the purposes
+  of this License, Derivative Works shall not include works that remain
+  separable from, or merely link (or bind by name) to the interfaces of,
+  the Work and Derivative Works thereof.
+
+  "Contribution" shall mean any work of authorship, including
+  the original version of the Work and any modifications or additions
+  to that Work or Derivative Works thereof, that is intentionally
+  submitted to Licensor for inclusion in the Work by the copyright owner
+  or by an individual or Legal Entity authorized to submit on behalf of
+  the copyright owner. For the purposes of this definition, "submitted"
+  means any form of electronic, verbal, or written communication sent
+  to the Licensor or its representatives, including but not limited to
+  communication on electronic mailing lists, source code control systems,
+  and issue tracking systems that are managed by, or on behalf of, the
+  Licensor for the purpose of discussing and improving the Work, but
+  excluding communication that is conspicuously marked or otherwise
+  designated in writing by the copyright owner as "Not a Contribution."
+
+  "Contributor" shall mean Licensor and any individual or Legal Entity
+  on behalf of whom a Contribution has been received by Licensor and
+  subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+  this License, each Contributor hereby grants to You a perpetual,
+  worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+  copyright license to reproduce, prepare Derivative Works of,
+  publicly display, publicly perform, sublicense, and distribute the
+  Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+  this License, each Contributor hereby grants to You a perpetual,
+  worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+  (except as stated in this section) patent license to make, have made,
+  use, offer to sell, sell, import, and otherwise transfer the Work,
+  where such license applies only to those patent claims licensable
+  by such Contributor that are necessarily infringed by their
+  Contribution(s) alone or by combination of their Contribution(s)
+  with the Work to which such Contribution(s) was submitted. If You
+  institute patent litigation against any entity (including a
+  cross-claim or counterclaim in a lawsuit) alleging that the Work
+  or a Contribution incorporated within the Work constitutes direct
+  or contributory patent infringement, then any patent licenses
+  granted to You under this License for that Work shall terminate
+  as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+  Work or Derivative Works thereof in any medium, with or without
+  modifications, and in Source or Object form, provided that You
+  meet the following conditions:
+
+  (a) You must give any other recipients of the Work or
+      Derivative Works a copy of this License; and
+
+  (b) You must cause any modified files to carry prominent notices
+      stating that You changed the files; and
+
+  (c) You must retain, in the Source form of any Derivative Works
+      that You distribute, all copyright, patent, trademark, and
+      attribution notices from the Source form of the Work,
+      excluding those notices that do not pertain to any part of
+      the Derivative Works; and
+
+  (d) If the Work includes a "NOTICE" text file as part of its
+      distribution, then any Derivative Works that You distribute must
+      include a readable copy of the attribution notices contained
+      within such NOTICE file, excluding those notices that do not
+      pertain to any part of the Derivative Works, in at least one
+      of the following places: within a NOTICE text file distributed
+      as part of the Derivative Works; within the Source form or
+      documentation, if provided along with the Derivative Works; or,
+      within a display generated by the Derivative Works, if and
+      wherever such third-party notices normally appear. The contents
+      of the NOTICE file are for informational purposes only and
+      do not modify the License. You may add Your own attribution
+      notices within Derivative Works that You distribute, alongside
+      or as an addendum to the NOTICE text from the Work, provided
+      that such additional attribution notices cannot be construed
+      as modifying the License.
+
+  You may add Your own copyright statement to Your modifications and
+  may provide additional or different license terms and conditions
+  for use, reproduction, or distribution of Your modifications, or
+  for any such Derivative Works as a whole, provided Your use,
+  reproduction, and distribution of the Work otherwise complies with
+  the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+  any Contribution intentionally submitted for inclusion in the Work
+  by You to the Licensor shall be under the terms and conditions of
+  this License, without any additional terms or conditions.
+  Notwithstanding the above, nothing herein shall supersede or modify
+  the terms of any separate license agreement you may have executed
+  with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+  names, trademarks, service marks, or product names of the Licensor,
+  except as required for reasonable and customary use in describing the
+  origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+  agreed to in writing, Licensor provides the Work (and each
+  Contributor provides its Contributions) on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+  implied, including, without limitation, any warranties or conditions
+  of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+  PARTICULAR PURPOSE. You are solely responsible for determining the
+  appropriateness of using or redistributing the Work and assume any
+  risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+  whether in tort (including negligence), contract, or otherwise,
+  unless required by applicable law (such as deliberate and grossly
+  negligent acts) or agreed to in writing, shall any Contributor be
+  liable to You for damages, including any direct, indirect, special,
+  incidental, or consequential damages of any character arising as a
+  result of this License or out of the use or inability to use the
+  Work (including but not limited to damages for loss of goodwill,
+  work stoppage, computer failure or malfunction, or any and all
+  other commercial damages or losses), even if such Contributor
+  has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+  the Work or Derivative Works thereof, You may choose to offer,
+  and charge a fee for, acceptance of support, warranty, indemnity,
+  or other liability obligations and/or rights consistent with this
+  License. However, in accepting such obligations, You may act only
+  on Your own behalf and on Your sole responsibility, not on behalf
+  of any other Contributor, and only if You agree to indemnify,
+  defend, and hold each Contributor harmless for any liability
+  incurred by, or claims asserted against, such Contributor by reason
+  of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+  To apply the Apache License to your work, attach the following
+  boilerplate notice, with the fields enclosed by brackets "[]"
+  replaced with your own identifying information. (Don't include
+  the brackets!)  The text should be enclosed in the appropriate
+  comment syntax for the file format. We also recommend that a
+  file or class name and description of purpose be included on the
+  same "printed page" as the copyright notice for easier
+  identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.

+ 22 - 0
pioneer/packages/app-123code/README.md

@@ -0,0 +1,22 @@
+# @polkadot/app-123code
+
+A simple template to get started with adding an "app" to this UI. It contains the bare minimum for a nicely hackable app (if you just want to code _somewhere_) and the steps needed to create, add and register an new app that appears in the UI.
+
+## adding an app
+
+If you want to add a new app to the UI, this is the place to start.
+
+1. Duplicate this `app-123code` folder and give it an appropriate name, in this case we will select `app-example` to keep things clear.
+2. Edit the `apps-example/package.json` app description, i.e. the name, author and relevant overview.
+
+And we have the basic app source setup, time to get the tooling correct.
+
+3. Add the new app to the TypeScript config in root, `tsconfig.json`, i.e. an entry such as `"@polkadot/app-example/*": [ "packages/app-example/src/*" ],`
+
+At this point the app should be buildable, but not quite reachable. The final step is to add it to the actual sidebar in `apps`.
+
+4. In `apps-routing/src` duplicate the `123code.ts` file to `example.ts` and edit it with the appropriate information, including the hash link, name and icon (any icon name from semantic-ui-react/font-awesome 4 should be appropriate).
+5. In the above description file, the `isHidden` field needs to be toggled to make it appear - the base template is hidden by default.
+6. Finally add the `template` to the `apps-routing/src/index.ts` file at the appropriate place for both full and light mode (either optional)
+
+Yes. After all that we have things hooked up. Run `yarn start` and your new app (non-coded) should show up. Now start having fun and building something great.

+ 16 - 0
pioneer/packages/app-123code/package.json

@@ -0,0 +1,16 @@
+{
+  "name": "@polkadot/app-123code",
+  "version": "0.37.0-beta.63",
+  "description": "A basic app that shows the ropes on customisation",
+  "main": "index.js",
+  "scripts": {},
+  "author": "Jaco Greeff <jacogr@gmail.com>",
+  "maintainers": [
+    "Jaco Greeff <jacogr@gmail.com>"
+  ],
+  "license": "Apache-2.0",
+  "dependencies": {
+    "@babel/runtime": "^7.7.1",
+    "@polkadot/react-components": "^0.37.0-beta.63"
+  }
+}

+ 49 - 0
pioneer/packages/app-123code/src/AccountSelector.tsx

@@ -0,0 +1,49 @@
+// Copyright 2017-2019 @polkadot/app-123code authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+import React, { useEffect, useState } from 'react';
+import styled from 'styled-components';
+import { Bubble, InputAddress } from '@polkadot/react-components';
+import { AccountIndex, Balance, Nonce } from '@polkadot/react-query';
+
+interface Props {
+  className?: string;
+  onChange: (accountId: string | null) => void;
+}
+
+function AccountSelector ({ className, onChange }: Props): React.ReactElement<Props> {
+  const [accountId, setAccountId] = useState<string | null>(null);
+
+  useEffect((): void => onChange(accountId), [accountId]);
+
+  return (
+    <section className={`template--AccountSelector ui--row ${className}`}>
+      <InputAddress
+        className='medium'
+        label='my default account'
+        onChange={setAccountId}
+        type='account'
+      />
+      <div className='medium'>
+        <Bubble color='teal' icon='address card' label='index'>
+          <AccountIndex params={accountId} />
+        </Bubble>
+        <Bubble color='yellow' icon='adjust' label='balance'>
+          <Balance params={accountId} />
+        </Bubble>
+        <Bubble color='yellow' icon='target' label='transactions'>
+          <Nonce params={accountId} />
+        </Bubble>
+      </div>
+    </section>
+  );
+}
+
+export default styled(AccountSelector)`
+  align-items: flex-end;
+
+  .summary {
+    text-align: center;
+  }
+`;

+ 28 - 0
pioneer/packages/app-123code/src/Summary.tsx

@@ -0,0 +1,28 @@
+// Copyright 2017-2019 @polkadot/app-123code authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+import { BareProps } from '@polkadot/react-components/types';
+
+import React from 'react';
+import styled from 'styled-components';
+
+interface Props extends BareProps {
+  children: React.ReactNode;
+}
+
+function Summary ({ children, className, style }: Props): React.ReactElement<Props> {
+  return (
+    <div
+      className={className}
+      style={style}
+    >
+      {children}
+    </div>
+  );
+}
+
+export default styled(Summary)`
+  opacity: 0.5;
+  padding: 1rem 1.5rem;
+`;

+ 65 - 0
pioneer/packages/app-123code/src/SummaryBar.tsx

@@ -0,0 +1,65 @@
+/* eslint-disable @typescript-eslint/camelcase */
+// Copyright 2017-2019 @polkadot/app-123code authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+import { AccountId } from '@polkadot/types/interfaces';
+import { BareProps, I18nProps } from '@polkadot/react-components/types';
+
+import BN from 'bn.js';
+import React, { useContext } from 'react';
+import { ApiContext, withCalls } from '@polkadot/react-api';
+import { Bubble, IdentityIcon } from '@polkadot/react-components';
+import { formatBalance, formatNumber } from '@polkadot/util';
+
+import translate from './translate';
+
+interface Props extends BareProps, I18nProps {
+  balances_totalIssuance?: BN;
+  chain_bestNumber?: BN;
+  chain_bestNumberLag?: BN;
+  staking_validators?: AccountId[];
+}
+
+function SummaryBar ({ balances_totalIssuance, chain_bestNumber, chain_bestNumberLag, staking_validators }: Props): React.ReactElement<Props> {
+  const { api, systemChain, systemName, systemVersion } = useContext(ApiContext);
+
+  return (
+    <summary>
+      <div>
+        <Bubble icon='tty' label='node'>
+          {systemName} v{systemVersion}
+        </Bubble>
+        <Bubble icon='chain' label='chain'>
+          {systemChain}
+        </Bubble>
+        <Bubble icon='code' label='runtime'>
+          {api.runtimeVersion.implName} v{api.runtimeVersion.implVersion}
+        </Bubble>
+        <Bubble icon='bullseye' label='best #'>
+          {formatNumber(chain_bestNumber)} ({formatNumber(chain_bestNumberLag)} lag)
+        </Bubble>
+        {staking_validators && (
+          <Bubble icon='chess queen' label='validators'>{
+            staking_validators.map((accountId, index): React.ReactNode => (
+              <IdentityIcon key={index} value={accountId} size={20} />
+            ))
+          }</Bubble>
+        )}
+        <Bubble icon='circle' label='total tokens'>
+          {formatBalance(balances_totalIssuance)}
+        </Bubble>
+      </div>
+    </summary>
+  );
+}
+
+// inject the actual API calls automatically into props
+export default translate(
+  withCalls<Props>(
+    'derive.chain.bestNumber',
+    'derive.chain.bestNumberLag',
+    'derive.staking.validators',
+    'query.balances.totalIssuance'
+  )(SummaryBar)
+);

+ 47 - 0
pioneer/packages/app-123code/src/Transfer.tsx

@@ -0,0 +1,47 @@
+// Copyright 2017-2019 @polkadot/app-123code authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+import BN from 'bn.js';
+import React, { useState } from 'react';
+import { Button, InputAddress, InputBalance, TxButton } from '@polkadot/react-components';
+
+import Summary from './Summary';
+
+interface Props {
+  accountId?: string | null;
+}
+
+export default function Transfer ({ accountId }: Props): React.ReactElement<Props> {
+  const [amount, setAmount] = useState<BN | undefined | null>(null);
+  const [recipientId, setRecipientId] = useState<string | null>(null);
+
+  return (
+    <section>
+      <h1>transfer</h1>
+      <div className='ui--row'>
+        <div className='large'>
+          <InputAddress
+            label='recipient address for this transfer'
+            onChange={setRecipientId}
+            type='all'
+          />
+          <InputBalance
+            label='amount to transfer'
+            onChange={setAmount}
+          />
+          <Button.Group>
+            <TxButton
+              accountId={accountId}
+              icon='send'
+              label='make transfer'
+              params={[recipientId, amount]}
+              tx='balances.transfer'
+            />
+          </Button.Group>
+        </div>
+        <Summary className='small'>Make a transfer from any account you control to another account. Transfer fees and per-transaction fees apply and will be calculated upon submission.</Summary>
+      </div>
+    </section>
+  );
+}

+ 38 - 0
pioneer/packages/app-123code/src/index.tsx

@@ -0,0 +1,38 @@
+// Copyright 2017-2019 @polkadot/app-123code authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+// some types, AppProps for the app and I18nProps to indicate
+// translatable strings. Generally the latter is quite "light",
+// `t` is inject into props (see the HOC export) and `t('any text')
+// does the translation
+import { AppProps, I18nProps } from '@polkadot/react-components/types';
+
+// external imports (including those found in the packages/*
+// of this repo)
+import React, { useState } from 'react';
+
+// local imports and components
+import AccountSelector from './AccountSelector';
+import SummaryBar from './SummaryBar';
+import Transfer from './Transfer';
+import translate from './translate';
+
+// define our internal types
+interface Props extends AppProps, I18nProps {}
+
+function App ({ className }: Props): React.ReactElement<Props> {
+  const [accountId, setAccountId] = useState<string | null>(null);
+
+  return (
+    // in all apps, the main wrapper is setup to allow the padding
+    // and margins inside the application. (Just from a consistent pov)
+    <main className={className}>
+      <SummaryBar />
+      <AccountSelector onChange={setAccountId} />
+      <Transfer accountId={accountId} />
+    </main>
+  );
+}
+
+export default translate(App);

+ 7 - 0
pioneer/packages/app-123code/src/translate.ts

@@ -0,0 +1,7 @@
+// Copyright 2017-2019 @polkadot/app-123code authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+import { withTranslation } from 'react-i18next';
+
+export default withTranslation(['app-123code']);

+ 201 - 0
pioneer/packages/app-accounts/LICENSE

@@ -0,0 +1,201 @@
+                              Apache License
+                        Version 2.0, January 2004
+                    http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+  "License" shall mean the terms and conditions for use, reproduction,
+  and distribution as defined by Sections 1 through 9 of this document.
+
+  "Licensor" shall mean the copyright owner or entity authorized by
+  the copyright owner that is granting the License.
+
+  "Legal Entity" shall mean the union of the acting entity and all
+  other entities that control, are controlled by, or are under common
+  control with that entity. For the purposes of this definition,
+  "control" means (i) the power, direct or indirect, to cause the
+  direction or management of such entity, whether by contract or
+  otherwise, or (ii) ownership of fifty percent (50%) or more of the
+  outstanding shares, or (iii) beneficial ownership of such entity.
+
+  "You" (or "Your") shall mean an individual or Legal Entity
+  exercising permissions granted by this License.
+
+  "Source" form shall mean the preferred form for making modifications,
+  including but not limited to software source code, documentation
+  source, and configuration files.
+
+  "Object" form shall mean any form resulting from mechanical
+  transformation or translation of a Source form, including but
+  not limited to compiled object code, generated documentation,
+  and conversions to other media types.
+
+  "Work" shall mean the work of authorship, whether in Source or
+  Object form, made available under the License, as indicated by a
+  copyright notice that is included in or attached to the work
+  (an example is provided in the Appendix below).
+
+  "Derivative Works" shall mean any work, whether in Source or Object
+  form, that is based on (or derived from) the Work and for which the
+  editorial revisions, annotations, elaborations, or other modifications
+  represent, as a whole, an original work of authorship. For the purposes
+  of this License, Derivative Works shall not include works that remain
+  separable from, or merely link (or bind by name) to the interfaces of,
+  the Work and Derivative Works thereof.
+
+  "Contribution" shall mean any work of authorship, including
+  the original version of the Work and any modifications or additions
+  to that Work or Derivative Works thereof, that is intentionally
+  submitted to Licensor for inclusion in the Work by the copyright owner
+  or by an individual or Legal Entity authorized to submit on behalf of
+  the copyright owner. For the purposes of this definition, "submitted"
+  means any form of electronic, verbal, or written communication sent
+  to the Licensor or its representatives, including but not limited to
+  communication on electronic mailing lists, source code control systems,
+  and issue tracking systems that are managed by, or on behalf of, the
+  Licensor for the purpose of discussing and improving the Work, but
+  excluding communication that is conspicuously marked or otherwise
+  designated in writing by the copyright owner as "Not a Contribution."
+
+  "Contributor" shall mean Licensor and any individual or Legal Entity
+  on behalf of whom a Contribution has been received by Licensor and
+  subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+  this License, each Contributor hereby grants to You a perpetual,
+  worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+  copyright license to reproduce, prepare Derivative Works of,
+  publicly display, publicly perform, sublicense, and distribute the
+  Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+  this License, each Contributor hereby grants to You a perpetual,
+  worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+  (except as stated in this section) patent license to make, have made,
+  use, offer to sell, sell, import, and otherwise transfer the Work,
+  where such license applies only to those patent claims licensable
+  by such Contributor that are necessarily infringed by their
+  Contribution(s) alone or by combination of their Contribution(s)
+  with the Work to which such Contribution(s) was submitted. If You
+  institute patent litigation against any entity (including a
+  cross-claim or counterclaim in a lawsuit) alleging that the Work
+  or a Contribution incorporated within the Work constitutes direct
+  or contributory patent infringement, then any patent licenses
+  granted to You under this License for that Work shall terminate
+  as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+  Work or Derivative Works thereof in any medium, with or without
+  modifications, and in Source or Object form, provided that You
+  meet the following conditions:
+
+  (a) You must give any other recipients of the Work or
+      Derivative Works a copy of this License; and
+
+  (b) You must cause any modified files to carry prominent notices
+      stating that You changed the files; and
+
+  (c) You must retain, in the Source form of any Derivative Works
+      that You distribute, all copyright, patent, trademark, and
+      attribution notices from the Source form of the Work,
+      excluding those notices that do not pertain to any part of
+      the Derivative Works; and
+
+  (d) If the Work includes a "NOTICE" text file as part of its
+      distribution, then any Derivative Works that You distribute must
+      include a readable copy of the attribution notices contained
+      within such NOTICE file, excluding those notices that do not
+      pertain to any part of the Derivative Works, in at least one
+      of the following places: within a NOTICE text file distributed
+      as part of the Derivative Works; within the Source form or
+      documentation, if provided along with the Derivative Works; or,
+      within a display generated by the Derivative Works, if and
+      wherever such third-party notices normally appear. The contents
+      of the NOTICE file are for informational purposes only and
+      do not modify the License. You may add Your own attribution
+      notices within Derivative Works that You distribute, alongside
+      or as an addendum to the NOTICE text from the Work, provided
+      that such additional attribution notices cannot be construed
+      as modifying the License.
+
+  You may add Your own copyright statement to Your modifications and
+  may provide additional or different license terms and conditions
+  for use, reproduction, or distribution of Your modifications, or
+  for any such Derivative Works as a whole, provided Your use,
+  reproduction, and distribution of the Work otherwise complies with
+  the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+  any Contribution intentionally submitted for inclusion in the Work
+  by You to the Licensor shall be under the terms and conditions of
+  this License, without any additional terms or conditions.
+  Notwithstanding the above, nothing herein shall supersede or modify
+  the terms of any separate license agreement you may have executed
+  with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+  names, trademarks, service marks, or product names of the Licensor,
+  except as required for reasonable and customary use in describing the
+  origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+  agreed to in writing, Licensor provides the Work (and each
+  Contributor provides its Contributions) on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+  implied, including, without limitation, any warranties or conditions
+  of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+  PARTICULAR PURPOSE. You are solely responsible for determining the
+  appropriateness of using or redistributing the Work and assume any
+  risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+  whether in tort (including negligence), contract, or otherwise,
+  unless required by applicable law (such as deliberate and grossly
+  negligent acts) or agreed to in writing, shall any Contributor be
+  liable to You for damages, including any direct, indirect, special,
+  incidental, or consequential damages of any character arising as a
+  result of this License or out of the use or inability to use the
+  Work (including but not limited to damages for loss of goodwill,
+  work stoppage, computer failure or malfunction, or any and all
+  other commercial damages or losses), even if such Contributor
+  has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+  the Work or Derivative Works thereof, You may choose to offer,
+  and charge a fee for, acceptance of support, warranty, indemnity,
+  or other liability obligations and/or rights consistent with this
+  License. However, in accepting such obligations, You may act only
+  on Your own behalf and on Your sole responsibility, not on behalf
+  of any other Contributor, and only if You agree to indemnify,
+  defend, and hold each Contributor harmless for any liability
+  incurred by, or claims asserted against, such Contributor by reason
+  of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+  To apply the Apache License to your work, attach the following
+  boilerplate notice, with the fields enclosed by brackets "[]"
+  replaced with your own identifying information. (Don't include
+  the brackets!)  The text should be enclosed in the appropriate
+  comment syntax for the file format. We also recommend that a
+  file or class name and description of purpose be included on the
+  same "printed page" as the copyright notice for easier
+  identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.

+ 5 - 0
pioneer/packages/app-accounts/README.md

@@ -0,0 +1,5 @@
+# @polkadot/app-accounts
+
+## vanity
+
+Running `yarn run vanitygen --match <string>` runs the generator as a Node CLI app. (Orders of a magnitude faster due to the use of libsoldium bindings)

+ 23 - 0
pioneer/packages/app-accounts/package.json

@@ -0,0 +1,23 @@
+{
+  "name": "@polkadot/app-accounts",
+  "version": "0.37.0-beta.63",
+  "main": "index.js",
+  "repository": "https://github.com/polkadot-js/apps.git",
+  "author": "Jaco Greeff <jacogr@gmail.com>",
+  "maintainers": [
+    "Jaco Greeff <jacogr@gmail.com>"
+  ],
+  "contributors": [],
+  "license": "Apache-2.0",
+  "dependencies": {
+    "@babel/runtime": "^7.7.1",
+    "@polkadot/react-components": "^0.37.0-beta.63",
+    "@polkadot/react-qr": "^0.47.0-beta.3",
+    "@types/file-saver": "^2.0.0",
+    "@types/yargs": "^13.0.2",
+    "babel-plugin-module-resolver": "^3.1.1",
+    "detect-browser": "^4.8.0",
+    "file-saver": "^2.0.0",
+    "yargs": "^14.2.0"
+  }
+}

+ 26 - 0
pioneer/packages/app-accounts/scripts/vanitygen.js

@@ -0,0 +1,26 @@
+#!/usr/bin/env node
+/* eslint-disable @typescript-eslint/no-var-requires */
+// Copyright 2017-2019 @polkadot/app-accounts authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+const fs = require('fs');
+const path = require('path');
+
+const [compiled] = ['../vanitygen/cli.js']
+  .map((file) => path.join(__dirname, file))
+  .filter((file) => fs.existsSync(file));
+
+if (compiled) {
+  require(compiled);
+} else {
+  require('@babel/register')({
+    extensions: ['.js', '.ts'],
+    plugins: [
+      ['module-resolver', {
+        alias: {}
+      }]
+    ]
+  });
+  require('../src/vanitygen/cli.ts');
+}

+ 227 - 0
pioneer/packages/app-accounts/src/Account.tsx

@@ -0,0 +1,227 @@
+// Copyright 2017-2019 @polkadot/app-staking authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+import { ActionStatus } from '@polkadot/react-components/Status/types';
+import { I18nProps } from '@polkadot/react-components/types';
+
+import React, { useState, useEffect } from 'react';
+import { Popup } from 'semantic-ui-react';
+import styled from 'styled-components';
+import { AddressCard, AddressInfo, Button, ChainLock, Forget, Menu } from '@polkadot/react-components';
+import keyring from '@polkadot/ui-keyring';
+
+import Backup from './modals/Backup';
+import ChangePass from './modals/ChangePass';
+import Derive from './modals/Derive';
+import Transfer from './modals/Transfer';
+import translate from './translate';
+
+interface Props extends I18nProps {
+  address: string;
+  className?: string;
+}
+
+function Account ({ address, className, t }: Props): React.ReactElement<Props> {
+  const [genesisHash, setGenesisHash] = useState<string | null>(null);
+  const [isBackupOpen, setIsBackupOpen] = useState(false);
+  const [{ isDevelopment, isEditable, isExternal }, setFlags] = useState({ isDevelopment: false, isEditable: false, isExternal: false });
+  const [isDeriveOpen, setIsDeriveOpen] = useState(false);
+  const [isForgetOpen, setIsForgetOpen] = useState(false);
+  const [isPasswordOpen, setIsPasswordOpen] = useState(false);
+  const [isSettingPopupOpen, setIsSettingPopupOpen] = useState(false);
+  const [isTransferOpen, setIsTransferOpen] = useState(false);
+
+  useEffect((): void => {
+    const account = keyring.getAccount(address);
+
+    setGenesisHash((account && account.meta.genesisHash) || null);
+    setFlags({
+      isDevelopment: (account && account.meta.isTesting) || false,
+      isEditable: (account && !(account.meta.isInjected || account.meta.isHardware)) || false,
+      isExternal: (account && account.meta.isExternal) || false
+    });
+  }, [address]);
+
+  const _toggleBackup = (): void => setIsBackupOpen(!isBackupOpen);
+  const _toggleDerive = (): void => setIsDeriveOpen(!isDeriveOpen);
+  const _toggleForget = (): void => setIsForgetOpen(!isForgetOpen);
+  const _togglePass = (): void => setIsPasswordOpen(!isPasswordOpen);
+  const _toggleTransfer = (): void => setIsTransferOpen(!isTransferOpen);
+  const _toggleSettingPopup = (): void => setIsSettingPopupOpen(!isSettingPopupOpen);
+  const _onForget = (): void => {
+    if (!address) {
+      return;
+    }
+
+    const status: Partial<ActionStatus> = {
+      account: address,
+      action: 'forget'
+    };
+
+    try {
+      keyring.forgetAccount(address);
+      status.status = 'success';
+      status.message = t('account forgotten');
+    } catch (error) {
+      status.status = 'error';
+      status.message = error.message;
+    }
+  };
+  const _onGenesisChange = (genesisHash: string | null): void => {
+    const account = keyring.getPair(address);
+
+    account && keyring.saveAccountMeta(account, { ...account.meta, genesisHash });
+
+    setGenesisHash(genesisHash);
+  };
+
+  // FIXME It is a bit heavy-handled switching of being editable here completely
+  // (and removing the tags, however the keyring cannot save these)
+  return (
+    <AddressCard
+      buttons={
+        <div className='accounts--Account-buttons buttons'>
+          <div className='actions'>
+            {isEditable && !isDevelopment && (
+              <Button
+                isNegative
+                onClick={_toggleForget}
+                icon='trash'
+                size='small'
+                tooltip={t('Forget this account')}
+              />
+            )}
+            {isEditable && !isExternal && !isDevelopment && (
+              <>
+                <Button
+                  icon='cloud download'
+                  isPrimary
+                  onClick={_toggleBackup}
+                  size='small'
+                  tooltip={t('Create a backup file for this account')}
+                />
+                <Button
+                  icon='key'
+                  isPrimary
+                  onClick={_togglePass}
+                  size='small'
+                  tooltip={t("Change this account's password")}
+                />
+              </>
+            )}
+            <Button
+              icon='paper plane'
+              isPrimary
+              label={t('send')}
+              onClick={_toggleTransfer}
+              size='small'
+              tooltip={t('Send funds from this account')}
+            />
+            {isEditable && !isExternal && (
+              <Popup
+                onClose={_toggleSettingPopup}
+                open={isSettingPopupOpen}
+                position='bottom left'
+                trigger={
+                  <Button
+                    icon='setting'
+                    onClick={_toggleSettingPopup}
+                    size='small'
+                  />
+                }
+              >
+                <Menu
+                  vertical
+                  text
+                  onClick={_toggleSettingPopup}
+                >
+                  <Menu.Item onClick={_toggleDerive}>
+                    {t('Derive account from source')}
+                  </Menu.Item>
+                  <Menu.Item disabled>
+                    {t('Change on-chain nickname')}
+                  </Menu.Item>
+                </Menu>
+              </Popup>
+            )}
+          </div>
+          {isEditable && !isExternal && (
+            <div className='others'>
+              <ChainLock
+                genesisHash={genesisHash}
+                onChange={_onGenesisChange}
+              />
+            </div>
+          )}
+        </div>
+      }
+      className={className}
+      isEditable={isEditable}
+      type='account'
+      value={address}
+      withExplorer
+      withIndexOrAddress={false}
+      withTags
+    >
+      {address && (
+        <>
+          {isBackupOpen && (
+            <Backup
+              address={address}
+              key='modal-backup-account'
+              onClose={_toggleBackup}
+            />
+          )}
+          {isDeriveOpen && (
+            <Derive
+              from={address}
+              key='modal-derive-account'
+              onClose={_toggleDerive}
+            />
+          )}
+          {isForgetOpen && (
+            <Forget
+              address={address}
+              onForget={_onForget}
+              key='modal-forget-account'
+              onClose={_toggleForget}
+            />
+          )}
+          {isPasswordOpen && (
+            <ChangePass
+              address={address}
+              key='modal-change-pass'
+              onClose={_togglePass}
+            />
+          )}
+          {isTransferOpen && (
+            <Transfer
+              key='modal-transfer'
+              onClose={_toggleTransfer}
+              senderId={address}
+            />
+          )}
+        </>
+      )}
+      <AddressInfo
+        address={address}
+        withBalance
+        withExtended
+      />
+    </AddressCard>
+  );
+}
+
+export default translate(
+  styled(Account)`
+    .accounts--Account-buttons {
+      text-align: right;
+
+      .others {
+        margin-right: 0.125rem;
+        margin-top: 0.25rem;
+      }
+    }
+  `
+);

+ 105 - 0
pioneer/packages/app-accounts/src/Banner.tsx

@@ -0,0 +1,105 @@
+// Copyright 2017-2019 @polkadot/app-accounts authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+import { I18nProps } from '@polkadot/react-components/types';
+
+import { detect } from 'detect-browser';
+import React from 'react';
+import styled from 'styled-components';
+import { isWeb3Injected } from '@polkadot/extension-dapp';
+import { stringUpperFirst } from '@polkadot/util';
+
+import translate from './translate';
+
+// it would have been really good to import this from detect, however... not exported
+type Browser = 'chrome' | 'firefox';
+
+interface Extension {
+  desc: string;
+  link: string;
+  name: string;
+}
+
+interface Props extends I18nProps {
+  className?: string;
+}
+
+const available: Record<Browser, Extension[]> = {
+  chrome: [],
+  firefox: []
+};
+
+[
+  {
+    browsers: {
+      chrome: 'https://chrome.google.com/webstore/detail/polkadot%7Bjs%7D-extension/mopnmbcafieddcagagdcbnhejhlodfdd',
+      firefox: 'https://addons.mozilla.org/en-US/firefox/addon/polkadot-js-extension/'
+    },
+    desc: 'Basic account injection and signer',
+    name: 'polkadot-js extension'
+  }
+].forEach(({ browsers, desc, name }): void => {
+  Object.entries(browsers).forEach(([browser, link]): void => {
+    available[browser as Browser].push({ link, desc, name });
+  });
+});
+
+const browserInfo = detect();
+const browserName: Browser | null = (browserInfo && (browserInfo.name as Browser)) || null;
+const isSupported = browserName && Object.keys(available).includes(browserName);
+
+function Banner ({ className, t }: Props): React.ReactElement<Props> | null {
+  if (isWeb3Injected || !isSupported || !browserName) {
+    return null;
+  }
+
+  return (
+    <div className={className}>
+      <div className='box'>
+        <div className='info'>
+          <p>{t('It is recommended that you create/store your accounts securely and externally from the app. On {{yourBrowser}} the following browser extensions are available for use -', {
+            replace: {
+              yourBrowser: stringUpperFirst(browserName)
+            }
+          })}</p>
+          <ul>{available[browserName].map(({ desc, name, link }): React.ReactNode => (
+            <li key={name}>
+              <a
+                href={link}
+                rel='noopener noreferrer'
+                target='_blank'
+              >
+                {name}
+              </a> ({desc})
+            </li>
+          ))
+          }</ul>
+          <p>{t('Accounts injected from any of these extensions will appear in this application and be available for use. The above list is updated as more extensions with external signing capability become available.')}&nbsp;<a
+            href='https://github.com/polkadot-js/extension'
+            rel='noopener noreferrer'
+            target='_blank'
+          >{t('Learn more...')}</a></p>
+        </div>
+      </div>
+    </div>
+  );
+}
+
+export default translate(
+  styled(Banner)`
+    padding: 0 0.5rem 0.5rem;
+
+    .box {
+      background: #fff6e5;
+      border-left: 0.25rem solid darkorange;
+      border-radius: 0 0.25rem 0.25rem 0;
+      box-sizing: border-box;
+      padding: 1rem 1.5rem;
+
+      .info {
+        max-width: 50rem;
+      }
+    }
+  `
+);

+ 50 - 0
pioneer/packages/app-accounts/src/MemoForm.tsx

@@ -0,0 +1,50 @@
+import React from 'react';
+import { Labelled } from '@polkadot/react-components/index';
+
+import MemoEdit from '@polkadot/joy-utils/memo/MemoEdit';
+import TxButton from '@polkadot/joy-utils/TxButton';
+import { withMyAccount, MyAccountProps } from '@polkadot/joy-utils/MyAccount';
+import { Text } from '@polkadot/types';
+
+type Props = MyAccountProps & {};
+
+type State = {
+  memo: string;
+  modified: boolean;
+};
+
+class Component extends React.PureComponent<Props, State> {
+  state: State = {
+    memo: '',
+    modified: false
+  };
+
+  render () {
+    const { myAddress } = this.props;
+    const { memo, modified } = this.state;
+    return (
+      <>
+        <MemoEdit accountId={myAddress || ''} onChange={this.onChangeMemo} onReset={this.onResetMemo} />
+        <Labelled style={{ marginTop: '.5rem' }}>
+          <TxButton
+            size='large'
+            isDisabled={!modified}
+            label='Update memo'
+            params={[new Text(memo)]}
+            tx='memo.updateMemo'
+          />
+        </Labelled>
+      </>
+    );
+  }
+
+  onChangeMemo = (memo: string): void => {
+    this.setState({ memo, modified: true });
+  }
+
+  onResetMemo = (memo: string): void => {
+    this.setState({ memo, modified: false });
+  }
+}
+
+export default withMyAccount(Component);

+ 110 - 0
pioneer/packages/app-accounts/src/Overview.tsx

@@ -0,0 +1,110 @@
+// Copyright 2017-2019 @polkadot/app-staking authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+import { I18nProps } from '@polkadot/react-components/types';
+import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
+import { ComponentProps } from './types';
+
+import React, { useState } from 'react';
+import { Link, useLocation } from 'react-router-dom';
+import { Button as SUIButton } from 'semantic-ui-react';
+import keyring from '@polkadot/ui-keyring';
+import accountObservable from '@polkadot/ui-keyring/observable/accounts';
+import { getLedger, isLedger, withMulti, withObservable } from '@polkadot/react-api';
+import { Button, CardGrid } from '@polkadot/react-components';
+
+import CreateModal from './modals/Create';
+import ImportModal from './modals/Import';
+import Account from './Account';
+import translate from './translate';
+
+interface Props extends ComponentProps, I18nProps {
+  accounts?: SubjectInfo[];
+}
+
+// query the ledger for the address, adding it to the keyring
+async function queryLedger (): Promise<void> {
+  const ledger = getLedger();
+
+  try {
+    const { address } = await ledger.getAddress();
+
+    keyring.addHardware(address, 'ledger', { name: 'ledger' });
+  } catch (error) {
+    console.error(error);
+  }
+}
+
+function Overview ({ accounts, onStatusChange, t }: Props): React.ReactElement<Props> {
+  const { pathname } = useLocation();
+  const [isCreateOpen, setIsCreateOpen] = useState(false);
+  const [isImportOpen, setIsImportOpen] = useState(false);
+  const emptyScreen = !(isCreateOpen || isImportOpen) && accounts && (Object.keys(accounts).length === 0);
+
+  const _toggleCreate = (): void => setIsCreateOpen(!isCreateOpen);
+  const _toggleImport = (): void => setIsImportOpen(!isImportOpen);
+
+  return (
+    <CardGrid
+      topButtons={
+        !emptyScreen && <SUIButton as={Link} to={`${pathname}/vanity`}>Generate a vanity address</SUIButton>
+      }
+      buttons={
+        <Button.Group>
+          <Button
+            icon='add'
+            isPrimary
+            label={t('Add account')}
+            onClick={_toggleCreate}
+          />
+          <Button.Or />
+          <Button
+            icon='sync'
+            isPrimary
+            label={t('Restore JSON')}
+            onClick={_toggleImport}
+          />
+          {isLedger() && (
+            <>
+              <Button.Or />
+              <Button
+                icon='question'
+                isPrimary
+                label={t('Query Ledger')}
+                onClick={queryLedger}
+              />
+            </>
+          )}
+        </Button.Group>
+      }
+      isEmpty={emptyScreen}
+      emptyText={t('No account yet?')}
+    >
+      {isCreateOpen && (
+        <CreateModal
+          onClose={_toggleCreate}
+          onStatusChange={onStatusChange}
+        />
+      )}
+      {isImportOpen && (
+        <ImportModal
+          onClose={_toggleImport}
+          onStatusChange={onStatusChange}
+        />
+      )}
+      {accounts && Object.keys(accounts).map((address): React.ReactNode => (
+        <Account
+          address={address}
+          key={address}
+        />
+      ))}
+    </CardGrid>
+  );
+}
+
+export default withMulti(
+  Overview,
+  translate,
+  withObservable(accountObservable.subject, { propName: 'accounts' })
+);

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác