Browse Source

Merge pull request #310 from Joystream/joystream-next

Rome
Mokhtar Naamani 5 years ago
parent
commit
26025983ab
100 changed files with 5287 additions and 3240 deletions
  1. 1 0
      .123trigger
  2. 4 0
      .eslintignore
  3. 16 0
      .eslintrc.js
  4. 71 0
      .github/workflows/pr-any.yml
  5. 32 0
      .github/workflows/push-master.yml
  6. 10 1
      .gitignore
  7. 16 3
      .gitlab-ci.yml
  8. 3 0
      .storybook/addons.ts
  9. 19 0
      .storybook/config.tsx
  10. 4 0
      .storybook/style.css
  11. 81 0
      .storybook/webpack.config.js
  12. 0 1
      .travis.yml
  13. 19 0
      BOUNTIES.md
  14. 67 0
      CHANGELOG.md
  15. 15 6
      README.md
  16. 2 1
      babel.config.js
  17. 89 0
      i18next-scanner.config.js
  18. 1 0
      jest.config.js
  19. 1 1
      lerna.json
  20. 52 23
      package.json
  21. 2 2
      packages/app-123code/README.md
  22. 3 3
      packages/app-123code/package.json
  23. 41 45
      packages/app-123code/src/AccountSelector.tsx
  24. 28 0
      packages/app-123code/src/Summary.tsx
  25. 40 76
      packages/app-123code/src/SummaryBar.tsx
  26. 35 48
      packages/app-123code/src/Transfer.tsx
  27. 0 16
      packages/app-123code/src/index.css
  28. 17 31
      packages/app-123code/src/index.tsx
  29. 1 1
      packages/app-123code/src/translate.ts
  30. 7 5
      packages/app-accounts/package.json
  31. 2 4
      packages/app-accounts/scripts/vanitygen.js
  32. 227 0
      packages/app-accounts/src/Account.tsx
  33. 0 138
      packages/app-accounts/src/Backup.tsx
  34. 105 0
      packages/app-accounts/src/Banner.tsx
  35. 0 166
      packages/app-accounts/src/ChangePass.tsx
  36. 0 436
      packages/app-accounts/src/Creator.tsx
  37. 0 377
      packages/app-accounts/src/Editor.tsx
  38. 0 71
      packages/app-accounts/src/Forgetting.tsx
  39. 13 10
      packages/app-accounts/src/MemoForm.tsx
  40. 104 0
      packages/app-accounts/src/Overview.tsx
  41. 0 161
      packages/app-accounts/src/Restore.tsx
  42. 79 59
      packages/app-accounts/src/Vanity/Match.tsx
  43. 0 57
      packages/app-accounts/src/Vanity/index.css
  44. 139 79
      packages/app-accounts/src/Vanity/index.tsx
  45. 1 1
      packages/app-accounts/src/Vanity/translate.ts
  46. 3 3
      packages/app-accounts/src/bipWorker.ts
  47. 0 14
      packages/app-accounts/src/index.css
  48. 57 122
      packages/app-accounts/src/index.tsx
  49. 44 0
      packages/app-accounts/src/md/basic.md
  50. 144 0
      packages/app-accounts/src/modals/Backup.tsx
  51. 183 0
      packages/app-accounts/src/modals/ChangePass.tsx
  52. 193 0
      packages/app-accounts/src/modals/Create.tsx
  53. 60 0
      packages/app-accounts/src/modals/CreateConfirmation.tsx
  54. 240 0
      packages/app-accounts/src/modals/Derive.tsx
  55. 180 0
      packages/app-accounts/src/modals/Import.tsx
  56. 119 0
      packages/app-accounts/src/modals/Qr.tsx
  57. 178 0
      packages/app-accounts/src/modals/Transfer.tsx
  58. 1 1
      packages/app-accounts/src/translate.ts
  59. 21 5
      packages/app-accounts/src/types.ts
  60. 26 6
      packages/app-accounts/src/vanitygen/calculate.ts
  61. 81 37
      packages/app-accounts/src/vanitygen/cli.ts
  62. 14 6
      packages/app-accounts/src/vanitygen/generate.ts
  63. 4 6
      packages/app-accounts/src/vanitygen/index.ts
  64. 2 2
      packages/app-accounts/src/vanitygen/sort.ts
  65. 24 18
      packages/app-accounts/src/vanitygen/types.d.ts
  66. 0 8
      packages/app-accounts/src/worker-loader.d.ts
  67. 3 3
      packages/app-address-book/package.json
  68. 153 0
      packages/app-address-book/src/Address.tsx
  69. 0 204
      packages/app-address-book/src/Creator.tsx
  70. 0 265
      packages/app-address-book/src/Editor.tsx
  71. 0 90
      packages/app-address-book/src/Forgetting.tsx
  72. 63 0
      packages/app-address-book/src/Overview.tsx
  73. 0 7
      packages/app-address-book/src/index.css
  74. 45 118
      packages/app-address-book/src/index.tsx
  75. 6 0
      packages/app-address-book/src/md/basic.md
  76. 135 0
      packages/app-address-book/src/modals/Create.tsx
  77. 1 1
      packages/app-address-book/src/translate.ts
  78. 10 2
      packages/app-address-book/src/types.ts
  79. 0 0
      packages/app-claims/LICENSE
  80. 1 0
      packages/app-claims/README.md
  81. 17 0
      packages/app-claims/package.json
  82. 107 0
      packages/app-claims/src/Claim.tsx
  83. 259 0
      packages/app-claims/src/index.tsx
  84. 8 0
      packages/app-claims/src/secp256k1.d.ts
  85. 7 0
      packages/app-claims/src/translate.ts
  86. 19 0
      packages/app-claims/src/util.spec.ts
  87. 111 0
      packages/app-claims/src/util.ts
  88. 4 3
      packages/app-contracts/package.json
  89. 163 36
      packages/app-contracts/src/ABI.tsx
  90. 0 183
      packages/app-contracts/src/Call.tsx
  91. 0 278
      packages/app-contracts/src/Code.tsx
  92. 113 0
      packages/app-contracts/src/Codes/Add.tsx
  93. 208 0
      packages/app-contracts/src/Codes/Code.tsx
  94. 130 0
      packages/app-contracts/src/Codes/Upload.tsx
  95. 76 0
      packages/app-contracts/src/Codes/ValidateCode.tsx
  96. 98 0
      packages/app-contracts/src/Codes/index.tsx
  97. 135 0
      packages/app-contracts/src/Contracts/Add.tsx
  98. 273 0
      packages/app-contracts/src/Contracts/Call.tsx
  99. 117 0
      packages/app-contracts/src/Contracts/Contract.tsx
  100. 104 0
      packages/app-contracts/src/Contracts/Outcome.tsx

+ 1 - 0
.123trigger

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

+ 4 - 0
.eslintignore

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

+ 16 - 0
.eslintrc.js

@@ -0,0 +1,16 @@
+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'
+  }
+};

+ 71 - 0
.github/workflows/pr-any.yml

@@ -0,0 +1,71 @@
+name: PR
+on: [pull_request]
+
+jobs:
+  lint:
+    name: 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 lint
+
+  test:
+    name: Testing
+    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: test
+      run: |
+        yarn install --frozen-lockfile
+        yarn test
+
+  build_code:
+    name: Build Code
+    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 build:code
+
+  build_i18n:
+    name: Build i18n
+    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 build:i18n

+ 32 - 0
.github/workflows/push-master.yml

@@ -0,0 +1,32 @@
+name: Master
+on:
+  push:
+    branches:
+      - master
+
+jobs:
+  build_code:
+    name: Build Code
+    if: "! contains(github.event.head_commit.message, '[CI Skip]')"
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        node-version: [12.x]
+    steps:
+    - uses: actions/checkout@v1
+      with:
+        token: ${{ secrets.GH_PAT }}
+    - name: Use Node.js ${{ matrix.node-version }}
+      uses: actions/setup-node@v1
+      with:
+        node-version: ${{ matrix.node-version }}
+    - name: build
+      env:
+        CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
+        GH_PAGES_SRC: packages/apps/build
+        GH_PAT: ${{ secrets.GH_PAT }}
+        NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+      run: |
+        yarn install --frozen-lockfile
+        yarn polkadot-dev-ghact-build
+        yarn polkadot-dev-ghact-docs

+ 10 - 1
.gitignore

@@ -2,15 +2,24 @@ build/
 coverage/
 node_modules/
 tmp/
-.idea
+.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/

+ 16 - 3
.gitlab-ci.yml

@@ -16,9 +16,17 @@ stages:
   - 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
-  <<: *kubernetes
+  environment:
+    name: infrastructure_build
+  tags:
+    - kubernetes-parity-build
   image: docker:git
   services:
     - docker:dind
@@ -26,9 +34,14 @@ dockerize:
     DOCKER_DRIVER: overlay2
     DOCKER_HOST: tcp://localhost:2375
   script:
-    - build
+    - 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:
-    - branches
+    - master
 
 review:
   stage: review

+ 3 - 0
.storybook/addons.ts

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

+ 19 - 0
.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
.storybook/style.css

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

+ 81 - 0
.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: /\.(ts|tsx)$/,
+  exclude: /(node_modules)/,
+  use: [
+    {
+      loader: require.resolve('babel-loader'),
+      options: require('@polkadot/dev-react/config/babel')
+    },
+  ],
+});
+config.resolve.extensions.push('.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;
+};

+ 0 - 1
.travis.yml

@@ -1,6 +1,5 @@
 language: node_js
 node_js:
-  - "10"
   - "12"
 cache:
   yarn: true

+ 19 - 0
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.

+ 67 - 0
CHANGELOG.md

@@ -1,3 +1,70 @@
+# 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

+ 15 - 6
README.md

@@ -18,16 +18,15 @@ The repo is split into a number of packages, each representing an application. T
 - [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-nodeinfo](packages/app-nodeinfo/) Node information and status
 - [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/) Sumission of raw data to RPC endpoints and utility hashing functions.
-- [app-transfer](packages/app-transfer/) A basic account management app, allowing transfer of DOTs between accounts.
+- [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)
 
-- [ui-app](packages/ui-app/) A reactive (using RxJS) application framework with a number of useful shared components.
-- [ui-signer](packages/ui-signer/) Signer implementation for apps.
-- [ui-react-rx](packages/ui-react-rx) Base components that use the RxJS Observable APIs
+- [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
 
@@ -43,3 +42,13 @@ To get started -
 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.

+ 2 - 1
babel.config.js

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

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

+ 1 - 0
jest.config.js

@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-var-requires */
 const config = require('@polkadot/dev-react/config/jest');
 const findPackages = require('./scripts/findPackages');
 

+ 1 - 1
lerna.json

@@ -10,5 +10,5 @@
   "packages": [
     "packages/*"
   ],
-  "version": "0.32.0-beta.6"
+  "version": "0.37.0-beta.63"
 }

+ 52 - 23
package.json

@@ -1,5 +1,5 @@
 {
-  "version": "0.32.0-beta.6",
+  "version": "0.37.0-beta.63",
   "private": true,
   "engines": {
     "node": ">=10.13.0",
@@ -10,45 +10,74 @@
     "packages/*"
   ],
   "resolutions": {
-    "@polkadot/api": "^0.77.0-beta.4",
-    "@polkadot/keyring": "^0.76.1",
-    "@polkadot/types": "^0.77.0-beta.4",
-    "@polkadot/util": "^0.76.1",
-    "@polkadot/util-crypto": "^0.76.1",
+    "@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",
-    "rxjs": "^6.4.0",
-    "typescript": "^3.4.5"
+    "typescript": "^3.7.2"
   },
   "scripts": {
     "analyze": "yarn run build && cd packages/apps && yarn run source-map-explorer build/main.*.js",
-    "build": "NODE_ENV=production polkadot-dev-build-ts",
-    "check": "yarn lint",
-    "lint": "tslint --project . && tsc --noEmit --pretty",
+    "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",
     "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"
+    "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.4.3",
-    "@babel/runtime": "^7.4.3",
-    "@polkadot/dev-react": "^0.30.0-beta.1",
-    "@polkadot/ts": "^0.1.56",
-    "autoprefixer": "^9.4.9",
+    "@babel/core": "^7.7.0",
+    "@babel/runtime": "^7.7.1",
+    "@polkadot/dev-react": "^0.32.0-beta.13",
+    "@polkadot/ts": "^0.1.84",
+    "@storybook/addon-knobs": "^5.2.5",
+    "@storybook/addon-storysource": "^5.2.5",
+    "@types/jest": "^24.0.22",
+    "autoprefixer": "^9.7.1",
     "empty": "^0.10.1",
-    "gh-pages": "^2.0.1",
     "html-loader": "^0.5.5",
-    "markdown-loader": "^5.0.0",
-    "postcss": "^7.0.13",
+    "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.1.2",
-    "postcss-sass": "^0.3.5",
+    "postcss-nested": "^4.2.1",
+    "postcss-sass": "^0.4.1",
     "postcss-simple-vars": "^5.0.0",
     "precss": "^4.0.0",
-    "source-map-explorer": "^1.7.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"
+  },
+  "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"
   }
 }

+ 2 - 2
packages/app-123code/README.md

@@ -15,8 +15,8 @@ And we have the basic app source setup, time to get the tooling correct.
 
 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/src/routing/` 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).
+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/src/routing/index.ts` file at the appropriate place for both full and light mode (either optional)
+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.

+ 3 - 3
packages/app-123code/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@polkadot/app-123code",
-  "version": "0.32.0-beta.6",
+  "version": "0.37.0-beta.63",
   "description": "A basic app that shows the ropes on customisation",
   "main": "index.js",
   "scripts": {},
@@ -10,7 +10,7 @@
   ],
   "license": "Apache-2.0",
   "dependencies": {
-    "@babel/runtime": "^7.4.3",
-    "@polkadot/ui-app": "^0.32.0-beta.6"
+    "@babel/runtime": "^7.7.1",
+    "@polkadot/react-components": "^0.37.0-beta.63"
   }
 }

+ 41 - 45
packages/app-123code/src/AccountSelector.tsx

@@ -2,52 +2,48 @@
 // This software may be modified and distributed under the terms
 // of the Apache-2.0 license. See the LICENSE file for details.
 
-import React from 'react';
-import { Bubble, InputAddress } from '@polkadot/ui-app';
-import { AccountIndex, Balance, Nonce } from '@polkadot/ui-reactive';
-
-type Props = {
-  onChange: (accountId?: string) => void
-};
-
-type State = {
-  accountId?: string
-};
-
-export default class AccountSelector extends React.PureComponent<Props, State> {
-  state: State = {};
-
-  render () {
-    const { accountId } = this.state;
+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;
+}
 
-    return (
-      <section className='template--AccountSelector ui--row'>
-        <InputAddress
-          className='medium'
-          label='my default account'
-          onChange={this.onChange}
-          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>
-    );
-  }
+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>
+  );
+}
 
-  private onChange = (accountId?: string): void => {
-    const { onChange } = this.props;
+export default styled(AccountSelector)`
+  align-items: flex-end;
 
-    this.setState({ accountId }, () =>
-      onChange(accountId)
-    );
+  .summary {
+    text-align: center;
   }
-}
+`;

+ 28 - 0
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;
+`;

+ 40 - 76
packages/app-123code/src/SummaryBar.tsx

@@ -1,89 +1,57 @@
+/* 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 { ApiProps } from '@polkadot/ui-api/types';
-import { BareProps, I18nProps } from '@polkadot/ui-app/types';
+import { AccountId } from '@polkadot/types/interfaces';
+import { BareProps, I18nProps } from '@polkadot/react-components/types';
 
 import BN from 'bn.js';
-import React from 'react';
-import { AccountId, RuntimeVersion } from '@polkadot/types';
-import { withCalls } from '@polkadot/ui-api/with';
-import { Bubble, IdentityIcon } from '@polkadot/ui-app';
+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';
 
-type Props = ApiProps & BareProps & I18nProps & {
-  balances_totalIssuance?: BN,
-  chain_bestNumber?: BN,
-  chain_bestNumberLag?: BN,
-  chain_getRuntimeVersion?: RuntimeVersion,
-  session_validators?: Array<AccountId>,
-  staking_intentions?: Array<AccountId>,
-  system_chain?: string,
-  system_name?: string,
-  system_version?: string
-};
-type State = {
-  nextUp: Array<AccountId>
-};
-
-class SummaryBar extends React.PureComponent<Props, State> {
-  state: State = {
-    nextUp: []
-  };
-
-  static getDerivedStateFromProps ({ staking_intentions, session_validators }: Props): State | null {
-    if (!staking_intentions || !session_validators) {
-      return null;
-    }
-
-    return {
-      nextUp: staking_intentions.filter((accountId) =>
-        !session_validators.find((validatorId) => validatorId.eq(accountId))
-      )
-    };
-  }
+interface Props extends BareProps, I18nProps {
+  balances_totalIssuance?: BN;
+  chain_bestNumber?: BN;
+  chain_bestNumberLag?: BN;
+  staking_validators?: AccountId[];
+}
 
-  render () {
-    const { balances_totalIssuance, chain_bestNumber, chain_bestNumberLag, chain_getRuntimeVersion, session_validators = [], system_chain, system_name, system_version } = this.props;
-    const { nextUp } = this.state;
+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'>
-            {system_name} v{system_version}
-          </Bubble>
-          <Bubble icon='chain' label='chain'>
-            {system_chain}
-          </Bubble>
-          <Bubble icon='code' label='runtime'>{
-            chain_getRuntimeVersion
-              ? `${chain_getRuntimeVersion.implName} v${chain_getRuntimeVersion.implVersion}`
-              : undefined
-          }</Bubble>
-          <Bubble icon='bullseye' label='best #'>
-            {formatNumber(chain_bestNumber)} ({formatNumber(chain_bestNumberLag)} lag)
-          </Bubble>
+  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'>{
-            session_validators.map((accountId, index) => (
-              <IdentityIcon key={index} value={accountId} size={20} />
-            ))
-          }</Bubble>
-          <Bubble icon='chess bishop' label='next up'>{
-            nextUp.map((accountId, index) => (
+            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>
-    );
-  }
+        )}
+        <Bubble icon='circle' label='total tokens'>
+          {formatBalance(balances_totalIssuance)}
+        </Bubble>
+      </div>
+    </summary>
+  );
 }
 
 // inject the actual API calls automatically into props
@@ -91,11 +59,7 @@ export default translate(
   withCalls<Props>(
     'derive.chain.bestNumber',
     'derive.chain.bestNumberLag',
-    'query.balances.totalIssuance',
-    'query.session.validators',
-    'rpc.chain.getRuntimeVersion',
-    'rpc.system.chain',
-    'rpc.system.name',
-    'rpc.system.version'
+    'derive.staking.validators',
+    'query.balances.totalIssuance'
   )(SummaryBar)
 );

+ 35 - 48
packages/app-123code/src/Transfer.tsx

@@ -3,58 +3,45 @@
 // of the Apache-2.0 license. See the LICENSE file for details.
 
 import BN from 'bn.js';
-import React from 'react';
-import { Button, InputAddress, InputBalance, TxButton } from '@polkadot/ui-app';
+import React, { useState } from 'react';
+import { Button, InputAddress, InputBalance, TxButton } from '@polkadot/react-components';
 
-type Props = {
-  accountId?: string
-};
-type State = {
-  amount?: BN;
-  recipientId?: string;
-};
+import Summary from './Summary';
 
-export default class Transfer extends React.PureComponent<Props> {
-  state: State = {};
+interface Props {
+  accountId?: string | null;
+}
 
-  render () {
-    const { accountId } = this.props;
-    const { amount, recipientId } = this.state;
+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={this.onChangeRecipient}
-              type='all'
-            />
-            <InputBalance
-              label='amount to transfer'
-              onChange={this.onChangeAmount}
+  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>
-              <TxButton
-                accountId={accountId}
-                label='make transfer'
-                params={[recipientId, amount]}
-                tx='balances.transfer'
-              />
-            </Button.Group>
-          </div>
-          <div className='template--summary 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.</div>
+          </Button.Group>
         </div>
-      </section>
-    );
-  }
-
-  private onChangeAmount = (amount?: BN): void => {
-    this.setState({ amount });
-  }
-
-  private onChangeRecipient = (recipientId?: string): void => {
-    this.setState({ recipientId });
-  }
+        <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>
+  );
 }

+ 0 - 16
packages/app-123code/src/index.css

@@ -1,16 +0,0 @@
-/* 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. */
-
-.template--summary {
-  opacity: 0.5;
-  padding: 1rem 1.5rem;
-}
-
-.template--AccountSelector {
-  align-items: flex-end;
-
-  .summary {
-    text-align: center;
-  }
-}

+ 17 - 31
packages/app-123code/src/index.tsx

@@ -6,14 +6,11 @@
 // 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/ui-app/types';
+import { AppProps, I18nProps } from '@polkadot/react-components/types';
 
 // external imports (including those found in the packages/*
 // of this repo)
-import React from 'react';
-
-// our app-specific styles
-import './index.css';
+import React, { useState } from 'react';
 
 // local imports and components
 import AccountSelector from './AccountSelector';
@@ -21,32 +18,21 @@ import SummaryBar from './SummaryBar';
 import Transfer from './Transfer';
 import translate from './translate';
 
-// define out internal types
-type Props = AppProps & I18nProps;
-type State = {
-  accountId?: string
-};
-
-class App extends React.PureComponent<Props, State> {
-  state: State = {};
-
-  render () {
-    const { accountId } = this.state;
-
-    return (
-      // in all apps, the main wrapper is setup to allow the padding
-      // and margins inside the application. (Just from a consistent pov)
-      <main>
-        <SummaryBar />
-        <AccountSelector onChange={this.onAccountChange} />
-        <Transfer accountId={accountId} />
-      </main>
-    );
-  }
-
-  private onAccountChange = (accountId?: string): void => {
-    this.setState({ accountId });
-  }
+// 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);

+ 1 - 1
packages/app-123code/src/translate.ts

@@ -4,4 +4,4 @@
 
 import { withTranslation } from 'react-i18next';
 
-export default withTranslation(['123code', 'ui']);
+export default withTranslation(['app-123code']);

+ 7 - 5
packages/app-accounts/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@polkadot/app-accounts",
-  "version": "0.32.0-beta.6",
+  "version": "0.37.0-beta.63",
   "main": "index.js",
   "repository": "https://github.com/polkadot-js/apps.git",
   "author": "Jaco Greeff <jacogr@gmail.com>",
@@ -10,12 +10,14 @@
   "contributors": [],
   "license": "Apache-2.0",
   "dependencies": {
-    "@babel/runtime": "^7.4.3",
-    "@polkadot/ui-app": "^0.32.0-beta.6",
+    "@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": "^12.0.11",
+    "@types/yargs": "^13.0.2",
     "babel-plugin-module-resolver": "^3.1.1",
+    "detect-browser": "^4.8.0",
     "file-saver": "^2.0.0",
-    "yargs": "^13.2.0"
+    "yargs": "^14.2.0"
   }
 }

+ 2 - 4
packages/app-accounts/scripts/vanitygen.js

@@ -1,4 +1,5 @@
 #!/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.
@@ -17,10 +18,7 @@ if (compiled) {
     extensions: ['.js', '.ts'],
     plugins: [
       ['module-resolver', {
-        alias: {
-          '^@polkadot/client-(chains|db-chain|db|p2p-messages|p2p|rpc-handlers|rpc|runtime|telemetry|wasm)(.*)': './packages/client-\\1/src\\2',
-          '^@polkadot/client(.*)': './packages/client/src\\1'
-        }
+        alias: {}
       }]
     ]
   });

+ 227 - 0
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;
+      }
+    }
+  `
+);

+ 0 - 138
packages/app-accounts/src/Backup.tsx

@@ -1,138 +0,0 @@
-// 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 { KeyringPair } from '@polkadot/keyring/types';
-import { I18nProps } from '@polkadot/ui-app/types';
-
-import FileSaver from 'file-saver';
-import React from 'react';
-import { AddressSummary, Button, Modal, Password } from '@polkadot/ui-app';
-import { ActionStatus } from '@polkadot/ui-app/Status/types';
-import keyring from '@polkadot/ui-keyring';
-
-import translate from './translate';
-import { isEmptyStr } from '@polkadot/joy-utils/';
-
-type Props = I18nProps & {
-  onStatusChange: (status: ActionStatus) => void,
-  onClose: () => void,
-  pair: KeyringPair
-};
-
-type State = {
-  isPassValid: boolean,
-  password: string
-};
-
-class Backup extends React.PureComponent<Props, State> {
-  state: State = {
-    isPassValid: true,
-    password: ''
-  };
-
-  render () {
-    return (
-      <Modal
-        className='app--accounts-Modal'
-        dimmer='inverted'
-        open
-        size='tiny'
-      >
-        {this.renderContent()}
-        {this.renderButtons()}
-      </Modal>
-    );
-  }
-
-  renderButtons () {
-    const { onClose, t } = this.props;
-    const { isPassValid } = this.state;
-
-    return (
-      <Modal.Actions>
-        <Button.Group>
-          <Button
-            isNegative
-            label={t('Cancel')}
-            onClick={onClose}
-          />
-          <Button.Or />
-          <Button
-            isDisabled={!isPassValid}
-            isPrimary
-            label={t('Download')}
-            onClick={this.doBackup}
-          />
-        </Button.Group>
-      </Modal.Actions>
-    );
-  }
-
-  renderContent () {
-    const { pair, t } = this.props;
-    const { isPassValid, password } = this.state;
-
-    return (
-      <>
-        <Modal.Header>
-          {t('Backup this key')}
-        </Modal.Header>
-        <Modal.Content className='app--account-Backup-content'>
-          <AddressSummary value={pair.address()} />
-          <Password
-            isError={!isPassValid}
-            label={t('unlock key using the password')}
-            onChange={this.onChangePass}
-            tabIndex={0}
-            value={password}
-          />
-        </Modal.Content>
-      </>
-    );
-  }
-
-  private doBackup = (): void => {
-    const { onClose, onStatusChange, pair, t } = this.props;
-    const { password } = this.state;
-
-    if (!pair) {
-      return;
-    }
-
-    const status = {
-      action: 'backup'
-    } as ActionStatus;
-
-    try {
-      const json = keyring.backupAccount(pair, password);
-      const blob = new Blob([JSON.stringify(json)], { type: 'application/json; charset=utf-8' });
-
-      status.account = pair.address();
-      status.status = blob ? 'success' : 'error';
-      status.message = t('key backed up');
-
-      FileSaver.saveAs(blob, `${pair.address()}.json`);
-    } catch (error) {
-      this.setState({ isPassValid: false });
-      console.error(error);
-
-      status.status = 'error';
-      status.message = error.message;
-      return;
-    }
-
-    onStatusChange(status);
-
-    onClose();
-  }
-
-  private onChangePass = (password: string) => {
-    this.setState({
-      isPassValid: isEmptyStr(password) || keyring.isPassValid(password),
-      password
-    });
-  }
-}
-
-export default translate(Backup);

+ 105 - 0
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;
+      }
+    }
+  `
+);

+ 0 - 166
packages/app-accounts/src/ChangePass.tsx

@@ -1,166 +0,0 @@
-// 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 { KeyringPair } from '@polkadot/keyring/types';
-import { I18nProps } from '@polkadot/ui-app/types';
-
-import React from 'react';
-import { AddressSummary, Button, Modal, Password } from '@polkadot/ui-app';
-import { ActionStatus } from '@polkadot/ui-app/Status/types';
-import keyring from '@polkadot/ui-keyring';
-
-import translate from './translate';
-
-type Props = I18nProps & {
-  account: KeyringPair,
-  onClose: () => void,
-  onStatusChange: (status: ActionStatus) => void
-};
-
-type State = {
-  isNewValid: boolean,
-  isOldValid: boolean,
-  newPass: string,
-  oldPass: string
-};
-
-class ChangePass extends React.PureComponent<Props, State> {
-  state: State = {
-    isNewValid: false,
-    isOldValid: false,
-    newPass: '',
-    oldPass: ''
-  };
-
-  render () {
-    return (
-      <Modal
-        className='app--accounts-Modal'
-        dimmer='inverted'
-        open
-        size='tiny'
-      >
-        {this.renderContent()}
-        {this.renderButtons()}
-      </Modal>
-    );
-  }
-
-  private renderButtons () {
-    const { onClose, t } = this.props;
-    const { isNewValid, isOldValid } = this.state;
-
-    return (
-      <Modal.Actions>
-        <Button.Group>
-          <Button
-            isNegative
-            label={t('Cancel')}
-            onClick={onClose}
-          />
-          <Button.Or />
-          <Button
-            isDisabled={!isNewValid || !isOldValid}
-            isPrimary
-            label={t('Change')}
-            onClick={this.doChange}
-          />
-        </Button.Group>
-      </Modal.Actions>
-    );
-  }
-
-  private renderContent () {
-    const { account, t } = this.props;
-    const { isNewValid, isOldValid, newPass, oldPass } = this.state;
-
-    return (
-      <>
-        <Modal.Header>
-          {t('Change password for this key')}
-        </Modal.Header>
-        <Modal.Content>
-          <AddressSummary value={account.address()} />
-          <Password
-            autoFocus
-            isError={!isOldValid}
-            label={t('your current password')}
-            onChange={this.onChangeOld}
-            tabIndex={1}
-            value={oldPass}
-          />
-          <Password
-            isError={!isNewValid}
-            label={t('your new password')}
-            onChange={this.onChangeNew}
-            tabIndex={2}
-            value={newPass}
-          />
-        </Modal.Content>
-      </>
-    );
-  }
-
-  private doChange = (): void => {
-    const { account, onClose, onStatusChange, t } = this.props;
-    const { newPass, oldPass } = this.state;
-
-    const status = {
-      action: 'changePassword'
-    } as ActionStatus;
-
-    try {
-      if (!account.isLocked()) {
-        account.lock();
-      }
-
-      account.decodePkcs8(oldPass);
-    } catch (error) {
-      this.setState({ isOldValid: false });
-
-      status.message = error.message;
-
-      return;
-    }
-
-    try {
-      keyring.encryptAccount(account, newPass);
-
-      status.account = account.address();
-      status.status = 'success';
-      status.message = t('password changed');
-    } catch (error) {
-      this.setState({ isNewValid: false });
-
-      status.status = 'error';
-      status.message = error.message;
-
-      return;
-    }
-
-    onStatusChange(status);
-
-    onClose();
-  }
-
-  private onChangeNew = (newPass: string) => {
-    this.setState({
-      isNewValid: this.validatePass(newPass),
-      newPass
-    });
-  }
-
-  private onChangeOld = (oldPass: string) => {
-    this.setState({
-      isOldValid: this.validatePass(oldPass),
-      oldPass
-    });
-  }
-
-  private validatePass (password: string): boolean {
-    return keyring.isPassValid(password);
-  }
-}
-
-export default translate(ChangePass);

+ 0 - 436
packages/app-accounts/src/Creator.tsx

@@ -1,436 +0,0 @@
-// 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/ui-app/types';
-import { ActionStatus } from '@polkadot/ui-app/Status/types';
-import { KeypairType } from '@polkadot/util-crypto/types';
-import { ComponentProps } from './types';
-
-import FileSaver from 'file-saver';
-import React from 'react';
-import { AddressSummary, Button, Dropdown, Input, Labelled, Modal, Password } from '@polkadot/ui-app';
-import { InputAddress } from '@polkadot/ui-app/InputAddress';
-import keyring from '@polkadot/ui-keyring';
-import uiSettings from '@polkadot/joy-settings/';
-import { isHex, u8aToHex } from '@polkadot/util';
-import { keyExtractPath, mnemonicGenerate, mnemonicValidate, randomAsU8a } from '@polkadot/util-crypto';
-
-import translate from './translate';
-import { isEmptyStr } from '@polkadot/joy-utils/';
-
-type Props = ComponentProps & I18nProps & {
-  match: {
-    params: {
-      seed?: string
-    }
-  }
-};
-
-type SeedType = 'bip' | 'raw';
-
-type State = {
-  address: string,
-  deriveError: string | null,
-  derivePath: string,
-  isNameValid: boolean,
-  isSeedValid: boolean,
-  isPassValid: boolean,
-  isValid: boolean,
-  name: string,
-  pairType: KeypairType,
-  password: string,
-  seed: string,
-  seedOptions: Array<{ value: SeedType, text: string }>,
-  seedType: SeedType,
-  showWarning: boolean
-};
-
-const DEFAULT_TYPE: KeypairType = 'ed25519';
-
-function deriveValidate (derivePath: string, pairType: KeypairType): string | null {
-  try {
-    const { path } = keyExtractPath(derivePath);
-
-    // we don't allow soft for ed25519
-    if (pairType === 'ed25519') {
-      const firstSoft = path.find(({ isSoft }) => isSoft);
-
-      if (firstSoft) {
-        return 'Soft derivation paths are not allowed on ed25519';
-      }
-    }
-  } catch (error) {
-    return error.message;
-  }
-
-  return null;
-}
-
-function isHexSeed (seed: string): boolean {
-  return isHex(seed) && seed.length === 66;
-}
-
-function rawValidate (seed: string): boolean {
-  return seed.length <= 32 || isHexSeed(seed);
-}
-
-function addressFromSeed (phrase: string, derivePath: string, pairType: KeypairType): string {
-  return keyring
-    .createFromUri(`${phrase}${derivePath}`, {}, pairType)
-    .address();
-}
-
-class Creator extends React.PureComponent<Props, State> {
-  state: State = { seedType: 'bip' } as State;
-
-  constructor (props: Props) {
-    super(props);
-
-    const { match: { params: { seed } }, t } = this.props;
-
-    this.state = {
-      ...this.emptyState(seed || null, '', DEFAULT_TYPE),
-      seedOptions: [
-        { value: 'bip', text: t('Mnemonic') },
-        { value: 'raw', text: t('Raw seed') }
-      ]
-    };
-  }
-
-  render () {
-    const { address, isSeedValid } = this.state;
-
-    return (
-      <div className='accounts--Creator'>
-        <div className='ui--grid'>
-          <AddressSummary
-            className='shrink'
-            value={
-              isSeedValid
-                ? address
-                : ''
-            }
-            withBonded
-          />
-          {this.renderInput()}
-        </div>
-        {this.renderButtons()}
-      </div>
-    );
-  }
-
-  private renderButtons () {
-    const { t } = this.props;
-    const { isValid } = this.state;
-
-    return (
-      <Button.Group>
-        <Button
-          label={t('Reset')}
-          onClick={this.onDiscard}
-        />
-        <Button.Or />
-        <Button
-          isDisabled={!isValid}
-          isPrimary
-          label={t('Save')}
-          onClick={this.onShowWarning}
-        />
-      </Button.Group>
-    );
-  }
-
-  private renderInput () {
-    const { t } = this.props;
-    const { deriveError, derivePath, isNameValid, isPassValid, isSeedValid, name, pairType, password, seed, seedOptions, seedType, showWarning } = this.state;
-
-    return (
-      <div className='grow'>
-        <div className='ui--row'>
-          <Input
-            autoFocus
-            className='full'
-            isError={!isNameValid}
-            label={t('name your key')}
-            onChange={this.onChangeName}
-            value={name}
-          />
-        </div>
-        <div className='ui--row'>
-          <Input
-            className='full'
-            isAction
-            isError={!isSeedValid}
-            label={
-              seedType === 'bip'
-                ? t('create from the following mnemonic seed')
-                : t('create from the following seed (hex or string)')
-            }
-            onChange={this.onChangeSeed}
-            value={seed}
-          >
-            <Dropdown
-              isButton
-              defaultValue={seedType}
-              onChange={this.selectSeedType}
-              options={seedOptions}
-            />
-          </Input>
-        </div>
-        <div className='ui--row'>
-          <Password
-            className='full'
-            isError={!isPassValid}
-            label={t('your password for this key')}
-            onChange={this.onChangePass}
-            value={password}
-          />
-        </div>
-        {
-          isEmptyStr(password) &&
-            <Labelled label=''><article className='warning'>
-              Although it is recommended to use a password to protect your key, you can still leave it empty.
-            </article></Labelled>
-        }
-        <details
-          className='accounts--Creator-advanced'
-          open={uiSettings.isFullMode}
-        >
-          <summary>{t('Advanced creation options')}</summary>
-          <div className='ui--Params'>
-            <div className='ui--row'>
-              <Dropdown
-                defaultValue={pairType}
-                label={t('keypair crypto type')}
-                onChange={this.onChangePairType}
-                options={uiSettings.availableCryptos}
-              />
-            </div>
-            <div className='ui--row'>
-              <Input
-                className='full'
-                isError={!!deriveError}
-                label={t('secret derivation path')}
-                onChange={this.onChangeDerive}
-                value={derivePath}
-              />
-            </div>
-            {
-              deriveError
-                ? <Labelled label=''><article className='error'>{deriveError}</article></Labelled>
-                : null
-            }
-            {
-              pairType === 'sr25519' &&
-                <Labelled label=''><article className='warning'>
-                  Choosing Schnorrkel (sr25519) will restrict your key from certain uses
-                </article></Labelled>
-            }
-          </div>
-        </details>
-        <Modal
-          className='app--accounts-Modal'
-          dimmer='inverted'
-          open={showWarning}
-          size='small'
-        >
-          {this.renderModalContent()}
-          {this.renderModalButtons()}
-        </Modal>
-      </div>
-    );
-  }
-
-  private renderModalButtons () {
-    const { t } = this.props;
-
-    return (
-      <Modal.Actions>
-        <Button.Group>
-          <Button
-            isNegative
-            label={t('Cancel')}
-            onClick={this.onHideWarning}
-          />
-          <Button.Or />
-          <Button
-            isPrimary
-            label={t('Create and backup key')}
-            onClick={this.onCommit}
-          />
-        </Button.Group>
-      </Modal.Actions>
-    );
-  }
-
-  private renderModalContent () {
-    const { t } = this.props;
-    const { address } = this.state;
-
-    return (
-      <>
-        <Modal.Header>
-          {t('Important notice!')}
-        </Modal.Header>
-        <Modal.Content>
-          {t('We will provide you with a generated backup file after your key is created. As long as you have access to your key you can always redownload this file later.')}
-          <Modal.Description>
-            {t('Please make sure to save this file in a secure location as it is the only way to restore your key.')}
-          </Modal.Description>
-          <AddressSummary
-            className='accounts--Modal-Address'
-            value={address}
-          />
-        </Modal.Content>
-      </>
-    );
-  }
-
-  private generateSeed (_seed: string | null, derivePath: string, seedType: SeedType, pairType: KeypairType): State {
-    const seed = seedType === 'bip'
-      ? mnemonicGenerate()
-      : _seed || u8aToHex(randomAsU8a());
-    const address = addressFromSeed(seed, derivePath, pairType);
-
-    return {
-      address,
-      deriveError: null,
-      derivePath,
-      seed
-    } as State;
-  }
-
-  private emptyState (seed: string | null, derivePath: string, pairType: KeypairType): State {
-    const seedType = seed
-      ? 'raw'
-      : this.state.seedType;
-
-    return {
-      ...this.generateSeed(seed, derivePath, seedType, pairType),
-      isNameValid: true,
-      isPassValid: true,
-      isSeedValid: true,
-      isValid: false,
-      name: 'new keypair',
-      password: '',
-      pairType,
-      seedType,
-      showWarning: false
-    };
-  }
-
-  private nextState (newState: State): void {
-    this.setState(
-      (prevState: State, props: Props): State => {
-        const { derivePath = prevState.derivePath, name = prevState.name, pairType = prevState.pairType, password = prevState.password, seed = prevState.seed, seedOptions = prevState.seedOptions, seedType = prevState.seedType, showWarning = prevState.showWarning } = newState;
-        let address = prevState.address;
-        const deriveError = deriveValidate(derivePath, pairType);
-        const isNameValid = !!name;
-        const isSeedValid = seedType === 'bip'
-          ? mnemonicValidate(seed)
-          : rawValidate(seed);
-        const isPassValid = isEmptyStr(password) || keyring.isPassValid(password);
-
-        if (!deriveError && isSeedValid && (seed !== prevState.seed || derivePath !== prevState.derivePath || pairType !== prevState.pairType)) {
-          address = addressFromSeed(seed, derivePath, pairType);
-        }
-
-        return {
-          address,
-          deriveError,
-          derivePath,
-          isNameValid,
-          isPassValid,
-          isSeedValid,
-          isValid: isNameValid && isPassValid && isSeedValid,
-          name,
-          pairType,
-          password,
-          seed,
-          seedOptions,
-          seedType,
-          showWarning
-        };
-      }
-    );
-  }
-
-  private onChangeDerive = (derivePath: string): void => {
-    this.nextState({ derivePath } as State);
-  }
-
-  private onChangeName = (name: string): void => {
-    this.nextState({ name } as State);
-  }
-
-  private onChangePass = (password: string): void => {
-    this.nextState({ password } as State);
-  }
-
-  private onChangeSeed = (seed: string): void => {
-    this.nextState({ seed } as State);
-  }
-
-  private onChangePairType = (pairType: KeypairType): void => {
-    this.nextState({ pairType } as State);
-  }
-
-  private onShowWarning = (): void => {
-    this.nextState({ showWarning: true } as State);
-  }
-
-  private onHideWarning = (): void => {
-    this.nextState({ showWarning: false } as State);
-  }
-
-  private onCommit = (): void => {
-    const { basePath, onStatusChange, t } = this.props;
-    const { derivePath, name, pairType, password, seed } = this.state;
-
-    const status = {
-      action: 'create'
-    } as ActionStatus;
-
-    try {
-      const { json, pair } = keyring.addUri(`${seed}${derivePath}`, password, { name }, pairType);
-      const blob = new Blob([JSON.stringify(json)], { type: 'application/json; charset=utf-8' });
-
-      FileSaver.saveAs(blob, `${pair.address()}.json`);
-
-      status.account = pair.address();
-      status.status = pair ? 'success' : 'error';
-      status.message = t('created key');
-
-      InputAddress.setLastValue('account', pair.address());
-    } catch (error) {
-      status.status = 'error';
-      status.message = error.message;
-    }
-
-    this.onHideWarning();
-
-    onStatusChange(status);
-
-    window.location.hash = basePath;
-  }
-
-  private onDiscard = (): void => {
-    this.setState(({ pairType }) =>
-      this.emptyState(null, '', pairType)
-    );
-  }
-
-  private selectSeedType = (seedType: SeedType): void => {
-    if (seedType === this.state.seedType) {
-      return;
-    }
-
-    this.setState(({ derivePath, pairType }: State) => ({
-      ...this.generateSeed(null, derivePath, seedType, pairType),
-      seedType
-    }));
-  }
-}
-
-export default translate(Creator);

+ 0 - 377
packages/app-accounts/src/Editor.tsx

@@ -1,377 +0,0 @@
-// 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 { KeyringPair } from '@polkadot/keyring/types';
-import { I18nProps } from '@polkadot/ui-app/types';
-import { ActionStatus } from '@polkadot/ui-app/Status/types';
-import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
-import { ComponentProps } from './types';
-
-import React from 'react';
-import { AddressSummary, Button, Input, InputAddress, Labelled, Dropdown } from '@polkadot/ui-app';
-import keyring from '@polkadot/ui-keyring';
-import uiSettings from '@polkadot/joy-settings/';
-
-import Backup from './Backup';
-import ChangePass from './ChangePass';
-import Forgetting from './Forgetting';
-import translate from './translate';
-import MemoView from '@polkadot/joy-utils/memo/MemoView';
-import { MyAccountContext, MyAccountContextProps } from '@polkadot/joy-utils/MyAccountContext';
-
-type Props = ComponentProps & I18nProps & {
-  allAccounts?: SubjectInfo
-};
-
-type State = {
-  current: KeyringPair | null,
-  editedName: string,
-  isBackupOpen: boolean,
-  isEdited: boolean,
-  isForgetOpen: boolean,
-  isPasswordOpen: boolean
-};
-
-class Editor extends React.PureComponent<Props, State> {
-
-  static contextType = MyAccountContext;
-
-  state: State;
-
-  constructor (props: Props) {
-    super(props);
-
-    this.state = this.createState(null);
-  }
-
-  render () {
-    return (
-      <div className='accounts--Editor'>
-        {this.renderModals()}
-        {this.renderData()}
-        {this.renderButtons()}
-      </div>
-    );
-  }
-
-  renderButtons () {
-    const { t } = this.props;
-    const { current, isEdited } = this.state;
-
-    if (!current) {
-      return null;
-    }
-
-    return (
-      <Button.Group>
-        <Button
-          isNegative
-          onClick={this.toggleForget}
-          label={t('Forget')}
-        />
-        <Button.Group.Divider />
-        <Button
-          isDisabled={isEdited}
-          onClick={this.toggleBackup}
-          label={t('Backup')}
-        />
-        <Button.Or />
-        <Button
-          isDisabled={isEdited}
-          onClick={this.togglePass}
-          label={t('Change Password')}
-        />
-        <Button.Group.Divider />
-        <Button
-          isDisabled={!isEdited}
-          onClick={this.onDiscard}
-          label={t('Reset')}
-        />
-        <Button.Or />
-        <Button
-          isDisabled={!isEdited}
-          isPrimary
-          onClick={this.onCommit}
-          label={t('Save')}
-        />
-      </Button.Group>
-    );
-  }
-
-  renderData () {
-    const { t } = this.props;
-    const { current, editedName } = this.state;
-
-    const address = current
-      ? current.address()
-      : undefined;
-    const type = current
-      ? current.type
-      : 'ed25519';
-
-    return (
-      <div className='ui--grid'>
-        <AddressSummary
-          className='shrink'
-          value={address || ''}
-          showFaucet={true}
-        />
-        <div className='grow'>
-          <div className='ui--row'>
-            <InputAddress
-              className='full'
-              hideAddress
-              isInput={false}
-              label={t('using my key')}
-              onChange={this.onChangeAccount}
-              type='account'
-              value={address}
-            />
-          </div>
-          <div className='ui--row'>
-            <Input
-              className='full'
-              isEditable
-              label={t('identified by the name')}
-              onChange={this.onChangeName}
-              value={editedName}
-            />
-          </div>
-          <Labelled label='address:' style={{ marginTop: '.5rem' }}>
-            <code>{address}</code>
-          </Labelled>
-          <div className='ui--row'>
-            <Dropdown
-              defaultValue={type}
-              isDisabled
-              label={t('keypair crypto type')}
-              options={uiSettings.availableCryptos}
-            />
-          </div>
-          {address && <Labelled label='memo:' style={{ marginTop: '.5rem' }}>
-            <MemoView accountId={address} />
-          </Labelled>}
-        </div>
-      </div>
-    );
-  }
-
-  renderModals () {
-    const { onStatusChange } = this.props;
-    const { current, isBackupOpen, isForgetOpen, isPasswordOpen } = this.state;
-
-    if (!current) {
-      return null;
-    }
-
-    const address = current.address();
-    const modals = [];
-
-    if (isBackupOpen) {
-      modals.push(
-        <Backup
-          key='modal-backup-account'
-          onClose={this.toggleBackup}
-          onStatusChange={onStatusChange}
-          pair={current}
-        />
-      );
-    }
-
-    if (isForgetOpen) {
-      modals.push(
-        <Forgetting
-          address={address}
-          doForget={this.onForget}
-          key='modal-forget-account'
-          onClose={this.toggleForget}
-        />
-      );
-    }
-
-    if (isPasswordOpen) {
-      modals.push(
-        <ChangePass
-          account={current}
-          key='modal-change-pass'
-          onClose={this.togglePass}
-          onStatusChange={onStatusChange}
-        />
-      );
-    }
-
-    return modals;
-  }
-
-  createState (current: KeyringPair | null): State {
-    return {
-      current,
-      editedName: current
-        ? current.getMeta().name || ''
-        : '',
-      isBackupOpen: false,
-      isEdited: false,
-      isForgetOpen: false,
-      isPasswordOpen: false
-    };
-  }
-
-  nextState (newState: State = {} as State): void {
-    this.setState(
-      (prevState: State): State => {
-        let { current = prevState.current, editedName = prevState.editedName } = newState;
-        const previous = prevState.current || { address: () => undefined };
-        let isEdited = false;
-
-        if (current) {
-          if (current.address() !== previous.address()) {
-            editedName = current.getMeta().name || '';
-          } else if (editedName !== current.getMeta().name) {
-            isEdited = true;
-          }
-        } else {
-          editedName = '';
-        }
-
-        return {
-          current,
-          editedName,
-          isBackupOpen: false,
-          isEdited,
-          isForgetOpen: false,
-          isPasswordOpen: false
-        };
-      }
-    );
-  }
-
-  onChangeAccount = (accountId?: string): void => {
-    const current = accountId
-        ? keyring.getPair(accountId)
-        : null;
-
-    this.nextState({
-      current
-    } as State);
-  }
-
-  onChangeName = (editedName: string): void => {
-    this.nextState({ editedName } as State);
-  }
-
-  onCommit = (): void => {
-    const { onStatusChange, t } = this.props;
-    const { current, editedName } = this.state;
-
-    if (!current) {
-      return;
-    }
-
-    const status = {
-      account: current.address(),
-      action: 'edit'
-    } as ActionStatus;
-
-    try {
-      keyring.saveAccountMeta(current, {
-        name: editedName,
-        whenEdited: Date.now()
-      });
-
-      status.status = current.getMeta().name === editedName ? 'success' : 'error';
-      status.message = t('name edited');
-    } catch (error) {
-      status.status = 'error';
-      status.message = error.message;
-    }
-
-    onStatusChange(status);
-
-    this.nextState({} as State);
-  }
-
-  onDiscard = (): void => {
-    const { current } = this.state;
-
-    if (!current) {
-      return;
-    }
-
-    this.nextState({
-      editedName: current.getMeta().name
-    } as State);
-  }
-
-  toggleBackup = (): void => {
-    this.setState(
-      ({ isBackupOpen }: State) => ({
-        isBackupOpen: !isBackupOpen
-      })
-    );
-  }
-
-  toggleForget = (): void => {
-    this.setState(
-      ({ isForgetOpen }: State) => ({
-        isForgetOpen: !isForgetOpen
-      })
-    );
-  }
-
-  togglePass = (): void => {
-    this.setState(
-      ({ current, isPasswordOpen }: State) => {
-        if (!current) {
-          return null;
-        }
-
-        // NOTE We re-get the account from the keyring, if changed it will load the
-        // new instance (this is not quite obvious...)
-        return {
-          current: keyring.getPair(current.publicKey()),
-          isPasswordOpen: !isPasswordOpen
-        };
-      }
-    );
-  }
-
-  onForget = (): void => {
-    const { onStatusChange, t } = this.props;
-    const { current } = this.state;
-
-    if (!current) {
-      return;
-    }
-
-    this.setState(
-      this.createState(null),
-      () => {
-        const status = {
-          account: current.address(),
-          action: 'forget'
-        } as ActionStatus;
-
-        try {
-          keyring.forgetAccount(
-            current.address()
-          );
-          status.status = 'success';
-          status.message = t('key forgotten');
-
-          // Delete my current address (key) from the local sotorage:
-          const myAccountCtx = this.context as MyAccountContextProps;
-          myAccountCtx.forget(current.address());
-
-        } catch (error) {
-          status.status = 'error';
-          status.message = error.message;
-        }
-
-        onStatusChange(status);
-      }
-    );
-  }
-}
-
-export default translate(Editor);

+ 0 - 71
packages/app-accounts/src/Forgetting.tsx

@@ -1,71 +0,0 @@
-// 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/ui-app/types';
-
-import React from 'react';
-import { AddressSummary, Button, Modal } from '@polkadot/ui-app';
-
-import translate from './translate';
-
-type Props = I18nProps & {
-  address: string,
-  onClose: () => void,
-  doForget: () => void
-};
-
-class Forgetting extends React.PureComponent<Props> {
-  render () {
-    return (
-      <Modal
-        className='accounts--Forgetting-Modal'
-        dimmer='inverted'
-        open
-        size='tiny'
-      >
-        {this.renderContent()}
-        {this.renderButtons()}
-      </Modal>
-    );
-  }
-
-  renderButtons () {
-    const { onClose, doForget, t } = this.props;
-
-    return (
-      <Modal.Actions>
-        <Button.Group>
-          <Button
-            isNegative
-            onClick={onClose}
-            label={t('Cancel')}
-          />
-          <Button.Or />
-          <Button
-            isPrimary
-            onClick={doForget}
-            label={t('Forget')}
-          />
-        </Button.Group>
-      </Modal.Actions>
-    );
-  }
-
-  renderContent () {
-    const { address, t } = this.props;
-
-    return (
-      <>
-        <Modal.Header>
-          {t('Confirm key removal')}
-        </Modal.Header>
-        <Modal.Content>
-          <AddressSummary value={address} />
-        </Modal.Content>
-      </>
-    );
-  }
-}
-
-export default translate(Forgetting);

+ 13 - 10
packages/app-accounts/src/MemoForm.tsx

@@ -1,37 +1,37 @@
 import React from 'react';
-import { Labelled } from '@polkadot/ui-app/index';
+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 { nonEmptyStr } from '@polkadot/joy-utils/index';
+import { Text } from '@polkadot/types';
 
 type Props = MyAccountProps & {};
 
 type State = {
   memo: string,
-  isMemoValid: boolean
+  modified: boolean,
 };
 
 class Component extends React.PureComponent<Props, State> {
 
   state: State = {
     memo: '',
-    isMemoValid: false
+    modified: false,
   };
 
   render () {
     const { myAddress } = this.props;
-    const { memo, isMemoValid } = this.state;
+    const { memo, modified } = this.state;
     return (
       <>
-        <MemoEdit accountId={myAddress || ''} onChange={this.onChangeMemo} />
+        <MemoEdit accountId={myAddress || ''} onChange={this.onChangeMemo} onReset={this.onResetMemo} />
         <Labelled style={{ marginTop: '.5rem' }}>
           <TxButton
             size='large'
-            isDisabled={!isMemoValid}
+            isDisabled={!modified}
             label='Update memo'
-            params={[memo.trim()]}
+            params={[new Text(memo)]}
             tx='memo.updateMemo'
           />
         </Labelled>
@@ -40,8 +40,11 @@ class Component extends React.PureComponent<Props, State> {
   }
 
   onChangeMemo = (memo: string): void => {
-    const isMemoValid = nonEmptyStr(memo);
-    this.setState({ memo, isMemoValid });
+    this.setState({ memo, modified: true });
+  }
+
+  onResetMemo = (memo: string): void => {
+    this.setState({ memo, modified: false });
   }
 }
 

+ 104 - 0
packages/app-accounts/src/Overview.tsx

@@ -0,0 +1,104 @@
+// 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 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 [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
+      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' })
+);

+ 0 - 161
packages/app-accounts/src/Restore.tsx

@@ -1,161 +0,0 @@
-// 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 { KeyringPair$Json } from '@polkadot/keyring/types';
-import { I18nProps } from '@polkadot/ui-app/types';
-import { ComponentProps } from './types';
-
-import React from 'react';
-import { AddressSummary, Button, InputFile, Password } from '@polkadot/ui-app';
-import { InputAddress } from '@polkadot/ui-app/InputAddress';
-import { isHex, isObject, u8aToString } from '@polkadot/util';
-import keyring from '@polkadot/ui-keyring';
-
-import translate from './translate';
-import { ActionStatus } from '@polkadot/ui-app/Status/types';
-import { isEmptyStr } from '@polkadot/joy-utils/';
-
-type Props = ComponentProps & I18nProps;
-
-type State = {
-  isFileValid: boolean,
-  isPassValid: boolean,
-  json: KeyringPair$Json | null,
-  password: string
-};
-
-class Restore extends React.PureComponent<Props, State> {
-  state: State = {
-    isFileValid: false,
-    isPassValid: true,
-    json: null,
-    password: ''
-  };
-
-  render () {
-    const { t } = this.props;
-    const { isFileValid, isPassValid, json } = this.state;
-
-    return (
-      <div className='accounts--Restore'>
-        <div className='ui--grid'>
-          <AddressSummary
-            className='shrink'
-            value={
-              isFileValid && json
-                ? json.address
-                : null
-              }
-          />
-          {this.renderInput()}
-        </div>
-        <Button.Group>
-        <Button
-          isDisabled={!isFileValid || !isPassValid}
-          isPrimary
-          onClick={this.onSave}
-          label={t('Restore')}
-        />
-      </Button.Group>
-      </div>
-    );
-  }
-
-  private renderInput () {
-    const { t } = this.props;
-    const { isFileValid, isPassValid, password } = this.state;
-    const acceptedFormats = ['application/json', 'text/plain'].join(', ');
-
-    return (
-      <div className='grow'>
-        <div className='ui--row'>
-          <InputFile
-            accept={acceptedFormats}
-            className='full'
-            isError={!isFileValid}
-            label={t('previously backed-up json keyfile')}
-            onChange={this.onChangeFile}
-            withLabel
-          />
-        </div>
-        <div className='ui--row'>
-          <Password
-            autoFocus
-            className='full'
-            isError={!isPassValid}
-            label={t('decrypt keyfile using the password')}
-            onChange={this.onChangePass}
-            value={password}
-          />
-        </div>
-      </div>
-    );
-  }
-
-  private onChangeFile = (file: Uint8Array): void => {
-    try {
-      const json = JSON.parse(u8aToString(file));
-      const isFileValid = keyring.decodeAddress(json.address).length === 32 && isHex(json.encoded) && isObject(json.meta) && (
-        Array.isArray(json.encoding.content)
-          ? json.encoding.content[0] === 'pkcs8'
-          : json.encoding.content === 'pkcs8'
-      );
-
-      this.setState({
-        isFileValid,
-        json
-      });
-    } catch (error) {
-      this.setState({
-        isFileValid: false,
-        json: null
-      });
-      console.error(error);
-    }
-  }
-
-  private onChangePass = (password: string): void => {
-    this.setState({
-      isPassValid: isEmptyStr(password) || keyring.isPassValid(password),
-      password
-    });
-  }
-
-  private onSave = (): void => {
-    const { basePath, onStatusChange, t } = this.props;
-    const { json, password } = this.state;
-
-    if (!json) {
-      return;
-    }
-
-    const status = {
-      action: 'restore'
-    } as ActionStatus;
-
-    try {
-      const pair = keyring.restoreAccount(json, password);
-
-      status.status = pair ? 'success' : 'error';
-      status.account = pair.address();
-      status.message = t('key restored');
-
-      InputAddress.setLastValue('account', pair.address());
-    } catch (error) {
-      this.setState({ isPassValid: false });
-
-      status.status = 'error';
-      status.message = error.message;
-      console.error(error);
-    }
-
-    onStatusChange(status);
-
-    if (status.status !== 'error') {
-      window.location.hash = basePath;
-    }
-  }
-}
-
-export default translate(Restore);

+ 79 - 59
packages/app-accounts/src/Vanity/Match.tsx

@@ -2,83 +2,103 @@
 // 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/ui-app/types';
+import { BareProps } from '@polkadot/react-components/types';
 
-import React from 'react';
-import { Button, IdentityIcon } from '@polkadot/ui-app';
+import React, { useEffect, useState } from 'react';
+import styled from 'styled-components';
+import { Button, IdentityIcon } from '@polkadot/react-components';
 import { u8aToHex } from '@polkadot/util';
 
-type Props = BareProps & {
+interface Props extends BareProps {
   address: string;
   count: number;
   offset: number;
-  onCreateToggle: (passthrough: string) => void,
-  onRemove: (address: string) => void,
+  onCreateToggle: (seed: string) => void;
+  onRemove: (address: string) => void;
   seed: Uint8Array;
-};
-
-type State = {
-  hexSeed: string
-};
-
-export default class Match extends React.PureComponent<Props, State> {
-  state: State = {} as State;
+}
 
-  static getDerivedStateFromProps ({ seed }: Props): State {
-    return {
-      hexSeed: u8aToHex(seed)
-    };
-  }
+function Match ({ address, className, count, offset, onCreateToggle, onRemove, seed }: Props): React.ReactElement<Props> {
+  const [hexSeed, setHexSeed] = useState('');
+  const _onCreate = (): void => onCreateToggle(hexSeed);
+  const _onRemove = (): void => onRemove(address);
 
-  render () {
-    const { address, count, offset } = this.props;
-    const { hexSeed } = this.state;
+  useEffect((): void => {
+    setHexSeed(u8aToHex(seed));
+  }, [seed]);
 
-    return (
-      <div className='vanity--Match'>
-        <div className='vanity--Match-item'>
-          <IdentityIcon
-            className='vanity--Match-icon'
-            size={48}
-            value={address}
-          />
-          <div className='vanity--Match-data'>
-            <div className='vanity--Match-addr'>
-              <span className='no'>{address.slice(0, offset)}</span><span className='yes'>{address.slice(offset, count + offset)}</span><span className='no'>{address.slice(count + offset)}</span>
-            </div>
-            <div className='vanity--Match-seed'>
-              {hexSeed}
-            </div>
+  return (
+    <div className={className}>
+      <div className='vanity--Match-item'>
+        <IdentityIcon
+          className='vanity--Match-icon'
+          size={48}
+          value={address}
+        />
+        <div className='vanity--Match-data'>
+          <div className='vanity--Match-addr'>
+            <span className='no'>{address.slice(0, offset)}</span><span className='yes'>{address.slice(offset, count + offset)}</span><span className='no'>{address.slice(count + offset)}</span>
           </div>
-          <div className='vanity--Match-buttons'>
-            <Button
-              icon='plus'
-              isPrimary
-              onClick={this.onCreate}
-              size='tiny'
-            />
-            <Button
-              icon='close'
-              isNegative
-              onClick={this.onRemove}
-              size='tiny'
-            />
+          <div className='vanity--Match-seed'>
+            {hexSeed}
           </div>
         </div>
+        <div className='vanity--Match-buttons'>
+          <Button
+            icon='plus'
+            isPrimary
+            onClick={_onCreate}
+            size='tiny'
+          />
+          <Button
+            icon='close'
+            isNegative
+            onClick={_onRemove}
+            size='tiny'
+          />
+        </div>
       </div>
-    );
+    </div>
+  );
+}
+
+export default styled(Match)`
+  text-align: center;
+
+  &:hover {
+    background: #f9f9f9;
   }
 
-  onCreate = (): void => {
-    const { onCreateToggle } = this.props;
-    const { hexSeed } = this.state;
+  .vanity--Match-addr {
+    font-size: 1.5rem;
+    padding: 0 1rem;
+
+    .no {
+      color: inherit;
+    }
 
-    onCreateToggle(hexSeed);
+    .yes {
+      color: red;
+    }
   }
 
-  onRemove = (): void => {
-    const { address, onRemove } = this.props;
+  .vanity--Match-buttons,
+  .vanity--Match-data,
+  .vanity--Match-icon {
+    display: inline-block;
+    vertical-align: middle;
+  }
 
-    onRemove(address);
+  .vanity--Match-item {
+    display: inline-block;
+    font-family: monospace;
+    margin: 0 auto;
+    padding: 0.5em;
+    position: relative;
   }
-}
+
+  .vanity--Match-seed {
+    opacity: 0.45;
+    padding: 0 1rem;
+  }
+`;

+ 0 - 57
packages/app-accounts/src/Vanity/index.css

@@ -1,57 +0,0 @@
-/* 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. */
-
-.vanity--App-matches {
-  padding: 1em 0;
-}
-
-.vanity--App-stats {
-  padding: 1em 0 0 0;
-  opacity: 0.45;
-  text-align: center;
-}
-
-.vanity--Match {
-  text-align: center;
-}
-
-.vanity--Match:hover {
-  background: #f9f9f9;
-}
-
-.vanity--Match-item {
-  display: inline-block;
-  font-family: monospace;
-  margin: 0 auto;
-  padding: 0.5em;
-  position: relative;
-}
-
-.vanity--Match-icon,
-.vanity--Match-data,
-.vanity--Match-buttons {
-  display: inline-block;
-  vertical-align: middle;
-}
-
-.vanity--Match-addr,
-.vanity--Match-seed {
-  padding: 0 1rem;
-}
-
-.vanity--Match-addr {
-  font-size: 1.5rem;
-
-  .no {
-    color: inherit;
-  }
-
-  .yes {
-    color: red;
-  }
-}
-
-.vanity--Match-seed {
-  opacity: 0.45;
-}

+ 139 - 79
packages/app-accounts/src/Vanity/index.tsx

@@ -2,34 +2,39 @@
 // 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/ui-app/types';
-import { Generator$Matches, Generator$Result } from '../vanitygen/types';
+import { I18nProps } from '@polkadot/react-components/types';
+import { KeypairType } from '@polkadot/util-crypto/types';
+import { GeneratorMatches, GeneratorMatch, GeneratorResult } from '../vanitygen/types';
 import { ComponentProps } from '../types';
 
-import './index.css';
-
 import React from 'react';
-import { Button, Dropdown, Input } from '@polkadot/ui-app';
+import styled from 'styled-components';
+import { Button, Dropdown, Input, TxComponent } from '@polkadot/react-components';
+import uiSettings from '@polkadot/ui-settings';
 
+import CreateModal from '../modals/Create';
 import generator from '../vanitygen';
 import matchRegex from '../vanitygen/regex';
 import generatorSort from '../vanitygen/sort';
 import Match from './Match';
 import translate from './translate';
 
-type Props = ComponentProps & I18nProps;
-
-type State = {
-  elapsed: number,
-  isMatchValid: boolean,
-  isRunning: boolean,
-  keyCount: 0,
-  keyTime: 0,
-  match: string,
-  matches: Generator$Matches,
-  startAt: number,
-  withCase: boolean
-};
+interface Props extends ComponentProps, I18nProps {}
+
+interface State {
+  createSeed: string | null;
+  elapsed: number;
+  isMatchValid: boolean;
+  isRunning: boolean;
+  keyCount: 0;
+  keyTime: 0;
+  match: string;
+  matches: GeneratorMatches;
+  startAt: number;
+  type: KeypairType;
+  withCase: boolean;
+  withHex: boolean;
+}
 
 const DEFAULT_MATCH = 'Some';
 const BOOL_OPTIONS = [
@@ -37,9 +42,11 @@ const BOOL_OPTIONS = [
   { text: 'Yes', value: true }
 ];
 
-class VanityApp extends React.PureComponent<Props, State> {
-  results: Array<Generator$Result> = [];
-  state: State = {
+class VanityApp extends TxComponent<Props, State> {
+  private results: GeneratorResult[] = [];
+
+  public state: State = {
+    createSeed: null,
     elapsed: 0,
     isMatchValid: true,
     isRunning: false,
@@ -48,33 +55,51 @@ class VanityApp extends React.PureComponent<Props, State> {
     match: DEFAULT_MATCH,
     matches: [],
     startAt: 0,
-    withCase: true
+    type: 'ed25519',
+    withCase: true,
+    withHex: true
   };
 
-  private _isActive: boolean = false;
+  private _isActive = false;
 
-  componentWillUnmount () {
+  public componentWillUnmount (): void {
     this._isActive = false;
   }
 
-  render () {
+  public render (): React.ReactNode {
+    const { className, onStatusChange } = this.props;
+    const { createSeed, type } = this.state;
+
     return (
-      <div className='accounts--Vanity'>
+      <div className={className}>
         {this.renderOptions()}
         {this.renderButtons()}
         {this.renderStats()}
         {this.renderMatches()}
+        {createSeed && (
+          <CreateModal
+            onClose={this.closeCreate}
+            onStatusChange={onStatusChange}
+            seed={createSeed}
+            type={type}
+          />
+        )}
       </div>
     );
   }
 
-  renderButtons () {
+  private renderButtons (): React.ReactNode {
     const { t } = this.props;
     const { isMatchValid, isRunning } = this.state;
 
     return (
       <Button.Group>
         <Button
+          icon={
+            isRunning
+              ? 'stop'
+              : 'sign-in'
+          }
           isDisabled={!isMatchValid}
           isPrimary={!isRunning}
           onClick={this.toggleStart}
@@ -83,17 +108,18 @@ class VanityApp extends React.PureComponent<Props, State> {
               ? t('Stop generation')
               : t('Start generation')
           }
+          ref={this.button}
         />
       </Button.Group>
     );
   }
 
-  renderMatches () {
+  private renderMatches (): React.ReactNode {
     const { matches } = this.state;
 
     return (
       <div className='vanity--App-matches'>
-        {matches.map((match) => (
+        {matches.map((match): React.ReactNode => (
           <Match
             {...match}
             key={match.address}
@@ -105,34 +131,49 @@ class VanityApp extends React.PureComponent<Props, State> {
     );
   }
 
-  renderOptions () {
+  private renderOptions (): React.ReactNode {
     const { t } = this.props;
-    const { isMatchValid, isRunning, match, withCase } = this.state;
+    const { isMatchValid, isRunning, match, type, withCase } = this.state;
 
     return (
-      <div className='ui--row'>
-        <Input
-          autoFocus
-          className='medium'
-          isDisabled={isRunning}
-          isError={!isMatchValid}
-          label={t('generate address with ? as a wildcard')}
-          onChange={this.onChangeMatch}
-          value={match}
-        />
-        <Dropdown
-          className='medium'
-          isDisabled={isRunning}
-          label={t('perform a case sensitive search/match')}
-          options={BOOL_OPTIONS}
-          onChange={this.onChangeCase}
-          value={withCase}
-        />
-      </div>
+      <>
+        <div className='ui--row'>
+          <Input
+            autoFocus
+            className='medium'
+            help={t('Type here what you would like your address to contain. This tool will generate the keys and show the associated addresses that best match your search. You can use "?" as a wildcard for a character.')}
+            isDisabled={isRunning}
+            isError={!isMatchValid}
+            label={t('Search for')}
+            onChange={this.onChangeMatch}
+            onEnter={this.submit}
+            value={match}
+          />
+          <Dropdown
+            className='medium'
+            help={t('Should the search be case sensitive, e.g if you select "no" your search for "Some" may return addresses containing "somE" or "sOme"...')}
+            isDisabled={isRunning}
+            label={t('case sensitive')}
+            options={BOOL_OPTIONS}
+            onChange={this.onChangeCase}
+            value={withCase}
+          />
+        </div>
+        <div className='ui--row'>
+          <Dropdown
+            className='medium'
+            defaultValue={type}
+            help={t('Determines what cryptography will be used to create this account. Note that to validate on Polkadot, the session account must use "ed25519".')}
+            label={t('keypair crypto type')}
+            onChange={this.onChangeType}
+            options={uiSettings.availableCryptos}
+          />
+        </div>
+      </>
     );
   }
 
-  renderStats () {
+  private renderStats (): React.ReactNode {
     const { t } = this.props;
     const { elapsed, keyCount } = this.state;
 
@@ -155,7 +196,7 @@ class VanityApp extends React.PureComponent<Props, State> {
     );
   }
 
-  checkMatches (): void {
+  private checkMatches (): void {
     const results = this.results;
 
     this.results = [];
@@ -165,24 +206,23 @@ class VanityApp extends React.PureComponent<Props, State> {
     }
 
     this.setState(
-      (prevState: State) => {
-        let newKeyCount = prevState.keyCount;
-        let newKeyTime = prevState.keyTime;
-
-        const matches = results
-          .reduce((result, { elapsed, found }) => {
+      ({ keyCount, keyTime, matches, startAt }: State): Pick<State, never> => {
+        let newKeyCount = keyCount;
+        let newKeyTime = keyTime;
+        const newMatches = results
+          .reduce((result, { elapsed, found }): GeneratorMatch[] => {
             newKeyCount += found.length;
             newKeyTime += elapsed;
 
             return result.concat(found);
-          }, prevState.matches)
+          }, matches)
           .sort(generatorSort)
           .slice(0, 25);
-        const elapsed = Date.now() - prevState.startAt;
+        const elapsed = Date.now() - startAt;
 
         return {
           elapsed,
-          matches,
+          matches: newMatches,
           keyCount: newKeyCount,
           keyTime: newKeyTime
         };
@@ -190,24 +230,28 @@ class VanityApp extends React.PureComponent<Props, State> {
     );
   }
 
-  executeGeneration = (): void => {
+  private executeGeneration = (): void => {
     if (!this.state.isRunning) {
       this.checkMatches();
 
       return;
     }
 
-    setTimeout(() => {
+    setTimeout((): void => {
       if (this._isActive) {
         if (this.results.length === 25) {
           this.checkMatches();
         }
 
+        const { match, type, withCase, withHex } = this.state;
+
         this.results.push(
           generator({
-            match: this.state.match,
+            match,
             runs: 10,
-            withCase: this.state.withCase
+            type,
+            withCase,
+            withHex
           })
         );
 
@@ -216,17 +260,15 @@ class VanityApp extends React.PureComponent<Props, State> {
     }, 0);
   }
 
-  private onCreateToggle = (seed: string) => {
-    const { basePath } = this.props;
-
-    window.location.hash = `${basePath}/create/${seed}`;
+  private onCreateToggle = (createSeed: string): void => {
+    this.setState({ createSeed });
   }
 
-  onChangeCase = (withCase: boolean): void => {
+  private onChangeCase = (withCase: boolean): void => {
     this.setState({ withCase });
   }
 
-  onChangeMatch = (match: string): void => {
+  private onChangeMatch = (match: string): void => {
     this.setState({
       isMatchValid:
         matchRegex.test(match) &&
@@ -236,21 +278,23 @@ class VanityApp extends React.PureComponent<Props, State> {
     });
   }
 
-  onRemove = (address: string): void => {
+  private onChangeType = (type: KeypairType): void => {
+    this.setState({ type });
+  }
+
+  private onRemove = (address: string): void => {
     this.setState(
-      (prevState: State) => ({
-        matches: prevState.matches.filter((item) =>
+      ({ matches }: State): Pick<State, never> => ({
+        matches: matches.filter((item): boolean =>
           item.address !== address
         )
       })
     );
   }
 
-  toggleStart = (): void => {
+  private toggleStart = (): void => {
     this.setState(
-      (prevState: State) => {
-        const { isRunning, keyCount, keyTime, startAt } = prevState;
-
+      ({ isRunning, keyCount, keyTime, startAt }: State): Pick<State, never> => {
         this._isActive = !isRunning;
 
         return {
@@ -263,6 +307,22 @@ class VanityApp extends React.PureComponent<Props, State> {
       this.executeGeneration
     );
   }
+
+  private closeCreate = (): void => {
+    this.setState({ createSeed: null });
+  }
 }
 
-export default translate(VanityApp);
+export default translate(
+  styled(VanityApp)`
+    .vanity--App-matches {
+      padding: 1em 0;
+    }
+
+    .vanity--App-stats {
+      padding: 1em 0 0 0;
+      opacity: 0.45;
+      text-align: center;
+    }
+  `
+);

+ 1 - 1
packages/app-accounts/src/Vanity/translate.ts

@@ -4,4 +4,4 @@
 
 import { withTranslation } from 'react-i18next';
 
-export default withTranslation(['vanitygen', 'ui']);
+export default withTranslation(['app-accounts']);

+ 3 - 3
packages/app-accounts/src/bipWorker.ts

@@ -4,13 +4,13 @@
 
 import { cryptoWaitReady, mnemonicGenerate, mnemonicToMiniSecret, naclKeypairFromSeed, schnorrkelKeypairFromSeed } from '@polkadot/util-crypto';
 
-const ctx: Worker = self as any;
+const ctx: Worker = self as unknown as Worker;
 
-cryptoWaitReady().catch(() => {
+cryptoWaitReady().catch((): void => {
   // ignore
 });
 
-ctx.onmessage = async ({ data: { pairType } }) => {
+ctx.onmessage = async ({ data: { pairType } }): Promise<void> => {
   await cryptoWaitReady();
 
   const seed = mnemonicGenerate();

+ 0 - 14
packages/app-accounts/src/index.css

@@ -1,14 +0,0 @@
-/* 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 '../../ui-app/src/styles/partials/_variables.css';
-
-.app--accounts-Modal .ui--AddressSummary {
-  display: inline-block;
-  margin-bottom: 1rem;
-}
-
-.accounts--Creator-advanced {
-  margin-top: 1rem;
-}

+ 57 - 122
packages/app-accounts/src/index.tsx

@@ -2,139 +2,42 @@
 // This software may be modified and distributed under the terms
 // of the Apache-2.0 license. See the LICENSE file for details.
 
-import uiSettings from '@polkadot/joy-settings/';
-import { AppProps, I18nProps } from '@polkadot/ui-app/types';
-import { TabItem } from '@polkadot/ui-app/Tabs';
-import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
+import { AppProps, I18nProps } from '@polkadot/react-components/types';
 import { ComponentProps, LocationProps } from './types';
+import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
 
-import './index.css';
-
-import React from 'react';
+import React, { useEffect, useState } from 'react';
 import { Route, Switch } from 'react-router';
 import accountObservable from '@polkadot/ui-keyring/observable/accounts';
-import { Tabs } from '@polkadot/ui-app';
-import { withMulti, withObservable } from '@polkadot/ui-api';
+import { HelpOverlay, Tabs } from '@polkadot/react-components';
+import { withMulti, withObservable } from '@polkadot/react-api';
 
-import Creator from './Creator';
-import Editor from './Editor';
-import Restore from './Restore';
-import Vanity from './Vanity';
-import MemoForm from './MemoForm';
+import basicMd from './md/basic.md';
+import Overview from './Overview';
 import translate from './translate';
+import Vanity from './Vanity';
 
-type Props = AppProps & I18nProps & {
-  allAccounts?: SubjectInfo
-};
-
-type State = {
-  hidden: Array<string>,
-  tabs: Array<TabItem>
-};
-
-class AccountsApp extends React.PureComponent<Props, State> {
-  state: State;
-
-  constructor (props: Props) {
-    super(props);
-
-    const { allAccounts = {}, t } = props;
-    const baseState = Object.keys(allAccounts).length !== 0
-      ? AccountsApp.showEditState()
-      : AccountsApp.hideEditState();
-
-    this.state = {
-      ...baseState,
-      tabs: [
-        {
-          name: 'edit',
-          text: t('Default key')
-        },
-        {
-          hasParams: true,
-          name: 'create',
-          text: t('Create key')
-        },
-        {
-          name: 'restore',
-          text: t('Restore key')
-        },
-        uiSettings.isBasicMode ? null : {
-          name: 'vanity',
-          text: t('Vanity address')
-        },
-        {
-          name: 'memo',
-          text: t('My memo')
-        }
-      ].filter(x => x !== null) as TabItem[]
-    };
-  }
-
-  static showEditState () {
-    return {
-      hidden: []
-    };
-  }
-
-  static hideEditState () {
-    // Hide vanity as well - since the route order and matching changes, the
-    // /create/:seed route become problematic, so don't allow that option
-    return {
-      hidden: ['edit', 'vanity']
-    };
-  }
-
-  static getDerivedStateFromProps ({ allAccounts = {} }: Props, { hidden }: State) {
-    const hasAddresses = Object.keys(allAccounts).length !== 0;
-
-    if (hidden.length === 0) {
-      return hasAddresses
-        ? null
-        : AccountsApp.hideEditState();
-    }
+import MemoForm from './MemoForm';
 
-    return hasAddresses
-      ? AccountsApp.showEditState()
-      : null;
-  }
+interface Props extends AppProps, I18nProps {
+  allAccounts?: SubjectInfo;
+  location: any;
+}
 
-  render () {
-    const { basePath } = this.props;
-    const { hidden, tabs } = this.state;
-    const renderCreator = this.renderComponent(Creator);
+function AccountsApp ({ allAccounts = {}, basePath, location, onStatusChange, t }: Props): React.ReactElement<Props> {
+  const [hidden, setHidden] = useState<string[]>(['vanity']);
 
-    return (
-      <main className='accounts--App'>
-        <header>
-          <Tabs
-            basePath={basePath}
-            hidden={hidden}
-            items={tabs}
-          />
-        </header>
-        <Switch>
-          <Route path={`${basePath}/create/:seed`} render={renderCreator} />
-          <Route path={`${basePath}/create`} render={renderCreator} />
-          <Route path={`${basePath}/restore`} render={this.renderComponent(Restore)} />
-          <Route path={`${basePath}/vanity`} render={this.renderComponent(Vanity)} />
-          <Route path={`${basePath}/memo`} component={MemoForm} />
-          <Route
-            render={
-              hidden.includes('edit')
-                ? renderCreator
-                : this.renderComponent(Editor)
-            }
-          />
-        </Switch>
-      </main>
+  useEffect((): void => {
+    setHidden(
+      Object.keys(allAccounts).length !== 0
+        ? []
+        : ['vanity']
     );
-  }
-
-  private renderComponent (Component: React.ComponentType<ComponentProps>) {
-    return ({ match }: LocationProps) => {
-      const { basePath, location, onStatusChange } = this.props;
+  }, [allAccounts]);
 
+  const _renderComponent = (Component: React.ComponentType<ComponentProps>): (props: LocationProps) => React.ReactNode => {
+    // eslint-disable-next-line react/display-name
+    return ({ match }: LocationProps): React.ReactNode => {
       return (
         <Component
           basePath={basePath}
@@ -144,7 +47,39 @@ class AccountsApp extends React.PureComponent<Props, State> {
         />
       );
     };
-  }
+  };
+
+  return (
+    <main className='accounts--App'>
+      <HelpOverlay md={basicMd} />
+      <header>
+        <Tabs
+          basePath={basePath}
+          hidden={hidden}
+          items={[
+            {
+              isRoot: true,
+              name: 'overview',
+              text: t('My accounts')
+            },
+            {
+              name: 'vanity',
+              text: t('Vanity address')
+            },
+            {
+              name: 'memo',
+              text: t('My memo')
+            }
+          ]}
+        />
+      </header>
+      <Switch>
+        <Route path={`${basePath}/vanity`} render={_renderComponent(Vanity)} />
+        <Route path={`${basePath}/memo`} component={MemoForm} />
+        <Route render={_renderComponent(Overview)} />
+      </Switch>
+    </main>
+  );
 }
 
 export default withMulti(

+ 44 - 0
packages/app-accounts/src/md/basic.md

@@ -0,0 +1,44 @@
+# Account
+
+An account is identified by its public address on the network. It is totally fine to give away this address, this is also the only information needed to receive funds. The network will **not** know about the name you give to this account in this application.
+
+# Balances
+
+The balances for each account is broken down into a number of areas, giving an overview of the totals, transferable and bonded funds as well as the funds currently being unbonded or redeemable. These are -
+
+- **total**: The overall amount of funds in the account, this includes the vested balance, available for transfer and locked.
+- **available**: The funds that can be transferred or bonded, i.e. the funds that are available for any transaction.
+- **bonded**: The funds bonded for validating or nominating. They are locked and cannot be transferred, although it can be unlocked for future actions.
+- **redeemable**: The funds that can get redeemed, e.g made available for withdrawal, by clicking on the "lock" icon.
+- **unbonding**: The funds that are being unbonded. The funds will be redeemable after the bonding period has passed. These funds can still be slashed. The information icon tells the amount of blocks left before the funds can be redeemed.
+
+# Security
+
+The public address is generated from a private key, itself generated from a seed or a mnemonic phrase. The seed or the mnemonic phrase should **never be shared with anybody** as they give access to your funds. It must be stored securely.
+The password needed to create an account is used to encrypt your private key. You must choose a strong and unique password.
+This password is also used to encrypt the private key in the backup file downloaded upon account creation. Thanks to this file together with your account password, you can recover your account.
+
+# Account recovery
+
+You can recover an account from its:
+- seed or mnemonic:
+  Click on the "Add account" button, type your seed or mnemonic in the associated field.
+
+- backup file (also called JSON keystore file) and the account's password:
+  Click on "Restore JSON" button. Upload your backup file and type in the password associated.
+
+# Minimum allowed balance
+
+Accounts with a balance lower than the minimal amount, 100 milliUnits (miliDOTs for Polkadot) as of writing are considered as nonexistent for the network. If an account's balance ever drops below this amount, it is removed from the network. In this application, it will still be visible, but with a balance of 0.
+
+For a fund transfer to a **new account** (read an account with a balance of 0), if the amount transferred is less than the minimum allowed balance, then the transfer will "succeed" but the destination account will not be created (read its balance will remain 0); this essentially burns the transfer balance from the sender, because the receiver's balance never exceed the minimum allowed balance.
+
+If the receiver already exists (read it has a balance greater than 0), it is perfectly possible to transfer very low amounts.
+
+# Cryptography
+
+Substrate and Polkadot use Schnorrkel/Ristretto x25519 ("sr25519") as its key derivation and signing algorithm.
+
+Sr25519 is based on the same underlying Curve25519 as its EdDSA counterpart, Ed25519. However, it uses Schnorr signatures instead of the EdDSA scheme. Schnorr signatures bring some noticeable benefits over the ECDSA/EdDSA schemes. For one, it is more efficient and still retains the same feature set and security assumptions. Additionally, it allows for native multisignature through signature aggregation.
+
+If you wish to validate, the `session` account needs to use "ed25519" cryptography.

+ 144 - 0
packages/app-accounts/src/modals/Backup.tsx

@@ -0,0 +1,144 @@
+// 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 FileSaver from 'file-saver';
+import React from 'react';
+import { AddressRow, Button, Modal, Password, TxComponent } from '@polkadot/react-components';
+import { ActionStatus } from '@polkadot/react-components/Status/types';
+import keyring from '@polkadot/ui-keyring';
+import { isPasswordValid } from '@polkadot/joy-utils/accounts';
+
+import translate from '../translate';
+
+interface Props extends I18nProps {
+  onClose: () => void;
+  address: string;
+}
+
+interface State {
+  isPassValid: boolean;
+  password: string;
+}
+
+class Backup extends TxComponent<Props, State> {
+  public state: State = {
+    isPassValid: true,
+    password: ''
+  };
+
+  public render (): React.ReactNode {
+    const { t } = this.props;
+
+    return (
+      <Modal
+        className='app--accounts-Modal'
+        dimmer='inverted'
+        open
+      >
+        <Modal.Header>{t('Backup account')}</Modal.Header>
+        {this.renderContent()}
+        {this.renderButtons()}
+      </Modal>
+    );
+  }
+
+  private renderButtons (): React.ReactNode {
+    const { onClose, t } = this.props;
+    const { isPassValid } = this.state;
+
+    return (
+      <Modal.Actions>
+        <Button.Group>
+          <Button
+            icon='cancel'
+            isNegative
+            label={t('Cancel')}
+            onClick={onClose}
+          />
+          <Button.Or />
+          <Button
+            icon='download'
+            isDisabled={!isPassValid}
+            label={t('Download')}
+            onClick={this.doBackup}
+            ref={this.button}
+          />
+        </Button.Group>
+      </Modal.Actions>
+    );
+  }
+
+  private renderContent (): React.ReactNode {
+    const { address, t } = this.props;
+    const { isPassValid, password } = this.state;
+
+    return (
+      <Modal.Content>
+        <AddressRow
+          isInline
+          value={address}
+        >
+          <p>{t('An encrypted backup file will be created once you have pressed the "Download" button. This can be used to re-import your account on any other machine.')}</p>
+          <p>{t('Save this backup file in a secure location. Additionally, the password associated with this account is needed together with this backup file in order to restore your account.')}</p>
+          <div>
+            <Password
+              help={t('The account password as specified when creating the account. This is used to encrypt the backup file and subsequently decrypt it when restoring the account.')}
+              isError={!isPassValid}
+              label={t('password')}
+              onChange={this.onChangePass}
+              onEnter={this.submit}
+              tabIndex={0}
+              value={password}
+            />
+          </div>
+        </AddressRow>
+      </Modal.Content>
+    );
+  }
+
+  private doBackup = (): void => {
+    const { onClose, address, t } = this.props;
+    const { password } = this.state;
+
+    if (!address) {
+      return;
+    }
+
+    const status: Partial<ActionStatus> = {
+      action: 'backup'
+    };
+
+    try {
+      const addressKeyring = address && keyring.getPair(address);
+      const json = addressKeyring && keyring.backupAccount(addressKeyring, password);
+      const blob = new Blob([JSON.stringify(json)], { type: 'application/json; charset=utf-8' });
+
+      status.account = address;
+      status.status = blob ? 'success' : 'error';
+      status.message = t('account backed up');
+
+      FileSaver.saveAs(blob, `${address}.json`);
+    } catch (error) {
+      this.setState({ isPassValid: true });
+      console.error(error);
+
+      status.status = 'error';
+      status.message = error.message;
+      return;
+    }
+
+    onClose();
+  }
+
+  private onChangePass = (password: string): void => {
+    this.setState({
+      isPassValid: isPasswordValid(password),
+      password
+    });
+  }
+}
+
+export default translate(Backup);

+ 183 - 0
packages/app-accounts/src/modals/ChangePass.tsx

@@ -0,0 +1,183 @@
+// 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 React from 'react';
+import { AddressRow, Button, Modal, Password, TxComponent } from '@polkadot/react-components';
+import { ActionStatus } from '@polkadot/react-components/Status/types';
+import keyring from '@polkadot/ui-keyring';
+import { isPasswordValid } from '@polkadot/joy-utils/accounts';
+
+import translate from '../translate';
+
+interface Props extends I18nProps {
+  address: string;
+  onClose: () => void;
+}
+
+interface State {
+  isNewValid: boolean;
+  isOldValid: boolean;
+  newPass: string;
+  oldPass: string;
+}
+
+class ChangePass extends TxComponent<Props, State> {
+  public state: State = {
+    isNewValid: false,
+    isOldValid: false,
+    newPass: '',
+    oldPass: ''
+  };
+
+  public render (): React.ReactNode {
+    const { t } = this.props;
+
+    return (
+      <Modal
+        className='app--accounts-Modal'
+        dimmer='inverted'
+        open
+      >
+        <Modal.Header>{t('Change account password')}</Modal.Header>
+        {this.renderContent()}
+        {this.renderButtons()}
+      </Modal>
+    );
+  }
+
+  private renderButtons (): React.ReactNode {
+    const { onClose, t } = this.props;
+    const { isNewValid, isOldValid } = this.state;
+
+    return (
+      <Modal.Actions>
+        <Button.Group>
+          <Button
+            icon='cancel'
+            isNegative
+            label={t('Cancel')}
+            onClick={onClose}
+          />
+          <Button.Or />
+          <Button
+            icon='sign-in'
+            isDisabled={!isNewValid || !isOldValid}
+            isPrimary
+            label={t('Change')}
+            onClick={this.doChange}
+            ref={this.button}
+          />
+        </Button.Group>
+      </Modal.Actions>
+    );
+  }
+
+  private renderContent (): React.ReactNode {
+    const { address, t } = this.props;
+    const { isNewValid, isOldValid, newPass, oldPass } = this.state;
+
+    return (
+      <Modal.Content>
+        <AddressRow
+          isInline
+          value={address}
+        >
+          <p>{t('This will apply to any future use of this account as stored on this browser. Ensure that you securely store this new password and that it is strong and unique to the account.')}</p>
+          <div>
+            <Password
+              autoFocus
+              help={t('The existing account password as specified when this account was created or when it was last changed.')}
+              isError={!isOldValid}
+              label={t('your current password')}
+              onChange={this.onChangeOld}
+              tabIndex={1}
+              value={oldPass}
+            />
+            <Password
+              help={t('The new account password. Once set, all future account unlocks will be performed with this new password.')}
+              isError={!isNewValid}
+              label={t('your new password')}
+              onChange={this.onChangeNew}
+              onEnter={this.submit}
+              tabIndex={2}
+              value={newPass}
+            />
+          </div>
+        </AddressRow>
+      </Modal.Content>
+    );
+  }
+
+  private doChange = (): void => {
+    const { address, onClose, t } = this.props;
+    const { newPass, oldPass } = this.state;
+    const status: Partial<ActionStatus> = {
+      action: 'changePassword'
+    };
+
+    try {
+      const account = address && keyring.getPair(address);
+
+      if (!account) {
+        status.message = t(`No keypair found for this address ${address}`);
+
+        return;
+      }
+
+      try {
+        if (!account.isLocked) {
+          account.lock();
+        }
+
+        account.decodePkcs8(oldPass);
+      } catch (error) {
+        this.setState({ isOldValid: false });
+        status.message = error.message;
+
+        return;
+      }
+
+      try {
+        keyring.encryptAccount(account, newPass);
+        status.account = address;
+        status.status = 'success';
+        status.message = t('password changed');
+      } catch (error) {
+        this.setState({ isNewValid: false });
+        status.status = 'error';
+        status.message = error.message;
+
+        return;
+      }
+    } catch (error) {
+      status.message = error.message;
+
+      return;
+    }
+
+    onClose();
+  }
+
+  private onChangeNew = (newPass: string): void => {
+    this.setState({
+      isNewValid: this.validatePass(newPass),
+      newPass
+    });
+  }
+
+  private onChangeOld = (oldPass: string): void => {
+    this.setState({
+      isOldValid: this.validatePass(oldPass),
+      oldPass
+    });
+  }
+
+  private validatePass (password: string): boolean {
+    return isPasswordValid(password);
+  }
+}
+
+export default translate(ChangePass);

+ 193 - 0
packages/app-accounts/src/modals/Create.tsx

@@ -0,0 +1,193 @@
+// 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 { KeypairType } from '@polkadot/util-crypto/types';
+import { ModalProps } from '../types';
+
+import { useMyAccount } from '@polkadot/joy-utils/MyAccountContext';
+import { generateSeed, updateAddress, createAccount, isPasswordValid, AddressState, SeedType } from '@polkadot/joy-utils/accounts';
+import React, { useContext, useState } from 'react';
+import styled from 'styled-components';
+import { ApiContext } from '@polkadot/react-api';
+import { AddressRow, Button, Dropdown, Input, Modal, Password } from '@polkadot/react-components';
+import uiSettings from '@polkadot/ui-settings';
+
+import translate from '../translate';
+import CreateConfirmation from './CreateConfirmation';
+
+interface Props extends ModalProps, I18nProps {
+  seed?: string;
+  type?: KeypairType;
+}
+
+function Create ({ className, onClose, onStatusChange, seed: propsSeed, t, type: propsType }: Props): React.ReactElement<Props> {
+  const { isDevelopment } = useContext(ApiContext);
+  const [{ address, deriveError, derivePath, isSeedValid, pairType, seed, seedType }, setAddress] = useState<AddressState>(generateSeed(propsSeed, '', propsSeed ? 'raw' : 'bip', propsType));
+  const [isConfirmationOpen, setIsConfirmationOpen] = useState(false);
+  const [{ isNameValid, name }, setName] = useState({ isNameValid: false, name: '' });
+  const [{ isPassValid, password }, setPassword] = useState({ isPassValid: false, password: '' });
+  const isValid = !!address && !deriveError && isNameValid && isPassValid && isSeedValid;
+
+  const _onChangePass = (password: string): void =>
+    setPassword({ isPassValid: isPasswordValid(password), password });
+  const _onChangeDerive = (newDerivePath: string): void =>
+    setAddress(updateAddress(seed, newDerivePath, seedType, pairType));
+  const _onChangeSeed = (newSeed: string): void =>
+    setAddress(updateAddress(newSeed, derivePath, seedType, pairType));
+  const _onChangePairType = (newPairType: KeypairType): void =>
+    setAddress(updateAddress(seed, derivePath, seedType, newPairType));
+  const _selectSeedType = (newSeedType: SeedType): void => {
+    if (newSeedType !== seedType) {
+      setAddress(generateSeed(null, derivePath, newSeedType, pairType));
+    }
+  };
+  const _onChangeName = (name: string): void => setName({ isNameValid: !!name.trim(), name });
+  const _toggleConfirmation = (): void => setIsConfirmationOpen(!isConfirmationOpen);
+  const context = useMyAccount()
+
+  const _onCommit = (): void => {
+    if (!isValid) {
+      return;
+    }
+
+    const status = createAccount(`${seed}${derivePath}`, pairType, name, password, t('created account'));
+    context.set(status.account as string)
+
+    _toggleConfirmation();
+    onStatusChange(status);
+    onClose();
+  };
+
+  return (
+    <Modal
+      className={className}
+      dimmer='inverted'
+      open
+    >
+      <Modal.Header>{t('Add an account via seed')}</Modal.Header>
+      {address && isConfirmationOpen && (
+        <CreateConfirmation
+          address={address}
+          name={name}
+          onCommit={_onCommit}
+          onClose={_toggleConfirmation}
+        />
+      )}
+      <Modal.Content>
+        <AddressRow
+          defaultName={name}
+          noDefaultNameOpacity
+          value={isSeedValid ? address : ''}
+        >
+          <Input
+            autoFocus
+            className='full'
+            help={t('Name given to this account. You can edit it. To use the account to validate or nominate, it is a good practice to append the function of the account in the name, e.g "name_you_want - stash".')}
+            isError={!isNameValid}
+            label={t('name')}
+            onChange={_onChangeName}
+            onEnter={_onCommit}
+            placeholder={t('new account')}
+            value={name}
+          />
+          <Input
+            className='full'
+            help={t('The private key for your account is derived from this seed. This seed must be kept secret as anyone in its possession has access to the funds of this account. If you validate, use the seed of the session account as the "--key" parameter of your node.')}
+            isAction
+            isError={!isSeedValid}
+            isReadOnly={seedType === 'dev'}
+            label={
+              seedType === 'bip'
+                ? t('mnemonic seed')
+                : seedType === 'dev'
+                  ? t('development seed')
+                  : t('seed (hex or string)')
+            }
+            onChange={_onChangeSeed}
+            onEnter={_onCommit}
+            value={seed}
+          >
+            <Dropdown
+              isButton
+              defaultValue={seedType}
+              onChange={_selectSeedType}
+              options={
+                (
+                  isDevelopment
+                    ? [{ value: 'dev', text: t('Development') }]
+                    : []
+                ).concat(
+                  { value: 'bip', text: t('Mnemonic') },
+                  { value: 'raw', text: t('Raw seed') }
+                )
+              }
+            />
+          </Input>
+          <Password
+            className='full'
+            help={t('This password is used to encrypt your private key. It must be strong and unique! You will need it to sign transactions with this account. You can recover this account using this password together with the backup file (generated in the next step).')}
+            isError={!isPassValid}
+            label={t('password')}
+            onChange={_onChangePass}
+            onEnter={_onCommit}
+            value={password}
+          />
+          <details
+            className='accounts--Creator-advanced'
+            open
+          >
+            <summary>{t('Advanced creation options')}</summary>
+            <Dropdown
+              defaultValue={pairType}
+              help={t('Determines what cryptography will be used to create this account. Note that to validate on Polkadot, the session account must use "ed25519".')}
+              label={t('keypair crypto type')}
+              onChange={_onChangePairType}
+              options={uiSettings.availableCryptos}
+            />
+            <Input
+              className='full'
+              help={t('You can set a custom derivation path for this account using the following syntax "/<soft-key>//<hard-key>///<password>". The "/<soft-key>" and "//<hard-key>" may be repeated and mixed`. The "///password" is optional and should only occur once.')}
+              isError={!!deriveError}
+              label={t('secret derivation path')}
+              onChange={_onChangeDerive}
+              onEnter={_onCommit}
+              placeholder={t('//hard/soft///password')}
+              value={derivePath}
+            />
+            {deriveError && (
+              <article className='error'>{deriveError}</article>
+            )}
+          </details>
+        </AddressRow>
+      </Modal.Content>
+      <Modal.Actions>
+        <Button.Group>
+          <Button
+            icon='cancel'
+            isNegative
+            label={t('Cancel')}
+            onClick={onClose}
+          />
+          <Button.Or />
+          <Button
+            icon='plus'
+            isDisabled={!isValid}
+            isPrimary
+            label={t('Save')}
+            onClick={_toggleConfirmation}
+          />
+        </Button.Group>
+      </Modal.Actions>
+    </Modal>
+  );
+}
+
+export default translate(
+  styled(Create)`
+    .accounts--Creator-advanced {
+      margin-top: 1rem;
+    }
+  `
+);

+ 60 - 0
packages/app-accounts/src/modals/CreateConfirmation.tsx

@@ -0,0 +1,60 @@
+// 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 React from 'react';
+import { AddressRow, Button, Modal } from '@polkadot/react-components';
+
+import translate from '../translate';
+
+interface Props extends I18nProps {
+  address: string;
+  name: string;
+  onClose: () => void;
+  onCommit: () => void;
+}
+
+function CreateConfirmation ({ address, name, onClose, onCommit, t }: Props): React.ReactElement<Props> | null {
+  return (
+    <Modal
+      dimmer='inverted'
+      open
+    >
+      <Modal.Header>
+        {t('Important notice')}
+      </Modal.Header>
+      <Modal.Content>
+        <AddressRow
+          defaultName={name}
+          isInline
+          noDefaultNameOpacity
+          value={address}
+        >
+          <p>{t('We will provide you with a generated backup file after your account is created. As long as you have access to your account you can always download this file later by clicking on "Backup" button from the Accounts section.')}</p>
+          <p>{t('Please make sure to save this file in a secure location as it is required, together with your password, to restore your account.')}</p>
+        </AddressRow>
+      </Modal.Content>
+      <Modal.Actions>
+        <Button.Group>
+          <Button
+            icon='cancel'
+            isNegative
+            label={t('Cancel')}
+            onClick={onClose}
+          />
+          <Button.Or />
+          <Button
+            icon='plus'
+            isPrimary
+            label={t('Create and backup account')}
+            onClick={onCommit}
+          />
+        </Button.Group>
+      </Modal.Actions>
+    </Modal>
+  );
+}
+
+export default translate(CreateConfirmation);

+ 240 - 0
packages/app-accounts/src/modals/Derive.tsx

@@ -0,0 +1,240 @@
+// 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 { KeyringPair } from '@polkadot/keyring/types';
+import { ActionStatus } from '@polkadot/react-components/Status/types';
+import { I18nProps } from '@polkadot/react-components/types';
+import { KeypairType } from '@polkadot/util-crypto/types';
+
+import React, { useContext, useEffect, useState } from 'react';
+import { AddressRow, Button, Input, InputAddress, Modal, Password, StatusContext } from '@polkadot/react-components';
+import { useDebounce } from '@polkadot/react-components/hooks';
+import keyring from '@polkadot/ui-keyring';
+import { isPasswordValid } from '@polkadot/joy-utils/accounts';
+import { keyExtractPath } from '@polkadot/util-crypto';
+
+import { useMyAccount } from '@polkadot/joy-utils/MyAccountContext';
+
+import translate from '../translate';
+import { downloadAccount } from '@polkadot/joy-utils/accounts';
+import CreateConfirmation from './CreateConfirmation';
+
+interface Props extends I18nProps {
+  from: string;
+  onClose: () => void;
+}
+
+interface DerivedAddress {
+  address: string | null;
+  deriveError: string | null;
+}
+
+function deriveValidate (suri: string, pairType: KeypairType): string | null {
+  try {
+    const { path } = keyExtractPath(suri);
+
+    // we don't allow soft for ed25519
+    if (pairType === 'ed25519' && path.some(({ isSoft }): boolean => isSoft)) {
+      return 'Soft derivation paths are not allowed on ed25519';
+    }
+  } catch (error) {
+    return error.message;
+  }
+
+  return null;
+}
+
+function createAccount (source: KeyringPair, suri: string, name: string, password: string, success: string): ActionStatus {
+  // we will fill in all the details below
+  const status = { action: 'create' } as ActionStatus;
+
+  try {
+    const derived = source.derive(suri);
+
+    derived.setMeta({ ...derived.meta, name, tags: [] });
+
+    const result = keyring.addPair(derived, password || '');
+    const { address } = result.pair;
+
+    status.account = address;
+    status.status = 'success';
+    status.message = success;
+
+    downloadAccount(result);
+  } catch (error) {
+    status.status = 'error';
+    status.message = error.message;
+  }
+
+  return status;
+}
+
+function Derive ({ className, from, onClose, t }: Props): React.ReactElement {
+  const { queueAction } = useContext(StatusContext);
+  const [source] = useState(keyring.getPair(from));
+  const [{ address, deriveError }, setDerived] = useState<DerivedAddress>({ address: null, deriveError: null });
+  const [isConfirmationOpen, setIsConfirmationOpen] = useState(false);
+  const [isLocked, setIsLocked] = useState(source.isLocked);
+  const [{ isNameValid, name }, setName] = useState({ isNameValid: false, name: '' });
+  const [{ isPassValid, password }, setPassword] = useState({ isPassValid: true, password: '' });
+  const [rootPass, setRootPass] = useState('');
+  const [suri, setSuri] = useState('');
+  const debouncedSuri = useDebounce(suri);
+  const isValid = !!address && !deriveError && isNameValid && isPassValid;
+  const context = useMyAccount()
+
+  useEffect((): void => {
+    setIsLocked(source.isLocked);
+  }, [source]);
+
+  useEffect((): void => {
+    setDerived((): DerivedAddress => {
+      let address: string | null = null;
+      const deriveError = deriveValidate(debouncedSuri, source.type);
+
+      if (!deriveError) {
+        const result = source.derive(suri);
+
+        address = result.address;
+      }
+
+      return { address, deriveError };
+    });
+  }, [debouncedSuri]);
+
+  const _onChangeName = (name: string): void => setName({ isNameValid: !!name.trim(), name });
+  const _onChangePass = (password: string): void => setPassword({ isPassValid: isPasswordValid(password), password });
+  const _toggleConfirmation = (): void => setIsConfirmationOpen(!isConfirmationOpen);
+  const _onUnlock = (): void => {
+    try {
+      source.decodePkcs8(rootPass);
+    } catch (error) {
+      console.error(error);
+    }
+
+    setIsLocked(source.isLocked);
+  };
+
+  const _onCommit = (): void => {
+    if (!isValid) {
+      return;
+    }
+
+    const status = createAccount(source, suri, name, password, t('created account'));
+    context.set(status.account as string)
+
+    _toggleConfirmation();
+    queueAction(status);
+    onClose();
+  };
+
+  const sourceStatic = (
+    <InputAddress
+      help={t('The selected account to perform the derivation on.')}
+      isDisabled
+      label={t('derive root account')}
+      value={from}
+    />
+  );
+
+  return (
+    <Modal
+      className={className}
+      dimmer='inverted'
+      open
+    >
+      <Modal.Header>{t('Derive account from pair')}</Modal.Header>
+      {address && isConfirmationOpen && (
+        <CreateConfirmation
+          address={address}
+          name={name}
+          onCommit={_onCommit}
+          onClose={_toggleConfirmation}
+        />
+      )}
+      <Modal.Content>
+        {isLocked && (
+          <>
+            {sourceStatic}
+            <Password
+              autoFocus
+              help={t('The password to unlock the selected account.')}
+              label={t('password')}
+              onChange={setRootPass}
+              value={rootPass}
+            />
+          </>
+        )}
+        {!isLocked && (
+          <AddressRow
+            defaultName={name}
+            noDefaultNameOpacity
+            value={deriveError ? '' : address}
+          >
+            {sourceStatic}
+            <Input
+              autoFocus
+              help={t('You can set a custom derivation path for this account using the following syntax "/<soft-key>//<hard-key>///<password>". The "/<soft-key>" and "//<hard-key>" may be repeated and mixed`. The "///password" is optional and should only occur once.')}
+              label={t('derivation path')}
+              onChange={setSuri}
+              placeholder={t('//hard/soft')}
+            />
+            <Input
+              className='full'
+              help={t('Name given to this account. You can edit it. To use the account to validate or nominate, it is a good practice to append the function of the account in the name, e.g "name_you_want - stash".')}
+              isError={!isNameValid}
+              label={t('name')}
+              onChange={_onChangeName}
+              onEnter={_onCommit}
+              placeholder={t('new account')}
+              value={name}
+            />
+            <Password
+              className='full'
+              help={t('This password is used to encrypt your private key. It must be strong and unique! You will need it to sign transactions with this account. You can recover this account using this password together with the backup file (generated in the next step).')}
+              isError={!isPassValid}
+              label={t('password')}
+              onChange={_onChangePass}
+              onEnter={_onCommit}
+              value={password}
+            />
+          </AddressRow>
+        )}
+      </Modal.Content>
+      <Modal.Actions>
+        <Button.Group>
+          <Button
+            icon='cancel'
+            isNegative
+            label={t('Cancel')}
+            onClick={onClose}
+          />
+          <Button.Or />
+          {isLocked
+            ? (
+              <Button
+                icon='lock'
+                isDisabled={!rootPass}
+                isPrimary
+                label={t('Unlock')}
+                onClick={_onUnlock}
+              />
+            )
+            : (
+              <Button
+                icon='plus'
+                isDisabled={!isValid}
+                isPrimary
+                label={t('Save')}
+                onClick={_toggleConfirmation}
+              />
+            )
+          }
+        </Button.Group>
+      </Modal.Actions>
+    </Modal>
+  );
+}
+
+export default translate(Derive);

+ 180 - 0
packages/app-accounts/src/modals/Import.tsx

@@ -0,0 +1,180 @@
+// 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 { KeyringPair$Json } from '@polkadot/keyring/types';
+import { I18nProps } from '@polkadot/react-components/types';
+import { ActionStatus } from '@polkadot/react-components/Status/types';
+import { ModalProps } from '../types';
+
+import React from 'react';
+import { MyAccountContext } from '@polkadot/joy-utils/MyAccountContext';
+import { isPasswordValid } from '@polkadot/joy-utils/accounts';
+import { AddressRow, Button, InputAddress, InputFile, Modal, Password, TxComponent } from '@polkadot/react-components';
+import { isHex, isObject, u8aToString } from '@polkadot/util';
+import keyring from '@polkadot/ui-keyring';
+
+import translate from '../translate';
+
+interface Props extends ModalProps, I18nProps {}
+
+interface State {
+  address: string | null;
+  isFileValid: boolean;
+  isPassValid: boolean;
+  json: KeyringPair$Json | null;
+  password: string;
+}
+
+class Import extends TxComponent<Props, State> {
+  static contextType = MyAccountContext;
+  context!: React.ContextType<typeof MyAccountContext>;
+
+  public state: State = {
+    address: null,
+    isFileValid: false,
+    isPassValid: true,
+    json: null,
+    password: ''
+  };
+
+  public render (): React.ReactNode {
+    const { onClose, t } = this.props;
+    const { isFileValid, isPassValid } = this.state;
+
+    return (
+      <Modal
+        dimmer='inverted'
+        open
+      >
+        <Modal.Header>{t('Add via backup file')}</Modal.Header>
+        {this.renderInput()}
+        <Modal.Actions>
+          <Button.Group>
+            <Button
+              icon='cancel'
+              isNegative
+              label={t('Cancel')}
+              onClick={onClose}
+            />
+            <Button.Or />
+            <Button
+              icon='sync'
+              isDisabled={!isFileValid || !isPassValid}
+              isPrimary
+              onClick={this.onSave}
+              label={t('Restore')}
+              ref={this.button}
+            />
+          </Button.Group>
+        </Modal.Actions>
+      </Modal>
+    );
+  }
+
+  private renderInput (): React.ReactNode {
+    const { t } = this.props;
+    const { address, isFileValid, isPassValid, json, password } = this.state;
+    const acceptedFormats = ['application/json', 'text/plain'].join(', ');
+
+    return (
+      <Modal.Content>
+        <AddressRow
+          defaultName={isFileValid && json ? json.meta.name : null}
+          noDefaultNameOpacity
+          value={isFileValid && address ? address : null}
+        >
+          <InputFile
+            accept={acceptedFormats}
+            className='full'
+            help={t('Select the JSON key file that was downloaded when you created the account. This JSON file contains your private key encrypted with your password.')}
+            isError={!isFileValid}
+            label={t('backup file')}
+            onChange={this.onChangeFile}
+            withLabel
+          />
+          <Password
+            autoFocus
+            className='full'
+            help={t('Type the password chosen at the account creation. It was used to encrypt your account\'s private key in the backup file.')}
+            isError={!isPassValid}
+            label={t('password')}
+            onChange={this.onChangePass}
+            onEnter={this.submit}
+            value={password}
+          />
+        </AddressRow>
+      </Modal.Content>
+    );
+  }
+
+  private onChangeFile = (file: Uint8Array): void => {
+    try {
+      const json = JSON.parse(u8aToString(file));
+      const publicKey = keyring.decodeAddress(json.address, true);
+      const address = keyring.encodeAddress(publicKey);
+      const isFileValid = publicKey.length === 32 && isHex(json.encoded) && isObject(json.meta) && (
+        Array.isArray(json.encoding.content)
+          ? json.encoding.content[0] === 'pkcs8'
+          : json.encoding.content === 'pkcs8'
+      );
+
+      this.setState({
+        address,
+        isFileValid,
+        json
+      });
+    } catch (error) {
+      this.setState({
+        address: null,
+        isFileValid: false,
+        json: null
+      });
+      console.error(error);
+    }
+  }
+
+  private onChangePass = (password: string): void => {
+    this.setState({
+      isPassValid: isPasswordValid(password),
+      password
+    });
+  }
+
+  private onSave = (): void => {
+    const { onClose, onStatusChange, t } = this.props;
+    const { json, password } = this.state;
+
+    if (!json) {
+      return;
+    }
+
+    const status: Partial<ActionStatus> = { action: 'restore' };
+
+    try {
+      const pair = keyring.restoreAccount(json, password);
+      const { address } = pair;
+
+      status.status = pair ? 'success' : 'error';
+      status.account = address;
+      status.message = t('account restored');
+
+      InputAddress.setLastValue('account', address);
+      this.context.set(address)
+    } catch (error) {
+      this.setState({ isPassValid: true });
+
+      status.status = 'error';
+      status.message = error.message;
+      console.error(error);
+    }
+
+    onStatusChange(status as ActionStatus);
+
+    if (status.status !== 'error') {
+      onClose();
+    }
+  }
+}
+
+export default translate(Import);

+ 119 - 0
packages/app-accounts/src/modals/Qr.tsx

@@ -0,0 +1,119 @@
+// 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 { ModalProps } from '../types';
+
+import React, { useState } from 'react';
+import styled from 'styled-components';
+import { AddressRow, Button, Input, InputAddress, Modal } from '@polkadot/react-components';
+import { QrScanAddress } from '@polkadot/react-qr';
+import keyring from '@polkadot/ui-keyring';
+
+import { useMyAccount } from '@polkadot/joy-utils/MyAccountContext';
+
+import translate from '../translate';
+
+interface Scanned {
+  address: string;
+  genesisHash: string;
+}
+
+interface Props extends I18nProps, ModalProps {
+  className?: string;
+}
+
+function QrModal ({ className, onClose, onStatusChange, t }: Props): React.ReactElement<Props> {
+  const [{ isNameValid, name }, setName] = useState({ isNameValid: false, name: '' });
+  const [scanned, setScanned] = useState<Scanned | null>(null);
+  const context = useMyAccount()
+
+  const _onNameChange = (name: string): void => setName({ isNameValid: !!name.trim(), name });
+
+  const _onSave = (): void => {
+    if (!scanned || !isNameValid) {
+      return;
+    }
+
+    const { address, genesisHash } = scanned;
+
+    keyring.addExternal(address, { genesisHash, name: name.trim() });
+    InputAddress.setLastValue('account', address);
+    context.set(address)
+
+    onStatusChange({
+      account: address,
+      action: 'create',
+      message: t('created account'),
+      status: 'success'
+    });
+    onClose();
+  };
+
+  return (
+    <Modal
+      className={className}
+      dimmer='inverted'
+      open
+    >
+      <Modal.Header>{t('Add account via Qr')}</Modal.Header>
+      <Modal.Content>
+        {
+          scanned
+            ? (
+              <>
+                <AddressRow
+                  defaultName={name}
+                  noDefaultNameOpacity
+                  value={scanned.address}
+                />
+                <Input
+                  autoFocus
+                  className='full'
+                  help={t('Name given to this account. You can change it at any point in the future.')}
+                  isError={!isNameValid}
+                  label={t('name')}
+                  onChange={_onNameChange}
+                  onEnter={_onSave}
+                  value={name}
+                />
+              </>
+            )
+            : (
+              <div className='qr-wrapper'>
+                <QrScanAddress onScan={setScanned} />
+              </div>
+            )
+        }
+      </Modal.Content>
+      <Modal.Actions>
+        <Button.Group>
+          <Button
+            icon='cancel'
+            isNegative
+            label={t('Cancel')}
+            onClick={onClose}
+          />
+          <Button.Or />
+          <Button
+            icon='sign-in'
+            isDisabled={!scanned || !isNameValid}
+            isPrimary
+            onClick={_onSave}
+            label={t('Create')}
+          />
+        </Button.Group>
+      </Modal.Actions>
+    </Modal>
+  );
+}
+
+export default translate(
+  styled(QrModal)`
+    .qr-wrapper {
+      margin: 0 auto;
+      max-width: 30rem;
+    }
+  `
+);

+ 178 - 0
packages/app-accounts/src/modals/Transfer.tsx

@@ -0,0 +1,178 @@
+/* eslint-disable @typescript-eslint/camelcase */
+// 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 { SubmittableExtrinsic } from '@polkadot/api/promise/types';
+import { DerivedFees } from '@polkadot/api-derive/types';
+import { I18nProps } from '@polkadot/react-components/types';
+
+import BN from 'bn.js';
+import React, { useContext, useEffect, useState } from 'react';
+import styled from 'styled-components';
+import { Button, InputAddress, InputBalance, Modal, TxButton } from '@polkadot/react-components';
+import { Available } from '@polkadot/react-query';
+import Checks from '@polkadot/react-signer/Checks';
+import { ApiContext } from '@polkadot/react-api';
+
+import translate from '../translate';
+
+interface Props extends I18nProps {
+  balances_fees?: DerivedFees;
+  className?: string;
+  onClose: () => void;
+  recipientId?: string;
+  senderId?: string;
+}
+
+const ZERO = new BN(0);
+
+// TODO Re-enable when we have proper fee calculation (incl. weights)
+// async function calcMax (api: ApiPromise, balances_fees: DerivedFees | undefined, senderId: string, recipientId: string): Promise<BN> {
+//   let maxBalance = new BN(1);
+
+//   if (!balances_fees) {
+//     return maxBalance;
+//   }
+
+//   const { transferFee, transactionBaseFee, transactionByteFee, creationFee } = balances_fees;
+
+//   const [senderNonce, senderBalances, recipientBalances] = await Promise.all([
+//     api.query.system.accountNonce<Index>(senderId),
+//     api.derive.balances.all(senderId),
+//     api.derive.balances.all(recipientId)
+//   ]);
+
+//   let prevMax = new BN(0);
+
+//   // something goes screwy here when we move this out of the component :(
+//   let extrinsic: any;
+
+//   while (!prevMax.eq(maxBalance)) {
+//     prevMax = maxBalance;
+//     extrinsic = api.tx.balances.transfer(senderNonce, prevMax);
+
+//     const txLength = calcTxLength(extrinsic, senderNonce);
+//     const fees = transactionBaseFee
+//       .add(transactionByteFee.mul(txLength))
+//       .add(transferFee)
+//       .add(recipientBalances.availableBalance.isZero() ? creationFee : ZERO);
+
+//     maxBalance = bnMax(senderBalances.availableBalance.sub(fees), ZERO);
+//   }
+
+//   return maxBalance;
+// }
+
+function Transfer ({ className, onClose, recipientId: propRecipientId, senderId: propSenderId, t }: Props): React.ReactElement<Props> {
+  const { api } = useContext(ApiContext);
+  const [amount, setAmount] = useState<BN | undefined>(new BN(0));
+  const [extrinsic, setExtrinsic] = useState<SubmittableExtrinsic | null>(null);
+  const [hasAvailable, setHasAvailable] = useState(true);
+  const [maxBalance] = useState(new BN(0));
+  const [recipientId, setRecipientId] = useState<string | null>(propRecipientId || null);
+  const [senderId, setSenderId] = useState<string | null>(propSenderId || null);
+
+  useEffect((): void => {
+    if (senderId && recipientId) {
+      setExtrinsic(api.tx.balances.transfer(recipientId, amount || ZERO));
+
+      // We currently have not enabled the max functionality - we don't take care of weights
+      // calcMax(api, balances_fees, senderId, recipientId)
+      //   .then(([maxBalance]): void => setMaxBalance(maxBalance))
+      //   .catch((error: Error): void => console.error(error));
+    }
+  }, [amount, recipientId, senderId]);
+
+  const transferrable = <span className='label'>{t('transferrable ')}</span>;
+
+  return (
+    <Modal
+      className='app--accounts-Modal'
+      dimmer='inverted'
+      open
+    >
+      <Modal.Header>{t('Send funds')}</Modal.Header>
+      <Modal.Content>
+        <div className={className}>
+          <InputAddress
+            defaultValue={propSenderId}
+            help={t('The account you will send funds from.')}
+            isDisabled={!!propSenderId}
+            label={t('send from account')}
+            labelExtra={<Available label={transferrable} params={senderId} />}
+            onChange={setSenderId}
+            type='account'
+          />
+          <InputAddress
+            defaultValue={propRecipientId}
+            help={t('Select a contact or paste the address you want to send funds to.')}
+            isDisabled={!!propRecipientId}
+            label={t('send to address')}
+            labelExtra={<Available label={transferrable} params={recipientId} />}
+            onChange={setRecipientId}
+            type='allPlus'
+          />
+          <InputBalance
+            help={t('Type the amount you want to transfer. Note that you can select the unit on the right e.g sending 1 milli is equivalent to sending 0.001.')}
+            isError={!hasAvailable}
+            label={t('amount')}
+            maxValue={maxBalance}
+            onChange={setAmount}
+            withMax
+          />
+          <Checks
+            accountId={senderId}
+            extrinsic={extrinsic}
+            isSendable
+            onChange={setHasAvailable}
+          />
+        </div>
+      </Modal.Content>
+      <Modal.Actions>
+        <Button.Group>
+          <Button
+            icon='cancel'
+            isNegative
+            label={t('Cancel')}
+            onClick={onClose}
+          />
+          <Button.Or />
+          <TxButton
+            accountId={senderId}
+            extrinsic={extrinsic}
+            icon='send'
+            isDisabled={!hasAvailable}
+            isPrimary
+            label={t('Make Transfer')}
+            onStart={onClose}
+            withSpinner={false}
+          />
+        </Button.Group>
+      </Modal.Actions>
+    </Modal>
+  );
+}
+
+export default translate(
+  styled(Transfer)`
+    article.padded {
+      box-shadow: none;
+      margin-left: 2rem;
+    }
+
+    .balance {
+      margin-bottom: 0.5rem;
+      text-align: right;
+      padding-right: 1rem;
+
+      .label {
+        opacity: 0.7;
+      }
+    }
+
+    label.with-help {
+      flex-basis: 10rem;
+    }
+  `
+);

+ 1 - 1
packages/app-accounts/src/translate.ts

@@ -4,4 +4,4 @@
 
 import { withTranslation } from 'react-i18next';
 
-export default withTranslation(['accounts', 'ui']);
+export default withTranslation(['app-accounts']);

+ 21 - 5
packages/app-accounts/src/types.ts

@@ -2,12 +2,28 @@
 // This software may be modified and distributed under the terms
 // of the Apache-2.0 license. See the LICENSE file for details.
 
-import { AppProps } from '@polkadot/ui-app/types';
+import { AppProps } from '@polkadot/react-components/types';
+import { ActionStatus } from '@polkadot/react-components/Status/types';
 
-export type LocationProps = {
+import { WithTranslation } from 'react-i18next';
+
+export interface LocationProps {
+  location: any;
   match: {
-    params: { [index: string]: any }
-  }
-};
+    params: Record<string, string>;
+  };
+}
+
+export interface BareProps {
+  className?: string;
+  style?: Record<string, any>;
+}
+
+export type I18nProps = BareProps & WithTranslation;
 
 export type ComponentProps = AppProps & LocationProps;
+
+export interface ModalProps {
+  onClose: () => void;
+  onStatusChange: (status: ActionStatus) => void;
+}

+ 26 - 6
packages/app-accounts/src/vanitygen/calculate.ts

@@ -2,11 +2,13 @@
 // This software may be modified and distributed under the terms
 // of the Apache-2.0 license. See the LICENSE file for details.
 
-import { Generator$Calculation, Generator$Options } from './types';
+import { GeneratorCalculation, GeneratorOptions } from './types';
 
-function calculateAt (atOffset: number, test: Array<string>, address: string): Generator$Calculation {
+const MAX_OFFSET = 5;
+
+function calculateAtOne (atOffset: number, test: string[], address: string): GeneratorCalculation {
   return {
-    count: test.reduce((count, c, index) => {
+    count: test.reduce((count, c, index): number => {
       if (index === count) {
         count += (c === '?' || c === address.charAt(index + atOffset)) ? 1 : 0;
       }
@@ -17,7 +19,26 @@ function calculateAt (atOffset: number, test: Array<string>, address: string): G
   };
 }
 
-export default function calculate (test: Array<string>, _address: string, { atOffset = -1, withCase = false }: Generator$Options): Generator$Calculation {
+function calculateAt (atOffset: number, test: string[][], address: string): GeneratorCalculation {
+  let bestCount = 0;
+  let bestOffset = 1;
+
+  for (let i = 0; i < test.length; i++) {
+    const { count, offset } = calculateAtOne(atOffset, test[i], address);
+
+    if (count > bestCount) {
+      bestCount = count;
+      bestOffset = offset;
+    }
+  }
+
+  return {
+    count: bestCount,
+    offset: bestOffset
+  };
+}
+
+export default function calculate (test: string[][], _address: string, { atOffset = -1, withCase = false }: GeneratorOptions): GeneratorCalculation {
   const address = withCase
     ? _address
     : _address.toLowerCase();
@@ -26,11 +47,10 @@ export default function calculate (test: Array<string>, _address: string, { atOf
     return calculateAt(atOffset, test, address);
   }
 
-  const maxOffset = address.length - test.length - 1;
   let bestCount = 0;
   let bestOffset = 1;
 
-  for (let index = 1; index < maxOffset; index++) {
+  for (let index = 0; index < MAX_OFFSET; index++) {
     const { count, offset } = calculateAt(index, test, address);
 
     if (count > bestCount) {

+ 81 - 37
packages/app-accounts/src/vanitygen/cli.ts

@@ -3,36 +3,59 @@
 // This software may be modified and distributed under the terms
 // of the Apache-2.0 license. See the LICENSE file for details.
 
+import { KeypairType } from '@polkadot/util-crypto/types';
+import { GeneratorOptions } from './types';
+
 import yargs from 'yargs';
 import chalk from 'chalk';
 import { u8aToHex } from '@polkadot/util';
+import { cryptoWaitReady, setSS58Format } from '@polkadot/util-crypto';
 
 import generator from '.';
 import matchRegex from './regex';
 
-type Best = {
-  address: string,
-  count: number,
-  offset: number,
-  seed?: Uint8Array
-};
+interface Best {
+  address: string;
+  count: number;
+  mnemonic?: string;
+  offset: number;
+  seed?: Uint8Array;
+  withCase?: boolean;
+}
 
-const { match, withCase } = yargs
+const { match, mnemonic, network, type, withCase } = yargs
   .option('match', {
-    default: 'EEEEE'
+    default: 'Test',
+    type: 'string'
+  })
+  .option('mnemonic', {
+    default: false,
+    type: 'boolean'
+  })
+  .option('network', {
+    choices: ['substrate', 'polkadot', 'kusama'],
+    default: 'substrate'
+  })
+  .option('type', {
+    choices: ['ed25519', 'sr25519'],
+    default: 'sr25519'
   })
   .option('withCase', {
-    default: true
+    default: false,
+    type: 'boolean'
   })
   .argv;
 
 const INDICATORS = ['|', '/', '-', '\\'];
 const NUMBER_REGEX = new RegExp('(\\d+?)(?=(\\d{3})+(?!\\d)|$)', 'g');
 
-const options = {
+const options: GeneratorOptions = {
   match,
+  network,
   runs: 50,
-  withCase
+  type: type as KeypairType,
+  withCase,
+  withHex: !mnemonic
 };
 const startAt = Date.now();
 let best: Best = {
@@ -40,17 +63,34 @@ let best: Best = {
   count: -1,
   offset: 65536
 };
-let total: number = 0;
+let total = 0;
 let indicator = -1;
+const tests = options.match.split(',');
+
+tests.forEach((test): void => {
+  if (!matchRegex.test(test)) {
+    console.error("Invalid character found in match string, allowed is '1-9' (no '0'), 'A-H, J-N & P-Z' (no 'I' or 'O'), 'a-k & m-z' (no 'l') and '?' (wildcard)");
+    process.exit(-1);
+  }
+});
 
-if (!matchRegex.test(match)) {
-  console.error("Invalid character found in match string, allowed is '1-9' (no '0'), 'A-H, J-N & P-Z' (no 'I' or 'O'), 'a-k & m-z' (no 'l') and '?' (wildcard)");
-  process.exit(-1);
+switch (network) {
+  case 'kusama':
+    setSS58Format(2);
+    break;
+
+  case 'polkadot':
+    setSS58Format(0);
+    break;
+
+  default:
+    setSS58Format(42);
+    break;
 }
 
 console.log(options);
 
-function showProgress () {
+function showProgress (): void {
   const elapsed = (Date.now() - startAt) / 1000;
 
   indicator++;
@@ -62,28 +102,32 @@ function showProgress () {
   process.stdout.write(`\r[${INDICATORS[indicator]}] ${(total.toString().match(NUMBER_REGEX) || []).join(',')} keys in ${(elapsed).toFixed(2)}s (${(total / elapsed).toFixed(0)} keys/s)`);
 }
 
-function showBest () {
-  const { address, count, offset, seed } = best;
+function showBest (): void {
+  const { address, count, mnemonic, offset, seed } = best;
 
-  console.log(`\r::: ${address.slice(0, offset)}${chalk.cyan(address.slice(offset, count + offset))}${address.slice(count + offset)} <= ${u8aToHex(seed)} (count=${count}, offset=${offset})`);
+  console.log(`\r::: ${address.slice(0, offset)}${chalk.cyan(address.slice(offset, count + offset))}${address.slice(count + offset)} <= ${u8aToHex(seed)} (count=${count}, offset=${offset})${mnemonic ? '\n                                                        ' + mnemonic : ''}`);
 }
 
-while (true) {
-  const nextBest = generator(options).found.reduce((best, match) => {
-    if ((match.count > best.count) || ((match.count === best.count) && (match.offset <= best.offset))) {
-      return match;
+cryptoWaitReady()
+  .then((): void => {
+    while (true) {
+      const nextBest = generator(options).found.reduce((best, match): Best => {
+        if ((match.count > best.count) || ((match.count === best.count) && (match.offset <= best.offset))) {
+          return match;
+        }
+
+        return best;
+      }, best);
+
+      total += options.runs;
+
+      if (nextBest.address !== best.address) {
+        best = nextBest;
+        showBest();
+        showProgress();
+      } else if ((total % (options.withHex ? 1000 : 100)) === 0) {
+        showProgress();
+      }
     }
-
-    return best;
-  }, best);
-
-  total += options.runs;
-
-  if (nextBest.address !== best.address) {
-    best = nextBest;
-    showBest();
-    showProgress();
-  } else if ((total % 1000) === 0) {
-    showProgress();
-  }
-}
+  })
+  .catch((error: Error): void => console.error(error));

+ 14 - 6
packages/app-accounts/src/vanitygen/generate.ts

@@ -2,21 +2,29 @@
 // This software may be modified and distributed under the terms
 // of the Apache-2.0 license. See the LICENSE file for details.
 
-import { Generator$Match, Generator$Options } from './types';
+import { GeneratorMatch, GeneratorOptions } from './types';
 
-import { encodeAddress } from '@polkadot/keyring';
-import { naclKeypairFromSeed, randomAsU8a } from '@polkadot/util-crypto';
+import { encodeAddress, mnemonicGenerate, naclKeypairFromSeed, randomAsU8a, schnorrkelKeypairFromSeed, mnemonicToMiniSecret } from '@polkadot/util-crypto';
 
 import calculate from './calculate';
 
-export default function generator (test: Array<string>, options: Generator$Options): Generator$Match {
-  const seed = randomAsU8a();
-  const address = encodeAddress(naclKeypairFromSeed(seed).publicKey);
+export default function generator (test: string[][], options: GeneratorOptions): GeneratorMatch {
+  const mnemonic = options.withHex
+    ? undefined
+    : mnemonicGenerate(12);
+  const seed = mnemonic
+    ? mnemonicToMiniSecret(mnemonic)
+    : randomAsU8a();
+  const pair = options.type === 'sr25519'
+    ? schnorrkelKeypairFromSeed(seed)
+    : naclKeypairFromSeed(seed);
+  const address = encodeAddress(pair.publicKey);
   const { count, offset } = calculate(test, address, options);
 
   return {
     address,
     count,
+    mnemonic,
     offset,
     seed
   };

+ 4 - 6
packages/app-accounts/src/vanitygen/index.ts

@@ -2,17 +2,15 @@
 // This software may be modified and distributed under the terms
 // of the Apache-2.0 license. See the LICENSE file for details.
 
-import { Generator$Matches, Generator$Result, Generator$Options } from './types';
+import { GeneratorMatches, GeneratorResult, GeneratorOptions } from './types';
 
 import generate from './generate';
 
-export default function generator (options: Generator$Options): Generator$Result {
+export default function generator (options: GeneratorOptions): GeneratorResult {
   const { match, runs = 10, withCase = false } = options;
-  const test = withCase
-    ? match.split('')
-    : match.toLowerCase().split('');
+  const test = (withCase ? match : match.toLowerCase()).split(',').map((c): string[] => c.split(''));
   const startAt = Date.now();
-  const found: Generator$Matches = [];
+  const found: GeneratorMatches = [];
 
   while (found.length !== runs) {
     found.push(generate(test, options));

+ 2 - 2
packages/app-accounts/src/vanitygen/sort.ts

@@ -2,7 +2,7 @@
 // This software may be modified and distributed under the terms
 // of the Apache-2.0 license. See the LICENSE file for details.
 
-import { Generator$Match } from './types';
+import { GeneratorMatch } from './types';
 
 function numberSort (a: number, b: number): number {
   if (a > b) {
@@ -14,7 +14,7 @@ function numberSort (a: number, b: number): number {
   return 0;
 }
 
-export default function sort (a: Generator$Match, b: Generator$Match): number {
+export default function sort (a: GeneratorMatch, b: GeneratorMatch): number {
   const countResult = numberSort(a.count, b.count);
 
   if (countResult !== 0) {

+ 24 - 18
packages/app-accounts/src/vanitygen/types.d.ts

@@ -2,26 +2,32 @@
 // This software may be modified and distributed under the terms
 // of the Apache-2.0 license. See the LICENSE file for details.
 
-export type Generator$Calculation = {
-  count: number,
-  offset: number
-};
+import { KeypairType } from '@polkadot/util-crypto/types';
 
-export type Generator$Match = Generator$Calculation & {
-  address: string,
-  seed: Uint8Array
-};
+export interface GeneratorCalculation {
+  count: number;
+  offset: number;
+}
+
+export interface GeneratorMatch extends GeneratorCalculation {
+  address: string;
+  mnemonic?: string;
+  seed: Uint8Array;
+}
 
-export type Generator$Matches = Array<Generator$Match>;
+export type GeneratorMatches = GeneratorMatch[];
 
-export type Generator$Options = {
-  atOffset?: number,
-  match: string,
-  runs?: number,
-  withCase?: boolean
-};
+export interface GeneratorOptions {
+  atOffset?: number;
+  match: string;
+  network?: string;
+  runs: number;
+  type: KeypairType;
+  withCase?: boolean;
+  withHex?: boolean;
+}
 
-export type Generator$Result = {
-  elapsed: number,
-  found: Generator$Matches
+export interface GeneratorResult {
+  elapsed: number;
+  found: GeneratorMatches;
 }

+ 0 - 8
packages/app-accounts/src/worker-loader.d.ts

@@ -1,8 +0,0 @@
-declare module 'worker-loader!*' {
-  class WebpackWorker extends Worker {
-    constructor();
-  }
-
-  // @ts-ignore valid according to integration instructions: https://github.com/webpack-contrib/worker-loader#integrating-with-typescript
-  export = WebpackWorker;
-}

+ 3 - 3
packages/app-address-book/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@polkadot/app-address-book",
-  "version": "0.32.0-beta.6",
+  "version": "0.37.0-beta.63",
   "main": "index.js",
   "repository": "https://github.com/polkadot-js/apps.git",
   "author": "Jaco Greeff <jacogr@gmail.com>",
@@ -10,7 +10,7 @@
   "contributors": [],
   "license": "Apache-2.0",
   "dependencies": {
-    "@babel/runtime": "^7.4.3",
-    "@polkadot/ui-app": "^0.32.0-beta.6"
+    "@babel/runtime": "^7.7.1",
+    "@polkadot/react-components": "^0.37.0-beta.63"
   }
 }

+ 153 - 0
packages/app-address-book/src/Address.tsx

@@ -0,0 +1,153 @@
+// 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 { KeyringAddress } from '@polkadot/ui-keyring/types';
+import { ActionStatus } from '@polkadot/react-components/Status/types';
+import { I18nProps } from '@polkadot/react-components/types';
+
+import React, { useEffect, useState } from 'react';
+import styled from 'styled-components';
+import { AddressCard, AddressInfo, Button, ChainLock, Forget } from '@polkadot/react-components';
+import keyring from '@polkadot/ui-keyring';
+
+import Transfer from '@polkadot/app-accounts/modals/Transfer';
+
+import translate from './translate';
+
+interface Props extends I18nProps {
+  address: string;
+  className?: string;
+}
+
+const WITH_BALANCE = { available: true, bonded: true, free: true, locked: true, reserved: true, total: true };
+const WITH_EXTENDED = { nonce: true };
+
+const isEditable = true;
+
+function Address ({ address, className, t }: Props): React.ReactElement<Props> {
+  const [current, setCurrent] = useState<KeyringAddress | null>(null);
+  const [genesisHash, setGenesisHash] = useState<string | null>(null);
+  const [isForgetOpen, setIsForgetOpen] = useState(false);
+  const [isTransferOpen, setIsTransferOpen] = useState(false);
+
+  useEffect((): void => {
+    const current = keyring.getAddress(address);
+
+    setCurrent(current || null);
+    setGenesisHash((current && current.meta.genesisHash) || null);
+  }, []);
+
+  const _toggleForget = (): void => setIsForgetOpen(!isForgetOpen);
+  const _toggleTransfer = (): void => setIsTransferOpen(!isTransferOpen);
+  const _onForget = (): void => {
+    if (address) {
+      const status: Partial<ActionStatus> = {
+        account: address,
+        action: 'forget'
+      };
+
+      try {
+        keyring.forgetAddress(address);
+        status.status = 'success';
+        status.message = t('address forgotten');
+      } catch (error) {
+        status.status = 'error';
+        status.message = error.message;
+      }
+    }
+  };
+  const _onGenesisChange = (genesisHash: string | null): void => {
+    setGenesisHash(genesisHash);
+
+    const account = keyring.getAddress(address);
+
+    account && keyring.saveAddress(address, { ...account.meta, genesisHash });
+
+    setGenesisHash(genesisHash);
+  };
+
+  return (
+    <AddressCard
+      buttons={
+        <div className='addresses--Address-buttons buttons'>
+          <div className='actions'>
+            {isEditable && (
+              <Button
+                isNegative
+                onClick={_toggleForget}
+                icon='trash'
+                key='forget'
+                size='small'
+                tooltip={t('Forget this address')}
+              />
+            )}
+            <Button
+              icon='paper plane'
+              isPrimary
+              key='deposit'
+              label={t('deposit')}
+              onClick={_toggleTransfer}
+              size='small'
+              tooltip={t('Send funds to this address')}
+            />
+          </div>
+          {isEditable && (
+            <div className='others'>
+              <ChainLock
+                genesisHash={genesisHash}
+                onChange={_onGenesisChange}
+              />
+            </div>
+          )}
+        </div>
+      }
+      className={className}
+      isEditable={isEditable}
+      type='address'
+      value={address}
+      withExplorer
+      withIndexOrAddress={false}
+      withTags
+    >
+      {address && current && (
+        <>
+          {isForgetOpen && (
+            <Forget
+              address={current.address}
+              onForget={_onForget}
+              key='modal-forget-account'
+              mode='address'
+              onClose={_toggleForget}
+            />
+          )}
+          {isTransferOpen && (
+            <Transfer
+              key='modal-transfer'
+              onClose={_toggleTransfer}
+              recipientId={address}
+            />
+          )}
+        </>
+      )}
+      <AddressInfo
+        address={address}
+        withBalance={WITH_BALANCE}
+        withExtended={WITH_EXTENDED}
+      />
+    </AddressCard>
+  );
+}
+
+export default translate(
+  styled(Address)`
+    .addresses--Address-buttons {
+      text-align: right;
+
+      .others {
+        margin-right: 0.125rem;
+        margin-top: 0.25rem;
+      }
+    }
+  `
+);

+ 0 - 204
packages/app-address-book/src/Creator.tsx

@@ -1,204 +0,0 @@
-// Copyright 2017-2019 @polkadot/app-address-book 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/ui-app/types';
-import { ComponentProps } from './types';
-
-import React from 'react';
-
-import { AddressSummary, Button, Input } from '@polkadot/ui-app';
-import { ActionStatus } from '@polkadot/ui-app/Status/types';
-import { InputAddress } from '@polkadot/ui-app/InputAddress';
-import keyring from '@polkadot/ui-keyring';
-
-import translate from './translate';
-
-type Props = ComponentProps & I18nProps;
-
-type State = {
-  address: string,
-  isAddressExisting: boolean,
-  isAddressValid: boolean,
-  isNameValid: boolean,
-  isValid: boolean,
-  name: string
-};
-
-class Creator extends React.PureComponent<Props, State> {
-  state: State;
-
-  constructor (props: Props) {
-    super(props);
-
-    this.state = this.emptyState();
-  }
-
-  render () {
-    const { address } = this.state;
-
-    return (
-      <div className='address-book--Creator'>
-        <div className='ui--grid'>
-          <AddressSummary
-            className='shrink'
-            value={address}
-            withBonded
-          />
-          {this.renderInput()}
-        </div>
-        {this.renderButtons()}
-      </div>
-    );
-  }
-
-  renderButtons () {
-    const { t } = this.props;
-    const { isValid } = this.state;
-
-    return (
-      <Button.Group>
-        <Button
-          onClick={this.onDiscard}
-          label={t('Reset')}
-        />
-        <Button.Or />
-        <Button
-          isDisabled={!isValid}
-          isPrimary
-          onClick={this.onCommit}
-          label={t('Save')}
-        />
-      </Button.Group>
-    );
-  }
-
-  renderInput () {
-    const { t } = this.props;
-    const { address, isAddressValid, isNameValid, name } = this.state;
-
-    return (
-      <div className='grow'>
-        <div className='ui--row'>
-          <Input
-            autoFocus
-            className='full'
-            isError={!isAddressValid}
-            label={t('add the following address')}
-            onChange={this.onChangeAddress}
-            value={address}
-          />
-        </div>
-        <div className='ui--row'>
-          <Input
-            className='full'
-            isError={!isNameValid}
-            label={t('name the entry')}
-            onChange={this.onChangeName}
-            value={name}
-          />
-        </div>
-      </div>
-    );
-  }
-
-  emptyState (): State {
-    return {
-      address: '',
-      isAddressExisting: false,
-      isAddressValid: false,
-      isNameValid: true,
-      isValid: false,
-      name: 'new address'
-    };
-  }
-
-  nextState (newState: State, allowEdit: boolean = false): void {
-    this.setState(
-      (prevState: State, props: Props): State => {
-        let { address = prevState.address, name = prevState.name } = newState;
-
-        let isAddressValid = true;
-        let isAddressExisting = false;
-        let newAddress = address;
-
-        try {
-          newAddress = keyring.encodeAddress(
-            keyring.decodeAddress(address)
-          );
-          isAddressValid = keyring.isAvailable(newAddress);
-
-          if (!isAddressValid) {
-            const old = keyring.getAddress(newAddress);
-
-            if (old.isValid) {
-              if (!allowEdit) {
-                name = old.getMeta().name || name;
-              }
-
-              isAddressExisting = true;
-              isAddressValid = true;
-            }
-          }
-        } catch (error) {
-          isAddressValid = false;
-        }
-
-        const isNameValid = !!name;
-
-        return {
-          address: newAddress,
-          isAddressExisting,
-          isAddressValid,
-          isNameValid,
-          isValid: isAddressValid && isNameValid,
-          name
-        };
-      }
-    );
-  }
-
-  onChangeAddress = (address: string): void => {
-    this.nextState({ address } as State);
-  }
-
-  onChangeName = (name: string): void => {
-    this.nextState({ name } as State, true);
-  }
-
-  onCommit = (): void => {
-    const { basePath, onStatusChange, t } = this.props;
-    const { address, isAddressExisting, name } = this.state;
-
-    const status = {
-      action: 'create'
-    } as ActionStatus;
-
-    try {
-      keyring.saveAddress(address, { name });
-
-      status.account = address;
-      status.status = address ? 'success' : 'error';
-      status.message = isAddressExisting
-        ? t('address edited')
-        : t('address created');
-
-      InputAddress.setLastValue('address', address);
-    } catch (error) {
-      status.status = 'error';
-      status.message = error.message;
-    }
-
-    onStatusChange(status);
-
-    if (status.status !== 'error') {
-      window.location.hash = basePath;
-    }
-  }
-
-  onDiscard = (): void => {
-    this.setState(this.emptyState());
-  }
-}
-
-export default translate(Creator);

+ 0 - 265
packages/app-address-book/src/Editor.tsx

@@ -1,265 +0,0 @@
-// Copyright 2017-2019 @polkadot/app-address-book 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 { KeyringAddress } from '@polkadot/ui-keyring/types';
-import { I18nProps } from '@polkadot/ui-app/types';
-import { ComponentProps } from './types';
-
-import React from 'react';
-import { AddressSummary, Button, Input, InputAddress, Labelled } from '@polkadot/ui-app';
-import { ActionStatus } from '@polkadot/ui-app/Status/types';
-import keyring from '@polkadot/ui-keyring';
-
-import Forgetting from './Forgetting';
-import translate from './translate';
-import MemoView from '@polkadot/joy-utils/memo/MemoView';
-
-type Props = ComponentProps & I18nProps;
-
-type State = {
-  current: KeyringAddress | null,
-  editedName: string,
-  isEdited: boolean,
-  isForgetOpen: boolean
-};
-
-class Editor extends React.PureComponent<Props, State> {
-  state: State;
-
-  constructor (props: Props) {
-    super(props);
-
-    this.state = this.createState(null);
-  }
-
-  render () {
-    const { isForgetOpen, current } = this.state;
-    return (
-      <div className='address-book--Editor'>
-        <Forgetting
-          isOpen={isForgetOpen}
-          onClose={this.toggleForget}
-          doForget={this.onForget}
-          currentAddress={current}
-        />
-        {this.renderData()}
-        {this.renderButtons()}
-      </div>
-    );
-  }
-
-  renderButtons () {
-    const { t } = this.props;
-    const { current, isEdited } = this.state;
-
-    if (!current) {
-      return null;
-    }
-
-    return (
-      <Button.Group>
-        <Button
-          isNegative
-          onClick={this.toggleForget}
-          label={t('Forget')}
-        />
-        <Button.Group.Divider />
-        <Button
-          isDisabled={!isEdited}
-          onClick={this.onDiscard}
-          label={t('Reset')}
-        />
-        <Button.Or />
-        <Button
-          isDisabled={!isEdited}
-          isPrimary
-          onClick={this.onCommit}
-          label={t('Save')}
-        />
-      </Button.Group>
-    );
-  }
-
-  renderData () {
-    const { t } = this.props;
-    const { current, editedName } = this.state;
-
-    const address = current
-      ? current.address()
-      : undefined;
-
-    return (
-      <div className='ui--grid'>
-        <AddressSummary
-          className='shrink'
-          value={address || ''}
-          withBonded
-        />
-        <div className='grow'>
-          <div className='ui--row'>
-            <InputAddress
-              className='full'
-              hideAddress
-              isInput={false}
-              label={t('edit the selected address')}
-              onChange={this.onChangeAddress}
-              type='address'
-              value={address}
-            />
-          </div>
-          <div className='ui--row'>
-            <Input
-              className='full'
-              label={t('identified by the name')}
-              onChange={this.onChangeName}
-              value={editedName}
-            />
-          </div>
-          <Labelled label='address:' style={{ marginTop: '.5rem' }}>
-            <code>{address}</code>
-          </Labelled>
-          {address && <Labelled label='memo:' style={{ marginTop: '.5rem' }}>
-            <MemoView accountId={address} />
-          </Labelled>}
-        </div>
-      </div>
-    );
-  }
-
-  createState (current: KeyringAddress | null): State {
-    const { name = '' } = current
-      ? current.getMeta()
-      : {};
-
-    return {
-      current,
-      editedName: name,
-      isEdited: false,
-      isForgetOpen: false
-    };
-  }
-
-  nextState (newState: State = {} as State): void {
-    this.setState(
-      (prevState: State): State => {
-        let { current = prevState.current, editedName = prevState.editedName } = newState;
-        const previous = prevState.current || { address: () => null };
-        let isEdited = false;
-
-        if (current && current.isValid()) {
-          if (current.address() !== previous.address()) {
-            editedName = current.getMeta().name || '';
-          } else if (editedName !== current.getMeta().name) {
-            isEdited = true;
-          }
-        } else {
-          editedName = '';
-        }
-        let isForgetOpen = false;
-
-        return {
-          current,
-          editedName,
-          isEdited,
-          isForgetOpen
-        };
-      }
-    );
-  }
-
-  onChangeAddress = (accountId: string): void => {
-    const current = accountId && keyring.decodeAddress(accountId)
-      ? (keyring.getAddress(accountId) || null)
-      : null;
-
-    this.nextState({ current } as State);
-  }
-
-  onChangeName = (editedName: string): void => {
-    this.nextState({ editedName } as State);
-  }
-
-  onCommit = (): void => {
-    const { current, editedName } = this.state;
-    const { onStatusChange, t } = this.props;
-
-    if (!current) {
-      return;
-    }
-
-    const status = {
-      account: current.address(),
-      action: 'edit'
-    } as ActionStatus;
-
-    try {
-      keyring.saveAddress(current.address(), {
-        name: editedName,
-        whenEdited: Date.now()
-      });
-
-      status.status = current.getMeta().name === editedName ? 'success' : 'error';
-      status.message = t('name edited');
-    } catch (error) {
-      status.status = 'error';
-      status.message = error.message;
-    }
-
-    onStatusChange(status);
-  }
-
-  onDiscard = (): void => {
-    const { current } = this.state;
-
-    if (!current) {
-      return;
-    }
-
-    this.nextState({
-      editedName: current.getMeta().name
-    } as State);
-  }
-
-  toggleForget = (): void => {
-    this.setState(
-      ({ isForgetOpen }: State) => ({
-        isForgetOpen: !isForgetOpen
-      })
-    );
-  }
-
-  onForget = (): void => {
-    const { onStatusChange, t } = this.props;
-    const { current } = this.state;
-
-    if (!current) {
-      return;
-    }
-
-    this.setState(
-      this.createState(null),
-      () => {
-        const status = {
-          account: current.address(),
-          action: 'forget'
-        } as ActionStatus;
-
-        try {
-          keyring.forgetAddress(
-            current.address()
-          );
-          status.status = 'success';
-          status.message = t('address forgotten');
-        } catch (error) {
-          status.status = 'error';
-          status.message = error.message;
-        }
-
-        onStatusChange(status);
-      }
-    );
-  }
-}
-
-export default translate(Editor);

+ 0 - 90
packages/app-address-book/src/Forgetting.tsx

@@ -1,90 +0,0 @@
-// 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 { KeyringAddress } from '@polkadot/ui-keyring/types';
-import { I18nProps } from '@polkadot/ui-app/types';
-
-import React from 'react';
-import { AddressSummary, Button, Modal } from '@polkadot/ui-app';
-
-import translate from './translate';
-
-type Props = I18nProps & {
-  isOpen: boolean,
-  onClose: () => void,
-  doForget: () => void,
-  currentAddress: KeyringAddress | null
-};
-
-class Forgetting extends React.PureComponent<Props> {
-  constructor (props: Props) {
-    super(props);
-  }
-
-  render () {
-    const { isOpen, style } = this.props;
-
-    if (!isOpen) {
-      return null;
-    }
-
-    return (
-      <Modal
-        size='tiny'
-        dimmer='inverted'
-        open
-        style={style}
-      >
-        {this.renderContent()}
-        {this.renderButtons()}
-      </Modal>
-    );
-  }
-
-  renderButtons () {
-    const { onClose, doForget, t } = this.props;
-
-    return (
-      <Modal.Actions>
-        <Button.Group>
-          <Button
-            isNegative
-            onClick={onClose}
-            label={t('Cancel')}
-          />
-          <Button.Or />
-          <Button
-            isPrimary
-            onClick={doForget}
-            label={t('Forget')}
-          />
-        </Button.Group>
-      </Modal.Actions>
-    );
-  }
-
-  renderContent () {
-    const { t, currentAddress } = this.props;
-
-    const address = currentAddress
-      ? currentAddress.address()
-      : undefined;
-
-    return (
-      <>
-        <Modal.Header>
-          {t('Confirm address removal')}
-        </Modal.Header>
-        <Modal.Content className='forgetting-Address'>
-          <AddressSummary
-            className='ui--AddressSummary-base'
-            value={address || ''}
-          />
-        </Modal.Content>
-      </>
-    );
-  }
-}
-
-export default translate(Forgetting);

+ 63 - 0
packages/app-address-book/src/Overview.tsx

@@ -0,0 +1,63 @@
+// Copyright 2017-2019 @polkadot/app-address-book 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 { Button, CardGrid } from '@polkadot/react-components';
+import addressObservable from '@polkadot/ui-keyring/observable/addresses';
+import { withMulti, withObservable } from '@polkadot/react-api';
+
+import CreateModal from './modals/Create';
+import Address from './Address';
+import translate from './translate';
+
+interface Props extends ComponentProps, I18nProps {
+  addresses?: SubjectInfo[];
+}
+
+function Overview ({ addresses, onStatusChange, t }: Props): React.ReactElement<Props> {
+  const [isCreateOpen, setIsCreateOpen] = useState(false);
+  const emptyScreen = !isCreateOpen && (!addresses || Object.keys(addresses).length === 0);
+
+  const _toggleCreate = (): void => setIsCreateOpen(!isCreateOpen);
+
+  return (
+    <CardGrid
+      buttons={
+        <Button.Group>
+          <Button
+            icon='add'
+            isPrimary
+            label={t('Add contact')}
+            onClick={_toggleCreate}
+          />
+        </Button.Group>
+      }
+      isEmpty={emptyScreen}
+      emptyText={t('No contacts found.')}
+    >
+      {isCreateOpen && (
+        <CreateModal
+          onClose={_toggleCreate}
+          onStatusChange={onStatusChange}
+        />
+      )}
+      {addresses && Object.keys(addresses).map((address): React.ReactNode => (
+        <Address
+          address={address}
+          key={address}
+        />
+      ))}
+    </CardGrid>
+  );
+}
+
+export default withMulti(
+  Overview,
+  translate,
+  withObservable(addressObservable.subject, { propName: 'addresses' })
+);

+ 0 - 7
packages/app-address-book/src/index.css

@@ -1,7 +0,0 @@
-/* Copyright 2017-2019 @polkadot/app-address-book authors & contributors
-/* This software may be modified and distributed under the terms
-/* of the Apache-2.0 license. See the LICENSE file for details. */
-
-.forgetting-Address {
-  text-align: center;
-}

+ 45 - 118
packages/app-address-book/src/index.tsx

@@ -2,134 +2,61 @@
 // This software may be modified and distributed under the terms
 // of the Apache-2.0 license. See the LICENSE file for details.
 
-import { AppProps, I18nProps } from '@polkadot/ui-app/types';
+import { AppProps, I18nProps } from '@polkadot/react-components/types';
 import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
 import { ComponentProps } from './types';
 
-import './index.css';
-
 import React from 'react';
 import { Route, Switch } from 'react-router';
-import addressObservable from '@polkadot/ui-keyring/observable/addresses';
-import Tabs, { TabItem } from '@polkadot/ui-app/Tabs';
-import { withMulti, withObservable } from '@polkadot/ui-api';
+import { HelpOverlay } from '@polkadot/react-components';
+import Tabs from '@polkadot/react-components/Tabs';
 
-import Creator from './Creator';
-import Editor from './Editor';
-import MemoByAccount from './MemoByAccount';
+import basicMd from './md/basic.md';
+import Overview from './Overview';
 import translate from './translate';
+import MemoByAccount from './MemoByAccount';
 
-type Props = AppProps & I18nProps & {
-  allAddresses?: SubjectInfo
-};
-
-type State = {
-  hidden: Array<string>,
-  items: Array<TabItem>
-};
-
-class AddressBookApp extends React.PureComponent<Props, State> {
-  state: State;
-
-  constructor (props: Props) {
-    super(props);
-
-    const { allAddresses = {}, t } = props;
-    const baseState = Object.keys(allAddresses).length !== 0
-      ? AddressBookApp.showEditState()
-      : AddressBookApp.hideEditState();
-
-    this.state = {
-      ...baseState,
-      items: [
-        {
-          name: 'edit',
-          text: t('Edit contact')
-        },
-        {
-          name: 'create',
-          text: t('Add contact')
-        },
-        {
-          name: 'memo',
-          text: t('View memo')
-        }
-      ]
-    };
-  }
-
-  static showEditState () {
-    return {
-      hidden: []
-    };
-  }
-
-  static hideEditState () {
-    return {
-      hidden: ['edit']
-    };
-  }
-
-  static getDerivedStateFromProps ({ allAddresses = {} }: Props, { hidden }: State) {
-    const hasAddresses = Object.keys(allAddresses).length !== 0;
-
-    if (hidden.length === 0) {
-      return hasAddresses
-        ? null
-        : AddressBookApp.hideEditState();
-    }
-
-    return hasAddresses
-      ? AddressBookApp.showEditState()
-      : null;
-  }
-
-  render () {
-    const { basePath } = this.props;
-    const { hidden, items } = this.state;
-    const renderCreator = this.renderComponent(Creator);
-
-    return (
-      <main className='address-book--App'>
-        <header>
-          <Tabs
-            basePath={basePath}
-            hidden={hidden}
-            items={items}
-          />
-        </header>
-        <Switch>
-          <Route path={`${basePath}/create`} render={renderCreator} />
-          <Route path={`${basePath}/memo/:accountId?`} component={MemoByAccount} />
-          <Route
-            render={
-              hidden.includes('edit')
-                ? renderCreator
-                : this.renderComponent(Editor)
-            }
-          />
-        </Switch>
-      </main>
-    );
-  }
-
-  private renderComponent (Component: React.ComponentType<ComponentProps>) {
-    return () => {
-      const { basePath, location, onStatusChange } = this.props;
+interface Props extends AppProps, I18nProps {
+  allAddresses?: SubjectInfo;
+  location: any;
+}
 
-      return (
-        <Component
+function AddressBookApp ({ basePath, onStatusChange, t }: Props): React.ReactElement<Props> {
+  const _renderComponent = (Component: React.ComponentType<ComponentProps>): () => React.ReactNode => {
+    // eslint-disable-next-line react/display-name
+    return (): React.ReactNode =>
+      <Component
+        basePath={basePath}
+        location={location}
+        onStatusChange={onStatusChange}
+      />;
+  };
+
+  return (
+    <main className='address-book--App'>
+      <HelpOverlay md={basicMd} />
+      <header>
+        <Tabs
           basePath={basePath}
-          location={location}
-          onStatusChange={onStatusChange}
+          items={[
+            {
+              isRoot: true,
+              name: 'overview',
+              text: t('My contacts')
+            },
+            {
+              name: 'memo',
+              text: t('View memo')
+            }
+          ]}
         />
-      );
-    };
-  }
+      </header>
+      <Switch>
+        <Route path={`${basePath}/memo/:accountId?`} component={MemoByAccount} />
+        <Route render={_renderComponent(Overview)} />
+      </Switch>
+    </main>
+  );
 }
 
-export default withMulti(
-  AddressBookApp,
-  translate,
-  withObservable(addressObservable.subject, { propName: 'allAddresses' })
-);
+export default translate(AddressBookApp);

+ 6 - 0
packages/app-address-book/src/md/basic.md

@@ -0,0 +1,6 @@
+# Address book
+
+You can store and get quick access to the most commonly used address, such as a friends' account.
+Any contact you create in this interface will be reflected in the application.
+
+You can edit the name of a contact by clicking on it. To remove a contact from the list, click on the trash icon to "Forget" it.

+ 135 - 0
packages/app-address-book/src/modals/Create.tsx

@@ -0,0 +1,135 @@
+// Copyright 2017-2019 @polkadot/app-address-book 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 { ActionStatus } from '@polkadot/react-components/Status/types';
+import { ModalProps } from '../types';
+
+import React, { useState } from 'react';
+
+import { AddressRow, Button, Input, InputAddress, Modal } from '@polkadot/react-components';
+import keyring from '@polkadot/ui-keyring';
+
+import translate from '../translate';
+
+interface Props extends ModalProps, I18nProps {}
+
+function Create ({ onClose, onStatusChange, t }: Props): React.ReactElement<Props> {
+  const [{ isNameValid, name }, setName] = useState<{ isNameValid: boolean; name: string }>({ isNameValid: false, name: '' });
+  const [{ address, isAddressExisting, isAddressValid }, setAddress] = useState<{ address: string; isAddressExisting: boolean; isAddressValid: boolean }>({ address: '', isAddressExisting: false, isAddressValid: false });
+  const isValid = isAddressValid && isNameValid;
+
+  const _onChangeAddress = (input: string): void => {
+    let address = '';
+    let isAddressValid = true;
+    let isAddressExisting = false;
+
+    try {
+      address = keyring.encodeAddress(
+        keyring.decodeAddress(input)
+      );
+      isAddressValid = keyring.isAvailable(address);
+
+      if (!isAddressValid) {
+        const old = keyring.getAddress(address);
+
+        if (old) {
+          const newName = old.meta.name || name;
+
+          isAddressExisting = true;
+          isAddressValid = true;
+
+          setName({ isNameValid: !!(newName || '').trim(), name: newName });
+        }
+      }
+    } catch (error) {
+      isAddressValid = false;
+    }
+
+    setAddress({ address: address || input, isAddressExisting, isAddressValid });
+  };
+  const _onChangeName = (name: string): void => setName({ isNameValid: !!name.trim(), name });
+  const _onCommit = (): void => {
+    const status = { action: 'create' } as ActionStatus;
+
+    if (!isValid) {
+      return;
+    }
+
+    try {
+      keyring.saveAddress(address, { name: name.trim(), genesisHash: keyring.genesisHash, tags: [] });
+
+      status.account = address;
+      status.status = address ? 'success' : 'error';
+      status.message = isAddressExisting
+        ? t('address edited')
+        : t('address created');
+
+      InputAddress.setLastValue('address', address);
+    } catch (error) {
+      status.status = 'error';
+      status.message = error.message;
+    }
+
+    onStatusChange(status);
+    onClose();
+  };
+
+  return (
+    <Modal
+      dimmer='inverted'
+      open
+    >
+      <Modal.Header>{t('Add an address')}</Modal.Header>
+      <Modal.Content>
+        <AddressRow
+          defaultName={name}
+          noDefaultNameOpacity
+          value={address}
+        >
+          <Input
+            autoFocus
+            className='full'
+            help={t('Paste here the address of the contact you want to add to your address book.')}
+            isError={!isAddressValid}
+            label={t('address')}
+            onChange={_onChangeAddress}
+            onEnter={_onCommit}
+            placeholder={t('new address')}
+            value={address}
+          />
+          <Input
+            className='full'
+            help={t('Type the name of your contact. This name will be used across all the apps. It can be edited later on.')}
+            isError={!isNameValid}
+            label={t('name')}
+            onChange={_onChangeName}
+            onEnter={_onCommit}
+            value={name}
+          />
+        </AddressRow>
+      </Modal.Content>
+      <Modal.Actions>
+        <Button.Group>
+          <Button
+            icon='cancel'
+            isNegative
+            onClick={onClose}
+            label={t('Cancel')}
+          />
+          <Button.Or />
+          <Button
+            icon='save'
+            isDisabled={!isValid}
+            isPrimary
+            onClick={_onCommit}
+            label={t('Save')}
+          />
+        </Button.Group>
+      </Modal.Actions>
+    </Modal>
+  );
+}
+
+export default translate(Create);

+ 1 - 1
packages/app-address-book/src/translate.ts

@@ -4,4 +4,4 @@
 
 import { withTranslation } from 'react-i18next';
 
-export default withTranslation(['address-book', 'ui']);
+export default withTranslation(['app-address-book']);

+ 10 - 2
packages/app-address-book/src/types.ts

@@ -2,6 +2,14 @@
 // This software may be modified and distributed under the terms
 // of the Apache-2.0 license. See the LICENSE file for details.
 
-import { AppProps } from '@polkadot/ui-app/types';
+import { AppProps } from '@polkadot/react-components/types';
+import { ActionStatus } from '@polkadot/react-components/Status/types';
 
-export type ComponentProps = AppProps;
+export interface ComponentProps extends AppProps {
+  location: any;
+}
+
+export interface ModalProps {
+  onClose: () => void;
+  onStatusChange: (status: ActionStatus) => void;
+}

+ 0 - 0
packages/ui-api/LICENSE → packages/app-claims/LICENSE


+ 1 - 0
packages/app-claims/README.md

@@ -0,0 +1 @@
+# @polkadot/app-claims

+ 17 - 0
packages/app-claims/package.json

@@ -0,0 +1,17 @@
+{
+  "name": "@polkadot/app-claims",
+  "version": "0.37.0-beta.63",
+  "description": "An app for claiming Polkadot tokens",
+  "main": "index.js",
+  "scripts": {},
+  "author": "Keith Ingram <keith@parity.io>",
+  "maintainers": [
+    "Keith Ingram <keith@parity.io>",
+    "Jaco Greeff <jacogr@gmail.com>"
+  ],
+  "license": "Apache-2.0",
+  "dependencies": {
+    "@babel/runtime": "^7.7.1",
+    "@polkadot/react-components": "^0.37.0-beta.63"
+  }
+}

+ 107 - 0
packages/app-claims/src/Claim.tsx

@@ -0,0 +1,107 @@
+// 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 { Option } from '@polkadot/types';
+import { BalanceOf, EthereumAddress } from '@polkadot/types/interfaces';
+import { I18nProps } from '@polkadot/react-components/types';
+
+import React, { useContext, useEffect, useState } from 'react';
+import styled from 'styled-components';
+import { ApiContext } from '@polkadot/react-api';
+import { Button, Card } from '@polkadot/react-components';
+import { formatBalance } from '@polkadot/util';
+
+import translate from './translate';
+import { addrToChecksum } from './util';
+
+interface Props extends I18nProps {
+  button: React.ReactNode;
+  ethereumAddress: EthereumAddress | null;
+}
+
+function Claim ({ button, className, ethereumAddress, t }: Props): React.ReactElement<Props> | null {
+  const { api } = useContext(ApiContext);
+  const [claimValue, setClaimValue] = useState<BalanceOf | null>(null);
+  const [claimAddress, setClaimAddress] = useState<EthereumAddress | null>(null);
+  const [isBusy, setIsBusy] = useState(false);
+
+  const _fetchClaim = (address: EthereumAddress): void => {
+    setIsBusy(true);
+
+    api.query.claims
+      .claims<Option<BalanceOf>>(address)
+      .then((claim): void => {
+        setClaimValue(claim.unwrapOr(null));
+        setIsBusy(false);
+      })
+      .catch((): void => setIsBusy(false));
+  };
+
+  useEffect((): void => {
+    if (ethereumAddress !== claimAddress) {
+      setClaimAddress(ethereumAddress);
+      ethereumAddress && _fetchClaim(ethereumAddress);
+    }
+  }, [ethereumAddress]);
+
+  if (isBusy || !claimAddress) {
+    return null;
+  }
+
+  const hasClaim = claimValue && claimValue.gten(0);
+
+  return (
+    <Card
+      isError={!hasClaim}
+      isSuccess={!!hasClaim}
+    >
+      <div className={className}>
+        {t('Your Ethereum account')}
+        <h3>{addrToChecksum(claimAddress.toString())}</h3>
+        {hasClaim && claimValue
+          ? (
+            <>
+              {t('has a valid claim for')}
+              <h2>{formatBalance(claimValue)}</h2>
+              <Button.Group>{button}</Button.Group>
+            </>
+          )
+          : (
+            <>
+              {t('does not appear to have a valid claim. Please double check that you have signed the transaction correctly on the correct ETH account.')}
+            </>
+          )}
+      </div>
+    </Card>
+  );
+}
+
+export default translate(
+  styled(Claim)`
+    font-size: 1.15rem;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    min-height: 12rem;
+    align-items: center;
+    margin: 0 1rem;
+
+    h3 {
+      font-family: monospace;
+      font-size: 1.5rem;
+      max-width: 100%;
+      margin: 0.5rem;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+
+    h2 {
+      margin: 0.5rem 0 2rem;
+      font-family: monospace;
+      font-size: 2.5rem;
+      font-weight: 200;
+    }
+  `
+);

+ 259 - 0
packages/app-claims/src/index.tsx

@@ -0,0 +1,259 @@
+/* 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 { Compact } from '@polkadot/types';
+import { Balance, EcdsaSignature, EthereumAddress } from '@polkadot/types/interfaces';
+import { AppProps, I18nProps } from '@polkadot/react-components/types';
+import { ApiProps } from '@polkadot/react-api/types';
+
+import React from 'react';
+import { Trans } from 'react-i18next';
+import styled from 'styled-components';
+import CopyToClipboard from 'react-copy-to-clipboard';
+import { withApi, withMulti } from '@polkadot/react-api';
+import { Button, Card, Columar, Column, InputAddress, Tooltip } from '@polkadot/react-components';
+import { InputNumber } from '@polkadot/react-components/InputNumber';
+import TxModal, { TxModalState, TxModalProps } from '@polkadot/react-components/TxModal';
+import { u8aToHex, u8aToString } from '@polkadot/util';
+import { decodeAddress } from '@polkadot/util-crypto';
+
+import ClaimDisplay from './Claim';
+import { recoverFromJSON } from './util';
+
+import translate from './translate';
+
+enum Step {
+  Account = 0,
+  Sign = 1,
+  Claim = 2,
+}
+
+interface Props extends AppProps, ApiProps, I18nProps, TxModalProps {}
+
+interface State extends TxModalState {
+  didCopy: boolean;
+  ethereumAddress: EthereumAddress | null;
+  claim?: Balance | null;
+  signature?: EcdsaSignature | null;
+  step: Step;
+}
+
+const Payload = styled.pre`
+  cursor: copy;
+  font-family: monospace;
+  border: 1px dashed #c2c2c2;
+  background: #fafafa;
+  padding: 1rem;
+  width: 100%;
+  margin: 1rem 0;
+  white-space: normal;
+  word-break: break-all;
+`;
+
+const Signature = styled.textarea`
+  font-family: monospace;
+  padding: 1rem;
+  border: 1px solid rgba(34, 36, 38, 0.15);
+  border-radius: 0.25rem;
+  margin: 1rem 0;
+  resize: none;
+  width: 100%;
+
+  &::placeholder {
+    color: rgba(0, 0, 0, 0.5);
+  }
+
+  &:-ms-input-placeholder {
+    color: rgba(0, 0, 0, 0.5);
+  }
+
+  &::-ms-input-placeholder {
+    color: rgba(0, 0, 0, 0.5);
+  }
+`;
+
+class App extends TxModal<Props, State> {
+  constructor (props: Props) {
+    super(props);
+
+    this.defaultState = {
+      ...this.defaultState,
+      claim: null,
+      didCopy: false,
+      ethereumAddress: null,
+      signature: null,
+      step: 0
+    };
+    this.state = this.defaultState;
+  }
+
+  public componentDidUpdate (): void {
+    if (this.state.didCopy) {
+      setTimeout((): void => {
+        this.setState({ didCopy: false });
+      }, 1000);
+    }
+  }
+
+  public render (): React.ReactNode {
+    const { api, systemChain = '', t } = this.props;
+    const { accountId, didCopy, ethereumAddress, signature, step } = this.state;
+
+    const payload = accountId
+      ? (
+        u8aToString(Compact.stripLengthPrefix(api.consts.claims.prefix.toU8a(true))) +
+        u8aToHex(decodeAddress(accountId), -1, false)
+      )
+      : '';
+
+    return (
+      <main>
+        <header />
+        <h1>
+          <Trans>claim your <em>{InputNumber.units}</em> tokens</Trans>
+        </h1>
+        <Columar>
+          <Column>
+            <Card withBottomMargin>
+              <h3>{t('1. Select your {{chain}} account', {
+                replace: {
+                  chain: systemChain
+                }
+              })}</h3>
+              <InputAddress
+                defaultValue={this.state.accountId}
+                help={t('The account you want to claim to.')}
+                label={t('claim to account')}
+                onChange={this.onChangeAccount}
+                type='all'
+              />
+              {(step === Step.Account) && (
+                <Button.Group>
+                  <Button
+                    icon='sign-in'
+                    isPrimary
+                    onClick={this.setStep(Step.Sign)}
+                    label={t('Continue')}
+                  />
+                </Button.Group>
+              )}
+            </Card>
+            {(step >= Step.Sign && !!accountId) && (
+              <Card>
+                <h3>{t('2. Sign ETH transaction')}</h3>
+                <CopyToClipboard
+                  onCopy={this.onCopy}
+                  text={payload}
+                >
+                  <Payload
+                    data-for='tx-payload'
+                    data-tip
+                  >
+                    {payload}
+                  </Payload>
+                </CopyToClipboard>
+                <Tooltip
+                  place='right'
+                  text={didCopy ? t('copied') : t('click to copy')}
+                  trigger='tx-payload'
+                />
+                <div>
+                  {t('Copy the above string and sign an Ethereum transaction with the account you used during the pre-sale in the wallet of your choice, using the string as the payload, and then paste the transaction signature object below')}
+                  :
+                </div>
+                <Signature
+                  onChange={this.onChangeSignature}
+                  placeholder='{\n  "address": "0x ...",\n  "msg": "Pay KSMs to the Kusama account: ...",\n  "sig": "0x ...",\n  "version": "2"\n}'
+                  rows={10}
+                />
+                {(step === Step.Sign) && (
+                  <Button.Group>
+                    <Button
+                      icon='sign-in'
+                      isDisabled={!accountId || !signature}
+                      isPrimary
+                      onClick={this.setStep(Step.Claim)}
+                      label={t('Confirm claim')}
+                    />
+                  </Button.Group>
+                )}
+              </Card>
+            )}
+          </Column>
+          <Column showEmptyText={false}>
+            {(step >= Step.Claim) && (
+              <ClaimDisplay
+                button={this.renderTxButton()}
+                ethereumAddress={ethereumAddress}
+              />
+            )}
+          </Column>
+        </Columar>
+      </main>
+    );
+  }
+
+  protected isDisabled = (): boolean => {
+    const { accountId, signature } = this.state;
+
+    return !accountId || !signature;
+  }
+
+  protected isUnsigned = (): boolean => true;
+
+  protected submitLabel = (): React.ReactNode => this.props.t('Redeem');
+
+  protected txMethod = (): string => 'claims.claim';
+
+  protected txParams = (): [string | null, EcdsaSignature | null] => {
+    const { accountId, signature } = this.state;
+
+    return [
+      accountId ? accountId.toString() : null,
+      signature || null
+    ];
+  }
+
+  protected onChangeAccount = (accountId: string | null): void => {
+    this.setState(({ step }: State): Pick<State, never> => {
+      return {
+        ...(
+          step > Step.Account
+            ? this.defaultState
+            : {}
+        ),
+        accountId
+      };
+    });
+  }
+
+  protected onChangeSignature = (event: React.SyntheticEvent<Element>): void => {
+    const { value: signatureJson } = event.target as HTMLInputElement;
+
+    this.setState(({ step }: State): Pick<State, never> => ({
+      ...(
+        step > Step.Sign
+          ? { step: Step.Sign }
+          : {}
+      ),
+      ...recoverFromJSON(signatureJson)
+    }));
+  }
+
+  private onCopy = (): void => {
+    this.setState({ didCopy: true });
+  }
+
+  private setStep = (step: Step): () => void =>
+    (): void => {
+      this.setState({ step });
+    }
+}
+
+export default withMulti(
+  App,
+  translate,
+  withApi
+);

+ 8 - 0
packages/app-claims/src/secp256k1.d.ts

@@ -0,0 +1,8 @@
+// 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.
+
+declare module 'secp256k1/elliptic' {
+  export function publicKeyConvert (publicKey: Buffer, expanded: boolean): Buffer;
+  export function recover (msgHash: Buffer, signature: Buffer, recovery: number): Buffer;
+}

+ 7 - 0
packages/app-claims/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-claims']);

+ 19 - 0
packages/app-claims/src/util.spec.ts

@@ -0,0 +1,19 @@
+// Copyright 2017-2019 @polkadot/react-components 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 { hexToU8a } from '@polkadot/util';
+
+import { publicToAddr } from './util';
+
+describe('publicToAddr', (): void => {
+  it('converts a publicKey to address', (): void => {
+    expect(
+      publicToAddr(
+        hexToU8a(
+          '0x836b35a026743e823a90a0ee3b91bf615c6a757e2b60b9e1dc1826fd0dd16106f7bc1e8179f665015f43c6c81f39062fc2086ed849625c06e04697698b21855e'
+        )
+      )
+    ).toEqual('0x0BED7ABd61247635c1973eB38474A2516eD1D884');
+  });
+});

+ 111 - 0
packages/app-claims/src/util.ts

@@ -0,0 +1,111 @@
+/* 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 { EthereumAddress, EcdsaSignature } from '@polkadot/types/interfaces';
+
+import secp256k1 from 'secp256k1/elliptic';
+import { createType } from '@polkadot/types';
+import { assert, hexToU8a, stringToU8a, u8aToBuffer, u8aConcat } from '@polkadot/util';
+import { keccakAsHex, keccakAsU8a } from '@polkadot/util-crypto';
+
+interface RecoveredSignature {
+  error: Error | null;
+  ethereumAddress: EthereumAddress | null;
+  signature: EcdsaSignature | null;
+}
+
+interface SignatureParts {
+  recovery: number;
+  signature: Buffer;
+}
+
+// converts an Ethereum address to a checksum representation
+export function addrToChecksum (_address: string): string {
+  const address = _address.toLowerCase();
+  const hash = keccakAsHex(address.substr(2)).substr(2);
+  let result = '0x';
+
+  for (let n = 0; n < 40; n++) {
+    result = `${result}${
+      parseInt(hash[n], 16) > 7
+        ? address[n + 2].toUpperCase()
+        : address[n + 2]
+    }`;
+  }
+
+  return result;
+}
+
+// convert a give public key to an Ethereum address (the last 20 bytes of an _exapnded_ key keccack)
+export function publicToAddr (publicKey: Uint8Array): string {
+  return addrToChecksum(`0x${keccakAsHex(publicKey).slice(-40)}`);
+}
+
+// hash a message for use in signature recovery, adding the standard Ethereum header
+export function hashMessage (message: string): Buffer {
+  const expanded = stringToU8a(`\x19Ethereum Signed Message:\n${message.length.toString()}${message}`);
+  const hashed = keccakAsU8a(expanded);
+
+  return u8aToBuffer(hashed);
+}
+
+// split is 65-byte signature into the r, s (combined) and recovery number (derived from v)
+export function sigToParts (_signature: string): SignatureParts {
+  const signature = hexToU8a(_signature);
+
+  assert(signature.length === 65, `Invalid signature length, expected 65 found ${signature.length}`);
+
+  let v = signature[64];
+
+  if (v < 27) {
+    v += 27;
+  }
+
+  const recovery = v - 27;
+
+  assert(recovery === 0 || recovery === 1, 'Invalid signature v value');
+
+  return {
+    recovery,
+    signature: u8aToBuffer(signature.slice(0, 64))
+  };
+}
+
+// recover an address from a given message and a recover/signature combination
+export function recoverAddress (message: string, { recovery, signature }: SignatureParts): string {
+  const msgHash = hashMessage(message);
+  const senderPubKey = secp256k1.recover(msgHash, signature, recovery);
+
+  return publicToAddr(
+    secp256k1.publicKeyConvert(senderPubKey, false).slice(1)
+  );
+}
+
+// recover an address from a signature JSON (as supplied by e.g. MyCrypto)
+export function recoverFromJSON (signatureJson: string | null): RecoveredSignature {
+  try {
+    const { msg, sig } = JSON.parse(signatureJson || '{}');
+
+    if (!msg || !sig) {
+      throw new Error('Invalid signature object');
+    }
+
+    const parts = sigToParts(sig);
+
+    return {
+      error: null,
+      ethereumAddress: createType('EthereumAddress', recoverAddress(msg, parts)),
+      signature: createType('EcdsaSignature', u8aConcat(parts.signature, new Uint8Array([parts.recovery])))
+    };
+  } catch (error) {
+    console.error(error);
+
+    return {
+      error,
+      ethereumAddress: null,
+      signature: null
+    };
+  }
+}

+ 4 - 3
packages/app-contracts/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@polkadot/app-contracts",
-  "version": "0.32.0-beta.6",
+  "version": "0.37.0-beta.63",
   "description": "Deployment and management of substrate contracts",
   "main": "index.js",
   "scripts": {},
@@ -10,7 +10,8 @@
   ],
   "license": "Apache-2.0",
   "dependencies": {
-    "@babel/runtime": "^7.4.3",
-    "@polkadot/ui-app": "^0.32.0-beta.6"
+    "@babel/runtime": "^7.7.1",
+    "@polkadot/api-contract": "^0.96.1",
+    "@polkadot/react-components": "^0.37.0-beta.63"
   }
 }

+ 163 - 36
packages/app-contracts/src/ABI.tsx

@@ -2,68 +2,195 @@
 // 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/ui-app/types';
+import { I18nProps } from '@polkadot/react-components/types';
 
 import React from 'react';
-import { InputFile } from '@polkadot/ui-app';
-import { ContractAbi } from '@polkadot/types';
+import styled from 'styled-components';
+import { Abi } from '@polkadot/api-contract';
+import { InputFile, Labelled, Messages } from '@polkadot/react-components';
 import { u8aToString } from '@polkadot/util';
 
 import translate from './translate';
 
-type Props = I18nProps & {
-  help: React.ReactNode,
-  isError?: boolean,
-  label: React.ReactNode,
-  onChange: (json: string | null, contractAbi: ContractAbi | null) => void
-};
+interface Props extends I18nProps {
+  className?: string;
+  contractAbi?: Abi | null;
+  errorText?: string | null;
+  help?: React.ReactNode;
+  isError?: boolean;
+  isDisabled?: boolean;
+  isRequired?: boolean;
+  label?: React.ReactNode;
+  onChange: (json: string | null, contractAbi: Abi | null) => void;
+  onRemove?: () => void;
+  onRemoved?: () => void;
+  onSelect?: () => void;
+  onSelectConstructor?: (constructorIndex?: number) => void;
+}
 
-type State = {
-  abi?: Uint8Array | null,
-  isAbiValid: boolean,
-  name?: string,
-  placeholder?: React.ReactNode | null
-};
+interface State {
+  contractAbi: Abi | null;
+  errorText: string | null;
+  isAbiValid: boolean;
+  isEmpty: boolean;
+  isError: boolean;
+}
 
 class ABI extends React.PureComponent<Props, State> {
-  state: State = {
-    isAbiValid: true
+  public state: State = {
+    contractAbi: null,
+    errorText: null,
+    isAbiValid: false,
+    isEmpty: true,
+    isError: false
   };
 
-  render () {
-    const { help, isError, label } = this.props;
-    const { isAbiValid, placeholder } = this.state;
+  constructor (props: Props) {
+    super(props);
+
+    const { contractAbi, isError, isRequired } = this.props;
+    const isAbiValid = !!contractAbi;
+
+    this.state = {
+      contractAbi: contractAbi || null,
+      errorText: null,
+      isAbiValid,
+      isEmpty: !isAbiValid,
+      isError: isError || (isRequired && !isAbiValid) || false
+    };
+  }
+
+  public static getDerivedStateFromProps ({ contractAbi }: Props): Pick<State, never> | null {
+    if (contractAbi) {
+      return {
+        contractAbi,
+        isAbiValid: true,
+        isError: false
+      };
+    }
+    return null;
+  }
+
+  public render (): React.ReactNode {
+    const { className } = this.props;
+    const { contractAbi, isAbiValid } = this.state;
 
     return (
-      <InputFile
-        help={help}
-        isError={!isAbiValid || isError}
+      <div className={className}>
+        {
+          (contractAbi && isAbiValid)
+            ? this.renderMessages()
+            : this.renderInputFile()
+        }
+      </div>
+    );
+  }
+
+  private renderInputFile (): React.ReactNode {
+    const { className, help, isDisabled, isRequired, label, t } = this.props;
+    const { isAbiValid, isEmpty, isError, errorText } = this.state;
+
+    return (
+      <div className={className}>
+        <InputFile
+          help={help}
+          isDisabled={isDisabled}
+          isError={!isAbiValid && (isRequired || isError)}
+          label={label}
+          onChange={this.onChange}
+          placeholder={
+            !isEmpty && !isAbiValid
+              ? (
+                <>
+                  {t('invalid ABI file selected')}
+                  {!!errorText && (
+                    <>
+                      {' — '}
+                      {t(errorText)}
+                    </>
+                  )}
+                </>
+              )
+              : t('click to select or drag and drop a JSON ABI file')
+          }
+        />
+      </div>
+    );
+  }
+
+  private renderMessages (): React.ReactNode {
+    const { help, isDisabled, label, onRemove, onSelectConstructor } = this.props;
+    const { contractAbi } = this.state;
+
+    if (!contractAbi) {
+      return null;
+    }
+
+    return (
+      <Labelled
         label={label}
-        onChange={this.onChange}
-        placeholder={placeholder}
-      />
+        help={help}
+        withLabel={!!label}
+      >
+        <Messages
+          contractAbi={contractAbi}
+          onRemove={onRemove || this.onRemove}
+          isLabelled={!!label}
+          isRemovable={!isDisabled}
+          onSelectConstructor={onSelectConstructor}
+          withConstructors
+        />
+      </Labelled>
     );
   }
 
-  private onChange = (u8a: Uint8Array, name: string): void => {
-    const { onChange } = this.props;
+  private onChange = (u8a: Uint8Array): void => {
+    const { onChange, t } = this.props;
     const json = u8aToString(u8a);
-
     try {
-      const abi = new ContractAbi(JSON.parse(json));
+      const abi = JSON.parse(json);
+
+      if (abi.deploy || abi.messages) {
+        throw new Error(t('You are using an ABI with an outdated format. Please generate a new one.'));
+      }
+
+      const contractAbi = new Abi(abi);
 
       this.setState({
+        contractAbi,
         isAbiValid: true,
-        name,
-        placeholder: `${name} (${Object.keys(abi.messages).join(', ')})`
-      }, () => onChange(json, abi));
+        isEmpty: false,
+        isError: false
+      }, (): void => onChange(json, contractAbi));
     } catch (error) {
+      console.error(error);
+
       this.setState({
         isAbiValid: false,
-        placeholder: error.message
-      }, () => onChange(null, null));
+        isEmpty: false,
+        isError: true,
+        errorText: error
+      }, (): void => onChange(null, null));
     }
   }
+
+  private onRemove = (): void => {
+    const { onChange, onRemoved } = this.props;
+
+    this.setState(
+      {
+        contractAbi: null,
+        isAbiValid: false,
+        isEmpty: true
+      },
+      (): void => {
+        onChange(null, null);
+        onRemoved && onRemoved();
+      }
+    );
+  }
 }
 
-export default translate(ABI);
+export default translate(styled(ABI as React.ComponentClass<Props, State>)`
+  min-height: 4rem;
+`);

+ 0 - 183
packages/app-contracts/src/Call.tsx

@@ -1,183 +0,0 @@
-// Copyright 2017-2019 @polkadot/app-contracts 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/ui-app/types';
-import { ComponentProps } from './types';
-
-import BN from 'bn.js';
-import React from 'react';
-import { Button, Dropdown, InputAddress, InputBalance, InputNumber, TxButton } from '@polkadot/ui-app';
-import { ContractAbi } from '@polkadot/types';
-
-import store from './store';
-import translate from './translate';
-import Params from './Params';
-
-type Props = ComponentProps & I18nProps;
-
-type State = {
-  accountId: string | null,
-  address?: string,
-  contractAbi?: ContractAbi | null,
-  endowment: BN,
-  gasLimit: BN,
-  isAddressValid: boolean,
-  isBusy: boolean,
-  method?: string | null,
-  params: Array<any>
-};
-
-class Call extends React.PureComponent<Props, State> {
-  state: State = {
-    accountId: null,
-    endowment: new BN(0),
-    gasLimit: new BN(0),
-    isAddressValid: false,
-    isBusy: false,
-    params: []
-  };
-
-  render () {
-    const { t } = this.props;
-    const { accountId, address, contractAbi, endowment, gasLimit, isAddressValid, method } = this.state;
-    const contractOptions = store.getAllContracts().map(({ json: { address, name } }) => ({
-      text: `${name} (${address})`,
-      value: address
-    }));
-    const methodOptions = contractAbi
-      ? Object.keys(contractAbi.messages).map((key) => {
-        const fn = contractAbi.messages[key];
-        const type = fn.type ? `: ${fn.type}` : '';
-        const args = fn.args.map(({ name, type }) => `${name}: ${type}`);
-        const text = `${key}(${args.join(', ')})${type}`;
-
-        return {
-          key,
-          text,
-          value: key
-        };
-      })
-      : [];
-    const defaultContract = contractOptions.length
-      ? contractOptions[contractOptions.length - 1].value
-      : undefined;
-    const isEndowValid = !endowment.isZero();
-    const isGasValid = !gasLimit.isZero();
-    const isValid = !!accountId && isEndowValid && isGasValid && isAddressValid;
-
-    return (
-      <div className='contracts--Call'>
-        <InputAddress
-          help={t('Specify the user account to use for this contract call. And fees will be deducted from this account.')}
-          label={t('call from account')}
-          onChange={this.onChangeAccount}
-          type='account'
-        />
-        <Dropdown
-          defaultValue={defaultContract}
-          help={t('A deployed contract that has either been deployed or attached. The address and ABI are used to construct the parameters.')}
-          isError={!isAddressValid}
-          label={t('contract to use')}
-          onChange={this.onChangeAddress}
-          options={contractOptions}
-          value={address}
-        />
-        <Dropdown
-          defaultValue={method}
-          help={t('The message to send to this contract. Parameters are adjusted based on the ABI provided.')}
-          isError={!method}
-          label={t('message to send')}
-          onChange={this.onChangeMethod}
-          options={methodOptions}
-          value={method}
-        />
-        <Params
-          onChange={this.onChangeParams}
-          params={
-            method && contractAbi && contractAbi.messages[method]
-              ? contractAbi.messages[method].args
-              : undefined
-          }
-        />
-        <InputBalance
-          help={t('The allotted value for this contract, i.e. the amount transferred to the contract as part of this call.')}
-          isError={!isEndowValid}
-          label={t('value')}
-          onChange={this.onChangeEndowment}
-        />
-        <InputNumber
-          help={t('The maximum amount of gas that can be used by this deployment, if the code requires more, the deployment will fail.')}
-          isError={!isGasValid}
-          label={t('maximum gas allowed')}
-          onChange={this.onChangeGas}
-        />
-        <Button.Group>
-          <TxButton
-            accountId={accountId}
-            isDisabled={!isValid}
-            isPrimary
-            label={t('Call')}
-            onClick={this.toggleBusy}
-            onFailed={this.toggleBusy}
-            onSuccess={this.toggleBusy}
-            params={this.constructCall}
-            tx='contract.call'
-          />
-        </Button.Group>
-      </div>
-    );
-  }
-
-  private constructCall = (): Array<any> => {
-    const { address, contractAbi, endowment, gasLimit, method, params } = this.state;
-
-    if (!contractAbi || !method) {
-      return [];
-    }
-
-    return [address, endowment, gasLimit, contractAbi.messages[method](...params)];
-  }
-
-  private onChangeAccount = (accountId: string | null): void => {
-    this.setState({ accountId });
-  }
-
-  private onChangeAddress = (address: string): void => {
-    const contract = store.getContract(address);
-    const contractAbi = contract
-    ? contract.contractAbi
-    : null;
-
-    this.setState({ address, contractAbi, isAddressValid: !!contractAbi });
-    this.onChangeMethod(
-      contractAbi
-        ? Object.keys(contractAbi.messages)[0]
-        : null
-    );
-  }
-
-  private onChangeEndowment = (endowment?: BN | null): void => {
-    this.setState({ endowment: endowment || new BN(0) });
-  }
-
-  private onChangeGas = (gasLimit: BN | undefined): void => {
-    this.setState({ gasLimit: gasLimit || new BN(0) });
-  }
-
-  private onChangeMethod = (method: string | null): void => {
-    this.setState({ method, params: [] });
-  }
-
-  private onChangeParams = (params: Array<any>): void => {
-    this.setState({ params });
-  }
-
-  private toggleBusy = (): void => {
-    this.setState(({ isBusy }) => ({
-      isBusy: !isBusy
-    }));
-  }
-}
-
-export default translate(Call);

+ 0 - 278
packages/app-contracts/src/Code.tsx

@@ -1,278 +0,0 @@
-// Copyright 2017-2019 @polkadot/app-contracts 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 { SubmittableResult } from '@polkadot/api/SubmittableExtrinsic';
-import { I18nProps } from '@polkadot/ui-app/types';
-import { ComponentProps } from './types';
-
-import BN from 'bn.js';
-import React from 'react';
-import { Button, Input, InputAddress, InputFile, InputNumber, TxButton } from '@polkadot/ui-app';
-import { compactAddLength } from '@polkadot/util';
-import { Hash } from '@polkadot/types';
-
-import ABI from './ABI';
-import ValidateCode from './ValidateCode';
-import store from './store';
-import translate from './translate';
-
-type Props = ComponentProps & I18nProps;
-
-type State = {
-  abi?: string | null,
-  accountId?: string | null,
-  codeHash?: string | null,
-  gasLimit: BN,
-  isAbiValid: boolean,
-  isBusy: boolean,
-  isCodeValid: boolean,
-  isNameValid: boolean,
-  isNew: boolean,
-  isWasmValid: boolean,
-  name?: string | null,
-  wasm?: Uint8Array | null
-};
-
-class Deploy extends React.PureComponent<Props, State> {
-  state: State = {
-    accountId: null,
-    gasLimit: new BN(0),
-    isAbiValid: true,
-    isBusy: false,
-    isCodeValid: false,
-    isNew: true,
-    isNameValid: false,
-    isWasmValid: false
-  };
-
-  render () {
-    const { t } = this.props;
-    const { isNew } = this.state;
-
-    return (
-      <div className='contracts--Code'>
-        <Button.Group isBasic isCentered>
-          <Button
-            isBasic
-            isNegative={isNew}
-            label={t('deploy new')}
-            onClick={this.toggleNew}
-          />
-          <Button
-            isBasic
-            isNegative={!isNew}
-            label={t('attach existing')}
-            onClick={this.toggleNew}
-          />
-        </Button.Group>
-        {
-          isNew
-            ? this.renderDeploy()
-            : this.renderExisting()
-        }
-      </div>
-    );
-  }
-
-  private renderDeploy () {
-    const { t } = this.props;
-    const { accountId, gasLimit, isAbiValid, isBusy, isNameValid, isWasmValid, wasm } = this.state;
-    const isValid = !isBusy && isAbiValid && isNameValid && isWasmValid && !gasLimit.isZero() && !!accountId;
-
-    return (
-      <>
-        <InputAddress
-          help={t('Specify the user account to use for this deployment. And fees will be deducted from this account.')}
-          label={t('deployment account')}
-          onChange={this.onChangeAccount}
-          type='account'
-        />
-        <InputFile
-          help={t('The compiled WASM for the contract that you wish to deploy. Each unique code blob will be attached with a code hash that can be used to create new instances.')}
-          isError={!isWasmValid}
-          label={t('compiled contract WASM')}
-          onChange={this.onAddWasm}
-          placeholder={
-            wasm && !isWasmValid
-              ? t('The code is not recognized as being in valid WASM format')
-              : null
-          }
-        />
-        {this.renderInputName()}
-        {this.renderInputAbi()}
-        <InputNumber
-          help={t('The maximum amount of gas that can be used by this deployment, if the code requires more, the deployment will fail.')}
-          label={t('maximum gas allowed')}
-          onChange={this.onChangeGas}
-        />
-        <Button.Group>
-          <TxButton
-            accountId={accountId}
-            isDisabled={!isValid}
-            isPrimary
-            label={t('Deploy')}
-            onClick={this.toggleBusy}
-            onFailed={this.toggleBusy}
-            onSuccess={this.onSuccess}
-            params={[gasLimit, wasm]}
-            tx='contract.putCode'
-          />
-        </Button.Group>
-      </>
-    );
-  }
-
-  private renderExisting () {
-    const { t } = this.props;
-    const { codeHash, isAbiValid, isCodeValid, isNameValid } = this.state;
-    const isValid = isAbiValid && isCodeValid && isNameValid;
-
-    return (
-      <>
-        <Input
-          autoFocus
-          help={t('The code hash for the on-chain deployed code.')}
-          isError={!isCodeValid}
-          label={t('code hash')}
-          onChange={this.onChangeHash}
-          value={codeHash}
-        />
-        <ValidateCode
-          codeHash={codeHash}
-          onChange={this.onValidateCode}
-        />
-        {this.renderInputName()}
-        {this.renderInputAbi()}
-        <Button.Group>
-          <Button
-            isDisabled={!isValid}
-            isPrimary
-            label={t('Save')}
-            onClick={this.onSave}
-          />
-        </Button.Group>
-      </>
-    );
-  }
-
-  private renderInputAbi () {
-    const { t } = this.props;
-    const { isAbiValid } = this.state;
-
-    return (
-      <ABI
-        help={t('The ABI for the WASM code. In this step it is optional, but setting it here simplifies the setup of contract instances.')}
-        isError={!isAbiValid}
-        label={t('contract ABI (optional)')}
-        onChange={this.onAddAbi}
-      />
-    );
-  }
-
-  private renderInputName () {
-    const { t } = this.props;
-    const { isNameValid, name } = this.state;
-
-    return (
-      <Input
-        help={t('A name for this WASM code that helps to user distinguish. Only used for display purposes.')}
-        isError={!isNameValid}
-        label={t('code bundle name')}
-        onChange={this.onChangeName}
-        value={name}
-      />
-    );
-  }
-
-  private onAddAbi = (abi: string | null): void => {
-    this.setState({ abi, isAbiValid: !!abi });
-  }
-
-  private onAddWasm = (wasm: Uint8Array, name: string): void => {
-    const isWasmValid = wasm.subarray(0, 4).toString() === '0,97,115,109'; // '\0asm'
-
-    this.setState({ wasm: compactAddLength(wasm), isWasmValid });
-    this.onChangeName(name);
-  }
-
-  private onChangeAccount = (accountId: string | null): void => {
-    this.setState({ accountId });
-  }
-
-  private onChangeGas = (gasLimit: BN | undefined): void => {
-    this.setState({ gasLimit: gasLimit || new BN(0) });
-  }
-
-  private onChangeHash = (codeHash: string): void => {
-    this.setState({ codeHash, isCodeValid: false });
-  }
-
-  private onChangeName = (name: string): void => {
-    this.setState({ name, isNameValid: name.length !== 0 });
-  }
-
-  private onValidateCode = (isCodeValid: boolean): void => {
-    this.setState({ isCodeValid });
-  }
-
-  private toggleBusy = (): void => {
-    this.setState(({ isBusy }) => ({
-      isBusy: !isBusy
-    }));
-  }
-
-  private toggleNew = (): void => {
-    this.setState(({ isNew }) => ({
-      abi: null,
-      codeHash: null,
-      isAbiValid: true,
-      isCodeValid: false,
-      isNameValid: false,
-      name: '',
-      isNew: !isNew
-    }));
-  }
-
-  private onSave = (): void => {
-    const { abi, codeHash, name } = this.state;
-
-    if (!codeHash || !name) {
-      return;
-    }
-
-    store.saveCode(new Hash(codeHash), { abi, name }).catch((error) => {
-      console.error('Unable to save code', error);
-    });
-
-    this.redirect();
-  }
-
-  private onSuccess = (result: SubmittableResult): void => {
-    const record = result.findRecord('contract', 'CodeStored');
-
-    if (record) {
-      const codeHash = record.event.data[0];
-
-      this.setState(({ abi, name }) => {
-        if (!codeHash || !name) {
-          return;
-        }
-
-        store.saveCode(codeHash as Hash, { abi, name }).catch((error) => {
-          console.error('Unable to save code', error);
-        });
-
-        this.redirect();
-      });
-    }
-
-    this.toggleBusy();
-  }
-
-  private redirect () {
-    window.location.hash = this.props.basePath;
-  }
-}
-
-export default translate(Deploy);

+ 113 - 0
packages/app-contracts/src/Codes/Add.tsx

@@ -0,0 +1,113 @@
+// Copyright 2017-2019 @polkadot/app-contracts 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 from 'react';
+import { createType } from '@polkadot/types';
+import { Button, Input } from '@polkadot/react-components';
+
+import ContractModal, { ContractModalProps as Props, ContractModalState } from '../Modal';
+import ValidateCode from './ValidateCode';
+import store from '../store';
+import translate from '../translate';
+
+interface State extends ContractModalState {
+  codeHash: string;
+  isBusy: boolean;
+  isCodeValid: boolean;
+}
+
+class Add extends ContractModal<Props, State> {
+  constructor (props: Props) {
+    super(props);
+    this.defaultState = {
+      ...this.defaultState,
+      codeHash: '',
+      isBusy: false,
+      isCodeValid: false
+    };
+    this.state = this.defaultState;
+    this.headerText = props.t('Add an existing code hash');
+  }
+
+  protected renderContent = (): React.ReactNode => {
+    const { t } = this.props;
+    const { codeHash, isBusy, isCodeValid } = this.state;
+
+    return (
+      <>
+        <Input
+          autoFocus
+          help={t('The code hash for the on-chain deployed code.')}
+          isDisabled={isBusy}
+          isError={!isCodeValid}
+          label={t('code hash')}
+          onChange={this.onChangeHash}
+          onEnter={this.submit}
+          value={codeHash}
+        />
+        <ValidateCode
+          codeHash={codeHash}
+          onChange={this.onValidateCode}
+        />
+        {this.renderInputName()}
+        {this.renderInputAbi()}
+      </>
+    );
+  }
+
+  protected renderButtons = (): React.ReactNode => {
+    const { t } = this.props;
+    const { isBusy, isCodeValid, isNameValid } = this.state;
+    const isValid = !isBusy && isCodeValid && isNameValid;
+
+    return (
+      <Button.Group>
+        {this.renderCancelButton()}
+        <Button
+          icon='save'
+          isDisabled={!isValid}
+          isPrimary
+          label={t('Save')}
+          onClick={this.onSave}
+          ref={this.button}
+        />
+      </Button.Group>
+    );
+  }
+
+  private onChangeHash = (codeHash: string): void => {
+    this.setState({ codeHash, isCodeValid: false });
+  }
+
+  private onValidateCode = (isCodeValid: boolean): void => {
+    this.setState({ isCodeValid });
+  }
+
+  private onSave = (): void => {
+    const { abi, codeHash, name, tags } = this.state;
+
+    if (!codeHash || !name) {
+      return;
+    }
+
+    this.setState({ isBusy: true }, (): void => {
+      store
+        .saveCode(createType('Hash', codeHash), { abi, name, tags })
+        .then((): void => {
+          this.setState(
+            { isBusy: false },
+            (): void => this.onClose()
+          );
+        })
+        .catch((error): void => {
+          console.error('Unable to save code', error);
+          this.setState({ isBusy: false });
+        });
+    });
+
+    // this.redirect();
+  }
+}
+
+export default translate(Add);

+ 208 - 0
packages/app-contracts/src/Codes/Code.tsx

@@ -0,0 +1,208 @@
+// 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 { CodeStored } from '../types';
+
+import React from 'react';
+import styled from 'styled-components';
+import { RouteComponentProps } from 'react-router';
+import { withRouter } from 'react-router-dom';
+import { Button, Card, CodeRow, Forget } from '@polkadot/react-components';
+
+import ABI from '../ABI';
+import RemoveABI from '../RemoveABI';
+
+import contracts from '../store';
+import translate from '../translate';
+
+interface Props extends I18nProps, RouteComponentProps<{}> {
+  code: CodeStored;
+  showDeploy: (codeHash?: string, constructorIndex?: number) => () => void;
+}
+
+interface State {
+  isAbiOpen: boolean;
+  isForgetOpen: boolean;
+  isRemoveABIOpen: boolean;
+}
+
+const CodeCard = styled(Card)`
+  && {
+    max-width: 100%;
+    min-width: 100%;
+  }
+`;
+
+class Code extends React.PureComponent<Props, State> {
+  public state: State = {
+    isAbiOpen: false,
+    isForgetOpen: false,
+    isRemoveABIOpen: false
+  };
+
+  public render (): React.ReactNode {
+    const { code, code: { contractAbi }, t } = this.props;
+    const { isAbiOpen } = this.state;
+
+    const abi = (
+      <ABI
+        contractAbi={contractAbi}
+        onChange={this.onChangeABI}
+        onRemove={this.toggleRemoveABI}
+        onSelectConstructor={this.onDeployConstructor}
+      />
+    );
+
+    return (
+      <CodeCard>
+        {this.renderModals()}
+        <CodeRow
+          buttons={this.renderButtons()}
+          code={code}
+          isEditable
+          withTags
+        >
+          {contractAbi
+            ? (
+              <details
+                onClick={this.toggleAbi}
+                open={isAbiOpen}
+              >
+                <summary>{t('ABI')}</summary>
+                {abi}
+              </details>
+            )
+            : abi
+          }
+        </CodeRow>
+      </CodeCard>
+    );
+  }
+
+  private renderButtons (): React.ReactNode {
+    const { code: { json: { codeHash } }, showDeploy, t } = this.props;
+
+    return (
+      <>
+        <Button
+          isNegative
+          onClick={this.toggleForget}
+          icon='trash'
+          size='small'
+          tooltip={t('Forget this code hash')}
+        />
+        <Button
+          icon='cloud upload'
+          isPrimary
+          label={t('deploy')}
+          onClick={showDeploy(codeHash)}
+          size='small'
+          tooltip={t('Deploy this code hash as a smart contract')}
+        />
+      </>
+    );
+  }
+
+  private renderModals (): React.ReactNode {
+    const { code } = this.props;
+    const { isForgetOpen, isRemoveABIOpen } = this.state;
+
+    if (!code) {
+      return null;
+    }
+
+    const modals = [];
+
+    if (isForgetOpen) {
+      modals.push(
+        <Forget
+          code={code}
+          key='modal-forget-account'
+          mode='code'
+          onClose={this.toggleForget}
+          onForget={this.onForget}
+        />
+      );
+    }
+
+    if (isRemoveABIOpen) {
+      modals.push(
+        <RemoveABI
+          code={code}
+          key='modal-remove-abi'
+          onClose={this.toggleRemoveABI}
+          onRemove={this.onChangeABI}
+        />
+      );
+    }
+
+    return modals;
+  }
+
+  private toggleAbi = (event: React.MouseEvent): () => void => {
+    return (): void => {
+      event.preventDefault();
+      const { isAbiOpen } = this.state;
+
+      this.setState({
+        isAbiOpen: !isAbiOpen
+      });
+    };
+  }
+
+  private toggleForget = (): void => {
+    const { isForgetOpen } = this.state;
+
+    this.setState({
+      isForgetOpen: !isForgetOpen
+    });
+  }
+
+  private toggleRemoveABI = (): void => {
+    const { isRemoveABIOpen } = this.state;
+
+    this.setState({
+      isRemoveABIOpen: !isRemoveABIOpen
+    });
+  }
+
+  private onDeployConstructor = (constructorIndex = 0): void => {
+    const { code: { json: { codeHash } }, showDeploy } = this.props;
+
+    codeHash && showDeploy && showDeploy(codeHash, constructorIndex)();
+  }
+
+  private onForget = (): void => {
+    const { code: { json: { codeHash } } } = this.props;
+
+    if (!codeHash) {
+      return;
+    }
+
+    try {
+      contracts.forgetCode(codeHash);
+    } catch (error) {
+      console.error(error);
+    } finally {
+      this.toggleForget();
+    }
+  }
+
+  private onChangeABI = (abi: string | null = null): void => {
+    const { code: { json: { codeHash } } } = this.props;
+
+    this.setState(
+      { isAbiOpen: true },
+      (): void => {
+        contracts.saveCode(
+          codeHash,
+          { abi }
+        );
+      }
+    );
+  }
+}
+
+export default translate(withRouter(Code));

+ 130 - 0
packages/app-contracts/src/Codes/Upload.tsx

@@ -0,0 +1,130 @@
+// Copyright 2017-2019 @polkadot/app-contracts 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 { Hash } from '@polkadot/types/interfaces';
+import { ApiProps } from '@polkadot/react-api/types';
+
+import BN from 'bn.js';
+import React from 'react';
+import { SubmittableResult } from '@polkadot/api';
+import { withApi, withMulti } from '@polkadot/react-api';
+import { Button, InputFile, TxButton } from '@polkadot/react-components';
+import { compactAddLength } from '@polkadot/util';
+
+import ContractModal, { ContractModalProps, ContractModalState } from '../Modal';
+import store from '../store';
+import translate from '../translate';
+import { GAS_LIMIT } from '../constants';
+
+interface Props extends ContractModalProps, ApiProps {}
+
+interface State extends ContractModalState {
+  gasLimit: BN;
+  isWasmValid: boolean;
+  wasm?: Uint8Array | null;
+}
+
+class Upload extends ContractModal<Props, State> {
+  constructor (props: Props) {
+    super(props);
+
+    this.defaultState = {
+      ...this.defaultState,
+      gasLimit: new BN(GAS_LIMIT),
+      isWasmValid: false,
+      wasm: null
+    };
+    this.state = this.defaultState;
+    this.headerText = props.t('Upload WASM');
+  }
+
+  protected renderContent = (): React.ReactNode => {
+    const { t } = this.props;
+    const { isBusy, isWasmValid, wasm } = this.state;
+
+    return (
+      <>
+        {this.renderInputAccount()}
+        <InputFile
+          help={t('The compiled WASM for the contract that you wish to deploy. Each unique code blob will be attached with a code hash that can be used to create new instances.')}
+          isDisabled={isBusy}
+          isError={!isWasmValid}
+          label={t('compiled contract WASM')}
+          onChange={this.onAddWasm}
+          placeholder={
+            wasm && !isWasmValid
+              ? t('The code is not recognized as being in valid WASM format')
+              : null
+          }
+        />
+        {this.renderInputName()}
+        {this.renderInputAbi()}
+        {this.renderInputGas()}
+      </>
+    );
+  }
+
+  protected renderButtons = (): React.ReactNode => {
+    const { api, t } = this.props;
+    const { accountId, gasLimit, isBusy, isNameValid, isWasmValid, wasm } = this.state;
+    const isValid = !isBusy && accountId && isNameValid && isWasmValid && !gasLimit.isZero() && !!accountId;
+
+    return (
+      <Button.Group>
+        {this.renderCancelButton()}
+        <TxButton
+          accountId={accountId}
+          icon='upload'
+          isDisabled={!isValid}
+          isPrimary
+          label={t('Upload')}
+          onClick={this.toggleBusy(true)}
+          onSuccess={this.onSuccess}
+          onFailed={this.toggleBusy(false)}
+          params={[gasLimit, wasm]}
+          tx={api.tx.contracts ? 'contracts.putCode' : 'contract.putCode'}
+          ref={this.button}
+        />
+      </Button.Group>
+    );
+  }
+
+  private onAddWasm = (wasm: Uint8Array, name: string): void => {
+    const isWasmValid = wasm.subarray(0, 4).toString() === '0,97,115,109'; // '\0asm'
+
+    this.setState({ wasm: compactAddLength(wasm), isWasmValid });
+    this.onChangeName(name);
+  }
+
+  private onSuccess = (result: SubmittableResult): void => {
+    const { api } = this.props;
+
+    this.setState(({ abi, name, tags }): Pick<State, never> | null => {
+      const section = api.tx.contracts ? 'contracts' : 'contract';
+      const record = result.findRecord(section, 'CodeStored');
+
+      if (record) {
+        const codeHash = record.event.data[0];
+
+        if (!codeHash || !name) {
+          return null;
+        }
+
+        store.saveCode(codeHash as Hash, { abi, name, tags })
+          .then((): void => this.onClose())
+          .catch((error: any): void => {
+            console.error('Unable to save code', error);
+          });
+      }
+
+      return { isBusy: false };
+    });
+  }
+}
+
+export default withMulti(
+  Upload,
+  translate,
+  withApi
+);

+ 76 - 0
packages/app-contracts/src/Codes/ValidateCode.tsx

@@ -0,0 +1,76 @@
+/* eslint-disable @typescript-eslint/camelcase */
+// Copyright 2017-2019 @polkadot/app-contracts 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 { PrefabWasmModule } from '@polkadot/types/interfaces';
+import { I18nProps } from '@polkadot/react-components/types';
+import { ApiProps } from '@polkadot/react-api/types';
+
+import React from 'react';
+import { Option } from '@polkadot/types';
+import { withCalls } from '@polkadot/react-api';
+import { InfoForInput } from '@polkadot/react-components';
+import { isHex } from '@polkadot/util';
+
+import translate from '../translate';
+
+interface Props extends ApiProps, I18nProps {
+  codeHash?: string | null;
+  contracts_codeStorage?: Option<PrefabWasmModule>;
+  onChange: (isValid: boolean) => void;
+}
+
+interface State {
+  isStored: boolean;
+  isValidHex: boolean;
+  isValid: boolean;
+}
+
+class ValidateCode extends React.PureComponent<Props, State> {
+  public state: State = {
+    isStored: false,
+    isValidHex: false,
+    isValid: false
+  };
+
+  public static getDerivedStateFromProps ({ codeHash, contracts_codeStorage, onChange }: Props): State {
+    const isValidHex = !!codeHash && isHex(codeHash) && codeHash.length === 66;
+    const isStored = !!contracts_codeStorage && contracts_codeStorage.isSome;
+    const isValid = isValidHex && isStored;
+
+    // FIXME Really not convinced this is the correct place to do this type of callback?
+    onChange(isValid);
+
+    return {
+      isStored,
+      isValidHex,
+      isValid
+    };
+  }
+
+  public render (): React.ReactNode {
+    const { t } = this.props;
+    const { isValid, isValidHex } = this.state;
+
+    if (isValid || !isValidHex) {
+      return null;
+    }
+
+    return (
+      <InfoForInput type='error'>
+        {
+          isValidHex
+            ? t('Unable to find on-chain WASM code for the supplied codeHash')
+            : t('The codeHash is not a valid hex hash')
+        }
+      </InfoForInput>
+    );
+  }
+}
+
+export default translate(
+  withCalls<Props>(
+    ['query.contracts.codeStorage', { fallbacks: ['query.contract.codeStorage'], paramName: 'codeHash' }]
+  )(ValidateCode)
+);

+ 98 - 0
packages/app-contracts/src/Codes/index.tsx

@@ -0,0 +1,98 @@
+// 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 { ComponentProps } from '../types';
+
+import React from 'react';
+import { Button, CardGrid } from '@polkadot/react-components';
+
+import contracts from '../store';
+import translate from '../translate';
+
+import Code from './Code';
+import Upload from './Upload';
+import Add from './Add';
+
+interface Props extends ComponentProps, I18nProps {}
+
+interface State {
+  isAddOpen: boolean;
+  isUploadOpen: boolean;
+}
+
+class Codes extends React.PureComponent<Props, State> {
+  public state: State = {
+    isAddOpen: false,
+    isUploadOpen: false
+  };
+
+  public render (): React.ReactNode {
+    const { basePath, showDeploy, t } = this.props;
+    const { isAddOpen, isUploadOpen } = this.state;
+
+    return (
+      <>
+        <CardGrid
+          emptyText={t('No code hashes available')}
+          buttons={
+            <Button.Group>
+              <Button
+                icon='upload'
+                isPrimary
+                label={t('Upload WASM')}
+                onClick={this.showUpload}
+              />
+              <Button.Or />
+              <Button
+                icon='add'
+                label={t('Add an existing code hash')}
+                onClick={this.showAdd}
+              />
+            </Button.Group>
+          }
+        >
+          {contracts.getAllCode().map((code): React.ReactNode => {
+            return (
+              <Code
+                key={code.json.codeHash}
+                code={code}
+                showDeploy={showDeploy}
+              />
+            );
+          })}
+        </CardGrid>
+        <Upload
+          basePath={basePath}
+          isNew
+          onClose={this.hideUpload}
+          isOpen={isUploadOpen}
+        />
+        <Add
+          basePath={basePath}
+          onClose={this.hideAdd}
+          isOpen={isAddOpen}
+        />
+      </>
+    );
+  }
+
+  private showUpload = (): void => {
+    this.setState({ isUploadOpen: true });
+  }
+
+  private hideUpload = (): void => {
+    this.setState({ isUploadOpen: false });
+  }
+
+  private showAdd = (): void => {
+    this.setState({ isAddOpen: true });
+  }
+
+  private hideAdd = (): void => {
+    this.setState({ isAddOpen: false });
+  }
+}
+
+export default translate(Codes);

+ 135 - 0
packages/app-contracts/src/Contracts/Add.tsx

@@ -0,0 +1,135 @@
+// Copyright 2017-2019 @polkadot/app-contracts 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 { ApiProps } from '@polkadot/react-api/types';
+import { I18nProps } from '@polkadot/react-components/types';
+import { ActionStatus } from '@polkadot/react-components/Status/types';
+
+import React from 'react';
+import { withApi } from '@polkadot/react-api';
+import { AddressRow, Button, Input } from '@polkadot/react-components';
+import keyring from '@polkadot/ui-keyring';
+
+import ContractModal, { ContractModalProps, ContractModalState } from '../Modal';
+import ValidateAddr from './ValidateAddr';
+
+import translate from '../translate';
+
+interface Props extends ContractModalProps, ApiProps, I18nProps {}
+
+interface State extends ContractModalState {
+  address?: string | null;
+  isAddressValid: boolean;
+}
+
+class Add extends ContractModal<Props, State> {
+  constructor (props: Props) {
+    super(props);
+    this.defaultState = {
+      ...this.defaultState,
+      address: null,
+      name: 'New Contract',
+      isAddressValid: false,
+      isNameValid: true
+    };
+    this.state = this.defaultState;
+    this.headerText = props.t('Add an existing contract');
+  }
+
+  public isContract = true;
+
+  protected renderContent = (): React.ReactNode => {
+    const { t } = this.props;
+    const { address, isAddressValid, isBusy, name } = this.state;
+
+    return (
+      <AddressRow
+        defaultName={name}
+        isValid
+        value={address || null}
+      >
+        <Input
+          autoFocus
+          help={t('The address for the deployed contract instance.')}
+          isDisabled={isBusy}
+          isError={!isAddressValid}
+          label={t('contract address')}
+          onChange={this.onChangeAddress}
+          onEnter={this.submit}
+          value={address || ''}
+        />
+        <ValidateAddr
+          address={address}
+          onChange={this.onValidateAddr}
+        />
+        {this.renderInputName()}
+        {this.renderInputAbi()}
+      </AddressRow>
+    );
+  }
+
+  protected renderButtons = (): React.ReactNode => {
+    const { t } = this.props;
+    const { isAddressValid, isAbiValid, isNameValid } = this.state;
+    const isValid = isNameValid && isAddressValid && isAbiValid;
+
+    return (
+      <Button.Group>
+        {this.renderCancelButton()}
+        <Button
+          icon='save'
+          isDisabled={!isValid}
+          isPrimary
+          label={t('Save')}
+          onClick={this.onAdd}
+          ref={this.button}
+        />
+      </Button.Group>
+    );
+  }
+
+  private onChangeAddress = (address: string): void => {
+    this.setState({ address, isAddressValid: false });
+  }
+
+  private onValidateAddr = (isAddressValid: boolean): void => {
+    this.setState({ isAddressValid });
+  }
+
+  private onAdd = (): void => {
+    const { api } = this.props;
+    const status: Partial<ActionStatus> = { action: 'create' };
+    const { address, abi, name, tags } = this.state;
+
+    if (!address || !abi || !name) {
+      return;
+    }
+
+    try {
+      const json = {
+        name,
+        tags,
+        contract: {
+          abi,
+          genesisHash: api.genesisHash.toHex()
+        }
+      };
+
+      keyring.saveContract(address, json);
+
+      status.account = address;
+      status.status = address ? 'success' : 'error';
+      status.message = 'contract added';
+
+      this.onClose();
+    } catch (error) {
+      console.error(error);
+
+      status.status = 'error';
+      status.message = error.message;
+    }
+  }
+}
+
+export default translate(withApi(Add));

+ 273 - 0
packages/app-contracts/src/Contracts/Call.tsx

@@ -0,0 +1,273 @@
+// Copyright 2017-2019 @polkadot/app-contracts 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 { ContractCallOutcome } from '@polkadot/api-contract/types';
+import { ApiProps } from '@polkadot/react-api/types';
+import { BareProps, I18nProps, StringOrNull } from '@polkadot/react-components/types';
+import { ContractExecResult } from '@polkadot/types/interfaces/contracts';
+
+import BN from 'bn.js';
+import React, { useState, useEffect } from 'react';
+import styled from 'styled-components';
+import { Button, Dropdown, IconLink, InputAddress, InputBalance, InputNumber, Modal, Toggle, TxButton } from '@polkadot/react-components';
+import { PromiseContract as ApiContract } from '@polkadot/api-contract';
+import { withApi, withMulti } from '@polkadot/react-api';
+import { createValue } from '@polkadot/react-params/values';
+import { isNull } from '@polkadot/util';
+
+import Params from '../Params';
+import Outcome from './Outcome';
+
+import translate from '../translate';
+import { GAS_LIMIT } from '../constants';
+import { getCallMessageOptions } from './util';
+
+interface Props extends BareProps, I18nProps, ApiProps {
+  callContract: ApiContract | null;
+  callMessageIndex: number | null;
+  callResults: ContractExecResult[];
+  isOpen: boolean;
+  onChangeCallContractAddress: (callContractAddress: StringOrNull) => void;
+  onChangeCallMessageIndex: (callMessageIndex: number) => void;
+  onClose: () => void;
+}
+
+function Call (props: Props): React.ReactElement<Props> | null {
+  const { className, isOpen, callContract, callMessageIndex, onChangeCallContractAddress, onChangeCallMessageIndex, onClose, api, t } = props;
+
+  if (isNull(callContract) || isNull(callMessageIndex)) {
+    return null;
+  }
+
+  const hasRpc = api.rpc.contracts && api.rpc.contracts.call;
+  let callMessage = callContract.getMessage(callMessageIndex);
+
+  const [accountId, setAccountId] = useState<StringOrNull>(null);
+  const [endowment, setEndowment] = useState<BN>(new BN(0));
+  const [gasLimit, setGasLimit] = useState<BN>(new BN(GAS_LIMIT));
+  const [isBusy, setIsBusy] = useState(false);
+  const [outcomes, setOutcomes] = useState<ContractCallOutcome[]>([]);
+  const [params, setParams] = useState<any[]>(callMessage ? callMessage.def.args.map(({ type }): any => createValue({ type })) : []);
+  const [useRpc, setUseRpc] = useState(callMessage && !callMessage.def.mutates);
+
+  useEffect((): void => {
+    callMessage = callContract.getMessage(callMessageIndex);
+
+    setParams(callMessage ? callMessage.def.args.map(({ type }): any => createValue({ type })) : []);
+    if (!callMessage || callMessage.def.mutates) {
+      setUseRpc(false);
+    } else {
+      setUseRpc(true);
+    }
+  }, [callContract, callMessageIndex]);
+
+  useEffect((): void => {
+    setOutcomes([]);
+  }, [callContract]);
+
+  const _onChangeAccountId = (accountId: StringOrNull): void => setAccountId(accountId);
+
+  const _onChangeCallMessageIndexString = (callMessageIndexString: string): void => {
+    onChangeCallMessageIndex && onChangeCallMessageIndex(
+      parseInt(callMessageIndexString, 10) || 0
+    );
+  };
+
+  const _onChangeEndowment = (endowment?: BN): void => endowment && setEndowment(endowment);
+  const _onChangeGasLimit = (gasLimit?: BN): void => gasLimit && setGasLimit(gasLimit);
+
+  const _onChangeParams = (params: any[]): void => setParams(params);
+  const _toggleBusy = (): void => setIsBusy(!isBusy);
+
+  const _constructTx = (): any[] => {
+    if (!accountId || !callMessage || !callMessage.fn || !callContract || !callContract.address) {
+      return [];
+    }
+
+    return [callContract.address.toString(), endowment, gasLimit, callMessage.fn(...params)];
+  };
+
+  const _onSubmitRpc = (): void => {
+    if (!accountId) return;
+
+    callContract
+      .call('rpc', callMessage.def.name, endowment, gasLimit, ...params)
+      .send(accountId)
+      .then(
+        (outcome: ContractCallOutcome): void => {
+          setOutcomes([outcome, ...outcomes]);
+        }
+      );
+  };
+
+  const _onClearOutcomes = (): void => setOutcomes([]);
+  const _onClearOutcome = (outcomeIndex: number) => (): void => {
+    setOutcomes(outcomes.slice(0, outcomeIndex).concat(outcomes.slice(outcomeIndex + 1)));
+  };
+
+  const isEndowmentValid = true;
+  const isGasValid = !gasLimit.isZero();
+  const isValid = !!accountId && isEndowmentValid && isGasValid && callContract && callContract.address && callContract.abi;
+
+  return (
+    <Modal
+      className={[className || '', 'app--contracts-Modal'].join(' ')}
+      dimmer='inverted'
+      onClose={onClose}
+      open={isOpen}
+    >
+      <Modal.Header>
+        {t('Call a contract')}
+      </Modal.Header>
+      <Modal.Content>
+        {callContract && (
+          <div className='contracts--CallControls'>
+            <InputAddress
+              defaultValue={accountId}
+              help={t('Specify the user account to use for this contract call. And fees will be deducted from this account.')}
+              isDisabled={isBusy}
+              label={t('call from account')}
+              onChange={_onChangeAccountId}
+              type='account'
+              value={accountId}
+            />
+            <InputAddress
+              help={t('A deployed contract that has either been deployed or attached. The address and ABI are used to construct the parameters.')}
+              isDisabled={isBusy}
+              label={t('contract to use')}
+              onChange={onChangeCallContractAddress}
+              type='contract'
+              value={callContract.address.toString()}
+            />
+            {callMessageIndex !== null && (
+              <>
+                <Dropdown
+                  defaultValue={`${callMessage.index}`}
+                  help={t('The message to send to this contract. Parameters are adjusted based on the ABI provided.')}
+                  isDisabled={isBusy}
+                  isError={callMessage === null}
+                  label={t('message to send')}
+                  onChange={_onChangeCallMessageIndexString}
+                  options={getCallMessageOptions(callContract)}
+                  value={`${callMessage.index}`}
+                />
+                <Params
+                  isDisabled={isBusy}
+                  onChange={_onChangeParams}
+                  params={
+                    callMessage
+                      ? callMessage.def.args
+                      : undefined
+                  }
+                />
+              </>
+            )}
+            <InputBalance
+              help={t('The allotted value for this contract, i.e. the amount transferred to the contract as part of this call.')}
+              isDisabled={isBusy}
+              isError={!isEndowmentValid}
+              label={t('value')}
+              onChange={_onChangeEndowment}
+              value={endowment}
+            />
+            <InputNumber
+              defaultValue={gasLimit}
+              help={t('The maximum amount of gas that can be used by this call. If the code requires more, the call will fail.')}
+              isDisabled={isBusy}
+              isError={!isGasValid}
+              label={t('maximum gas allowed')}
+              onChange={_onChangeGasLimit}
+              value={gasLimit}
+            />
+          </div>
+        )}
+        {hasRpc && (
+          <Toggle
+            className='rpc-toggle'
+            isDisabled={!!callMessage && callMessage.def.mutates}
+            label={
+              useRpc
+                ? t('send as RPC call')
+                : t('send as transaction')
+            }
+            onChange={setUseRpc}
+            value={useRpc || false}
+          />
+        )}
+        <Button.Group>
+          <Button
+            icon='cancel'
+            isNegative
+            onClick={onClose}
+            label={t('Cancel')}
+          />
+          <Button.Or />
+          {useRpc
+            ? (
+              <Button
+                icon='sign-in'
+                isDisabled={!isValid}
+                isPrimary
+                label={t('Call')}
+                onClick={_onSubmitRpc}
+              />
+            )
+            : (
+              <TxButton
+                accountId={accountId}
+                icon='sign-in'
+                isDisabled={!isValid}
+                isPrimary
+                label={t('Call')}
+                onClick={_toggleBusy}
+                onFailed={_toggleBusy}
+                onSuccess={_toggleBusy}
+                params={_constructTx}
+                tx={api.tx.contracts ? 'contracts.call' : 'contract.call'}
+              />
+            )
+          }
+        </Button.Group>
+        {outcomes.length > 0 && (
+          <>
+            <h3>
+              {t('Call results')}
+              <IconLink
+                className='clear-all'
+                icon='close'
+                label={t('Clear all')}
+                onClick={_onClearOutcomes}
+              />
+            </h3>
+            <div>
+              {outcomes.map((outcome, index): React.ReactNode => (
+                <Outcome
+                  key={`outcome-${index}`}
+                  onClear={_onClearOutcome(index)}
+                  outcome={outcome}
+                />
+              ))}
+            </div>
+          </>
+        )}
+      </Modal.Content>
+    </Modal>
+  );
+}
+
+export default withMulti(
+  styled(Call)`
+    .rpc-toggle {
+      margin-top: 1rem;
+      display: flex;
+      justify-content: flex-end;
+    }
+
+    .clear-all {
+      float: right;
+    }
+  `,
+  translate,
+  withApi
+);

+ 117 - 0
packages/app-contracts/src/Contracts/Contract.tsx

@@ -0,0 +1,117 @@
+// 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 } from 'react';
+import styled from 'styled-components';
+import { RouteComponentProps } from 'react-router';
+import { withRouter } from 'react-router-dom';
+import keyring from '@polkadot/ui-keyring';
+import { PromiseContract as ApiContract } from '@polkadot/api-contract';
+import { AddressRow, Button, Card, Forget, Messages } from '@polkadot/react-components';
+
+import translate from '../translate';
+
+interface Props extends I18nProps, RouteComponentProps {
+  basePath: string;
+  contract: ApiContract;
+  onCall: (_?: number) => () => void;
+}
+
+const ContractCard = styled(Card)`
+  && {
+    min-width: 100%;
+    max-width: 100%;
+  }
+`;
+
+function Contract (props: Props): React.ReactElement<Props> | null {
+  const { contract: { abi, address }, onCall, t } = props;
+
+  if (!address || !abi) {
+    return null;
+  }
+
+  const [isForgetOpen, setIsForgetOpen] = useState(false);
+
+  const _toggleForget = (): void => setIsForgetOpen(!isForgetOpen);
+  const _onForget = (): void => {
+    if (!address) {
+      return;
+    }
+
+    const status: Partial<ActionStatus> = {
+      account: address,
+      action: 'forget'
+    };
+
+    try {
+      keyring.forgetContract(address.toString());
+      status.status = 'success';
+      status.message = t('address forgotten');
+    } catch (error) {
+      status.status = 'error';
+      status.message = error.message;
+    }
+    _toggleForget();
+  };
+
+  return (
+    <ContractCard>
+      {
+        isForgetOpen && (
+          <Forget
+            address={address.toString()}
+            mode='contract'
+            onForget={_onForget}
+            key='modal-forget-contract'
+            onClose={_toggleForget}
+          />
+        )
+      }
+      <AddressRow
+        buttons={
+          <div className='contracts--Contract-buttons'>
+            <Button
+              icon='trash'
+              isNegative
+              onClick={_toggleForget}
+              size='small'
+              tooltip={t('Forget this contract')}
+            />
+            <Button
+              icon='play'
+              isPrimary
+              label={t('execute')}
+              onClick={onCall()}
+              size='small'
+              tooltip={t('Call a method on this contract')}
+            />
+          </div>
+        }
+        isContract
+        isEditable
+        type='contract'
+        value={address}
+        withBalance={false}
+        withNonce={false}
+        withTags
+      >
+        <details>
+          <summary>{t('Messages')}</summary>
+          <Messages
+            address={address.toString()}
+            contractAbi={abi}
+            isRemovable={false}
+            onSelect={onCall}
+          />
+        </details>
+      </AddressRow>
+    </ContractCard>
+  );
+}
+
+export default translate(withRouter(Contract));

+ 104 - 0
packages/app-contracts/src/Contracts/Outcome.tsx

@@ -0,0 +1,104 @@
+// Copyright 2017-2019 @polkadot/app-contracts 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 { ContractCallOutcome } from '@polkadot/api-contract/types';
+
+import React from 'react';
+import styled from 'styled-components';
+import { AddressMini, Button, MessageSignature, Output } from '@polkadot/react-components';
+
+interface Props {
+  className?: string;
+  onClear?: () => void;
+  outcome: ContractCallOutcome;
+}
+
+function Outcome (props: Props): React.ReactElement<Props> | null {
+  const { className, onClear, outcome: { message, origin, output, params, isSuccess, time } } = props;
+  const dateTime = new Date(time);
+
+  return (
+    <div className={className}>
+      <div className='info'>
+        <AddressMini
+          className='origin'
+          value={origin}
+          withAddress={false}
+          isPadded={false}
+        />
+        <MessageSignature
+          message={message}
+          params={params}
+        />
+        <span className='date-time'>
+          {dateTime.toLocaleDateString()}
+          {' '}
+          {dateTime.toLocaleTimeString()}
+        </span>
+        <Button
+          className='icon-button clear-btn'
+          icon='close'
+          size='mini'
+          isPrimary
+          onClick={onClear}
+        />
+      </div>
+      <Output
+        isError={!isSuccess}
+        className='output'
+        value={(output || '()').toString()}
+        withCopy
+        withLabel={false}
+      />
+    </div>
+  );
+}
+
+export default styled(Outcome)`
+  & {
+    .info {
+      display: inline-flex;
+      justify-content: center;
+      align-items: center;
+      padding: 0.5rem;
+
+      & > *:not(:first-child) {
+        padding-left: 1.5rem !important;
+      }
+    }
+
+    .clear-btn {
+      opacity: 0;
+    }
+
+    .date-time {
+      color: #aaa;
+      white-space: nowrap;
+    }
+
+    .origin {
+      padding-left: 0 !important;
+
+      * {
+        margin-left: 0 !important;
+      }
+    }
+
+    .output {
+      font-family: monospace;
+      margin-left: 3.5rem;
+
+      .ui--output {
+        border-color: #aaa;
+        margin: 0;
+      }
+    }
+
+    &:hover {
+      .clear-btn {
+        opacity: 1;
+      }
+    }
+  }
+`;

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