Browse Source

Merge branch 'master' into joystream-update-upstream

Mokhtar Naamani 5 years ago
parent
commit
0629f8c2bf
100 changed files with 5972 additions and 3744 deletions
  1. 1 0
      .123trigger
  2. 27 0
      .circleci/config.yml
  3. 3 0
      .eslintignore
  4. 16 0
      .eslintrc.js
  5. 2 1
      .gitignore
  6. 16 3
      .gitlab-ci.yml
  7. 0 1
      .travis.yml
  8. 13 0
      BOUNTIES.md
  9. 45 0
      CHANGELOG.md
  10. 5 6
      README.md
  11. 1 0
      jest.config.js
  12. 1 1
      lerna.json
  13. 24 18
      package.json
  14. 2 2
      packages/app-123code/README.md
  15. 3 3
      packages/app-123code/package.json
  16. 41 45
      packages/app-123code/src/AccountSelector.tsx
  17. 28 0
      packages/app-123code/src/Summary.tsx
  18. 56 77
      packages/app-123code/src/SummaryBar.tsx
  19. 16 10
      packages/app-123code/src/Transfer.tsx
  20. 0 16
      packages/app-123code/src/index.css
  21. 17 31
      packages/app-123code/src/index.tsx
  22. 7 5
      packages/app-accounts/package.json
  23. 2 4
      packages/app-accounts/scripts/vanitygen.js
  24. 182 0
      packages/app-accounts/src/Account.tsx
  25. 0 138
      packages/app-accounts/src/Backup.tsx
  26. 105 0
      packages/app-accounts/src/Banner.tsx
  27. 0 166
      packages/app-accounts/src/ChangePass.tsx
  28. 0 436
      packages/app-accounts/src/Creator.tsx
  29. 0 377
      packages/app-accounts/src/Editor.tsx
  30. 0 71
      packages/app-accounts/src/Forgetting.tsx
  31. 1 1
      packages/app-accounts/src/MemoForm.tsx
  32. 122 0
      packages/app-accounts/src/Overview.tsx
  33. 0 161
      packages/app-accounts/src/Restore.tsx
  34. 79 59
      packages/app-accounts/src/Vanity/Match.tsx
  35. 0 57
      packages/app-accounts/src/Vanity/index.css
  36. 137 79
      packages/app-accounts/src/Vanity/index.tsx
  37. 3 3
      packages/app-accounts/src/bipWorker.ts
  38. 0 14
      packages/app-accounts/src/index.css
  39. 57 122
      packages/app-accounts/src/index.tsx
  40. 44 0
      packages/app-accounts/src/md/basic.md
  41. 144 0
      packages/app-accounts/src/modals/Backup.tsx
  42. 182 0
      packages/app-accounts/src/modals/ChangePass.tsx
  43. 407 0
      packages/app-accounts/src/modals/Create.tsx
  44. 59 0
      packages/app-accounts/src/modals/CreateConfirmation.tsx
  45. 174 0
      packages/app-accounts/src/modals/Import.tsx
  46. 115 0
      packages/app-accounts/src/modals/Qr.tsx
  47. 244 0
      packages/app-accounts/src/modals/Transfer.tsx
  48. 21 5
      packages/app-accounts/src/types.ts
  49. 26 6
      packages/app-accounts/src/vanitygen/calculate.ts
  50. 81 37
      packages/app-accounts/src/vanitygen/cli.ts
  51. 14 6
      packages/app-accounts/src/vanitygen/generate.ts
  52. 4 6
      packages/app-accounts/src/vanitygen/index.ts
  53. 2 2
      packages/app-accounts/src/vanitygen/sort.ts
  54. 24 18
      packages/app-accounts/src/vanitygen/types.d.ts
  55. 0 8
      packages/app-accounts/src/worker-loader.d.ts
  56. 3 3
      packages/app-address-book/package.json
  57. 183 0
      packages/app-address-book/src/Address.tsx
  58. 0 204
      packages/app-address-book/src/Creator.tsx
  59. 0 265
      packages/app-address-book/src/Editor.tsx
  60. 0 90
      packages/app-address-book/src/Forgetting.tsx
  61. 63 0
      packages/app-address-book/src/Overview.tsx
  62. 45 118
      packages/app-address-book/src/index.tsx
  63. 6 0
      packages/app-address-book/src/md/basic.md
  64. 186 0
      packages/app-address-book/src/modals/Create.tsx
  65. 10 2
      packages/app-address-book/src/types.ts
  66. 0 0
      packages/app-claims/LICENSE
  67. 1 0
      packages/app-claims/README.md
  68. 17 0
      packages/app-claims/package.json
  69. 152 0
      packages/app-claims/src/Claim.tsx
  70. 259 0
      packages/app-claims/src/index.tsx
  71. 8 0
      packages/app-claims/src/secp256k1.d.ts
  72. 7 0
      packages/app-claims/src/translate.ts
  73. 19 0
      packages/app-claims/src/util.spec.ts
  74. 111 0
      packages/app-claims/src/util.ts
  75. 4 3
      packages/app-contracts/package.json
  76. 138 34
      packages/app-contracts/src/ABI.tsx
  77. 0 183
      packages/app-contracts/src/Call.tsx
  78. 0 278
      packages/app-contracts/src/Code.tsx
  79. 113 0
      packages/app-contracts/src/Codes/Add.tsx
  80. 166 0
      packages/app-contracts/src/Codes/Code.tsx
  81. 128 0
      packages/app-contracts/src/Codes/Upload.tsx
  82. 24 22
      packages/app-contracts/src/Codes/ValidateCode.tsx
  83. 98 0
      packages/app-contracts/src/Codes/index.tsx
  84. 135 0
      packages/app-contracts/src/Contracts/Add.tsx
  85. 311 0
      packages/app-contracts/src/Contracts/Call.tsx
  86. 150 0
      packages/app-contracts/src/Contracts/Contract.tsx
  87. 27 22
      packages/app-contracts/src/Contracts/ValidateAddr.tsx
  88. 120 0
      packages/app-contracts/src/Contracts/index.tsx
  89. 287 0
      packages/app-contracts/src/Deploy.tsx
  90. 0 364
      packages/app-contracts/src/Instantiate.tsx
  91. 224 0
      packages/app-contracts/src/Modal.tsx
  92. 28 17
      packages/app-contracts/src/Params.tsx
  93. 64 0
      packages/app-contracts/src/RemoveABI.tsx
  94. 90 65
      packages/app-contracts/src/index.tsx
  95. 31 60
      packages/app-contracts/src/store.ts
  96. 27 19
      packages/app-contracts/src/types.ts
  97. 0 0
      packages/app-council/LICENSE
  98. 1 0
      packages/app-council/README.md
  99. 17 0
      packages/app-council/package.json
  100. 166 0
      packages/app-council/src/Motions/Motion.tsx

+ 1 - 0
.123trigger

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

+ 27 - 0
.circleci/config.yml

@@ -0,0 +1,27 @@
+version: 2
+jobs:
+  build:
+    branches:
+      ignore:
+        - gh-pages
+    working_directory: ~/polkadot-js
+    docker:
+      - image: circleci/node:10
+    steps:
+      - checkout
+      - restore_cache:
+          key: dependency-cache-{{ checksum "yarn.lock" }}
+      - run:
+          name: install-apt
+          command: sudo apt-get install libusb-1.0.0
+      - run:
+          name: install-deps
+          command: yarn install --frozen-lockfile
+      - save_cache:
+          key: dependency-cache-{{ checksum "yarn.lock" }}
+          paths:
+            - ./node_modules
+            - ~/.cache/yarn
+      - run:
+          name: build
+          command: yarn polkadot-dev-circleci-build

+ 3 - 0
.eslintignore

@@ -0,0 +1,3 @@
+**/build/*
+**/coverage/*
+**/node_modules/*

+ 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.eslint.json'
+    ]
+  },
+  rules: {
+    ...base.rules,
+    '@typescript-eslint/no-explicit-any': 'off'
+  }
+};

+ 2 - 1
.gitignore

@@ -9,8 +9,9 @@ tmp/
 .env.test.local
 .env.production.local
 .npmrc
+cc-test-reporter
 package-lock.json
 npm-debug.log*
 yarn-debug.log*
 yarn-error.log*
-!patches/**
+.idea/

+ 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

+ 0 - 1
.travis.yml

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

+ 13 - 0
BOUNTIES.md

@@ -0,0 +1,13 @@
+# 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.

+ 45 - 0
CHANGELOG.md

@@ -1,3 +1,48 @@
+# 0.35.1
+
+- Api 0.91.1, Util 1.2.1, Extension 0.10.1
+- Support for accouns 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 popint (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 unbonding & 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

+ 5 - 6
README.md

@@ -16,16 +16,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
 

+ 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.36.0-beta.24"
 }

+ 24 - 18
package.json

@@ -1,5 +1,5 @@
 {
-  "version": "0.32.0-beta.6",
+  "version": "0.36.0-beta.24",
   "private": true,
   "engines": {
     "node": ">=10.13.0",
@@ -10,45 +10,51 @@
     "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.93.0-beta.7",
+    "@polkadot/api-contract": "^0.93.0-beta.7",
+    "@polkadot/keyring": "^1.4.1",
+    "@polkadot/types": "^0.93.0-beta.7",
+    "@polkadot/util": "^1.4.1",
+    "@polkadot/util-crypto": "^1.4.1",
+    "@types/styled-components": "4.1.8",
     "babel-core": "^7.0.0-bridge.0",
     "rxjs": "^6.4.0",
-    "typescript": "^3.4.5"
+    "typescript": "^3.6.3"
   },
   "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",
     "clean": "polkadot-dev-clean-build",
+    "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"
   },
   "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.6.0",
+    "@babel/runtime": "^7.6.0",
+    "@polkadot/dev-react": "^0.31.0-beta.8",
+    "@polkadot/ts": "^0.1.72",
+    "autoprefixer": "^9.6.1",
     "empty": "^0.10.1",
-    "gh-pages": "^2.0.1",
+    "gh-pages": "^2.1.1",
     "html-loader": "^0.5.5",
-    "markdown-loader": "^5.0.0",
-    "postcss": "^7.0.13",
+    "markdown-loader": "^5.1.0",
+    "postcss": "^7.0.18",
     "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-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"
+  },
+  "dependencies": {
+    "@types/lodash": "^4.14.138",
+    "lodash": "^4.17.15"
   }
 }

+ 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.36.0-beta.24",
   "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.6.0",
+    "@polkadot/react-components": "^0.36.0-beta.24"
   }
 }

+ 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) => 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 | undefined>();
+
+  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;
+`;

+ 56 - 77
packages/app-123code/src/SummaryBar.tsx

@@ -1,89 +1,72 @@
+/* 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, useState, useEffect } 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>
-};
+interface Props extends BareProps, I18nProps {
+  balances_totalIssuance?: BN;
+  chain_bestNumber?: BN;
+  chain_bestNumberLag?: BN;
+  session_validators?: AccountId[];
+  staking_intentions?: AccountId[];
+}
 
-class SummaryBar extends React.PureComponent<Props, State> {
-  state: State = {
-    nextUp: []
-  };
+function SummaryBar ({ balances_totalIssuance, chain_bestNumber, chain_bestNumberLag, staking_intentions = [], session_validators = [] }: Props): React.ReactElement<Props> {
+  const { api, systemChain, systemName, systemVersion } = useContext(ApiContext);
+  const [nextUp, setNextUp] = useState<AccountId[]>([]);
 
-  static getDerivedStateFromProps ({ staking_intentions, session_validators }: Props): State | null {
-    if (!staking_intentions || !session_validators) {
-      return null;
+  useEffect((): void => {
+    if (staking_intentions && session_validators) {
+      setNextUp(staking_intentions.filter((accountId): boolean =>
+        !session_validators.find((validatorId): boolean =>
+          validatorId.eq(accountId)
+        )
+      ));
     }
+  }, [staking_intentions, session_validators]);
 
-    return {
-      nextUp: staking_intentions.filter((accountId) =>
-        !session_validators.find((validatorId) => validatorId.eq(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;
-
-    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>
-          <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) => (
-              <IdentityIcon key={index} value={accountId} size={20} />
-            ))
-          }</Bubble>
-          <Bubble icon='circle' label='total tokens'>
-            {formatBalance(balances_totalIssuance)}
-          </Bubble>
-        </div>
-      </summary>
-    );
-  }
+  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>
+        <Bubble icon='chess queen' label='validators'>{
+          session_validators.map((accountId, index): React.ReactNode => (
+            <IdentityIcon key={index} value={accountId} size={20} />
+          ))
+        }</Bubble>
+        <Bubble icon='chess bishop' label='next up'>{
+          nextUp.map((accountId, index): React.ReactNode => (
+            <IdentityIcon key={index} value={accountId} size={20} />
+          ))
+        }</Bubble>
+        <Bubble icon='circle' label='total tokens'>
+          {formatBalance(balances_totalIssuance)}
+        </Bubble>
+      </div>
+    </summary>
+  );
 }
 
 // inject the actual API calls automatically into props
@@ -92,10 +75,6 @@ export default translate(
     'derive.chain.bestNumber',
     'derive.chain.bestNumberLag',
     'query.balances.totalIssuance',
-    'query.session.validators',
-    'rpc.chain.getRuntimeVersion',
-    'rpc.system.chain',
-    'rpc.system.name',
-    'rpc.system.version'
+    'query.session.validators'
   )(SummaryBar)
 );

+ 16 - 10
packages/app-123code/src/Transfer.tsx

@@ -4,20 +4,22 @@
 
 import BN from 'bn.js';
 import React from 'react';
-import { Button, InputAddress, InputBalance, TxButton } from '@polkadot/ui-app';
+import { Button, InputAddress, InputBalance, TxButton, TxComponent } from '@polkadot/react-components';
 
-type Props = {
-  accountId?: string
-};
-type State = {
+import Summary from './Summary';
+
+interface Props {
+  accountId?: string;
+}
+interface State {
   amount?: BN;
   recipientId?: string;
-};
+}
 
-export default class Transfer extends React.PureComponent<Props> {
-  state: State = {};
+export default class Transfer extends TxComponent<Props, State> {
+  public state: State = {};
 
-  render () {
+  public render (): React.ReactNode {
     const { accountId } = this.props;
     const { amount, recipientId } = this.state;
 
@@ -29,22 +31,26 @@ export default class Transfer extends React.PureComponent<Props> {
             <InputAddress
               label='recipient address for this transfer'
               onChange={this.onChangeRecipient}
+              onEnter={this.sendTx}
               type='all'
             />
             <InputBalance
               label='amount to transfer'
               onChange={this.onChangeAmount}
+              onEnter={this.sendTx}
             />
             <Button.Group>
               <TxButton
                 accountId={accountId}
                 label='make transfer'
+                labelIcon='send'
                 params={[recipientId, amount]}
                 tx='balances.transfer'
+                ref={this.button}
               />
             </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>
+          <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 | undefined>();
+
+  return (
+    // in all apps, the main wrapper is setup to allow the padding
+    // and margins inside the application. (Just from a consistent pov)
+    <main className={className}>
+      <SummaryBar />
+      <AccountSelector onChange={setAccountId} />
+      <Transfer accountId={accountId} />
+    </main>
+  );
 }
 
 export default translate(App);

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

@@ -1,6 +1,6 @@
 {
   "name": "@polkadot/app-accounts",
-  "version": "0.32.0-beta.6",
+  "version": "0.36.0-beta.24",
   "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.6.0",
+    "@polkadot/react-components": "^0.36.0-beta.24",
+    "@polkadot/react-qr": "^0.45.0-beta.13",
     "@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.7.0",
     "file-saver": "^2.0.0",
-    "yargs": "^13.2.0"
+    "yargs": "^14.0.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: {}
       }]
     ]
   });

+ 182 - 0
packages/app-accounts/src/Account.tsx

@@ -0,0 +1,182 @@
+// 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 styled from 'styled-components';
+import { AddressCard, AddressInfo, Button, ChainLock, Forget, Icon } from '@polkadot/react-components';
+import keyring from '@polkadot/ui-keyring';
+
+import Backup from './modals/Backup';
+import ChangePass from './modals/ChangePass';
+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 [isEditable, setIsEditable] = useState(false);
+  const [isExternal, setIsExternal] = useState(false);
+  const [isForgetOpen, setIsForgetOpen] = useState(false);
+  const [isPasswordOpen, setIsPasswordOpen] = useState(false);
+  const [isTransferOpen, setIsTransferOpen] = useState(false);
+
+  useEffect((): void => {
+    const account = keyring.getAccount(address);
+
+    setGenesisHash((account && account.meta.genesisHash) || null);
+    setIsEditable((account && !(account.meta.isInjected || account.meta.isHardware)) || false);
+    setIsExternal((account && account.meta.isExternal) || false);
+  }, [address]);
+
+  const _toggleBackup = (): void => setIsBackupOpen(!isBackupOpen);
+  const _toggleForget = (): void => setIsForgetOpen(!isForgetOpen);
+  const _togglePass = (): void => setIsPasswordOpen(!isPasswordOpen);
+  const _toggleTransfer = (): void => setIsTransferOpen(!isTransferOpen);
+  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 });
+  };
+
+  // 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 && (
+              <Button
+                isNegative
+                onClick={_toggleForget}
+                icon='trash'
+                size='small'
+                tooltip={t('Forget this account')}
+              />
+            )}
+            {isEditable && !isExternal && (
+              <>
+                <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
+              isPrimary
+              label={<><Icon name='paper plane' /> {t('send')}</>}
+              onClick={_toggleTransfer}
+              size='small'
+              tooltip={t('Send funds from this account')}
+            />
+          </div>
+          {isEditable && !isExternal && (
+            <div className='others'>
+              <ChainLock
+                genesisHash={genesisHash}
+                onChange={_onGenesisChange}
+              />
+            </div>
+          )}
+        </div>
+      }
+      className={className}
+      isEditable={isEditable}
+      type='account'
+      value={address}
+      withExplorer
+      withIndex
+      withTags
+    >
+      {address && (
+        <>
+          {isBackupOpen && (
+            <Backup
+              key='modal-backup-account'
+              onClose={_toggleBackup}
+              address={address}
+            />
+          )}
+          {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);

+ 1 - 1
packages/app-accounts/src/MemoForm.tsx

@@ -1,5 +1,5 @@
 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';

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

@@ -0,0 +1,122 @@
+// 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 QrModal from './modals/Qr';
+import Account from './Account';
+import Banner from './Banner';
+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 [isQrOpen, setIsQrOpen] = useState(false);
+  const emptyScreen = !(isCreateOpen || isImportOpen || isQrOpen) && (Object.keys(accounts).length === 0);
+
+  const _toggleCreate = (): void => setIsCreateOpen(!isCreateOpen);
+  const _toggleImport = (): void => setIsImportOpen(!isImportOpen);
+  const _toggleQr = (): void => setIsQrOpen(!isQrOpen);
+
+  return (
+    <CardGrid
+      banner={<Banner />}
+      buttons={
+        <Button.Group>
+          <Button
+            isPrimary
+            label={t('Add account')}
+            labelIcon='add'
+            onClick={_toggleCreate}
+          />
+          <Button.Or />
+          <Button
+            isPrimary
+            label={t('Restore JSON')}
+            labelIcon='sync'
+            onClick={_toggleImport}
+          />
+          <Button.Or />
+          <Button
+            isPrimary
+            label={t('Add via Qr')}
+            labelIcon='qrcode'
+            onClick={_toggleQr}
+          />
+          {isLedger() && (
+            <>
+              <Button.Or />
+              <Button
+                isPrimary
+                label={t('Query Ledger')}
+                labelIcon='question'
+                onClick={queryLedger}
+              />
+            </>
+          )}
+        </Button.Group>
+      }
+      isEmpty={emptyScreen}
+      emptyText={t('No account yet?')}
+    >
+      {isCreateOpen && (
+        <CreateModal
+          onClose={_toggleCreate}
+          onStatusChange={onStatusChange}
+        />
+      )}
+      {isImportOpen && (
+        <ImportModal
+          onClose={_toggleImport}
+          onStatusChange={onStatusChange}
+        />
+      )}
+      {isQrOpen && (
+        <QrModal
+          onClose={_toggleQr}
+          onStatusChange={onStatusChange}
+        />
+      )}
+      {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;
-}

+ 137 - 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,27 +55,40 @@ 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;
 
@@ -83,17 +103,23 @@ class VanityApp extends React.PureComponent<Props, State> {
               ? t('Stop generation')
               : t('Start generation')
           }
+          labelIcon={
+            isRunning
+              ? 'stop'
+              : 'sign-in'
+          }
+          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,20 @@ 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;
+  }
+`);

+ 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 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: false,
+    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
+            isNegative
+            label={t('Cancel')}
+            labelIcon='cancel'
+            onClick={onClose}
+          />
+          <Button.Or />
+          <Button
+            isDisabled={!isPassValid}
+            isPrimary
+            label={t('Download')}
+            labelIcon='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: false });
+      console.error(error);
+
+      status.status = 'error';
+      status.message = error.message;
+      return;
+    }
+
+    onClose();
+  }
+
+  private onChangePass = (password: string): void => {
+    this.setState({
+      isPassValid: keyring.isPassValid(password),
+      password
+    });
+  }
+}
+
+export default translate(Backup);

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

@@ -0,0 +1,182 @@
+// 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 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
+            isNegative
+            label={t('Cancel')}
+            labelIcon='cancel'
+            onClick={onClose}
+          />
+          <Button.Or />
+          <Button
+            isDisabled={!isNewValid || !isOldValid}
+            isPrimary
+            label={t('Change')}
+            labelIcon='sign-in'
+            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 keyring.isPassValid(password);
+  }
+}
+
+export default translate(ChangePass);

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

@@ -0,0 +1,407 @@
+// 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 { ApiProps } from '@polkadot/react-api/types';
+import { ActionStatus } from '@polkadot/react-components/Status/types';
+import { KeypairType } from '@polkadot/util-crypto/types';
+import { ModalProps } from '../types';
+
+import FileSaver from 'file-saver';
+import React from 'react';
+import styled from 'styled-components';
+import { DEV_PHRASE } from '@polkadot/keyring/defaults';
+import { withApi, withMulti } from '@polkadot/react-api';
+import { AddressRow, Button, Dropdown, Input, Labelled, Modal, Password } from '@polkadot/react-components';
+import { InputAddress } from '@polkadot/react-components/InputAddress';
+import keyring from '@polkadot/ui-keyring';
+import uiSettings from '@polkadot/ui-settings';
+import { isHex, u8aToHex } from '@polkadot/util';
+import { keyExtractSuri, mnemonicGenerate, mnemonicValidate, randomAsU8a } from '@polkadot/util-crypto';
+
+import translate from '../translate';
+import CreateConfirmation from './CreateConfirmation';
+
+interface Props extends ModalProps, ApiProps, I18nProps {
+  seed?: string;
+  type?: KeypairType;
+}
+
+type SeedType = 'bip' | 'raw' | 'dev';
+
+interface SeedOption {
+  text: string;
+  value: SeedType;
+}
+
+interface AddressState {
+  address: string;
+  deriveError: string | null;
+  derivePath: string;
+  isSeedValid: boolean;
+  pairType: KeypairType;
+  seed: string;
+  seedType: SeedType;
+}
+
+interface State extends AddressState {
+  isNameValid: boolean;
+  isPassValid: boolean;
+  isValid: boolean;
+  name: string;
+  password: string;
+  seedOptions: SeedOption[];
+  showWarning: boolean;
+  tags: string[];
+}
+
+const DEFAULT_TYPE = 'sr25519';
+
+function deriveValidate (seed: string, derivePath: string, pairType: KeypairType): string | null {
+  try {
+    const { path } = keyExtractSuri(`${seed}${derivePath}`);
+
+    // we don't allow soft for ed25519
+    if (pairType === 'ed25519') {
+      const firstSoft = path.find(({ isSoft }): boolean => 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 > 0) && (seed.length <= 32)) || isHexSeed(seed);
+}
+
+function addressFromSeed (phrase: string, derivePath: string, pairType: KeypairType): string {
+  return keyring
+    .createFromUri(`${phrase.trim()}${derivePath}`, {}, pairType)
+    .address;
+}
+
+function generateSeed (_seed: string | null, derivePath: string, seedType: SeedType, pairType: KeypairType): AddressState {
+  const seed = ((): string => {
+    switch (seedType) {
+      case 'bip':
+        return mnemonicGenerate();
+      case 'dev':
+        return DEV_PHRASE;
+      default:
+        return _seed || u8aToHex(randomAsU8a());
+    }
+  })();
+  const address = addressFromSeed(seed, derivePath, pairType);
+
+  return {
+    address,
+    deriveError: null,
+    derivePath,
+    isSeedValid: true,
+    pairType,
+    seedType,
+    seed
+  };
+}
+
+class Create extends React.PureComponent<Props, State> {
+  public state: State;
+
+  public constructor (props: Props) {
+    super(props);
+
+    const { isDevelopment, seed, t, type } = this.props;
+    const seedOptions: SeedOption[] = [
+      { value: 'bip', text: t('Mnemonic') },
+      { value: 'raw', text: t('Raw seed') }
+    ];
+
+    if (isDevelopment) {
+      seedOptions.push({ value: 'dev', text: t('Development') });
+    }
+
+    const pairType = type || DEFAULT_TYPE;
+    const seedType = seed ? 'raw' : 'bip';
+
+    this.state = {
+      ...generateSeed(seed || null, '', seedType, pairType),
+      isNameValid: true,
+      isPassValid: false,
+      isValid: false,
+      name: 'new account',
+      password: '',
+      seedOptions,
+      showWarning: false,
+      tags: []
+    };
+  }
+
+  public render (): React.ReactNode {
+    const { className, t } = this.props;
+    const { address, isValid, showWarning } = this.state;
+
+    return (
+      <Modal
+        className={className}
+        dimmer='inverted'
+        open
+      >
+        <Modal.Header>{t('Add an account via seed')}</Modal.Header>
+        {showWarning && (
+          <CreateConfirmation
+            address={address}
+            name={name}
+            onCommit={this.onCommit}
+            onHideWarning={this.onHideWarning}
+          />
+        )}
+        {this.renderInput()}
+        <Modal.Actions>
+          <Button.Group>
+            <Button
+              isNegative
+              label={t('Cancel')}
+              labelIcon='cancel'
+              onClick={this.onDiscard}
+            />
+            <Button.Or />
+            <Button
+              isDisabled={!isValid}
+              isPrimary
+              label={t('Save')}
+              labelIcon='plus'
+              onClick={this.onShowWarning}
+            />
+          </Button.Group>
+        </Modal.Actions>
+      </Modal>
+    );
+  }
+
+  private renderInput (): React.ReactNode {
+    const { t } = this.props;
+    const { address, deriveError, derivePath, isNameValid, isPassValid, isSeedValid, name, pairType, password, seed, seedOptions, seedType } = this.state;
+    const isDevSeed = seedType === 'dev';
+    const seedLabel = ((): string => {
+      switch (seedType) {
+        case 'bip':
+          return t('mnemonic seed');
+        case 'dev':
+          return t('development seed');
+        default:
+          return t('seed (hex or string)');
+      }
+    })();
+
+    return (
+      <Modal.Content>
+        <AddressRow
+          defaultName={name}
+          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={this.onChangeName}
+            onEnter={this.onCommit}
+            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={isDevSeed}
+            label={seedLabel}
+            onChange={this.onChangeSeed}
+            onEnter={this.onCommit}
+            value={seed}
+          >
+            <Dropdown
+              isButton
+              defaultValue={seedType}
+              onChange={this.selectSeedType}
+              options={seedOptions}
+            />
+          </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={this.onChangePass}
+            onEnter={this.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={this.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>". The "/<soft-key>" and "//<hard-key>" may be repeated and mixed`.')}
+              isError={!!deriveError}
+              label={t('secret derivation path')}
+              onChange={this.onChangeDerive}
+              onEnter={this.onCommit}
+              value={derivePath}
+            />
+            {deriveError && (
+              <Labelled label=''><article className='error'>{deriveError}</article></Labelled>
+            )}
+          </details>
+        </AddressRow>
+      </Modal.Content>
+    );
+  }
+
+  private nextState (newState: Partial<State>): void {
+    this.setState(
+      (prevState: State): 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, tags = prevState.tags } = newState;
+        let address = prevState.address;
+        const deriveError = deriveValidate(seed, derivePath, pairType);
+        const isNameValid = !!name;
+        const isPassValid = keyring.isPassValid(password);
+        let isSeedValid = seedType === 'raw'
+          ? rawValidate(seed)
+          : mnemonicValidate(seed);
+
+        if (!deriveError && isSeedValid && (seed !== prevState.seed || derivePath !== prevState.derivePath || pairType !== prevState.pairType)) {
+          try {
+            address = addressFromSeed(seed, derivePath, pairType);
+          } catch (error) {
+            isSeedValid = false;
+          }
+        }
+
+        return {
+          address,
+          deriveError,
+          derivePath,
+          isNameValid,
+          isPassValid,
+          isSeedValid,
+          isValid: isNameValid && isPassValid && isSeedValid,
+          name,
+          pairType,
+          password,
+          seed,
+          seedOptions,
+          seedType,
+          showWarning,
+          tags
+        };
+      }
+    );
+  }
+
+  private onChangeDerive = (derivePath: string): void => {
+    this.nextState({ derivePath });
+  }
+
+  private onChangeName = (name: string): void => {
+    this.nextState({ name: name.trim() });
+  }
+
+  private onChangePairType = (pairType: KeypairType): void => {
+    this.nextState({ pairType });
+  }
+
+  private onChangePass = (password: string): void => {
+    this.nextState({ password });
+  }
+
+  private onChangeSeed = (seed: string): void => {
+    this.nextState({ seed });
+  }
+
+  private onShowWarning = (): void => {
+    this.nextState({ showWarning: true });
+  }
+
+  private onHideWarning = (): void => {
+    this.nextState({ showWarning: false });
+  }
+
+  private onCommit = (): void => {
+    const { onClose, onStatusChange, t } = this.props;
+    const { derivePath, isValid, name, pairType, password, seed, tags } = this.state;
+    const status: Partial<ActionStatus> = { action: 'create' };
+
+    if (!isValid) {
+      return;
+    }
+
+    try {
+      const { json, pair } = keyring.addUri(`${seed}${derivePath}`, password, { name, tags }, pairType);
+      const blob = new Blob([JSON.stringify(json)], { type: 'application/json; charset=utf-8' });
+      const { address } = pair;
+
+      FileSaver.saveAs(blob, `${address}.json`);
+
+      status.account = address;
+      status.status = pair ? 'success' : 'error';
+      status.message = t('created account');
+
+      InputAddress.setLastValue('account', address);
+    } catch (error) {
+      status.status = 'error';
+      status.message = error.message;
+    }
+
+    this.onHideWarning();
+
+    onStatusChange(status as ActionStatus);
+    onClose();
+  }
+
+  private onDiscard = (): void => {
+    const { onClose } = this.props;
+
+    onClose();
+  }
+
+  private selectSeedType = (seedType: SeedType): void => {
+    if (seedType === this.state.seedType) {
+      return;
+    }
+
+    this.setState(({ derivePath, pairType }: State): State => ({
+      ...(generateSeed(null, derivePath, seedType, pairType) as State),
+      seedType
+    }));
+  }
+}
+
+export default withMulti(
+  styled(Create)`
+    .accounts--Creator-advanced {
+      margin-top: 1rem;
+    }
+  `,
+  translate,
+  withApi
+);

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

@@ -0,0 +1,59 @@
+// 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;
+  onCommit: () => void;
+  onHideWarning: () => void;
+}
+
+function CreateConfirmation ({ address, name, onCommit, onHideWarning, t }: Props): React.ReactElement<Props> | null {
+  return (
+    <Modal
+      dimmer='inverted'
+      open
+    >
+      <Modal.Header>
+        {t('Important notice')}
+      </Modal.Header>
+      <Modal.Content>
+        <AddressRow
+          defaultName={name}
+          isInline
+          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
+            isNegative
+            label={t('Cancel')}
+            labelIcon='cancel'
+            onClick={onHideWarning}
+          />
+          <Button.Or />
+          <Button
+            isPrimary
+            label={t('Create and backup account')}
+            labelIcon='plus'
+            onClick={onCommit}
+          />
+        </Button.Group>
+      </Modal.Actions>
+    </Modal>
+  );
+}
+
+export default translate(CreateConfirmation);

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

@@ -0,0 +1,174 @@
+// 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 { AddressRow, Button, InputFile, Modal, Password, TxComponent } from '@polkadot/react-components';
+import { InputAddress } from '@polkadot/react-components/InputAddress';
+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> {
+  public state: State = {
+    address: null,
+    isFileValid: false,
+    isPassValid: false,
+    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
+              isNegative
+              label={t('Cancel')}
+              labelIcon='cancel'
+              onClick={onClose}
+            />
+            <Button.Or />
+            <Button
+              isDisabled={!isFileValid || !isPassValid}
+              isPrimary
+              onClick={this.onSave}
+              label={t('Restore')}
+              labelIcon='sync'
+              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}
+          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: keyring.isPassValid(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);
+    } catch (error) {
+      this.setState({ isPassValid: false });
+
+      status.status = 'error';
+      status.message = error.message;
+      console.error(error);
+    }
+
+    onStatusChange(status as ActionStatus);
+
+    if (status.status !== 'error') {
+      onClose();
+    }
+  }
+}
+
+export default translate(Import);

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

@@ -0,0 +1,115 @@
+// 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, Modal } from '@polkadot/react-components';
+import { InputAddress } from '@polkadot/react-components/InputAddress';
+import { QrScanAddress } from '@polkadot/react-qr';
+import keyring from '@polkadot/ui-keyring';
+
+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 [name, setName] = useState('');
+  const [scanned, setScanned] = useState<Scanned | null>(null);
+  const isNameValid = !!name;
+
+  const _onNameChange = (name: string): void => setName(name.trim());
+  const _onSave = (): void => {
+    if (!scanned || !isNameValid) {
+      return;
+    }
+
+    const { address, genesisHash } = scanned;
+
+    keyring.addExternal(address, { genesisHash, name });
+    InputAddress.setLastValue('account', 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}
+                  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
+            isNegative
+            label={t('Cancel')}
+            labelIcon='cancel'
+            onClick={onClose}
+          />
+          <Button.Or />
+          <Button
+            isDisabled={!scanned || !isNameValid}
+            isPrimary
+            onClick={_onSave}
+            label={t('Create')}
+            labelIcon='sign-in'
+          />
+        </Button.Group>
+      </Modal.Actions>
+    </Modal>
+  );
+}
+
+export default translate(
+  styled(QrModal)`
+    .qr-wrapper {
+      margin: 0 auto;
+      max-width: 30rem;
+    }
+  `
+);

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

@@ -0,0 +1,244 @@
+/* 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 { Index } from '@polkadot/types/interfaces';
+import { ApiProps } from '@polkadot/react-api/types';
+import { DerivedFees } from '@polkadot/api-derive/types';
+import { I18nProps } from '@polkadot/react-components/types';
+
+import BN from 'bn.js';
+import React 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, { calcTxLength } from '@polkadot/react-signer/Checks';
+import { withApi, withCalls, withMulti } from '@polkadot/react-api';
+import { ZERO_FEES } from '@polkadot/react-signer/Checks/constants';
+import { bnMax } from '@polkadot/util';
+
+import translate from '../translate';
+
+interface Props extends ApiProps, I18nProps {
+  balances_fees?: DerivedFees;
+  className?: string;
+  onClose: () => void;
+  recipientId?: string;
+  senderId?: string;
+  system_accountNonce?: BN;
+}
+
+interface State {
+  amount: BN;
+  extrinsic: SubmittableExtrinsic | null;
+  hasAvailable: boolean;
+  maxBalance?: BN;
+  recipientId?: string | null;
+  senderId?: string | null;
+}
+
+const ZERO = new BN(0);
+
+class Transfer extends React.PureComponent<Props, State> {
+  public state: State;
+
+  public constructor (props: Props) {
+    super(props);
+
+    this.state = {
+      amount: ZERO,
+      extrinsic: null,
+      hasAvailable: true,
+      maxBalance: ZERO,
+      recipientId: props.recipientId || null,
+      senderId: props.senderId || null
+    };
+  }
+
+  public componentDidUpdate (prevProps: Props, prevState: State): void {
+    const { balances_fees } = this.props;
+    const { extrinsic, recipientId, senderId } = this.state;
+    const hasLengthChanged = ((extrinsic && extrinsic.encodedLength) || 0) !== ((prevState.extrinsic && prevState.extrinsic.encodedLength) || 0);
+
+    if ((recipientId && prevState.recipientId !== recipientId) ||
+      (balances_fees !== prevProps.balances_fees) || (prevState.senderId !== senderId) ||
+      hasLengthChanged
+    ) {
+      this.setMaxBalance().catch((error: Error): void => console.error(error));
+    }
+  }
+
+  public render (): React.ReactNode {
+    const { className, onClose, recipientId: propRecipientId, senderId: propSenderId, t } = this.props;
+    const { extrinsic, hasAvailable, maxBalance, recipientId, senderId } = this.state;
+    const available = <span className='label'>{t('available ')}</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={available} params={senderId} />}
+              onChange={this.onChangeFrom}
+              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={available} params={recipientId} />}
+              onChange={this.onChangeTo}
+              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 mili is equivalent to sending 0.001.')}
+              isError={!hasAvailable}
+              label={t('amount')}
+              maxValue={maxBalance}
+              onChange={this.onChangeAmount}
+              withMax
+            />
+            <Checks
+              accountId={senderId}
+              extrinsic={extrinsic}
+              isSendable
+              onChange={this.onChangeFees}
+            />
+          </div>
+        </Modal.Content>
+        <Modal.Actions>
+          <Button.Group>
+            <Button
+              isNegative
+              label={t('Cancel')}
+              labelIcon='cancel'
+              onClick={onClose}
+            />
+            <Button.Or />
+            <TxButton
+              accountId={senderId}
+              extrinsic={extrinsic}
+              isDisabled={!hasAvailable}
+              isPrimary
+              label={t('Make Transfer')}
+              labelIcon='send'
+              onStart={onClose}
+              withSpinner={false}
+            />
+          </Button.Group>
+        </Modal.Actions>
+      </Modal>
+    );
+  }
+
+  private nextState (newState: Partial<State>): void {
+    this.setState((prevState: State): State => {
+      const { api } = this.props;
+      const { amount = prevState.amount, recipientId = prevState.recipientId, hasAvailable = prevState.hasAvailable, maxBalance = prevState.maxBalance, senderId = prevState.senderId } = newState;
+      const extrinsic = recipientId && senderId
+        ? api.tx.balances.transfer(recipientId, amount)
+        : null;
+
+      return {
+        amount,
+        extrinsic,
+        hasAvailable,
+        maxBalance,
+        recipientId,
+        senderId
+      };
+    });
+  }
+
+  private onChangeAmount = (amount: BN = new BN(0)): void => {
+    this.nextState({ amount });
+  }
+
+  private onChangeFrom = (senderId: string): void => {
+    this.nextState({ senderId });
+  }
+
+  private onChangeTo = (recipientId: string): void => {
+    this.nextState({ recipientId });
+  }
+
+  private onChangeFees = (hasAvailable: boolean): void => {
+    this.setState({ hasAvailable });
+  }
+
+  private setMaxBalance = async (): Promise<void> => {
+    const { api, balances_fees = ZERO_FEES } = this.props;
+    const { senderId, recipientId } = this.state;
+
+    if (!senderId || !recipientId) {
+      return;
+    }
+
+    const { transferFee, transactionBaseFee, transactionByteFee, creationFee } = balances_fees;
+    const accountNonce = await api.query.system.accountNonce<Index>(senderId);
+    const senderBalance = (await api.derive.balances.all(senderId)).availableBalance;
+    const recipientBalance = (await api.derive.balances.all(recipientId)).availableBalance;
+
+    let prevMax = new BN(0);
+    let maxBalance = new BN(1);
+    let extrinsic;
+
+    while (!prevMax.eq(maxBalance)) {
+      prevMax = maxBalance;
+      extrinsic = api.tx.balances.transfer(recipientId, prevMax);
+
+      const txLength = calcTxLength(extrinsic, accountNonce);
+      const fees = transactionBaseFee
+        .add(transactionByteFee.mul(txLength))
+        .add(transferFee)
+        .add(recipientBalance.isZero() ? creationFee : ZERO);
+
+      maxBalance = bnMax(senderBalance.sub(fees), ZERO);
+    }
+
+    this.nextState({
+      extrinsic,
+      maxBalance
+    });
+  }
+}
+
+export default withMulti(
+  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;
+    }
+  `,
+  translate,
+  withApi,
+  withCalls<Props>(
+    'derive.balances.fees'
+  )
+);

+ 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.36.0-beta.24",
   "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.6.0",
+    "@polkadot/react-components": "^0.36.0-beta.24"
   }
 }

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

@@ -0,0 +1,183 @@
+// 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 from 'react';
+import styled from 'styled-components';
+import { AddressCard, AddressInfo, Button, ChainLock, Forget, Icon } 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;
+}
+
+interface State {
+  current?: KeyringAddress;
+  genesisHash: string | null;
+  isEditable: boolean;
+  isForgetOpen: boolean;
+  isTransferOpen: boolean;
+}
+
+const WITH_BALANCE = { available: true, free: true, total: true };
+const WITH_EXTENDED = { nonce: true };
+
+class Address extends React.PureComponent<Props, State> {
+  public state: State;
+
+  public constructor (props: Props) {
+    super(props);
+
+    const { address } = this.props;
+    const current = keyring.getAddress(address);
+
+    this.state = {
+      current,
+      genesisHash: (current && current.meta.genesisHash) || null,
+      isEditable: true,
+      isForgetOpen: false,
+      isTransferOpen: false
+    };
+  }
+
+  public render (): React.ReactNode {
+    const { address, className, t } = this.props;
+    const { current, genesisHash, isEditable, isForgetOpen, isTransferOpen } = this.state;
+
+    return (
+      <AddressCard
+        buttons={
+          <div className='addresses--Address-buttons buttons'>
+            <div className='actions'>
+              {isEditable && (
+                <Button
+                  isNegative
+                  onClick={this.toggleForget}
+                  icon='trash'
+                  key='forget'
+                  size='small'
+                  tooltip={t('Forget this address')}
+                />
+              )}
+              <Button
+                isPrimary
+                key='deposit'
+                label={<><Icon name='paper plane' /> {t('deposit')}</>}
+                onClick={this.toggleTransfer}
+                size='small'
+                tooltip={t('Send funds to this address')}
+              />
+            </div>
+            {isEditable && (
+              <div className='others'>
+                <ChainLock
+                  genesisHash={genesisHash}
+                  onChange={this.onGenesisChange}
+                />
+              </div>
+            )}
+          </div>
+        }
+        className={className}
+        isEditable={isEditable}
+        type='address'
+        value={address}
+        withExplorer
+        withIndex
+        withTags
+      >
+        {address && current && (
+          <>
+            {isForgetOpen && (
+              <Forget
+                address={current.address}
+                onForget={this.onForget}
+                key='modal-forget-account'
+                mode='address'
+                onClose={this.toggleForget}
+              />
+            )}
+            {isTransferOpen && (
+              <Transfer
+                key='modal-transfer'
+                onClose={this.toggleTransfer}
+                recipientId={address}
+              />
+            )}
+          </>
+        )}
+        <AddressInfo
+          address={address}
+          withBalance={WITH_BALANCE}
+          withExtended={WITH_EXTENDED}
+        />
+      </AddressCard>
+    );
+  }
+
+  private toggleForget = (): void => {
+    this.setState(({ isForgetOpen }): Pick<State, never> => ({
+      isForgetOpen: !isForgetOpen
+    }));
+  }
+
+  private toggleTransfer = (): void => {
+    this.setState(({ isTransferOpen }): Pick<State, never> => ({
+      isTransferOpen: !isTransferOpen
+    }));
+  }
+
+  private onForget = (): void => {
+    const { address, t } = this.props;
+
+    if (!address) {
+      return;
+    }
+
+    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;
+    }
+  }
+
+  private onGenesisChange = (genesisHash: string | null): void => {
+    const { address } = this.props;
+
+    this.setState({ genesisHash }, (): void => {
+      const account = keyring.getAddress(address);
+
+      account && keyring.saveAddress(address, { ...account.meta, genesisHash });
+    });
+  }
+}
+
+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
+            isPrimary
+            label={t('Add contact')}
+            labelIcon='add'
+            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' })
+);

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

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

@@ -0,0 +1,186 @@
+// 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 from 'react';
+
+import { AddressRow, Button, Input, Modal } from '@polkadot/react-components';
+import { InputAddress } from '@polkadot/react-components/InputAddress';
+import keyring from '@polkadot/ui-keyring';
+
+import translate from '../translate';
+
+interface Props extends ModalProps, I18nProps {}
+
+interface State {
+  address: string;
+  isAddressExisting: boolean;
+  isAddressValid: boolean;
+  isNameValid: boolean;
+  isValid: boolean;
+  name: string;
+  tags: string[];
+}
+
+class Create extends React.PureComponent<Props, State> {
+  public state: State = {
+    address: '',
+    isAddressExisting: false,
+    isAddressValid: false,
+    isNameValid: true,
+    isValid: false,
+    name: 'new address',
+    tags: []
+  };
+
+  public render (): React.ReactNode {
+    const { t } = this.props;
+    const { address, isAddressValid, isNameValid, isValid, name } = this.state;
+
+    return (
+      <Modal
+        dimmer='inverted'
+        open
+      >
+        <Modal.Header>{t('Add an address')}</Modal.Header>
+        <Modal.Content>
+          <AddressRow
+            defaultName={name}
+            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={this.onChangeAddress}
+              onEnter={this.onCommit}
+              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={this.onChangeName}
+              onEnter={this.onCommit}
+              value={name}
+            />
+          </AddressRow>
+        </Modal.Content>
+        <Modal.Actions>
+          <Button.Group>
+            <Button
+              isNegative
+              onClick={this.onDiscard}
+              label={t('Cancel')}
+              labelIcon='cancel'
+            />
+            <Button.Or />
+            <Button
+              isDisabled={!isValid}
+              isPrimary
+              onClick={this.onCommit}
+              label={t('Save')}
+              labelIcon='save'
+            />
+          </Button.Group>
+        </Modal.Actions>
+      </Modal>
+    );
+  }
+
+  private nextState (newState: Partial<State>, allowEdit = false): void {
+    this.setState(
+      (prevState: State): State => {
+        let { address = prevState.address, name = prevState.name, tags = prevState.tags } = 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) {
+              if (!allowEdit) {
+                name = old.meta.name || name;
+              }
+
+              isAddressExisting = true;
+              isAddressValid = true;
+            }
+          }
+        } catch (error) {
+          isAddressValid = false;
+        }
+
+        const isNameValid = !!name;
+
+        return {
+          address: newAddress,
+          isAddressExisting,
+          isAddressValid,
+          isNameValid,
+          isValid: isAddressValid && isNameValid,
+          name,
+          tags
+        };
+      }
+    );
+  }
+
+  private onChangeAddress = (address: string): void => {
+    this.nextState({ address });
+  }
+
+  private onChangeName = (name: string): void => {
+    this.nextState({ name }, true);
+  }
+
+  private onCommit = (): void => {
+    const { onClose, onStatusChange, t } = this.props;
+    const { address, isAddressExisting, isValid, name, tags } = this.state;
+    const status: Partial<ActionStatus> = { action: 'create' };
+
+    if (!isValid) {
+      return;
+    }
+
+    try {
+      keyring.saveAddress(address, { name, 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 as ActionStatus);
+    onClose();
+  }
+
+  private onDiscard = (): void => {
+    const { onClose } = this.props;
+
+    onClose();
+  }
+}
+
+export default translate(Create);

+ 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.36.0-beta.24",
+  "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.6.0",
+    "@polkadot/react-components": "^0.36.0-beta.24"
+  }
+}

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

@@ -0,0 +1,152 @@
+/* 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 { Option } from '@polkadot/types';
+import { BalanceOf, EthereumAddress } from '@polkadot/types/interfaces';
+import { I18nProps } from '@polkadot/react-components/types';
+import { ApiProps } from '@polkadot/react-api/types';
+
+import React from 'react';
+import styled from 'styled-components';
+import { withApi, withMulti } from '@polkadot/react-api';
+import { Button, Card } from '@polkadot/react-components';
+import { formatBalance } from '@polkadot/util';
+
+import translate from './translate';
+import { addrToChecksum } from './util';
+
+const ClaimInner = styled.div`
+  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;
+  }
+`;
+
+interface Props extends ApiProps, I18nProps {
+  button: React.ReactNode;
+  ethereumAddress: EthereumAddress | null;
+}
+
+interface State {
+  claim: BalanceOf | null;
+  ethereumAddress: EthereumAddress | null;
+  hasClaim: boolean;
+  isBusy: boolean;
+}
+
+class Claim extends React.PureComponent<Props, State> {
+  public static getDerivedStateFromProps ({ ethereumAddress }: Props, state: State): Pick<State, never> {
+    if (ethereumAddress) {
+      return {
+        ethereumAddress,
+        hasClaim: state.claim && state.claim.gten(0)
+      };
+    }
+
+    return {
+      hasClaim: false
+    };
+  }
+
+  public state: State = {
+    claim: null,
+    ethereumAddress: null,
+    hasClaim: false,
+    isBusy: false
+  }
+
+  public componentDidMount (): void {
+    this.fetchClaim();
+  }
+
+  public componentDidUpdate (_: Props, prevState: State): void {
+    const { ethereumAddress } = this.state;
+    if (ethereumAddress !== prevState.ethereumAddress) {
+      this.fetchClaim();
+    }
+  }
+
+  public render (): React.ReactNode {
+    const { button, t } = this.props;
+    const { claim, ethereumAddress, hasClaim, isBusy } = this.state;
+
+    if (isBusy || !ethereumAddress) {
+      return null;
+    }
+
+    return (
+      <Card
+        isError={!hasClaim || !claim}
+        isSuccess={hasClaim && !!claim}
+      >
+        <ClaimInner>
+          {t('Your Ethereum account')}
+          <h3>{addrToChecksum(ethereumAddress.toString())}</h3>
+          {hasClaim && !!claim
+            ? (
+              <>
+                {t('has a valid claim for')}
+                <h2>{formatBalance(claim)}</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.')}
+              </>
+            )}
+        </ClaimInner>
+      </Card>
+    );
+  }
+
+  private fetchClaim (): void {
+    const { api } = this.props;
+    const { ethereumAddress } = this.state;
+
+    if (!ethereumAddress) {
+      return;
+    }
+
+    this.setState({ isBusy: true }, (): void => {
+      api.query.claims
+        .claims<Option<BalanceOf>>(ethereumAddress.toHex())
+        .then((claim): void => {
+          this.setState({
+            claim: claim.unwrapOr(null),
+            isBusy: false
+          });
+        });
+    });
+  }
+}
+
+export default withMulti(
+  Claim,
+  translate,
+  withApi
+);

+ 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> {
+  public 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
+                    isPrimary
+                    onClick={this.setStep(Step.Sign)}
+                    label={t('Continue')}
+                    labelIcon='sign-in'
+                  />
+                </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
+                      isDisabled={!accountId || !signature}
+                      isPrimary
+                      onClick={this.setStep(Step.Claim)}
+                      label={t('Confirm claim')}
+                      labelIcon='sign-in'
+                    />
+                  </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(['sudo', 'ui']);

+ 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.36.0-beta.24",
   "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.6.0",
+    "@polkadot/api-contract": "^0.93.0-beta.7",
+    "@polkadot/react-components": "^0.36.0-beta.24"
   }
 }

+ 138 - 34
packages/app-contracts/src/ABI.tsx

@@ -2,68 +2,172 @@
 // 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;
+  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;
+}
 
-type State = {
-  abi?: Uint8Array | null,
-  isAbiValid: boolean,
-  name?: string,
-  placeholder?: React.ReactNode | null
-};
+interface State {
+  contractAbi: Abi | null;
+  isAbiValid: boolean;
+  isEmpty: boolean;
+  isError: boolean;
+}
 
 class ABI extends React.PureComponent<Props, State> {
-  state: State = {
-    isAbiValid: true
+  public state: State = {
+    contractAbi: null,
+    isAbiValid: false,
+    isEmpty: true,
+    isError: false
   };
 
-  render () {
-    const { help, isError, label } = this.props;
-    const { isAbiValid, placeholder } = this.state;
+  public constructor (props: Props) {
+    super(props);
+
+    const { contractAbi, isError, isRequired } = this.props;
+    const isAbiValid = !!contractAbi;
+
+    this.state = {
+      contractAbi: contractAbi || 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 } = 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')
+              : t('click to select or drag and drop a JSON ABI file')
+          }
+        />
+      </div>
+    );
+  }
+
+  private renderMessages (): React.ReactNode {
+    const { help, isDisabled, label, onRemove } = 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}
+        />
+      </Labelled>
     );
   }
 
-  private onChange = (u8a: Uint8Array, name: string): void => {
+  private onChange = (u8a: Uint8Array): void => {
     const { onChange } = this.props;
     const json = u8aToString(u8a);
 
     try {
-      const abi = new ContractAbi(JSON.parse(json));
+      const contractAbi = new Abi(JSON.parse(json));
 
       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
+      }, (): 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> {
+  public 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
+          isDisabled={!isValid}
+          isPrimary
+          label={t('Save')}
+          labelIcon='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);

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

@@ -0,0 +1,166 @@
+// 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) => () => void;
+}
+
+interface State {
+  isForgetOpen: boolean;
+  isRemoveABIOpen: boolean;
+}
+
+const CodeCard = styled(Card)`
+  && {
+    min-height: 13rem;
+  }
+`;
+
+class Contract extends React.PureComponent<Props, State> {
+  public state: State = {
+    isForgetOpen: false,
+    isRemoveABIOpen: false
+  };
+
+  public render (): React.ReactNode {
+    const { code, code: { contractAbi } } = this.props;
+
+    return (
+      <CodeCard>
+        {this.renderModals()}
+        <CodeRow
+          buttons={this.renderButtons()}
+          code={code}
+          isEditable
+          withTags
+        >
+          <ABI
+            contractAbi={contractAbi}
+            onChange={this.onChangeABI}
+            onRemove={this.toggleRemoveABI}
+          />
+        </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
+          isPrimary
+          label={t('deploy')}
+          labelIcon='cloud upload'
+          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 toggleForget = (): void => {
+    const { isForgetOpen } = this.state;
+
+    this.setState({
+      isForgetOpen: !isForgetOpen
+    });
+  }
+
+  private toggleRemoveABI = (): void => {
+    const { isRemoveABIOpen } = this.state;
+
+    this.setState({
+      isRemoveABIOpen: !isRemoveABIOpen
+    });
+  }
+
+  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 = async (abi: string | null = null): Promise<void> => {
+    const { code: { json: { codeHash } } } = this.props;
+
+    await contracts.saveCode(
+      codeHash,
+      { abi }
+    );
+  }
+}
+
+export default translate(withRouter(Contract));

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

@@ -0,0 +1,128 @@
+// 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';
+
+interface Props extends ContractModalProps, ApiProps {}
+
+interface State extends ContractModalState {
+  gasLimit: BN;
+  isWasmValid: boolean;
+  wasm?: Uint8Array | null;
+}
+
+class Upload extends ContractModal<Props, State> {
+  public constructor (props: Props) {
+    super(props);
+
+    this.defaultState = {
+      ...this.defaultState,
+      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}
+          isDisabled={!isValid}
+          isPrimary
+          label={t('Upload')}
+          labelIcon='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
+);

+ 24 - 22
packages/app-contracts/src/ValidateCode.tsx → packages/app-contracts/src/Codes/ValidateCode.tsx

@@ -1,40 +1,42 @@
+/* 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 { I18nProps } from '@polkadot/ui-app/types';
-import { ApiProps } from '@polkadot/ui-api/types';
+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, PrefabWasmModule } from '@polkadot/types';
-import { withCalls } from '@polkadot/ui-api';
-import { InfoForInput } from '@polkadot/ui-app';
+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';
+import translate from '../translate';
 
-type Props = ApiProps & I18nProps & {
-  codeHash?: string | null,
-  contract_codeStorage?: Option<PrefabWasmModule>,
-  onChange: (isValid: boolean) => void
-};
+interface Props extends ApiProps, I18nProps {
+  codeHash?: string | null;
+  contracts_codeStorage?: Option<PrefabWasmModule>;
+  onChange: (isValid: boolean) => void;
+}
 
-type State = {
-  isStored: boolean,
-  isValidHex: boolean,
-  isValid: boolean
-};
+interface State {
+  isStored: boolean;
+  isValidHex: boolean;
+  isValid: boolean;
+}
 
-class ValidateCode extends React.PureComponent<Props> {
-  state: State = {
+class ValidateCode extends React.PureComponent<Props, State> {
+  public state: State = {
     isStored: false,
     isValidHex: false,
     isValid: false
   };
 
-  static getDerivedStateFromProps ({ codeHash, contract_codeStorage, onChange }: Props): State {
+  public static getDerivedStateFromProps ({ codeHash, contracts_codeStorage, onChange }: Props): State {
     const isValidHex = !!codeHash && isHex(codeHash) && codeHash.length === 66;
-    const isStored = !!contract_codeStorage && contract_codeStorage.isSome;
+    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?
@@ -47,7 +49,7 @@ class ValidateCode extends React.PureComponent<Props> {
     };
   }
 
-  render () {
+  public render (): React.ReactNode {
     const { t } = this.props;
     const { isValid, isValidHex } = this.state;
 
@@ -69,6 +71,6 @@ class ValidateCode extends React.PureComponent<Props> {
 
 export default translate(
   withCalls<Props>(
-    ['query.contract.codeStorage', { paramName: 'codeHash' }]
+    ['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
+                isPrimary
+                label={t('Upload WASM')}
+                labelIcon='upload'
+                onClick={this.showUpload}
+              />
+              <Button.Or />
+              <Button
+                label={t('Add an existing code hash')}
+                labelIcon='add'
+                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> {
+  public 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
+          isDisabled={!isValid}
+          isPrimary
+          label={t('Save')}
+          labelIcon='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));

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

@@ -0,0 +1,311 @@
+// 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 { BareProps, I18nProps } from '@polkadot/react-components/types';
+
+import BN from 'bn.js';
+import React from 'react';
+import { RouteComponentProps } from 'react-router';
+import { withRouter } from 'react-router-dom';
+import { Abi } from '@polkadot/api-contract';
+import { Button, Dropdown, InputAddress, InputBalance, InputNumber, Modal, TxButton, TxComponent } from '@polkadot/react-components';
+import { getContractAbi } from '@polkadot/react-components/util';
+import { withApi, withMulti } from '@polkadot/react-api';
+
+import translate from '../translate';
+import Params from '../Params';
+
+interface Props extends BareProps, I18nProps, ApiProps, RouteComponentProps<{}> {
+  address: string | null;
+  isOpen: boolean;
+  method: string | null;
+  onClose: () => void;
+}
+
+interface State {
+  accountId: string | null;
+  address: string | null;
+  contractAbi?: Abi | null;
+  endowment: BN;
+  gasLimit: BN;
+  isAddressValid: boolean;
+  isBusy: boolean;
+  method: string | null;
+  params: any[];
+}
+
+class Call extends TxComponent<Props, State> {
+  public defaultState: State = {
+    address: null,
+    accountId: null,
+    endowment: new BN(0),
+    gasLimit: new BN(0),
+    method: null,
+    isAddressValid: false,
+    isBusy: false,
+    params: []
+  };
+
+  public state: State = this.defaultState;
+
+  public static getDerivedStateFromProps ({ address: propsAddress, method: propsMethod, isOpen }: Props, { address, method }: State): Pick<State, never> | null {
+    if (!isOpen) {
+      return {
+        address: null,
+        method: null,
+        contractAbi: null,
+        isAddressValid: false
+      };
+    }
+
+    return {
+      ...(
+        !address
+          ? {
+            address: propsAddress,
+            contractAbi: propsAddress ? getContractAbi(propsAddress) : null,
+            isAddressValid: !!propsAddress
+          }
+          : {}
+      ),
+      ...(
+        !method
+          ? { method: propsMethod }
+          : {}
+      )
+    };
+  }
+
+  public render (): React.ReactNode {
+    const { isOpen, t } = this.props;
+
+    return (
+      <Modal
+        className='app--contracts-Modal'
+        dimmer='inverted'
+        onClose={this.onClose}
+        open={isOpen}
+      >
+        <Modal.Header>
+          {t('Call a contract')}
+        </Modal.Header>
+        <Modal.Content>
+          {this.renderContent()}
+        </Modal.Content>
+        <Modal.Actions>
+          {this.renderButtons()}
+        </Modal.Actions>
+      </Modal>
+    );
+  }
+
+  public renderContent (): React.ReactNode {
+    const { t } = this.props;
+    const { gasLimit } = this.state;
+
+    const [address, contractAbi, method] = this.getCallProps();
+    const isEndowValid = true;
+    const isGasValid = !gasLimit.isZero();
+
+    if (!address || !contractAbi) {
+      return null;
+    }
+
+    const methodOptions = contractAbi
+      ? Object.keys(contractAbi.messages).map((key): { key: string; text: string; value: string } => {
+        const fn = contractAbi.messages[key];
+        const type = fn.type ? `: ${fn.type}` : '';
+        const args = fn.args.map(({ name, type }): string => `${name}: ${type}`);
+        const text = `${key}(${args.join(', ')})${type}`;
+
+        return {
+          key,
+          text,
+          value: key
+        };
+      })
+      : [];
+
+    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'
+        />
+        <InputAddress
+          help={t('A deployed contract that has either been deployed or attached. The address and ABI are used to construct the parameters.')}
+          label={t('contract to use')}
+          onChange={this.onChangeAddress}
+          type='contract'
+          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}
+          style={{ fontFamily: 'monospace' }}
+          value={method}
+        />
+        <Params
+          onChange={this.onChangeParams}
+          onEnter={this.sendTx}
+          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}
+          onEnter={this.sendTx}
+        />
+      </div>
+    );
+  }
+
+  private renderButtons (): React.ReactNode {
+    const { api, t } = this.props;
+    const { accountId, gasLimit, isAddressValid } = this.state;
+    const isEndowValid = true; // !endowment.isZero();
+    const isGasValid = !gasLimit.isZero();
+    const isValid = !!accountId && isEndowValid && isGasValid && isAddressValid;
+
+    return (
+      <Button.Group>
+        <Button
+          isNegative
+          onClick={this.onClose}
+          label={t('Cancel')}
+          labelIcon='cancel'
+        />
+        <Button.Or />
+        <TxButton
+          accountId={accountId}
+          isDisabled={!isValid}
+          isPrimary
+          label={t('Call')}
+          labelIcon='sign-in'
+          onClick={this.toggleBusy}
+          onFailed={this.toggleBusy}
+          onSuccess={this.toggleBusy}
+          params={this.constructCall}
+          tx={api.tx.contracts ? 'contracts.call' : 'contract.call'}
+          ref={this.button}
+        />
+      </Button.Group>
+    );
+  }
+
+  private getCallProps = (): [string | null, Abi | null, string | null] => {
+    let address;
+    let contractAbi;
+    let method;
+
+    if (!this.state.address) {
+      return [null, null, null];
+    } else {
+      address = this.state.address;
+      contractAbi = this.state.contractAbi || getContractAbi(address);
+      method = contractAbi && this.state.method && contractAbi.messages[this.state.method]
+        ? this.state.method
+        : (
+          contractAbi
+            ? Object.keys(contractAbi.messages)[0]
+            : null
+        );
+    }
+
+    return [
+      address || null,
+      contractAbi || null,
+      method || null
+    ];
+  }
+
+  private constructCall = (): any[] => {
+    const {
+      endowment, gasLimit, params
+    } = this.state;
+
+    const [address, contractAbi, method] = this.getCallProps();
+
+    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 contractAbi = getContractAbi(address);
+
+    this.setState({ address, contractAbi, isAddressValid: !!contractAbi });
+  }
+
+  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: any[]): void => {
+    this.setState({ params });
+  }
+
+  private toggleBusy = (): void => {
+    this.setState(({ isBusy }): Pick<State, never> => ({
+      isBusy: !isBusy
+    }));
+  }
+
+  private reset = (): void => {
+    this.setState((state: State): Pick<State, never> => {
+      if (!state.isBusy) {
+        return {
+          ...state,
+          ...this.defaultState
+        };
+      }
+
+      return {};
+    });
+  }
+
+  private onClose = (): void => {
+    const { onClose } = this.props;
+
+    this.reset();
+    onClose && onClose();
+  }
+}
+
+export default withMulti(
+  translate(withRouter(Call)),
+  withApi
+);

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

@@ -0,0 +1,150 @@
+// 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 from 'react';
+import { RouteComponentProps } from 'react-router';
+import { withRouter } from 'react-router-dom';
+import keyring from '@polkadot/ui-keyring';
+import { AddressRow, Button, Card, Forget, Messages } from '@polkadot/react-components';
+import { getContractAbi } from '@polkadot/react-components/util';
+
+import translate from '../translate';
+
+interface Props extends I18nProps, RouteComponentProps {
+  basePath: string;
+  address: string;
+  onCall: (callAddress?: string, callMethod?: string) => void;
+}
+
+interface State {
+  isBackupOpen: boolean;
+  isForgetOpen: boolean;
+  isPasswordOpen: boolean;
+}
+
+class Contract extends React.PureComponent<Props, State> {
+  public state: State = {
+    isBackupOpen: false,
+    isForgetOpen: false,
+    isPasswordOpen: false
+  };
+
+  public render (): React.ReactNode {
+    const { address, onCall } = this.props;
+
+    const contractAbi = getContractAbi(address);
+
+    if (!contractAbi) {
+      return null;
+    }
+
+    return (
+      <Card>
+        {this.renderModals()}
+        <AddressRow
+          buttons={this.renderButtons()}
+          isContract
+          isEditable
+          type='contract'
+          value={address}
+          withBalance={false}
+          withNonce={false}
+          withTags
+        >
+          <Messages
+            address={address}
+            contractAbi={contractAbi}
+            isRemovable={false}
+            onSelect={onCall}
+          />
+        </AddressRow>
+      </Card>
+    );
+  }
+
+  private renderModals (): React.ReactNode {
+    const { address } = this.props;
+    const { isForgetOpen } = this.state;
+
+    if (!address) {
+      return null;
+    }
+
+    const modals = [];
+
+    if (isForgetOpen) {
+      modals.push(
+        <Forget
+          address={address}
+          mode='contract'
+          onForget={this.onForget}
+          key='modal-forget-contract'
+          onClose={this.toggleForget}
+        />
+      );
+    }
+
+    return modals;
+  }
+
+  private toggleForget = (): void => {
+    const { isForgetOpen } = this.state;
+
+    this.setState({
+      isForgetOpen: !isForgetOpen
+    });
+  }
+
+  private onForget = (): void => {
+    const { address, t } = this.props;
+
+    if (!address) {
+      return;
+    }
+
+    const status: Partial<ActionStatus> = {
+      account: address,
+      action: 'forget'
+    };
+
+    try {
+      keyring.forgetContract(address);
+      status.status = 'success';
+      status.message = t('address forgotten');
+    } catch (error) {
+      status.status = 'error';
+      status.message = error.message;
+    }
+    this.toggleForget();
+  }
+
+  private renderButtons (): React.ReactNode {
+    const { address, onCall, t } = this.props;
+
+    return (
+      <div className='contracts--Contract-buttons'>
+        <Button
+          isNegative
+          onClick={this.toggleForget}
+          icon='trash'
+          size='small'
+          tooltip={t('Forget this contract')}
+        />
+        <Button
+          isPrimary
+          label={t('execute')}
+          labelIcon='play'
+          onClick={(): void => onCall(address)}
+          size='small'
+          tooltip={t('Call a method on this contract')}
+        />
+      </div>
+    );
+  }
+}
+
+export default translate(withRouter(Contract));

+ 27 - 22
packages/app-contracts/src/ValidateAddr.tsx → packages/app-contracts/src/Contracts/ValidateAddr.tsx

@@ -1,38 +1,40 @@
+/* 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 { I18nProps } from '@polkadot/ui-app/types';
-import { ApiProps } from '@polkadot/ui-api/types';
+import { ContractInfo } from '@polkadot/types/interfaces';
+import { I18nProps } from '@polkadot/react-components/types';
+import { ApiProps } from '@polkadot/react-api/types';
 
 import React from 'react';
-import { CodeHash, Option } from '@polkadot/types';
-import { withCalls } from '@polkadot/ui-api';
-import { InfoForInput } from '@polkadot/ui-app';
+import { Option } from '@polkadot/types';
+import { withCalls } from '@polkadot/react-api';
+import { InfoForInput } from '@polkadot/react-components';
 import keyring from '@polkadot/ui-keyring';
 
-import translate from './translate';
+import translate from '../translate';
 
-type Props = ApiProps & I18nProps & {
-  address?: string | null,
-  contract_codeHashOf?: Option<CodeHash>,
-  onChange: (isValid: boolean) => void
-};
+interface Props extends ApiProps, I18nProps {
+  address?: string | null;
+  contracts_contractInfoOf?: Option<ContractInfo>;
+  onChange: (isValid: boolean) => void;
+}
 
-type State = {
-  isStored: boolean,
-  isValidAddr: boolean,
-  isValid: boolean
-};
+interface State {
+  isStored: boolean;
+  isValidAddr: boolean;
+  isValid: boolean;
+}
 
-class ValidateAddr extends React.PureComponent<Props> {
-  state: State = {
+class ValidateAddr extends React.PureComponent<Props, State> {
+  public state: State = {
     isStored: false,
     isValidAddr: false,
     isValid: false
   };
 
-  static getDerivedStateFromProps ({ address, contract_codeHashOf, onChange }: Props): State {
+  public static getDerivedStateFromProps ({ address, contracts_contractInfoOf, onChange }: Props): State {
     let isValidAddr = false;
 
     try {
@@ -43,7 +45,10 @@ class ValidateAddr extends React.PureComponent<Props> {
       // ignore
     }
 
-    const isStored = !!contract_codeHashOf && contract_codeHashOf.isSome;
+    const isStored = (
+      (!!contracts_contractInfoOf && contracts_contractInfoOf.isSome)
+      // (!!contract_codeHashOf && contract_codeHashOf.isSome)
+    );
     const isValid = isValidAddr && isStored;
 
     // FIXME Really not convinced this is the correct place to do this type of callback?
@@ -56,7 +61,7 @@ class ValidateAddr extends React.PureComponent<Props> {
     };
   }
 
-  render () {
+  public render (): React.ReactNode {
     const { t } = this.props;
     const { isValid, isValidAddr } = this.state;
 
@@ -78,6 +83,6 @@ class ValidateAddr extends React.PureComponent<Props> {
 
 export default translate(
   withCalls<Props>(
-    ['query.contract.codeHashOf', { paramName: 'address' }]
+    ['query.contracts.contractInfoOf', { fallbacks: ['query.contract.contractInfoOf'], paramName: 'address' }]
   )(ValidateAddr)
 );

+ 120 - 0
packages/app-contracts/src/Contracts/index.tsx

@@ -0,0 +1,120 @@
+// 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 { RouteComponentProps } from 'react-router';
+import { withRouter } from 'react-router-dom';
+import { Button, CardGrid } from '@polkadot/react-components';
+
+import translate from '../translate';
+import Add from './Add';
+import Contract from './Contract';
+import Call from './Call';
+
+interface Props extends ComponentProps, I18nProps, RouteComponentProps {}
+
+interface State {
+  isAddOpen: boolean;
+  isCallOpen: boolean;
+  callAddress: string | null;
+  callMethod: string | null;
+}
+
+class Contracts extends React.PureComponent<Props, State> {
+  public state: State = {
+    callAddress: null,
+    callMethod: null,
+    isAddOpen: false,
+    isCallOpen: false
+  };
+
+  public render (): React.ReactNode {
+    const { accounts, basePath, contracts, hasCode, showDeploy, t } = this.props;
+    const { callAddress, callMethod, isAddOpen, isCallOpen } = this.state;
+
+    return (
+      <>
+        <CardGrid
+          emptyText={t('No contracts available')}
+          buttons={
+            <Button.Group>
+              {hasCode && (
+                <>
+                  <Button
+                    isPrimary
+                    label={t('Deploy a code hash')}
+                    labelIcon='cloud upload'
+                    onClick={showDeploy()}
+                  />
+                  <Button.Or />
+                </>
+              )}
+              <Button
+                isPrimary
+                label={t('Add an existing contract')}
+                labelIcon='add'
+                onClick={this.showAdd}
+              />
+            </Button.Group>
+          }
+        >
+          {accounts && contracts && Object.keys(contracts).map((address): React.ReactNode => {
+            return (
+              <Contract
+                basePath={basePath}
+                address={address}
+                key={address}
+                onCall={this.showCall}
+              />
+            );
+          })}
+        </CardGrid>
+        <Add
+          basePath={basePath}
+          isOpen={isAddOpen}
+          onClose={this.hideAdd}
+        />
+        <Call
+          address={callAddress}
+          isOpen={isCallOpen}
+          method={callMethod}
+          onClose={this.hideCall}
+        />
+      </>
+    );
+  }
+
+  private showAdd = (): void => {
+    this.setState({
+      isAddOpen: true
+    });
+  }
+
+  private hideAdd = (): void => {
+    this.setState({
+      isAddOpen: false
+    });
+  }
+
+  private showCall = (callAddress?: string, callMethod?: string): void => {
+    this.setState({
+      isCallOpen: true,
+      callAddress: callAddress || null,
+      callMethod: callMethod || null
+    });
+  }
+
+  private hideCall = (): void => {
+    this.setState({
+      isCallOpen: false,
+      callAddress: null,
+      callMethod: null
+    });
+  }
+}
+
+export default translate(withRouter(Contracts));

+ 287 - 0
packages/app-contracts/src/Deploy.tsx

@@ -0,0 +1,287 @@
+// 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 { AccountId } from '@polkadot/types/interfaces';
+import { TypeDef } from '@polkadot/types/types';
+import { ApiProps } from '@polkadot/react-api/types';
+import { I18nProps } from '@polkadot/react-components/types';
+
+import BN from 'bn.js';
+import React from 'react';
+import { RouteComponentProps } from 'react-router';
+import { withRouter } from 'react-router-dom';
+import { SubmittableResult } from '@polkadot/api';
+import { Abi } from '@polkadot/api-contract';
+import { withApi, withMulti } from '@polkadot/react-api';
+import keyring from '@polkadot/ui-keyring';
+import { Button, Dropdown, InputBalance, TxButton } from '@polkadot/react-components';
+import { getTypeDef } from '@polkadot/types';
+import createValues from '@polkadot/react-params/values';
+
+import ContractModal, { ContractModalProps, ContractModalState } from './Modal';
+import Params from './Params';
+import store from './store';
+import translate from './translate';
+
+type ConstructOptions = { key: string; text: string; value: string }[];
+
+interface Props extends ContractModalProps, ApiProps, I18nProps, RouteComponentProps {
+  codeHash?: string;
+}
+
+interface State extends ContractModalState {
+  codeHash?: string;
+  constructOptions: ConstructOptions;
+  endowment: BN;
+  isHashValid: boolean;
+  params: any[];
+}
+
+class Deploy extends ContractModal<Props, State> {
+  protected headerText = 'Deploy a new contract';
+
+  public isContract = true;
+
+  public constructor (props: Props) {
+    super(props);
+
+    this.defaultState = {
+      ...this.defaultState,
+      constructOptions: [],
+      endowment: new BN(0),
+      isHashValid: false,
+      params: [],
+      ...Deploy.getCodeState(props.codeHash)
+    };
+    this.state = this.defaultState;
+  }
+
+  public static getDerivedStateFromProps (props: Props, state: State): Pick<State, never> {
+    if (props.codeHash && (!state.codeHash || state.codeHash !== props.codeHash)) {
+      return Deploy.getCodeState(props.codeHash);
+    }
+    return {};
+  }
+
+  private static getContractAbiState = (abi: string | null | undefined, contractAbi: Abi | null = null): Partial<State> => {
+    if (contractAbi) {
+      const args = contractAbi.deploy.args.map(({ name, type }): string => `${name}: ${type}`);
+      const text = `deploy(${args.join(', ')})`;
+
+      return {
+        abi,
+        constructOptions: [{
+          key: 'deploy',
+          text,
+          value: 'deploy'
+        }],
+        contractAbi,
+        isAbiValid: !!contractAbi,
+        params: createValues(
+          contractAbi.deploy.args.map(({ name, type }): { type: TypeDef } => ({
+            type: getTypeDef(type, name)
+          }))
+        )
+      };
+    } else {
+      return {
+        constructOptions: [] as ConstructOptions,
+        abi: null,
+        contractAbi: null,
+        isAbiSupplied: false,
+        isAbiValid: false,
+        params: [] as unknown[]
+      };
+    }
+  }
+
+  private static getCodeState = (codeHash: string | null = null): Pick<State, never> => {
+    if (codeHash) {
+      const code = store.getCode(codeHash);
+
+      if (code) {
+        const { contractAbi, json } = code;
+
+        return {
+          codeHash,
+          isAbiSupplied: !!contractAbi,
+          name: `${json.name} (instance)`,
+          isHashValid: true,
+          isNameValid: true,
+          ...Deploy.getContractAbiState(json.abi, contractAbi)
+        };
+      }
+    }
+
+    return {};
+  }
+
+  protected renderContent = (): React.ReactNode => {
+    const { t } = this.props;
+    const { codeHash, constructOptions, contractAbi, endowment, isAbiSupplied, isBusy, isHashValid } = this.state;
+
+    const isEndowValid = !endowment.isZero();
+    const codeOptions = store.getAllCode().map(({ json: { codeHash, name } }): { text: string; value: string } => ({
+      text: `${name} (${codeHash})`,
+      value: codeHash
+    }));
+
+    const defaultCode = codeOptions.length
+      ? codeOptions[codeOptions.length - 1].value
+      : undefined;
+
+    return (
+      <>
+        {this.renderInputAccount()}
+        <Dropdown
+          defaultValue={defaultCode}
+          help={t('The contract WASM previously deployed. Internally this is identified by the hash of the code, as either created or attached.')}
+          isDisabled={isBusy}
+          isError={!isHashValid}
+          label={t('code for this contract')}
+          onChange={this.onChangeCode}
+          options={codeOptions}
+          value={codeHash}
+        />
+        {this.renderInputName()}
+        {
+          isAbiSupplied
+            ? null
+            : this.renderInputAbi()
+        }
+        {
+          contractAbi
+            ? (
+              <Dropdown
+                defaultValue='deploy'
+                help={t('The deployment constructor information for this contract, as provided by the ABI.')}
+                isDisabled
+                label={t('constructor')}
+                options={constructOptions}
+                style={{ fontFamily: 'monospace' }}
+                value='deploy'
+              />
+            )
+            : null
+        }
+        <Params
+          isDisabled={isBusy}
+          onChange={this.onChangeParams}
+          onEnter={this.sendTx}
+          params={
+            contractAbi
+              ? contractAbi.deploy.args
+              : []
+          }
+        />
+        <InputBalance
+          defaultValue={endowment}
+          help={t('The allotted endownment for this contract, i.e. the amount transferred to the contract upon instantiation.')}
+          isDisabled={isBusy}
+          isError={!isEndowValid}
+          label={t('endowment')}
+          onChange={this.onChangeEndowment}
+          onEnter={this.sendTx}
+          value={endowment}
+        />
+        {this.renderInputGas()}
+      </>
+    );
+  }
+
+  protected renderButtons = (): React.ReactNode => {
+    const { api, t } = this.props;
+    const { accountId, endowment, gasLimit, isAbiValid, isHashValid, isNameValid } = this.state;
+    const isEndowValid = !endowment.isZero();
+    const isGasValid = !gasLimit.isZero();
+    const isValid = isAbiValid && isHashValid && isEndowValid && isGasValid && !!accountId && isNameValid;
+
+    return (
+      <Button.Group>
+        {this.renderCancelButton()}
+        <TxButton
+          accountId={accountId}
+          isDisabled={!isValid}
+          isPrimary
+          label={t('Deploy')}
+          labelIcon='cloud upload'
+          onClick={this.toggleBusy(true)}
+          onFailed={this.toggleBusy(false)}
+          onSuccess={this.onSuccess}
+          params={this.constructCall}
+          tx={api.tx.contracts ? 'contracts.create' : 'contract.create'}
+          ref={this.button}
+        />
+      </Button.Group>
+    );
+  }
+
+  private constructCall = (): any[] => {
+    const { codeHash, contractAbi, endowment, gasLimit, params } = this.state;
+
+    if (!contractAbi) {
+      return [];
+    }
+
+    return [endowment, gasLimit, codeHash, contractAbi.deploy(...params)];
+  }
+
+  protected onAddAbi = (abi: string | null | undefined, contractAbi?: Abi | null): void => {
+    this.setState({
+      ...(Deploy.getContractAbiState(abi, contractAbi) as State)
+    });
+  }
+
+  private onChangeCode = (codeHash: string): void => {
+    this.setState(
+      Deploy.getCodeState(codeHash)
+    );
+  }
+
+  private onChangeEndowment = (endowment?: BN | null): void => {
+    this.setState({ endowment: endowment || new BN(0) });
+  }
+
+  private onChangeParams = (params: any[]): void => {
+    this.setState({ params });
+  }
+
+  private onSuccess = (result: SubmittableResult): void => {
+    const { api, history } = this.props;
+
+    const section = api.tx.contracts ? 'contracts' : 'contract';
+    const record = result.findRecord(section, 'Instantiated');
+
+    if (record) {
+      const address = record.event.data[1] as unknown as AccountId;
+
+      this.setState(({ abi, name, tags }): Pick<State, never> | unknown => {
+        if (!abi || !name) {
+          return;
+        }
+
+        keyring.saveContract(address.toString(), {
+          name,
+          contract: {
+            abi,
+            genesisHash: api.genesisHash.toHex()
+          },
+          tags
+        });
+
+        history.push(this.props.basePath);
+
+        this.onClose();
+
+        return { isBusy: false };
+      });
+    }
+  }
+}
+
+export default withMulti(
+  withRouter(Deploy),
+  translate,
+  withApi
+);

+ 0 - 364
packages/app-contracts/src/Instantiate.tsx

@@ -1,364 +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, Dropdown, Input, InputAddress, InputBalance, InputNumber, TxButton } from '@polkadot/ui-app';
-import { AccountId, ContractAbi } from '@polkadot/types';
-
-import ABI from './ABI';
-import Params from './Params';
-import ValidateAddr from './ValidateAddr';
-import store from './store';
-import translate from './translate';
-
-type Props = ComponentProps & I18nProps;
-
-type State = {
-  abi?: string | null,
-  accountId: string | null,
-  address?: string | null,
-  codeHash?: string,
-  contractAbi?: ContractAbi | null,
-  endowment: BN,
-  gasLimit: BN,
-  isAbiValid: boolean,
-  isAbiSupplied: boolean,
-  isAddressValid: boolean,
-  isBusy: boolean,
-  isHashValid: boolean,
-  isNameValid: boolean,
-  isNew?: boolean,
-  name?: string | null,
-  params: Array<any>
-};
-
-class Create extends React.PureComponent<Props, State> {
-  state: State = {
-    accountId: null,
-    endowment: new BN(0),
-    gasLimit: new BN(0),
-    isAbiValid: false,
-    isAbiSupplied: false,
-    isAddressValid: false,
-    isBusy: false,
-    isHashValid: false,
-    isNameValid: false,
-    isNew: true,
-    params: []
-  };
-
-  render () {
-    const { t } = this.props;
-    const { isNew } = this.state;
-
-    return (
-      <div className='contracts--Instantiate'>
-        <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, codeHash, contractAbi, endowment, gasLimit, isAbiSupplied, isAbiValid, isHashValid, isNameValid } = this.state;
-    const isEndowValid = !endowment.isZero();
-    const isGasValid = !gasLimit.isZero();
-    const isValid = isAbiValid && isHashValid && isEndowValid && isGasValid && !!accountId && isNameValid;
-    const codeOptions = store.getAllCode().map(({ json: { codeHash, name } }) => ({
-      text: `${name} (${codeHash})`,
-      value: codeHash
-    }));
-    const defaultCode = codeOptions.length
-      ? codeOptions[codeOptions.length - 1].value
-      : undefined;
-    const constructOptions = contractAbi
-      ? (() => {
-        const args = contractAbi.deploy.args.map(({ name, type }) => name + ': ' + type);
-        const text = `deploy(${args.join(', ')})`;
-
-        return [{
-          key: 'deploy',
-          text,
-          value: 'deploy'
-        }];
-      })()
-      : [];
-
-    return (
-      <>
-        <InputAddress
-          help={t('Specify the user account to use for this contract creation. And fees will be deducted from this account.')}
-          label={t('deployment account')}
-          onChange={this.onChangeAccount}
-          type='account'
-        />
-        <Dropdown
-          defaultValue={defaultCode}
-          help={t('The contract WASM previously deployed. Internally this is identified by the hash of the code, as either created or attached.')}
-          isError={!isHashValid}
-          label={t('code for this contract')}
-          onChange={this.onChangeCode}
-          options={codeOptions}
-          value={codeHash}
-        />
-        {this.renderInputName()}
-        {
-          isAbiSupplied
-            ? null
-            : this.renderInputAbi()
-        }
-        {
-          contractAbi
-            ? (
-              <Dropdown
-                defaultValue='deploy'
-                help={t('The deployment constructor information for this contract, as provided by the ABI.')}
-                isDisabled
-                label={t('constructor')}
-                options={constructOptions}
-                value='deploy'
-              />
-            )
-            : null
-        }
-        <Params
-          onChange={this.onChangeParams}
-          params={
-            contractAbi
-              ? contractAbi.deploy.args
-              : undefined
-          }
-        />
-        <InputBalance
-          help={t('The allotted endownment for this contract, i.e. the amount transferred to the contract upon instantiation.')}
-          isError={!isEndowValid}
-          label={t('endowment')}
-          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('Instantiate')}
-            onClick={this.toggleBusy}
-            onFailed={this.toggleBusy}
-            onSuccess={this.onSuccess}
-            params={this.constructCall}
-            tx='contract.create'
-          />
-        </Button.Group>
-      </>
-    );
-  }
-
-  private renderExisting () {
-    const { t } = this.props;
-    const { address, isAddressValid, isAbiValid, isNameValid } = this.state;
-    const isValid = isNameValid && isAddressValid && isAbiValid;
-
-    return (
-      <>
-        <Input
-          autoFocus
-          help={t('The address for the deployed contract instance.')}
-          isError={!isAddressValid}
-          label={t('contract address')}
-          onChange={this.onChangeAddress}
-          value={address}
-        />
-        <ValidateAddr
-          address={address}
-          onChange={this.onValidateAddr}
-        />
-        {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. Since we will be making a call into the code, the ABI is required and stored for future operations such as sending messages.')}
-        isError={!isAbiValid}
-        label={t('Contract ABI')}
-        onChange={this.onAddAbi}
-      />
-    );
-  }
-
-  private renderInputName () {
-    const { t } = this.props;
-    const { isNameValid, name } = this.state;
-
-    return (
-      <Input
-        help={t('A name for the deployed contract to help you distinguish. Only used for display purposes.')}
-        isError={!isNameValid}
-        label={t('contract name')}
-        onChange={this.onChangeName}
-        value={name}
-      />
-    );
-  }
-
-  private constructCall = (): Array<any> => {
-    const { codeHash, contractAbi, endowment, gasLimit, params } = this.state;
-
-    if (!contractAbi) {
-      return [];
-    }
-
-    return [endowment, gasLimit, codeHash, contractAbi.deploy(...params)];
-  }
-
-  private onAddAbi = (abi: string | null | undefined, contractAbi: ContractAbi | null, isAbiSupplied: boolean = false): void => {
-    this.setState({ abi, contractAbi, isAbiSupplied, isAbiValid: !!abi });
-  }
-
-  private onChangeAccount = (accountId: string | null): void => {
-    this.setState({ accountId });
-  }
-
-  private onChangeAddress = (address: string): void => {
-    this.setState({ address, isAddressValid: false });
-  }
-
-  private onChangeCode = (codeHash: string): void => {
-    const code = store.getCode(codeHash);
-
-    this.setState({ codeHash, isHashValid: !!code });
-
-    if (code) {
-      if (code.contractAbi) {
-        this.onAddAbi(code.json.abi, code.contractAbi, true);
-      } else {
-        this.onAddAbi(null, null, false);
-      }
-
-      this.onChangeName(`${code.json.name} (instance)`);
-    }
-  }
-
-  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 onChangeName = (name: string): void => {
-    this.setState({ name, isNameValid: name.length !== 0 });
-  }
-
-  private onChangeParams = (params: Array<any>): void => {
-    this.setState({ params });
-  }
-
-  private onValidateAddr = (isAddressValid: boolean): void => {
-    this.setState({ isAddressValid });
-  }
-
-  private toggleBusy = (): void => {
-    this.setState(({ isBusy }) => ({
-      isBusy: !isBusy
-    }));
-  }
-
-  private toggleNew = (): void => {
-    this.setState(({ isNew }) => ({
-      address: null,
-      abi: null,
-      isAddressValid: false,
-      isAbiValid: false,
-      isNameValid: false,
-      isNew: !isNew,
-      name: ''
-    }));
-  }
-
-  private onSave = (): void => {
-    const { address, abi, name } = this.state;
-
-    if (!address || !abi || !name) {
-      return;
-    }
-
-    store.saveContract(new AccountId(address), { abi, name }).catch((error) => {
-      console.error('Unable to save contract', error);
-    });
-
-    this.redirect();
-  }
-
-  private onSuccess = (result: SubmittableResult): void => {
-    const record = result.findRecord('contract', 'Instantiated');
-
-    if (record) {
-      const address = record.event.data[1];
-
-      this.setState(({ abi, name }) => {
-        if (!abi || !name) {
-          return;
-        }
-
-        store.saveContract(address as AccountId, { abi, name }).catch((error) => {
-          console.error('Unable to save contract', error);
-        });
-
-        this.redirect();
-      });
-    }
-
-    this.toggleBusy();
-  }
-
-  private redirect () {
-    window.location.hash = this.props.basePath;
-  }
-}
-
-export default translate(Create);

+ 224 - 0
packages/app-contracts/src/Modal.tsx

@@ -0,0 +1,224 @@
+// 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/react-components/types';
+
+import BN from 'bn.js';
+import React from 'react';
+import { Abi } from '@polkadot/api-contract';
+import { Button, Input, InputAddress, InputNumber, Modal, TxComponent } from '@polkadot/react-components';
+
+import ABI from './ABI';
+
+export interface ContractModalProps extends I18nProps {
+  basePath: string;
+  isNew?: boolean;
+  isOpen: boolean;
+  onClose?: () => void;
+}
+
+export interface ContractModalState {
+  abi?: string | null;
+  accountId?: string | null;
+  contractAbi?: Abi | null;
+  gasLimit: BN;
+  isAbiSupplied: boolean;
+  isAbiValid: boolean;
+  isBusy: boolean;
+  isNameValid: boolean;
+  name?: string | null;
+  tags: string[];
+}
+
+class ContractModal<P extends ContractModalProps, S extends ContractModalState> extends TxComponent<P, S> {
+  // horrible :(
+  protected defaultState: S = {
+    accountId: null,
+    gasLimit: new BN(0),
+    isAbiSupplied: false,
+    isAbiValid: false,
+    isBusy: false,
+    isNameValid: false,
+    name: null,
+    tags: [] as string[]
+  } as S;
+
+  public state: S = this.defaultState;
+
+  protected isContract?: boolean;
+
+  public render (): React.ReactNode {
+    const { isOpen, t } = this.props;
+
+    return (
+      <Modal
+        className='app--contracts-Modal'
+        dimmer='inverted'
+        onClose={this.onClose}
+        open={isOpen}
+      >
+        <Modal.Header>
+          {t(this.headerText)}
+        </Modal.Header>
+        <Modal.Content>
+          {this.renderContent()}
+        </Modal.Content>
+        <Modal.Actions>
+          {this.renderButtons()}
+        </Modal.Actions>
+      </Modal>
+    );
+  }
+
+  protected headerText = '';
+
+  protected renderContent: () => React.ReactNode | null = (): React.ReactNode => null;
+
+  protected renderButtons: () => React.ReactNode | null = (): React.ReactNode => null;
+
+  protected renderInputAbi (): React.ReactNode {
+    const { t } = this.props;
+    const { isBusy } = this.state;
+
+    return (
+      <ABI
+        help={t(
+          this.isContract
+            ? 'The ABI for the WASM code. Since we will be making a call into the code, the ABI is required and stored for future operations such as sending messages.'
+            : 'The ABI for the WASM code. In this step it is optional, but setting it here simplifies the setup of contract instances.'
+        )}
+        label={t(
+          this.isContract
+            ? 'contract ABI'
+            : 'contract ABI (optional)'
+        )}
+        onChange={this.onAddAbi}
+        isDisabled={isBusy}
+        isRequired={this.isContract}
+      />
+    );
+  }
+
+  protected renderInputAccount (): React.ReactNode {
+    const { t } = this.props;
+    const { accountId, isBusy } = this.state;
+
+    return (
+      <InputAddress
+        defaultValue={accountId}
+        help={t('Specify the user account to use for this deployment. And fees will be deducted from this account.')}
+        isDisabled={isBusy}
+        isInput={false}
+        label={t('deployment account')}
+        onChange={this.onChangeAccount}
+        type='account'
+        value={accountId}
+      />
+    );
+  }
+
+  protected renderInputName (): React.ReactNode {
+    const { isNew, t } = this.props;
+    const { isBusy, isNameValid, name } = this.state;
+
+    return (
+      <Input
+        defaultValue={name}
+        help={t(
+          this.isContract
+            ? 'A name for the deployed contract to help users distinguish. Only used for display purposes.'
+            : 'A name for this WASM code to help users distinguish. Only used for display purposes.'
+        )}
+        isDisabled={isBusy}
+        isError={!isNameValid}
+        label={t(
+          this.isContract
+            ? 'contract name'
+            : 'code bundle name'
+        )}
+        onChange={this.onChangeName}
+        onEnter={this[isNew ? 'sendTx' : 'submit']}
+        value={name || ''}
+      />
+    );
+  }
+
+  protected renderInputGas (): React.ReactNode {
+    const { t } = this.props;
+    const { gasLimit, isBusy } = this.state;
+    const isGasValid = !gasLimit.isZero();
+
+    return (
+      <InputNumber
+        defaultValue={gasLimit}
+        help={t('The maximum amount of gas that can be used by this deployment, if the code requires more, the deployment will fail.')}
+        isDisabled={isBusy}
+        isError={!isGasValid}
+        label={t('maximum gas allowed')}
+        onChange={this.onChangeGas}
+        onEnter={this.sendTx}
+        value={gasLimit || ''}
+      />
+    );
+  }
+
+  protected renderCancelButton (): React.ReactNode {
+    const { t } = this.props;
+
+    return (
+      <>
+        <Button
+          isNegative
+          onClick={this.onClose}
+          label={t('Cancel')}
+          labelIcon='cancel'
+        />
+        <Button.Or />
+      </>
+    );
+  }
+
+  protected reset = (): void => {
+    this.setState(
+      this.defaultState
+    );
+  }
+
+  protected toggleBusy = (isBusy?: boolean): () => void =>
+    (): void => {
+      this.setState((state: S): S => {
+        return {
+          isBusy: isBusy === undefined ? !state.isBusy : isBusy
+        } as unknown as S;
+      });
+    }
+
+  protected onClose = (): void => {
+    const { onClose } = this.props;
+    const { isBusy } = this.state;
+
+    onClose && onClose();
+    if (!isBusy) {
+      this.reset();
+    }
+  }
+
+  protected onAddAbi = (abi: string | null | undefined, contractAbi: Abi | null = null, isAbiSupplied = false): void => {
+    this.setState({ abi, contractAbi, isAbiSupplied, isAbiValid: !!abi });
+  }
+
+  protected onChangeAccount = (accountId: string | null): void => {
+    this.setState({ accountId });
+  }
+
+  protected onChangeName = (name: string): void => {
+    this.setState({ name, isNameValid: name.length !== 0 });
+  }
+
+  protected onChangeGas = (gasLimit: BN | undefined): void => {
+    this.setState({ gasLimit: gasLimit || new BN(0) });
+  }
+}
+
+export default ContractModal;

+ 28 - 17
packages/app-contracts/src/Params.tsx

@@ -2,39 +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 { ContractABIArgs } from '@polkadot/types/ContractAbi';
-import { RawParams } from '@polkadot/ui-params/types';
+import { ContractABIFnArg } from '@polkadot/api-contract/types';
+import { TypeDef } from '@polkadot/types/types';
+import { RawParams } from '@polkadot/react-params/types';
 
 import React from 'react';
-import UIParams from '@polkadot/ui-params';
-import { getTypeDef, TypeDef } from '@polkadot/types';
+import UIParams from '@polkadot/react-params';
+import { getTypeDef } from '@polkadot/types';
 
-type Props = {
-  params?: ContractABIArgs,
-  onChange: (values: Array<any>) => void
-};
+interface Props {
+  isDisabled?: boolean;
+  params?: ContractABIFnArg[];
+  onChange: (values: any[]) => void;
+  onEnter?: () => void;
+}
+
+interface ParamDef {
+  name: string;
+  type: TypeDef;
+}
 
-type State = {
-  params: Array<{ name: string, type: TypeDef }>
-};
+interface State {
+  params: ParamDef[];
+}
 
 export default class Params extends React.PureComponent<Props, State> {
-  state: State = { params: [] };
+  public state: State = { params: [] };
 
-  static getDerivedStateFromProps ({ params }: Props): State | null {
+  public static getDerivedStateFromProps ({ params }: Props): State | null {
     if (!params) {
       return { params: [] };
     }
 
     return {
-      params: params.map(({ name, type }) => ({
+      params: params.map(({ name, type }): ParamDef => ({
         name,
         type: getTypeDef(type, name)
       }))
-    } as State;
+    };
   }
 
-  render () {
+  public render (): React.ReactNode {
+    const { isDisabled, onEnter } = this.props;
     const { params } = this.state;
 
     if (!params.length) {
@@ -43,7 +52,9 @@ export default class Params extends React.PureComponent<Props, State> {
 
     return (
       <UIParams
+        isDisabled={isDisabled}
         onChange={this.onChange}
+        onEnter={onEnter}
         params={params}
       />
     );
@@ -52,6 +63,6 @@ export default class Params extends React.PureComponent<Props, State> {
   private onChange = (values: RawParams): void => {
     const { onChange } = this.props;
 
-    onChange(values.map(({ value }) => value));
+    onChange(values.map(({ value }): any => value));
   }
 }

+ 64 - 0
packages/app-contracts/src/RemoveABI.tsx

@@ -0,0 +1,64 @@
+// 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 { CodeStored } from '@polkadot/app-contracts/types';
+
+import React from 'react';
+import { Button, CodeRow, Modal } from '@polkadot/react-components';
+
+import translate from './translate';
+
+interface Props extends I18nProps {
+  code: CodeStored;
+  onClose: () => void;
+  onRemove: () => void;
+}
+
+function RemoveABI ({ code, onClose, onRemove, t }: Props): React.ReactElement<Props> {
+  const _onRemove = (): void => {
+    onClose && onClose();
+    onRemove();
+  };
+  return (
+    <Modal
+      className='app--accounts-Modal'
+      dimmer='inverted'
+      onClose={onClose}
+      open
+    >
+      <Modal.Header>
+        {t('Confirm ABI removal')}
+      </Modal.Header>
+      <Modal.Content>
+        <CodeRow
+          code={code}
+          isInline
+        >
+          <p>{t('You are about to remove this code\'s ABI. Once completed, should you need to access it again, you will have to manually re-upload it.')}</p>
+          <p>{t('This operaion does not impact the associated on-chain code or any of its contracts.')}</p>
+        </CodeRow>
+      </Modal.Content>
+      <Modal.Actions>
+        <Button.Group>
+          <Button
+            isNegative
+            onClick={onClose}
+            label={t('Cancel')}
+            labelIcon='cancel'
+          />
+          <Button.Or />
+          <Button
+            isPrimary
+            onClick={_onRemove}
+            label={t('Remove')}
+            labelIcon='trash'
+          />
+        </Button.Group>
+      </Modal.Actions>
+    </Modal>
+  );
+}
+
+export default translate(RemoveABI);

+ 90 - 65
packages/app-contracts/src/index.tsx

@@ -2,72 +2,68 @@
 // 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 { TabItem } from '@polkadot/ui-app/Tabs';
-import { ComponentProps, LocationProps } from './types';
+import { AppProps, I18nProps } from '@polkadot/react-components/types';
+import { TabItem } from '@polkadot/react-components/Tabs';
+import { ComponentProps } from './types';
 
 import React from 'react';
-import { Route, Switch } from 'react-router';
-import { HelpOverlay, Tabs } from '@polkadot/ui-app';
+import { Route, Switch, RouteComponentProps } from 'react-router';
+import { withRouter } from 'react-router-dom';
+import { HelpOverlay, Tabs } from '@polkadot/react-components';
+import { withMulti, withObservable } from '@polkadot/react-api';
+import keyring from '@polkadot/ui-keyring';
+import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
 
 import introMd from './md/intro.md';
 import store from './store';
 import translate from './translate';
-import Call from './Call';
-import Code from './Code';
-import Instantiate from './Instantiate';
+import Contracts from './Contracts';
+import Codes from './Codes';
+import Deploy from './Deploy';
 
-type Props = AppProps & I18nProps;
-type State = {
-  tabs: Array<TabItem>,
-  updated: number
-};
+interface Props extends AppProps, I18nProps, RouteComponentProps {
+  accounts: SubjectInfo[];
+  contracts: SubjectInfo[];
+}
+
+interface State {
+  codeHash?: string;
+  hasContracts: boolean;
+  isDeployOpen: boolean;
+  updated: number;
+}
 
 class App extends React.PureComponent<Props, State> {
-  state: State;
+  public state: State = {
+    hasContracts: false,
+    isDeployOpen: false,
+    updated: 0
+  };
 
-  constructor (props: Props) {
+  public constructor (props: Props) {
     super(props);
 
-    const { t } = props;
-
     store.on('new-code', this.triggerUpdate);
-    store.on('new-contract', this.triggerUpdate);
+    store.on('removed-code', this.triggerUpdate);
 
     // since we have a dep on the async API, we load here
-    store.loadAll().catch(() => {
+    store.loadAll().catch((): void => {
       // noop, handled internally
     });
+  }
 
-    this.state = {
-      tabs: [
-        {
-          name: 'call',
-          text: t('Call')
-        },
-        {
-          name: 'instantiate',
-          text: t('Instance')
-        },
-        {
-          name: 'code',
-          text: t('Code')
-        }
-      ],
-      updated: 0
+  public static getDerivedStateFromProps ({ contracts }: Props): Pick<State, never> {
+    const hasContracts = !!contracts && Object.keys(contracts).length >= 1;
+
+    return {
+      hasContracts
     };
   }
 
-  render () {
-    const { basePath } = this.props;
-    const { tabs } = this.state;
-    const hidden = store.hasContracts
-      ? []
-      : ['call'];
-
-    if (!store.hasCode) {
-      hidden.push('instantiate');
-    }
+  public render (): React.ReactNode {
+    const { basePath, t } = this.props;
+    const { codeHash, isDeployOpen } = this.state;
+    const hidden: string[] = [];
 
     return (
       <main className='contracts--App'>
@@ -76,46 +72,75 @@ class App extends React.PureComponent<Props, State> {
           <Tabs
             basePath={basePath}
             hidden={hidden}
-            items={tabs}
+            items={[
+              {
+                name: 'code',
+                text: 'Code'
+              },
+              {
+                isRoot: true,
+                name: 'contracts',
+                text: 'Contracts'
+              }
+            ].map((tab): TabItem => ({ ...tab, text: t(tab.text) }))
+            }
           />
         </header>
         <Switch>
-          <Route path={`${basePath}/instantiate`} render={this.renderComponent(Instantiate)} />
-          <Route path={`${basePath}/code`} render={this.renderComponent(Code)} />
-          <Route
-            render={
-              hidden.includes('call')
-                ? (
-                  hidden.includes('instantiate')
-                    ? this.renderComponent(Code)
-                    : this.renderComponent(Instantiate)
-                )
-                : this.renderComponent(Call)
-            }
-          />
+          <Route path={`${basePath}/code`} render={this.renderComponent(Codes)} />
+          <Route render={this.renderComponent(Contracts)} exact />
         </Switch>
+        <Deploy
+          basePath={basePath}
+          codeHash={codeHash}
+          isOpen={isDeployOpen}
+          onClose={this.hideDeploy}
+        />
       </main>
     );
   }
 
-  private renderComponent (Component: React.ComponentType<ComponentProps>) {
-    return ({ match }: LocationProps) => {
-      const { basePath, location, onStatusChange } = this.props;
+  private renderComponent (Component: React.ComponentType<ComponentProps>): () => React.ReactNode {
+    return (): React.ReactNode => {
+      const { accounts, basePath, contracts, onStatusChange } = this.props;
+
+      if (!contracts) {
+        return null;
+      }
 
       return (
         <Component
+          accounts={accounts}
           basePath={basePath}
-          location={location}
-          match={match}
+          contracts={contracts}
+          hasCode={store.hasCode}
           onStatusChange={onStatusChange}
+          showDeploy={this.showDeploy}
         />
       );
     };
   }
 
+  private showDeploy = (codeHash?: string): () => void =>
+    (): void => {
+      this.setState({
+        codeHash: codeHash || undefined,
+        isDeployOpen: true
+      });
+    }
+
+  private hideDeploy = (): void => {
+    this.setState({ isDeployOpen: false });
+  }
+
   private triggerUpdate = (): void => {
     this.setState({ updated: Date.now() });
   }
 }
 
-export default translate(App);
+export default withMulti(
+  withRouter(App),
+  translate,
+  withObservable(keyring.accounts.subject, { propName: 'accounts' }),
+  withObservable(keyring.contracts.subject, { propName: 'contracts' })
+);

+ 31 - 60
packages/app-contracts/src/store.ts

@@ -2,107 +2,82 @@
 // This software may be modified and distributed under the terms
 // of the Apache-2.0 license. See the LICENSE file for details.
 
-import { CodeJson, ContractJson } from './types';
+import { Hash } from '@polkadot/types/interfaces';
+import { CodeJson, CodeStored } from './types';
 
 import EventEmitter from 'eventemitter3';
 import store from 'store';
-import { AccountId, ContractAbi, Hash } from '@polkadot/types';
-import { api } from '@polkadot/ui-api';
+import { Abi } from '@polkadot/api-contract';
+import { createType } from '@polkadot/types';
+import { api } from '@polkadot/react-api';
 
-const PREFIX = 'contract:';
-const KEY_CODE = `${PREFIX}code:`;
-const KEY_CONTRACT = `${PREFIX}addr:`;
-
-const codeRegex = new RegExp(`^${KEY_CODE}`, '');
-const contractRegex = new RegExp(`^${KEY_CONTRACT}`, '');
-
-type CodeStored = { json: CodeJson , contractAbi?: ContractAbi };
-type ContractStored = { json: ContractJson , contractAbi: ContractAbi };
+const KEY_CODE = 'code:';
 
 class Store extends EventEmitter {
-  private allCode: { [index: string]: CodeStored } = {};
-  private allContracts: { [index: string]: ContractStored } = {};
+  private allCode: Record<string, CodeStored> = {};
 
-  get hasCode (): boolean {
+  public get hasCode (): boolean {
     return Object.keys(this.allCode).length !== 0;
   }
 
-  get hasContracts (): boolean {
-    return Object.keys(this.allContracts).length !== 0;
-  }
-
-  getAllCode (): Array<CodeStored> {
+  public getAllCode (): CodeStored[] {
     return Object.values(this.allCode);
   }
 
-  getAllContracts (): Array<ContractStored> {
-    return Object.values(this.allContracts);
-  }
-
-  getCode (codeHash: string): CodeStored {
+  public getCode (codeHash: string): CodeStored {
     return this.allCode[codeHash];
   }
 
-  getContract (address: string): ContractStored {
-    return this.allContracts[address];
-  }
+  // eslint-disable-next-line @typescript-eslint/require-await
+  public async saveCode (codeHash: string | Hash, partial: Partial<CodeJson>): Promise<void> {
+    const hex = (typeof codeHash === 'string' ? createType('Hash', codeHash) : codeHash).toHex();
 
-  async saveCode (codeHash: Hash, partial: Partial<CodeJson>) {
-    await api.isReady;
+    const existing = this.getCode(hex);
 
     const json = {
+      ...(existing ? existing.json : {}),
       ...partial,
-      codeHash: codeHash.toHex(),
+      codeHash: hex,
       genesisHash: api.genesisHash.toHex()
-    } as CodeJson;
+    };
 
     store.set(`${KEY_CODE}${json.codeHash}`, json);
 
-    this.addCode(json);
+    this.addCode(json as CodeJson);
   }
 
-  async saveContract (address: AccountId, partial: Partial<ContractJson>) {
-    await api.isReady;
+  public forgetCode (codeHash: string): void {
+    store.remove(`${KEY_CODE}${codeHash}`);
 
-    const json = {
-      ...partial,
-      address: address.toString(),
-      genesisHash: api.genesisHash.toHex()
-    } as ContractJson;
-
-    store.set(`${KEY_CONTRACT}${address}`, json);
-
-    this.addContract(json);
+    this.removeCode(codeHash);
   }
 
-  async loadAll () {
+  public async loadAll (): Promise<void> {
     try {
       await api.isReady;
 
       const genesisHash = api.genesisHash.toHex();
 
-      store.each((json: CodeJson | ContractJson, key: string) => {
+      store.each((json: CodeJson, key: string): void => {
         if (json && json.genesisHash !== genesisHash) {
           return;
         }
 
-        if (codeRegex.test(key)) {
-          this.addCode(json as CodeJson);
-        } else if (contractRegex.test(key)) {
-          this.addContract(json as ContractJson);
+        if (key.startsWith(KEY_CODE)) {
+          this.addCode(json);
         }
       });
     } catch (error) {
-      console.error('Unable to load contracts', error);
+      console.error('Unable to load code', error);
     }
   }
 
-  private addCode (json: CodeJson) {
+  private addCode (json: CodeJson): void {
     try {
       this.allCode[json.codeHash] = {
         json,
         contractAbi: json.abi
-          ? new ContractAbi(JSON.parse(json.abi))
+          ? new Abi(JSON.parse(json.abi))
           : undefined
       };
 
@@ -112,14 +87,10 @@ class Store extends EventEmitter {
     }
   }
 
-  private addContract (json: ContractJson) {
+  private removeCode (codeHash: string): void {
     try {
-      this.allContracts[json.address] = {
-        json,
-        contractAbi: new ContractAbi(JSON.parse(json.abi))
-      };
-
-      this.emit('new-contract');
+      delete this.allCode[codeHash];
+      this.emit('removed-code');
     } catch (error) {
       console.error(error);
     }

+ 27 - 19
packages/app-contracts/src/types.ts

@@ -2,27 +2,35 @@
 // 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 { Abi } from '@polkadot/api-contract';
+import { AppProps } from '@polkadot/react-components/types';
+import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
 
-export type LocationProps = {
-  match: {
-    params: { [index: string]: any }
-  }
-};
+// export interface LocationProps extends RouteComponentProps {}
 
-export type ComponentProps = AppProps & LocationProps;
+export interface ComponentProps extends AppProps {
+  accounts: SubjectInfo[];
+  contracts: SubjectInfo[];
+  hasCode: boolean;
+  showDeploy: (codeHash?: string) => () => void;
+}
 
-type BaseInfo = {
-  name: string,
-  genesisHash: string
-};
+export interface CodeJson {
+  abi?: string | null;
+  codeHash: string;
+  name: string;
+  genesisHash: string;
+  tags: string[];
+}
 
-export type CodeJson = BaseInfo & {
-  abi?: string | null,
-  codeHash: string
-};
+export interface CodeStored {
+  json: CodeJson;
+  contractAbi?: Abi;
+}
 
-export type ContractJson = BaseInfo & {
-  abi: string,
-  address: string
-};
+export interface ContractJsonOld {
+  genesisHash: string;
+  abi: string;
+  address: string;
+  name: string;
+}

+ 0 - 0
packages/ui-app/LICENSE → packages/app-council/LICENSE


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

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

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

@@ -0,0 +1,17 @@
+{
+  "name": "@polkadot/app-council",
+  "version": "0.36.0-beta.24",
+  "description": "Council",
+  "main": "index.js",
+  "scripts": {},
+  "author": "Jaco Greeff <jacogr@gmail.com>",
+  "maintainers": [
+    "Jaco Greeff <jacogr@gmail.com>"
+  ],
+  "license": "Apache-2.0",
+  "dependencies": {
+    "@babel/runtime": "^7.6.0",
+    "@polkadot/react-components": "^0.36.0-beta.24",
+    "@polkadot/react-query": "^0.36.0-beta.24"
+  }
+}

+ 166 - 0
packages/app-council/src/Motions/Motion.tsx

@@ -0,0 +1,166 @@
+// Copyright 2017-2019 @polkadot/app-democracy 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 { Proposal as ProposalType, Votes } from '@polkadot/types/interfaces';
+import { I18nProps } from '@polkadot/react-components/types';
+
+import BN from 'bn.js';
+import React from 'react';
+import { Option } from '@polkadot/types';
+
+import { ActionItem, InputAddress, Labelled, Voting } from '@polkadot/react-components';
+import { withCalls, withMulti } from '@polkadot/react-api';
+
+import translate from '../translate';
+
+interface Props extends I18nProps {
+  chain_bestNumber?: BN;
+  hash: string;
+  proposal: ProposalType | null;
+  votes: Votes | null;
+}
+
+interface State {
+  votedTotal: number;
+  votedNay: number;
+  votedAye: number;
+}
+
+class Motion extends React.PureComponent<Props, State> {
+  public state: State = {
+    votedTotal: 0,
+    votedAye: 0,
+    votedNay: 0
+  };
+
+  public static getDerivedStateFromProps ({ votes }: Props): State | null {
+    if (!votes) {
+      return null;
+    }
+
+    const { ayes, nays } = votes;
+
+    const newState: State = {
+      votedNay: nays.length,
+      votedAye: ayes.length,
+      votedTotal: ayes.length + nays.length
+    };
+
+    return newState;
+  }
+
+  public render (): React.ReactNode {
+    const { className, hash, proposal, votes } = this.props;
+
+    if (!proposal || !votes) {
+      return null;
+    }
+
+    const { index } = votes;
+
+    return (
+      <ActionItem
+        className={className}
+        accessory={
+          <Voting
+            hash={hash}
+            isCouncil
+            idNumber={index}
+            proposal={proposal}
+          />
+        }
+        expandNested
+        idNumber={index}
+        proposal={proposal}
+      >
+        {this.renderInfo()}
+      </ActionItem>
+    );
+  }
+
+  private renderInfo (): React.ReactNode {
+    const { votes, t } = this.props;
+
+    if (!votes) {
+      return null;
+    }
+
+    const { ayes, nays, threshold } = votes;
+
+    return (
+      <div>
+        <h4>
+          {t(
+            'ayes ({{ayes}}/{{threshold}} to approve)',
+            {
+              replace: {
+                ayes: ayes.length,
+                threshold: threshold.toString()
+              }
+            }
+          )}
+        </h4>
+        {ayes.map((address, index): React.ReactNode => (
+          <Labelled
+            key={`${index}:${address}`}
+            label={t('Aye')}
+          >
+            <InputAddress
+              isDisabled
+              value={address}
+              withLabel={false}
+            />
+          </Labelled>
+        ))}
+        <h4>
+          {t(
+            'nays ({{nays}})',
+            {
+              replace: {
+                nays: nays.length
+              }
+            }
+          )}
+        </h4>
+        {nays.map((address, index): React.ReactNode => (
+          <Labelled
+            key={`${index}:${address}`}
+            label={t('Nay')}
+          >
+            <InputAddress
+              isDisabled
+              value={address}
+              withLabel={false}
+            />
+          </Labelled>
+        ))}
+      </div>
+    );
+  }
+}
+
+export default withMulti(
+  Motion,
+  translate,
+  withCalls<Props>(
+    [
+      'query.council.proposalOf',
+      {
+        paramName: 'hash',
+        propName: 'proposal',
+        transform: (value: Option<ProposalType>): ProposalType | null =>
+          value.unwrapOr(null)
+      }
+    ],
+    [
+      'query.council.voting',
+      {
+        paramName: 'hash',
+        propName: 'votes',
+        transform: (value: Option<Votes>): Votes | null =>
+          value.unwrapOr(null)
+      }
+    ]
+  )
+);

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