Browse Source

Merge branch 'master' into try-merge

Mokhtar Naamani 6 years ago
parent
commit
d3842be972
62 changed files with 804 additions and 615 deletions
  1. 1 1
      lerna.json
  2. 1 1
      package.json
  3. 2 2
      packages/app-123code/package.json
  4. 2 2
      packages/app-accounts/package.json
  5. 2 2
      packages/app-addresses/package.json
  6. 3 3
      packages/app-democracy/package.json
  7. 2 2
      packages/app-explorer/package.json
  8. 4 4
      packages/app-extrinsics/package.json
  9. 2 2
      packages/app-js/package.json
  10. 3 3
      packages/app-settings/package.json
  11. 9 12
      packages/app-settings/src/Developer.tsx
  12. 4 4
      packages/app-staking/package.json
  13. 23 19
      packages/app-staking/src/Account/Bonding.tsx
  14. 18 62
      packages/app-staking/src/Account/Nominating.tsx
  15. 31 6
      packages/app-staking/src/Account/SessionKey.tsx
  16. 4 4
      packages/app-staking/src/Account/Validating.tsx
  17. 56 38
      packages/app-staking/src/Account/index.tsx
  18. 161 82
      packages/app-staking/src/Overview/Address.tsx
  19. 31 15
      packages/app-staking/src/Overview/CurrentList.tsx
  20. 6 82
      packages/app-staking/src/Overview/Summary.tsx
  21. 2 2
      packages/app-staking/src/Overview/index.css
  22. 18 0
      packages/app-staking/src/StakeList.tsx
  23. 13 1
      packages/app-staking/src/index.css
  24. 5 1
      packages/app-staking/src/index.tsx
  25. 3 2
      packages/app-staking/src/types.ts
  26. 3 3
      packages/app-storage/package.json
  27. 2 2
      packages/app-toolbox/package.json
  28. 3 3
      packages/app-transfer/package.json
  29. 3 3
      packages/apps/package.json
  30. 3 2
      packages/apps/src/Content/index.tsx
  31. 17 16
      packages/apps/src/SideBar/NodeInfo.tsx
  32. 1 1
      packages/joy-election/package.json
  33. 1 1
      packages/joy-help/package.json
  34. 1 1
      packages/joy-media/package.json
  35. 1 1
      packages/joy-members/package.json
  36. 1 1
      packages/joy-proposals/package.json
  37. 2 2
      packages/joy-roles/package.json
  38. 1 1
      packages/joy-utils/package.json
  39. 1 1
      packages/ui-api/package.json
  40. 3 3
      packages/ui-app/package.json
  41. 2 13
      packages/ui-app/src/AddressMini.tsx
  42. 3 8
      packages/ui-app/src/AddressSummary.tsx
  43. 3 1
      packages/ui-app/src/CardSummary.tsx
  44. 7 1
      packages/ui-app/src/Dropdown.tsx
  45. 36 11
      packages/ui-app/src/IdentityIcon.tsx
  46. 22 2
      packages/ui-app/src/Input.tsx
  47. 64 16
      packages/ui-app/src/InputAddress/index.tsx
  48. 3 1
      packages/ui-app/src/InputBalance.tsx
  49. 3 1
      packages/ui-app/src/InputExtrinsic/index.tsx
  50. 26 21
      packages/ui-app/src/InputFile.tsx
  51. 100 99
      packages/ui-app/src/InputNumber.tsx
  52. 3 1
      packages/ui-app/src/InputRpc/index.tsx
  53. 3 1
      packages/ui-app/src/InputStorage/index.tsx
  54. 65 4
      packages/ui-app/src/Labelled.tsx
  55. 3 1
      packages/ui-app/src/Output.tsx
  56. 3 1
      packages/ui-app/src/Static.tsx
  57. 2 2
      packages/ui-app/src/styles/app.css
  58. 1 31
      packages/ui-app/src/styles/components.css
  59. 1 2
      packages/ui-app/src/styles/media.css
  60. 2 2
      packages/ui-params/package.json
  61. 1 1
      packages/ui-reactive/package.json
  62. 2 2
      packages/ui-signer/package.json

+ 1 - 1
lerna.json

@@ -10,5 +10,5 @@
   "packages": [
     "packages/*"
   ],
-  "version": "0.30.1"
+  "version": "0.31.0-beta.10"
 }

+ 1 - 1
package.json

@@ -1,5 +1,5 @@
 {
-  "version": "0.30.1",
+  "version": "0.31.0-beta.10",
   "private": true,
   "engines": {
     "node": ">=10.13.0",

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

@@ -1,6 +1,6 @@
 {
   "name": "@polkadot/app-123code",
-  "version": "0.30.1",
+  "version": "0.31.0-beta.10",
   "description": "A baasic app that shows the ropes on customisation",
   "main": "index.js",
   "scripts": {},
@@ -11,6 +11,6 @@
   "license": "Apache-2.0",
   "dependencies": {
     "@babel/runtime": "^7.4.3",
-    "@polkadot/ui-app": "^0.30.1"
+    "@polkadot/ui-app": "^0.31.0-beta.10"
   }
 }

+ 2 - 2
packages/app-accounts/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@polkadot/app-accounts",
-  "version": "0.30.1",
+  "version": "0.31.0-beta.10",
   "main": "index.js",
   "repository": "https://github.com/polkadot-js/apps.git",
   "author": "Jaco Greeff <jacogr@gmail.com>",
@@ -11,7 +11,7 @@
   "license": "Apache-2.0",
   "dependencies": {
     "@babel/runtime": "^7.4.3",
-    "@polkadot/ui-app": "^0.30.1",
+    "@polkadot/ui-app": "^0.31.0-beta.10",
     "@types/file-saver": "^2.0.0",
     "@types/yargs": "^12.0.11",
     "babel-plugin-module-resolver": "^3.1.1",

+ 2 - 2
packages/app-addresses/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@polkadot/app-addresses",
-  "version": "0.30.1",
+  "version": "0.31.0-beta.10",
   "main": "index.js",
   "repository": "https://github.com/polkadot-js/apps.git",
   "author": "Jaco Greeff <jacogr@gmail.com>",
@@ -11,6 +11,6 @@
   "license": "Apache-2.0",
   "dependencies": {
     "@babel/runtime": "^7.4.3",
-    "@polkadot/ui-app": "^0.30.1"
+    "@polkadot/ui-app": "^0.31.0-beta.10"
   }
 }

+ 3 - 3
packages/app-democracy/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@polkadot/app-democracy",
-  "version": "0.30.1",
+  "version": "0.31.0-beta.10",
   "description": "A referendum & proposal app",
   "main": "index.js",
   "scripts": {},
@@ -11,7 +11,7 @@
   "license": "Apache-2.0",
   "dependencies": {
     "@babel/runtime": "^7.4.3",
-    "@polkadot/ui-app": "^0.30.1",
-    "@polkadot/ui-reactive": "^0.30.1"
+    "@polkadot/ui-app": "^0.31.0-beta.10",
+    "@polkadot/ui-reactive": "^0.31.0-beta.10"
   }
 }

+ 2 - 2
packages/app-explorer/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@polkadot/app-explorer",
-  "version": "0.30.1",
+  "version": "0.31.0-beta.10",
   "main": "index.js",
   "repository": "https://github.com/polkadot-js/apps.git",
   "author": "Jaco Greeff <jacogr@gmail.com>",
@@ -11,6 +11,6 @@
   "license": "Apache-2.0",
   "dependencies": {
     "@babel/runtime": "^7.4.3",
-    "@polkadot/ui-app": "^0.30.1"
+    "@polkadot/ui-app": "^0.31.0-beta.10"
   }
 }

+ 4 - 4
packages/app-extrinsics/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@polkadot/app-extrinsics",
-  "version": "0.30.1",
+  "version": "0.31.0-beta.10",
   "main": "index.js",
   "repository": "https://github.com/polkadot-js/apps.git",
   "author": "Jaco Greeff <jacogr@gmail.com>",
@@ -11,8 +11,8 @@
   "license": "Apache-2.0",
   "dependencies": {
     "@babel/runtime": "^7.4.3",
-    "@polkadot/ui-app": "^0.30.1",
-    "@polkadot/ui-params": "^0.30.1",
-    "@polkadot/ui-signer": "^0.30.1"
+    "@polkadot/ui-app": "^0.31.0-beta.10",
+    "@polkadot/ui-params": "^0.31.0-beta.10",
+    "@polkadot/ui-signer": "^0.31.0-beta.10"
   }
 }

+ 2 - 2
packages/app-js/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@polkadot/app-js",
-  "version": "0.30.1",
+  "version": "0.31.0-beta.10",
   "description": "A simple JavaScript console for playing with the API",
   "main": "index.js",
   "scripts": {},
@@ -11,7 +11,7 @@
   "license": "Apache-2.0",
   "dependencies": {
     "@babel/runtime": "^7.4.3",
-    "@polkadot/ui-app": "^0.30.1",
+    "@polkadot/ui-app": "^0.31.0-beta.10",
     "snappyjs": "^0.6.0"
   }
 }

+ 3 - 3
packages/app-settings/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@polkadot/app-settings",
-  "version": "0.30.1",
+  "version": "0.31.0-beta.10",
   "description": "Settings management",
   "main": "index.js",
   "scripts": {},
@@ -11,7 +11,7 @@
   "license": "Apache-2.0",
   "dependencies": {
     "@babel/runtime": "^7.4.3",
-    "@polkadot/ui-app": "^0.30.1",
-    "@polkadot/ui-reactive": "^0.30.1"
+    "@polkadot/ui-app": "^0.31.0-beta.10",
+    "@polkadot/ui-reactive": "^0.31.0-beta.10"
   }
 }

+ 9 - 12
packages/app-settings/src/Developer.tsx

@@ -7,7 +7,7 @@ import { AppProps, I18nProps } from '@polkadot/ui-app/types';
 import React from 'react';
 import store from 'store';
 import { getTypeRegistry } from '@polkadot/types';
-import { Button, Editor, InputFile } from '@polkadot/ui-app';
+import { Button, Editor, InputFile, Labelled } from '@polkadot/ui-app';
 import { ActionStatus } from '@polkadot/ui-app/Status/types';
 import { isJsonObject, stringToU8a, u8aToString } from '@polkadot/util';
 
@@ -64,17 +64,14 @@ class Developer extends React.PureComponent<Props, State> {
         </div>
         <div className='ui--row'>
           <div className='full'>
-            <div className='ui--Labelled'>
-              <label>{t('Manually enter your custom type definitions as valid JSON')}</label>
-              <div className='ui--Labelled-content'>
-                <Editor
-                  className='editor'
-                  code={code}
-                  isValid={isJsonValid}
-                  onEdit={this.onEditTypes}
-                />
-              </div>
-            </div>
+            <Labelled label={t('Manually enter your custom type definitions as valid JSON')}>
+              <Editor
+                className='editor'
+                code={code}
+                isValid={isJsonValid}
+                onEdit={this.onEditTypes}
+              />
+            </Labelled>>
           </div>
         </div>
         <Button.Group>

+ 4 - 4
packages/app-staking/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@polkadot/app-staking",
-  "version": "0.30.1",
+  "version": "0.31.0-beta.10",
   "description": "A basic staking app",
   "main": "index.js",
   "scripts": {},
@@ -11,8 +11,8 @@
   "license": "Apache-2.0",
   "dependencies": {
     "@babel/runtime": "^7.4.3",
-    "@polkadot/app-explorer": "^0.30.1",
-    "@polkadot/ui-app": "^0.30.1",
-    "@polkadot/ui-reactive": "^0.30.1"
+    "@polkadot/app-explorer": "^0.31.0-beta.10",
+    "@polkadot/ui-app": "^0.31.0-beta.10",
+    "@polkadot/ui-reactive": "^0.31.0-beta.10"
   }
 }

+ 23 - 19
packages/app-staking/src/Account/Bonding.tsx

@@ -55,31 +55,31 @@ class Bonding extends React.PureComponent<Props, State> {
       >
         {this.renderContent()}
         <Modal.Actions>
-        <Button.Group>
-          <Button
-            isNegative
-            onClick={onClose}
-            label={t('Cancel')}
-          />
-          <Button.Or />
-          <TxButton
-            accountId={accountId}
-            isDisabled={!canSubmit}
-            isPrimary
-            label={t('Bond')}
-            onClick={onClose}
-            params={[controllerId, bondValue, destination]}
-            tx='staking.bond'
-          />
-        </Button.Group>
-      </Modal.Actions>
+          <Button.Group>
+            <Button
+              isNegative
+              onClick={onClose}
+              label={t('Cancel')}
+            />
+            <Button.Or />
+            <TxButton
+              accountId={accountId}
+              isDisabled={!canSubmit}
+              isPrimary
+              label={t('Bond')}
+              onClick={onClose}
+              params={[controllerId, bondValue, destination]}
+              tx='staking.bond'
+            />
+          </Button.Group>
+        </Modal.Actions>
       </Modal>
     );
   }
 
   private renderContent () {
     const { accountId, bondedId, t } = this.props;
-    const { controllerId, isValidController } = this.state;
+    const { controllerId, destination, isValidController } = this.state;
 
     return (
       <>
@@ -96,6 +96,7 @@ class Bonding extends React.PureComponent<Props, State> {
           <InputAddress
             className='medium'
             defaultValue={bondedId}
+            help={t('The controller is the account that will be used to control any nominating or validating actions')}
             label={t('controller account')}
             onChange={this.onChangeController}
             value={controllerId}
@@ -110,15 +111,18 @@ class Bonding extends React.PureComponent<Props, State> {
           <InputBalance
             autoFocus
             className='medium'
+            help={t('The total amount of the stash balance that will be at stake in any forthcoming rounds (should be less than the total amount available)')}
             label={t('value bonded')}
             onChange={this.onChangeValue}
           />
           <Dropdown
             className='medium'
             defaultValue={0}
+            help={t('The destination account for any payments as either a nominator or validator')}
             label={t('payment destination')}
             onChange={this.onChangeDestination}
             options={stashOptions}
+            value={destination}
           />
         </Modal.Content>
       </>

+ 18 - 62
packages/app-staking/src/Account/Nominating.tsx

@@ -3,10 +3,10 @@
 // of the Apache-2.0 license. See the LICENSE file for details.
 
 import { I18nProps } from '@polkadot/ui-app/types';
+import { KeyringSectionOption } from '@polkadot/ui-keyring/options/types';
 
 import React from 'react';
-import { Button, Input, InputAddress, Modal, TxButton } from '@polkadot/ui-app';
-import keyring from '@polkadot/ui-keyring';
+import { Button, InputAddress, Modal, TxButton } from '@polkadot/ui-app';
 
 import translate from '../translate';
 
@@ -15,19 +15,16 @@ type Props = I18nProps & {
   isOpen: boolean,
   onClose: () => void,
   intentions: Array<string>,
-  stashId: string
+  stashId: string,
+  targets: Array<KeyringSectionOption>
 };
 
 type State = {
-  isNomineeValid: boolean,
-  isAddressFormatValid: boolean,
   nominees: Array<string>
 };
 
 class Nominating extends React.PureComponent<Props, State> {
   state: State = {
-    isNomineeValid: false,
-    isAddressFormatValid: false,
     nominees: []
   };
 
@@ -53,7 +50,7 @@ class Nominating extends React.PureComponent<Props, State> {
 
   renderButtons () {
     const { accountId, onClose, t } = this.props;
-    const { isNomineeValid, nominees } = this.state;
+    const { nominees } = this.state;
 
     return (
       <Modal.Actions>
@@ -66,7 +63,7 @@ class Nominating extends React.PureComponent<Props, State> {
           <Button.Or />
           <TxButton
             accountId={accountId}
-            isDisabled={!isNomineeValid || !nominees.length}
+            isDisabled={nominees.length === 0}
             isPrimary
             onClick={onClose}
             params={[nominees]}
@@ -79,13 +76,12 @@ class Nominating extends React.PureComponent<Props, State> {
   }
 
   renderContent () {
-    const { accountId, stashId, t } = this.props;
-    const { isNomineeValid, nominees } = this.state;
+    const { accountId, stashId, t, targets } = this.props;
 
     return (
       <>
         <Modal.Header>
-          {t('Nominate Validator')}
+          {t('Nominate Validators')}
         </Modal.Header>
         <Modal.Content className='ui--signer-Signer-Content'>
           <InputAddress
@@ -100,63 +96,23 @@ class Nominating extends React.PureComponent<Props, State> {
             isDisabled
             label={t('stash account')}
           />
-          <Input
-            autoFocus
+          <InputAddress
             className='medium'
-            isError={!isNomineeValid}
-            label={t('nominate the following address (validator or intention)')}
-            onChange={this.onChangeNominee}
-            value={nominees[0]}
+            isMultiple
+            help={t('Stash accounts that are to be nominated. Block rewards are split between validators and nominators')}
+            label={t('nominate the following addresses')}
+            onChangeMulti={this.onChangeNominees}
+            options={targets}
+            placeholder={t('select accounts(s) nominate')}
+            type='account'
           />
-          {this.renderErrors()}
         </Modal.Content>
       </>
     );
   }
 
-  private renderErrors () {
-    const { t } = this.props;
-    const { isNomineeValid, isAddressFormatValid } = this.state;
-    const hasError = !isNomineeValid || !isAddressFormatValid;
-
-    if (!hasError) {
-      return null;
-    }
-
-    return (
-      <article className='error'>
-        {
-          !isNomineeValid && isAddressFormatValid
-            ? t('The address you input is not intending to stake, and is therefore invalid. Please try again with a validator address.')
-            : null
-        }
-        {
-          !isAddressFormatValid
-            ? t('The address does not conform to a recognized address format. Please make sure you enter a valid address.')
-            : null
-        }
-      </article>
-    );
-  }
-
-  private onChangeNominee = (nominee: string) => {
-    // const { intentions } = this.props;
-
-    let isAddressFormatValid = false;
-
-    try {
-      keyring.decodeAddress(nominee);
-
-      isAddressFormatValid = true;
-    } catch (err) {
-      console.error(err);
-    }
-
-    this.setState({
-      isNomineeValid: isAddressFormatValid, // intentions.includes(nominee),
-      isAddressFormatValid,
-      nominees: [nominee]
-    });
+  private onChangeNominees = (nominees: Array<string>) => {
+    this.setState({ nominees });
   }
 }
 

+ 31 - 6
packages/app-staking/src/Account/SessionKey.tsx

@@ -15,13 +15,24 @@ type Props = I18nProps & {
   onClose: () => void
 };
 
-type State = {};
+type State = {
+  sessionId: string
+};
 
 class Key extends React.PureComponent<Props, State> {
-  state: State = {};
+  state: State;
+
+  constructor (props: Props) {
+    super(props);
+
+    this.state = {
+      sessionId: props.accountId
+    };
+  }
 
   render () {
     const { accountId, isOpen, onClose, t } = this.props;
+    const { sessionId } = this.state;
 
     if (!isOpen) {
       return null;
@@ -45,10 +56,11 @@ class Key extends React.PureComponent<Props, State> {
           <Button.Or />
           <TxButton
             accountId={accountId}
+            isDisabled={!sessionId}
             isPrimary
             label={t('Set Session Key')}
             onClick={onClose}
-            params={[accountId]}
+            params={[sessionId]}
             tx='session.setKey'
           />
         </Button.Group>
@@ -59,23 +71,36 @@ class Key extends React.PureComponent<Props, State> {
 
   private renderContent () {
     const { accountId, t } = this.props;
+    const { sessionId } = this.state;
 
     return (
       <>
         <Modal.Header>
-          {t('Key Preferences')}
+          {t('Session Key')}
         </Modal.Header>
         <Modal.Content className='ui--signer-Signer-Content'>
           <InputAddress
             className='medium'
+            defaultValue={accountId}
             isDisabled
-            label={t('session account')}
-            value={accountId}
+            label={t('controller account')}
+          />
+          <InputAddress
+            className='medium'
+            help={t('Changing the key only takes effect at the start of the next session. If validating, you should (currently) use an ed25519 key.')}
+            label={t('session key')}
+            onChange={this.onChangeSession}
+            value={sessionId}
+            type='account'
           />
         </Modal.Content>
       </>
     );
   }
+
+  private onChangeSession = (sessionId: string) => {
+    this.setState({ sessionId });
+  }
 }
 
 export default translate(Key);

+ 4 - 4
packages/app-staking/src/Account/Validating.tsx

@@ -37,8 +37,6 @@ class Staking extends React.PureComponent<Props, State> {
       return null;
     }
 
-    console.error('preferences', props.preferences);
-
     const { unstakeThreshold, validatorPayment } = props.preferences;
 
     return {
@@ -103,7 +101,7 @@ class Staking extends React.PureComponent<Props, State> {
     return (
       <>
         <Modal.Header>
-          {t('Staking')}
+          {t('Validating')}
         </Modal.Header>
         <Modal.Content className='ui--signer-Signer-Content'>
           <InputAddress
@@ -122,16 +120,18 @@ class Staking extends React.PureComponent<Props, State> {
             autoFocus
             bitLength={32}
             className='medium'
+            help={t('The number of allowed slashes for this validator before being automatically unstaked (maximum of 10 allowed)')}
             label={t('unstake threshold')}
             onChange={this.onChangeThreshold}
             value={
               unstakeThreshold
                 ? unstakeThreshold.toString()
-                : '0'
+                : '3'
             }
           />
           <InputBalance
             className='medium'
+            help={t('Reward that validator takes up-front, the remainder is split between themselves and nominators')}
             label={t('payment preferences')}
             onChange={this.onChangePayment}
             value={

+ 56 - 38
packages/app-staking/src/Account/index.tsx

@@ -5,6 +5,7 @@
 import { DerivedBalancesMap } from '@polkadot/api-derive/types';
 import { I18nProps } from '@polkadot/ui-app/types';
 import { ApiProps } from '@polkadot/ui-api/types';
+import { KeyringSectionOption } from '@polkadot/ui-keyring/options/types';
 import { Nominators } from '../types';
 
 import React from 'react';
@@ -22,15 +23,16 @@ type Props = ApiProps & I18nProps & {
   accountId: string,
   balances: DerivedBalancesMap,
   balanceArray: (_address: AccountId | string) => Array<Balance> | undefined,
+  intentions: Array<string>,
+  isValidator: boolean,
   name: string,
+  nominators: Nominators,
   session_nextKeyFor?: Option<AccountId>,
   staking_bonded?: Option<AccountId>,
   staking_ledger?: Option<StakingLedger>,
   staking_stakers?: Exposure,
   staking_validators?: [ValidatorPrefs],
-  intentions: Array<string>,
-  nominators: Nominators,
-  isValidator: boolean,
+  targets: Array<KeyringSectionOption>,
   validators: Array<string>
 };
 
@@ -57,10 +59,7 @@ class Account extends React.PureComponent<Props, State> {
     stashId: null
   };
 
-  static getDerivedStateFromProps ({ session_nextKeyFor, staking_bonded, staking_ledger }: Props): Partial<State> {
-
-    // console.error('staking_stakers', JSON.stringify(staking_stakers));
-
+  static getDerivedStateFromProps ({ session_nextKeyFor, staking_bonded, staking_ledger }: Props, state: State): Partial<State> {
     return {
       bondedId: staking_bonded && staking_bonded.isSome
         ? staking_bonded.unwrap().toString()
@@ -93,6 +92,7 @@ class Account extends React.PureComponent<Props, State> {
             {this.renderButtons()}
             {this.renderBondedId()}
             {this.renderStashId()}
+            {this.renderSessionId()}
             {this.renderNominee()}
             {this.renderNominators()}
           </div>
@@ -173,6 +173,7 @@ class Account extends React.PureComponent<Props, State> {
   }
 
   private renderNominee () {
+    const { t } = this.props;
     const nominees = this.getNominees();
 
     if (!nominees || !nominees.length) {
@@ -181,7 +182,7 @@ class Account extends React.PureComponent<Props, State> {
 
     return (
       <div className='staking--Account-detail'>
-        <label className='staking--label'>nominating</label>
+        <label className='staking--label'>{t('nominating')}</label>
         {
           nominees.map((nomineeId, index) => (
             <AddressMini
@@ -220,6 +221,7 @@ class Account extends React.PureComponent<Props, State> {
   }
 
   private renderBondedId () {
+    const { t } = this.props;
     const { bondedId } = this.state;
 
     if (!bondedId) {
@@ -228,16 +230,30 @@ class Account extends React.PureComponent<Props, State> {
 
     return (
       <div className='staking--Account-detail'>
-        <label className='staking--label'>controller account</label>
-        <AddressMini
-          value={bondedId}
-          withBalance
-        />
+        <label className='staking--label'>{t('controller')}</label>
+        <AddressMini value={bondedId} />
+      </div>
+    );
+  }
+
+  private renderSessionId () {
+    const { t } = this.props;
+    const { sessionId } = this.state;
+
+    if (!sessionId) {
+      return null;
+    }
+
+    return (
+      <div className='staking--Account-detail'>
+        <label className='staking--label'>{t('session')}</label>
+        <AddressMini value={sessionId} />
       </div>
     );
   }
 
   private renderStashId () {
+    const { t } = this.props;
     const { stashId } = this.state;
 
     if (!stashId) {
@@ -246,17 +262,14 @@ class Account extends React.PureComponent<Props, State> {
 
     return (
       <div className='staking--Account-detail'>
-        <label className='staking--label'>stash account</label>
-        <AddressMini
-          value={stashId}
-          withBalance
-        />
+        <label className='staking--label'>{t('stash')}</label>
+        <AddressMini value={stashId} />
       </div>
     );
   }
 
   private renderNominating () {
-    const { accountId, intentions } = this.props;
+    const { accountId, intentions, targets } = this.props;
     const { isNominateOpen, stashId } = this.state;
 
     if (!stashId) {
@@ -270,34 +283,28 @@ class Account extends React.PureComponent<Props, State> {
         onClose={this.toggleNominate}
         intentions={intentions}
         stashId={stashId}
+        targets={targets}
       />
     );
   }
 
   private renderButtons () {
     const { accountId, intentions, t } = this.props;
-    const { sessionId, stashId } = this.state;
+    const { sessionId, bondedId, stashId } = this.state;
     const buttons = [];
 
     if (!stashId) {
-      if (sessionId) {
+      if (!bondedId) {
         buttons.push(
           <Button
             isPrimary
             key='bond'
             onClick={this.toggleBonding}
-            label={t('Bond')}
+            label={t('Bond Funds')}
           />
         );
       } else {
-        buttons.push(
-          <Button
-            isPrimary
-            key='session'
-            onClick={this.toggleSessionKey}
-            label={t('Set Session Key')}
-          />
-        );
+        return null;
       }
     } else {
       const nominees = this.getNominees();
@@ -315,14 +322,25 @@ class Account extends React.PureComponent<Props, State> {
           />
         );
       } else {
-        buttons.push(
-          <Button
-            isPrimary
-            key='validate'
-            onClick={this.toggleValidating}
-            label={t('Validate')}
-          />
-        );
+        if (!sessionId) {
+          buttons.push(
+            <Button
+              isPrimary
+              key='session'
+              onClick={this.toggleSessionKey}
+              label={t('Set Session Key')}
+            />
+          );
+        } else {
+          buttons.push(
+            <Button
+              isPrimary
+              key='validate'
+              onClick={this.toggleValidating}
+              label={t('Validate')}
+            />
+          );
+        }
         buttons.push(<Button.Or key='nominate.or' />);
         buttons.push(
           <Button

+ 161 - 82
packages/app-staking/src/Overview/Address.tsx

@@ -6,11 +6,13 @@ import { DerivedBalancesMap } from '@polkadot/api-derive/types';
 import { I18nProps } from '@polkadot/ui-app/types';
 import { Nominators, RecentlyOfflineMap } from '../types';
 
+import BN from 'bn.js';
 import React from 'react';
-import { AccountId, Balance, Option } from '@polkadot/types';
-import { withCall, withMulti } from '@polkadot/ui-api/with';
+import { AccountId, Balance, Option, StakingLedger } from '@polkadot/types';
+import { withCalls, withMulti } from '@polkadot/ui-api/with';
 import { AddressMini, AddressRow } from '@polkadot/ui-app';
 import keyring from '@polkadot/ui-keyring';
+import { formatNumber } from '@polkadot/util';
 
 import translate from '../translate';
 
@@ -23,106 +25,65 @@ type Props = I18nProps & {
   lastBlock: string,
   nominators: Nominators,
   recentlyOffline: RecentlyOfflineMap,
-  staking_bonded?: Option<AccountId>
+  session_nextKeyFor?: Option<AccountId>,
+  staking_bonded?: Option<AccountId>,
+  staking_ledger?: Option<StakingLedger>
 };
 
 type State = {
+  bondedId: string,
+  stashId: string | null,
+  sessionId: string | null,
   badgeExpanded: boolean;
 };
 
 class Address extends React.PureComponent<Props, State> {
-  state: State = {
-    badgeExpanded: false
-  };
+  state: State;
 
-  private getDisplayName = (): string | undefined => {
-    const { address, defaultName } = this.props;
-
-    const pair = keyring.getAccount(address).isValid()
-      ? keyring.getAccount(address)
-      : keyring.getAddress(address);
+  constructor (props: Props) {
+    super(props);
 
-    return pair.isValid()
-      ? pair.getMeta().name
-      : defaultName;
+    this.state = {
+      bondedId: props.address,
+      sessionId: null,
+      stashId: null,
+      badgeExpanded: false
+    };
   }
 
-  private onClickBadge = (): void => {
-    const { badgeExpanded } = this.state;
-
-    this.setState({ badgeExpanded: !badgeExpanded });
+  static getDerivedStateFromProps ({ address, session_nextKeyFor, staking_bonded, staking_ledger }: Props, prevState: State): State | null {
+    return {
+      bondedId: !staking_bonded || staking_bonded.isNone
+        ? prevState.bondedId
+        : staking_bonded.unwrap().toString(),
+      sessionId: !session_nextKeyFor || session_nextKeyFor.isNone
+        ? prevState.sessionId
+        : session_nextKeyFor.unwrap().toString(),
+      stashId: !staking_ledger || staking_ledger.isNone
+        ? prevState.stashId
+        : staking_ledger.unwrap().stash.toString()
+    } as State;
   }
 
   render () {
-    const { address, balanceArray, isAuthor, lastBlock, nominators, recentlyOffline, staking_bonded, t } = this.props;
-    const { badgeExpanded } = this.state;
-    const myNominators = Object.keys(nominators).filter((nominator) =>
-      nominators[nominator].indexOf(address) !== -1
-    );
-    const bondedId: string | null = staking_bonded && staking_bonded.isSome
-      ? staking_bonded.unwrap().toString()
-      : null;
-
-    const hasNominators = !!myNominators.length;
-    const isRecentlyOffline = bondedId && recentlyOffline[bondedId];
-
-    const children = (hasNominators || isRecentlyOffline) ? (
-      <>
-        <details className='staking--Account-detail'>
-          {myNominators.length && (
-          <>
-            <summary>
-              {t('Nominators ({{count}})', {
-                replace: {
-                  count: myNominators.length
-                }
-              })}
-            </summary>
-            {myNominators.map((accountId) =>
-              <AddressMini
-                key={accountId.toString()}
-                value={accountId}
-                withBalance
-              />
-            )}
-          </>
-        )}
-        </details>
-        {(bondedId && recentlyOffline[bondedId]) && (() => {
-          const { blockNumber, instances } = recentlyOffline[bondedId];
-
-          return (
-            <div
-              onClick={this.onClickBadge}
-              className={['recentlyOffline', badgeExpanded ? 'expand' : ''].join(' ')}
-            >
-              <div className='badge'>
-                {instances.toString()}
-              </div>
-              <div className='detail'>
-                {t('Reported offline {{instances}} times since block #{{blockNumber}}', {
-                  replace: {
-                    instances: instances.toString(),
-                    blockNumber
-                  }
-                })}
-              </div>
-            </div>
-          );
-        })()}
-      </>
-    ) : undefined;
+    const { balanceArray, isAuthor, lastBlock } = this.props;
+    const { bondedId, stashId } = this.state;
 
     return (
-      <article key={address}>
+      <article key={stashId || bondedId}>
         <AddressRow
-          balance={balanceArray(address)}
+          balance={balanceArray(stashId || '')}
           name={this.getDisplayName()}
-          value={address}
+          value={stashId}
           withCopy={false}
           withNonce={false}
         >
-          {children}
+          <div className='staking--accounts-info'>
+            {this.renderControllerId()}
+            {this.renderSessionId()}
+          </div>
+          {this.renderNominators()}
+          {this.renderOffline()}
         </AddressRow>
         <div
           className={['blockNumber', isAuthor ? 'latest' : ''].join(' ')}
@@ -133,10 +94,128 @@ class Address extends React.PureComponent<Props, State> {
       </article>
     );
   }
+
+  private getDisplayName = (): string | undefined => {
+    const { defaultName } = this.props;
+    const { stashId } = this.state;
+
+    if (!stashId) {
+      return defaultName;
+    }
+
+    const pair = keyring.getAccount(stashId).isValid()
+      ? keyring.getAccount(stashId)
+      : keyring.getAddress(stashId);
+
+    return pair.isValid()
+      ? (pair.getMeta().name || defaultName)
+      : defaultName;
+  }
+
+  private toggleBadge = (): void => {
+    const { badgeExpanded } = this.state;
+
+    this.setState({ badgeExpanded: !badgeExpanded });
+  }
+
+  private renderControllerId () {
+    const { t } = this.props;
+    const { bondedId } = this.state;
+
+    if (!bondedId) {
+      return null;
+    }
+
+    return (
+      <div>
+        <label className='staking--label'>{t('controller')}</label>
+        <AddressMini value={bondedId} />
+      </div>
+    );
+  }
+
+  private renderSessionId () {
+    const { t } = this.props;
+    const { sessionId } = this.state;
+
+    if (!sessionId) {
+      return null;
+    }
+
+    return (
+      <div>
+        <label className='staking--label'>{t('session')}</label>
+        <AddressMini value={sessionId} />
+      </div>
+    );
+  }
+
+  private renderNominators () {
+    const { address, nominators, t } = this.props;
+    const myNominators = Object.keys(nominators).filter((nominator) =>
+      nominators[nominator].indexOf(address) !== -1
+    );
+
+    return (
+      <details className='staking--Account-detail'>
+        <summary>
+          {t('Nominators ({{count}})', {
+            replace: {
+              count: myNominators.length
+            }
+          })}
+        </summary>
+        {myNominators.map((accountId) =>
+          <AddressMini
+            key={accountId.toString()}
+            value={accountId}
+            withBalance
+          />
+        )}
+      </details>
+    );
+  }
+
+  private renderOffline () {
+    const { recentlyOffline, t } = this.props;
+    const { badgeExpanded, stashId } = this.state;
+
+    if (!stashId || !recentlyOffline[stashId]) {
+      return null;
+    }
+
+    const offline = recentlyOffline[stashId];
+    const count = offline.reduce((total, { count }) => total.add(count), new BN(0));
+
+    const blockNumbers = offline.map(({ blockNumber }) => `#${formatNumber(blockNumber)}`);
+
+    return (
+      <div
+        className={['recentlyOffline', badgeExpanded ? 'expand' : ''].join(' ')}
+        onClick={this.toggleBadge}
+      >
+        <div className='badge'>
+          {count.toString()}
+        </div>
+        <div className='detail'>
+          {t('Reported offline {{count}} times, last at {{blockNumber}}', {
+            replace: {
+              count: count.toString(),
+              blockNumber: blockNumbers[blockNumbers.length - 1]
+            }
+          })}
+        </div>
+      </div>
+    );
+  }
 }
 
 export default withMulti(
   Address,
   translate,
-  withCall('query.staking.bonded', { paramName: 'address' })
+  withCalls<Props>(
+    ['query.session.nextKeyFor', { paramName: 'address' }],
+    ['query.staking.ledger', { paramName: 'address' }],
+    ['query.staking.bonded', { paramName: 'address' }]
+  )
 );

+ 31 - 15
packages/app-staking/src/Overview/CurrentList.tsx

@@ -24,7 +24,33 @@ type Props = I18nProps & {
   staking_recentlyOffline?: RecentlyOffline
 };
 
-class CurrentList extends React.PureComponent<Props> {
+type State = {
+  recentlyOffline: RecentlyOfflineMap
+};
+
+class CurrentList extends React.PureComponent<Props, State> {
+  state: State = { recentlyOffline: {} };
+
+  static getDerivedStateFromProps ({ staking_recentlyOffline = [] }: Props): State {
+    return {
+      recentlyOffline: staking_recentlyOffline.reduce(
+        (result, [accountId, blockNumber, count]) => {
+          const account = accountId.toString();
+
+          if (!result[account]) {
+            result[account] = [];
+          }
+
+          result[account].push({
+            blockNumber,
+            count
+          });
+
+          return result;
+        }, {} as RecentlyOfflineMap)
+    };
+  }
+
   render () {
     return (
       <div className='validator--ValidatorsList ui--flex-medium'>
@@ -50,7 +76,7 @@ class CurrentList extends React.PureComponent<Props> {
             }
           })}
         </h1>
-        {this.renderColumn(current, t('validator'))}
+        {this.renderColumn(current, t('validator (stash)'))}
       </>
     );
   }
@@ -61,13 +87,14 @@ class CurrentList extends React.PureComponent<Props> {
     return (
       <>
         <h1>{t('next up')}</h1>
-        {this.renderColumn(next, t('intention'))}
+        {this.renderColumn(next, t('intention (stash)'))}
       </>
     );
   }
 
   private renderColumn (addresses: Array<string>, defaultName: string) {
-    const { balances, balanceArray, chain_subscribeNewHead, nominators, staking_recentlyOffline, t } = this.props;
+    const { balances, balanceArray, chain_subscribeNewHead, nominators, t } = this.props;
+    const { recentlyOffline } = this.state;
 
     if (addresses.length === 0) {
       return (
@@ -83,17 +110,6 @@ class CurrentList extends React.PureComponent<Props> {
       lastAuthor = (chain_subscribeNewHead.author || '').toString();
     }
 
-    const recentlyOffline: RecentlyOfflineMap = (staking_recentlyOffline || []).reduce(
-      (result, [accountId, blockNumber, instances]) => ({
-        ...result,
-        [accountId.toString()]: {
-          blockNumber,
-          instances
-        }
-      }),
-      {}
-    );
-
     return (
       <div>
         {addresses.map((address) => (

+ 6 - 82
packages/app-staking/src/Overview/Summary.tsx

@@ -2,14 +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 { DerivedBalances, DerivedBalancesMap } from '@polkadot/api-derive/types';
+import { DerivedBalancesMap } from '@polkadot/api-derive/types';
 import { I18nProps } from '@polkadot/ui-app/types';
 
 import BN from 'bn.js';
 import React from 'react';
 import SummarySession from '@polkadot/app-explorer/SummarySession';
 import { SummaryBox, CardSummary } from '@polkadot/ui-app';
-import { formatBalance } from '@polkadot/util';
 import { withCall, withMulti } from '@polkadot/ui-api';
 
 import translate from '../translate';
@@ -25,6 +24,9 @@ type Props = I18nProps & {
 class Summary extends React.PureComponent<Props> {
   render () {
     const { className, intentions, style, t, staking_validatorCount, validators } = this.props;
+    const waiting = intentions.length > validators.length
+      ? (intentions.length - validators.length)
+      : 0;
 
     return (
       <SummaryBox
@@ -35,94 +37,16 @@ class Summary extends React.PureComponent<Props> {
           <CardSummary label={t('validators')}>
             {validators.length}/{staking_validatorCount ? staking_validatorCount.toString() : '-'}
           </CardSummary>
-          <CardSummary label={t('intentions')}>
-            {intentions.length}
+          <CardSummary label={t('waiting')}>
+            {waiting}
           </CardSummary>
         </section>
         <section className='ui--media-medium'>
           <SummarySession withBroken={false} />
         </section>
-        {this.renderBalances()}
       </SummaryBox>
     );
   }
-
-  private renderBalances () {
-    const { intentions, t } = this.props;
-
-    if (!intentions || !intentions.length) {
-      return null;
-    }
-
-    return (
-      <section className='ui--media-large'>
-        <CardSummary label={t('balances')}>
-          {this.renderBalancesCalculation()}
-        </CardSummary>
-      </section>
-    );
-  }
-
-  private renderBalancesCalculation () {
-    const { t } = this.props;
-    const intentionHigh = this.calcIntentionsHigh();
-    const validatorLow = this.calcValidatorLow();
-    const nominatedLow = validatorLow && validatorLow.nominatedBalance.gtn(0)
-      ? `(+${formatBalance(validatorLow.nominatedBalance)})`
-      : '';
-    const nominatedHigh = intentionHigh && intentionHigh.nominatedBalance.gtn(0)
-      ? `(+${formatBalance(intentionHigh.nominatedBalance)})`
-      : '';
-
-    return (
-      <div className='staking--Summary-text'>
-        <div>{t('lowest validator {{validatorLow}}', {
-          replace: {
-            validatorLow: validatorLow && validatorLow.stakingBalance
-              ? `${formatBalance(validatorLow.stakingBalance)} ${nominatedLow}`
-              : '-'
-          }
-        })}</div>
-        <div>{t('highest intention {{intentionHigh}}', {
-          replace: {
-            intentionHigh: intentionHigh
-              ? `${formatBalance(intentionHigh.stakingBalance)} ${nominatedHigh}`
-              : '-'
-          }
-        })}</div>
-      </div>
-    );
-  }
-
-  private calcIntentionsHigh (): DerivedBalances | null {
-    const { balances, intentions, validators } = this.props;
-
-    return intentions.reduce((high: DerivedBalances | null, addr) => {
-      const balance = validators.includes(addr) || !balances[addr]
-        ? null
-        : balances[addr];
-
-      if (high === null || (balance && high.stakingBalance.lt(balance.stakingBalance))) {
-        return balance;
-      }
-
-      return high;
-    }, null);
-  }
-
-  private calcValidatorLow (): DerivedBalances | null {
-    const { balances, validators } = this.props;
-
-    return validators.reduce((low: DerivedBalances | null, addr) => {
-      const balance = balances[addr] || null;
-
-      if (low === null || (balance && low.stakingBalance.gt(balance.stakingBalance))) {
-        return balance;
-      }
-
-      return low;
-    }, null);
-  }
 }
 
 export default withMulti(

+ 2 - 2
packages/app-staking/src/Overview/index.css

@@ -86,12 +86,12 @@
 
     .recentlyOffline {
       position: absolute;
+      bottom: 0.75rem;
       font-size: 12px;
       cursor: help;
       display: flex;
       justify-content: center;
-      right: 12px;
-      top: 88px;
+      right: 0.75rem;
       width: 22px;
       height: 22px;
       padding: 0;

+ 18 - 0
packages/app-staking/src/StakeList.tsx

@@ -7,9 +7,11 @@ import { ComponentProps } from './types';
 
 import React from 'react';
 import keyring from '@polkadot/ui-keyring';
+import createOption from '@polkadot/ui-keyring/options/item';
 
 import Account from './Account';
 import translate from './translate';
+import { KeyringSectionOption } from '@polkadot/ui-keyring/options/types';
 
 type Props = I18nProps & ComponentProps;
 
@@ -33,6 +35,7 @@ class StakeList extends React.PureComponent<Props> {
               key={address}
               name={name}
               nominators={nominators}
+              targets={this.getTargetOptions()}
               validators={validators}
             />
           );
@@ -40,6 +43,21 @@ class StakeList extends React.PureComponent<Props> {
       </div>
     );
   }
+
+  private getTargetOptions (): Array<KeyringSectionOption> {
+    const { targets } = this.props;
+
+    return targets.map((stashId) => {
+      const pair = keyring.getAccount(stashId).isValid()
+        ? keyring.getAccount(stashId)
+        : keyring.getAddress(stashId);
+      const name = pair.isValid()
+        ? pair.getMeta().name
+        : undefined;
+
+      return createOption(stashId, name);
+    });
+  }
 }
 
 export default translate(StakeList);

+ 13 - 1
packages/app-staking/src/index.css

@@ -99,6 +99,18 @@
   margin-top: 0.75rem;
 
   .staking--label {
-    margin-bottom: -0.25rem;
+    margin-bottom: -0.5rem;
+  }
+}
+
+.staking--accounts-info {
+  position: absolute;
+  top: 0.25rem;
+  right: 1rem;
+  margin-top: 0.75rem;
+  text-align: right;
+
+  .staking--label {
+    margin-bottom: -0.5rem;
   }
 }

+ 5 - 1
packages/app-staking/src/index.tsx

@@ -33,6 +33,7 @@ type State = {
   intentions: Array<string>,
   nominators: Nominators,
   tabs: Array<TabItem>,
+  targets: Array<string>,
   validators: Array<string>
 };
 
@@ -57,6 +58,7 @@ class App extends React.PureComponent<Props, State> {
           text: t('Validator Staking')
         }
       ],
+      targets: [],
       validators: []
     };
   }
@@ -73,6 +75,7 @@ class App extends React.PureComponent<Props, State> {
 
         return result;
       }, {} as Nominators),
+      targets: staking_controllers[0].map((accountId) => accountId.toString()),
       validators: session_validators.map((authorityId) =>
         authorityId.toString()
       )
@@ -105,7 +108,7 @@ class App extends React.PureComponent<Props, State> {
 
   private renderComponent (Component: React.ComponentType<ComponentProps>) {
     return (): React.ReactNode => {
-      const { intentions, nominators, validators } = this.state;
+      const { intentions, nominators, targets, validators } = this.state;
       const { balances = {} } = this.props;
 
       return (
@@ -114,6 +117,7 @@ class App extends React.PureComponent<Props, State> {
           balanceArray={this.balanceArray}
           intentions={intentions}
           nominators={nominators}
+          targets={targets}
           validators={validators}
         />
       );

+ 3 - 2
packages/app-staking/src/types.ts

@@ -16,16 +16,17 @@ export type ComponentProps = {
   balanceArray: (_address: AccountId | string) => Array<Balance> | undefined,
   intentions: Array<string>,
   nominators: Nominators,
+  targets: Array<string>,
   validators: Array<string>
 };
 
 export type RecentlyOffline = Array<[AccountId, BlockNumber, BN]>;
 
 export type RecentlyOfflineMap = {
-  [s: string]: OfflineStatus
+  [s: string]: Array<OfflineStatus>
 };
 
 export interface OfflineStatus {
   blockNumber: BlockNumber;
-  instances: BN;
+  count: BN;
 }

+ 3 - 3
packages/app-storage/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@polkadot/app-storage",
-  "version": "0.30.1",
+  "version": "0.31.0-beta.10",
   "main": "index.js",
   "repository": "https://github.com/polkadot-js/apps.git",
   "author": "Jaco Greeff <jacogr@gmail.com>",
@@ -11,7 +11,7 @@
   "license": "Apache-2.0",
   "dependencies": {
     "@babel/runtime": "^7.4.3",
-    "@polkadot/ui-app": "^0.30.1",
-    "@polkadot/ui-params": "^0.30.1"
+    "@polkadot/ui-app": "^0.31.0-beta.10",
+    "@polkadot/ui-params": "^0.31.0-beta.10"
   }
 }

+ 2 - 2
packages/app-toolbox/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@polkadot/app-toolbox",
-  "version": "0.30.1",
+  "version": "0.31.0-beta.10",
   "main": "index.js",
   "repository": "https://github.com/polkadot-js/apps.git",
   "author": "Jaco Greeff <jacogr@gmail.com>",
@@ -11,6 +11,6 @@
   "license": "Apache-2.0",
   "dependencies": {
     "@babel/runtime": "^7.4.3",
-    "@polkadot/ui-app": "^0.30.1"
+    "@polkadot/ui-app": "^0.31.0-beta.10"
   }
 }

+ 3 - 3
packages/app-transfer/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@polkadot/app-transfer",
-  "version": "0.30.1",
+  "version": "0.31.0-beta.10",
   "description": "A basic transfer app",
   "main": "index.js",
   "scripts": {},
@@ -11,7 +11,7 @@
   "license": "Apache-2.0",
   "dependencies": {
     "@babel/runtime": "^7.4.3",
-    "@polkadot/ui-app": "^0.30.1",
-    "@polkadot/ui-reactive": "^0.30.1"
+    "@polkadot/ui-app": "^0.31.0-beta.10",
+    "@polkadot/ui-reactive": "^0.31.0-beta.10"
   }
 }

+ 3 - 3
packages/apps/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@polkadot/apps",
-  "version": "0.30.1",
+  "version": "0.31.0-beta.10",
   "description": "An Apps portal into the Polkadot network",
   "main": "index.js",
   "homepage": ".",
@@ -13,9 +13,9 @@
   "license": "Apache-2.0",
   "dependencies": {
     "@babel/runtime": "^7.4.3",
-    "@polkadot/ui-app": "^0.30.1",
+    "@polkadot/ui-app": "^0.31.0-beta.10",
     "@polkadot/ui-assets": "^0.37.1",
-    "@polkadot/ui-signer": "^0.30.1",
+    "@polkadot/ui-signer": "^0.31.0-beta.10",
     "@types/react-tooltip": "^3.9.1",
     "react-tooltip": "^3.9.2"
   }

+ 3 - 2
packages/apps/src/Content/index.tsx

@@ -99,9 +99,10 @@ export default withMulti(
   // These API queries are used in a number of places, warm them up
   // to avoid constant un-/re-subscribe on these
   withCalls<Props>(
-    'query.session.validators',
     'derive.accounts.indexes',
     'derive.balances.fees',
-    'derive.staking.controllers'
+    'derive.staking.controllers',
+    'query.staking.nominators',
+    'query.session.validators'
   )
 );

+ 17 - 16
packages/apps/src/SideBar/NodeInfo.tsx

@@ -36,28 +36,31 @@ const Wrapper = styled.div`
   }
 `;
 
-const HEALTH_POLL = 22500;
 const pkgJson = require('../../package.json');
 
 class NodeInfo extends React.PureComponent<Props> {
-  componentDidMount () {
-    const { api } = this.props;
-
-    window.setInterval(() => {
-      api.rpc.system
-        .health()
-        .catch(() => {
-          // ignore
-        });
-    }, HEALTH_POLL);
-  }
-
   render () {
     const { api } = this.props;
     const uiInfo = `apps v${pkgJson.version}`;
 
     return (
       <Wrapper>
+        {this.renderNode()}
+        <div>{api.libraryInfo.replace('@polkadot/', '')}</div>
+        <div>{uiInfo}</div>
+      </Wrapper>
+    );
+  }
+
+  private renderNode () {
+    const { isApiReady } = this.props;
+
+    if (!isApiReady) {
+      return null;
+    }
+
+    return (
+      <>
         <div>
           <Chain />&nbsp;
           <BestNumber label='#' />
@@ -67,9 +70,7 @@ class NodeInfo extends React.PureComponent<Props> {
           <NodeVersion label='v' />
         </div>
         <div className='spacer' />
-        <div>{api.libraryInfo.replace('@polkadot/', '')}</div>
-        <div>{uiInfo}</div>
-      </Wrapper>
+      </>
     );
   }
 }

+ 1 - 1
packages/joy-election/package.json

@@ -8,7 +8,7 @@
   "maintainers": [],
   "dependencies": {
     "@babel/runtime": "^7.4.3",
-    "@polkadot/ui-app": "^0.30.1",
+    "@polkadot/ui-app": "^0.31.0-beta.10",
     "@polkadot/joy-utils": "^0.1.1"
   }
 }

+ 1 - 1
packages/joy-help/package.json

@@ -8,7 +8,7 @@
   "maintainers": [],
   "dependencies": {
     "@babel/runtime": "^7.4.3",
-    "@polkadot/ui-app": "^0.30.1",
+    "@polkadot/ui-app": "^0.31.0-beta.10",
     "@polkadot/joy-utils": "^0.1.1"
   }
 }

+ 1 - 1
packages/joy-media/package.json

@@ -8,7 +8,7 @@
   "maintainers": [],
   "dependencies": {
     "@babel/runtime": "^7.4.3",
-    "@polkadot/ui-app": "^0.30.1",
+    "@polkadot/ui-app": "^0.31.0-beta.10",
     "@types/mime-types": "^2.1.0",
     "@polkadot/joy-utils": "^0.1.1",
     "aplayer": "^1.10.1",

+ 1 - 1
packages/joy-members/package.json

@@ -8,7 +8,7 @@
   "maintainers": [],
   "dependencies": {
     "@babel/runtime": "^7.4.3",
-    "@polkadot/ui-app": "^0.30.1",
+    "@polkadot/ui-app": "^0.31.0-beta.10",
     "@polkadot/joy-utils": "^0.1.1"
   }
 }

+ 1 - 1
packages/joy-proposals/package.json

@@ -8,7 +8,7 @@
   "maintainers": [],
   "dependencies": {
     "@babel/runtime": "^7.4.3",
-    "@polkadot/ui-app": "^0.30.1",
+    "@polkadot/ui-app": "^0.31.0-beta.10",
     "@polkadot/joy-utils": "^0.1.1"
   }
 }

+ 2 - 2
packages/joy-roles/package.json

@@ -8,8 +8,8 @@
   "maintainers": [],
   "dependencies": {
     "@babel/runtime": "^7.4.3",
-    "@polkadot/ui-app": "^0.30.1",
+    "@polkadot/ui-app": "^0.31.0-beta.10",
     "@polkadot/joy-utils": "^0.1.1",
-    "@polkadot/ui-reactive": "^0.30.1"
+    "@polkadot/ui-reactive": "^0.31.0-beta.10"
   }
 }

+ 1 - 1
packages/joy-utils/package.json

@@ -8,7 +8,7 @@
   "maintainers": [],
   "dependencies": {
     "@babel/runtime": "^7.4.3",
-    "@polkadot/ui-app": "^0.30.1",
+    "@polkadot/ui-app": "^0.31.0-beta.10",
     "@types/query-string": "^6.2.0",
     "@types/uuid": "^3.4.4",
     "@types/yup": "^0.26.10",

+ 1 - 1
packages/ui-api/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@polkadot/ui-api",
-  "version": "0.30.1",
+  "version": "0.31.0-beta.10",
   "description": "A collection of RxJs React components the Polkadot JS API",
   "main": "index.js",
   "keywords": [

+ 3 - 3
packages/ui-app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@polkadot/ui-app",
-  "version": "0.30.1",
+  "version": "0.31.0-beta.10",
   "main": "index.js",
   "repository": "https://github.com/polkadot-js/apps.git",
   "author": "Jaco Greeff <jacogr@gmail.com>",
@@ -11,10 +11,10 @@
   "license": "Apache-2.0",
   "dependencies": {
     "@babel/runtime": "^7.4.3",
-    "@polkadot/ui-api": "^0.30.1",
+    "@polkadot/ui-api": "^0.31.0-beta.10",
     "@polkadot/ui-identicon": "^0.37.1",
     "@polkadot/ui-keyring": "^0.37.1",
-    "@polkadot/ui-reactive": "^0.30.1",
+    "@polkadot/ui-reactive": "^0.31.0-beta.10",
     "@polkadot/joy-settings": "^1.0.0",
     "@types/chart.js": "^2.7.45",
     "@types/classnames": "^2.2.7",

+ 2 - 13
packages/ui-app/src/AddressMini.tsx

@@ -7,7 +7,6 @@ import { BareProps } from './types';
 import BN from 'bn.js';
 import React from 'react';
 import { AccountId, AccountIndex, Address, Balance } from '@polkadot/types';
-import { withCall, withMulti } from '@polkadot/ui-api';
 
 import { classes, toShortAddress } from './util';
 import BalanceDisplay from './Balance';
@@ -18,24 +17,20 @@ type Props = BareProps & {
   children?: React.ReactNode,
   isPadded?: boolean,
   isShort?: boolean,
-  session_validators?: Array<AccountId>,
   value?: AccountId | AccountIndex | Address | string,
   withAddress?: boolean,
   withBalance?: boolean
 };
 
-class AddressMini extends React.PureComponent<Props> {
+export default class AddressMini extends React.PureComponent<Props> {
   render () {
-    const { children, className, isPadded = true, session_validators, style, value } = this.props;
+    const { children, className, isPadded = true, style, value } = this.props;
 
     if (!value) {
       return null;
     }
 
     const address = value.toString();
-    const isValidator = (session_validators || []).find((validator) =>
-      validator.toString() === address
-    );
 
     return (
       <div
@@ -44,7 +39,6 @@ class AddressMini extends React.PureComponent<Props> {
       >
         <div className='ui--AddressMini-info'>
           <IdentityIcon
-            isHighlight={!!isValidator}
             size={24}
             value={address}
           />
@@ -84,8 +78,3 @@ class AddressMini extends React.PureComponent<Props> {
     );
   }
 }
-
-export default withMulti(
-  AddressMini,
-  withCall('query.session.validators')
-);

+ 3 - 8
packages/ui-app/src/AddressSummary.tsx

@@ -151,7 +151,7 @@ class AddressSummary extends React.PureComponent<Props> {
   }
 
   protected renderIcon (className: string = 'ui--AddressSummary-icon', size?: number) {
-    const { accounts_idAndIndex = [], identIconSize = 96, session_validators, value, withIcon = true } = this.props;
+    const { accounts_idAndIndex = [], identIconSize = 96, value, withIcon = true } = this.props;
 
     if (!withIcon) {
       return null;
@@ -159,16 +159,12 @@ class AddressSummary extends React.PureComponent<Props> {
 
     const [_accountId] = accounts_idAndIndex;
     const accountId = (_accountId || value || '').toString();
-    const isValidator = (session_validators || []).find((validator) =>
-      validator.toString() === accountId
-    );
 
     return (
       <IdentityIcon
         className={className}
-        isHighlight={!!isValidator}
         size={size || identIconSize}
-        value={value ? value.toString() : DEFAULT_ADDR}
+        value={accountId || DEFAULT_ADDR}
       />
     );
   }
@@ -220,7 +216,6 @@ export {
 
 export default translate(
   withCalls<Props>(
-    ['derive.accounts.idAndIndex', { paramName: 'value' }],
-    'query.session.validators'
+    ['derive.accounts.idAndIndex', { paramName: 'value' }]
   )(AddressSummary)
 );

+ 3 - 1
packages/ui-app/src/CardSummary.tsx

@@ -75,13 +75,14 @@ type ProgressProps = {
 
 type Props = BareProps & {
   children?: React.ReactNode,
+  help?: React.ReactNode,
   label: React.ReactNode,
   progress?: ProgressProps
 };
 
 export default class CardSummary extends React.PureComponent<Props> {
   render () {
-    const { children, className, label, progress, style } = this.props;
+    const { children, className, help, label, progress, style } = this.props;
     const value = progress && progress.value;
     const total = progress && progress.total;
     const left = progress && !isUndefined(value) && !isUndefined(total) && value.gten(0) && total.gtn(0)
@@ -110,6 +111,7 @@ export default class CardSummary extends React.PureComponent<Props> {
         style={style}
       >
         <Labelled
+          help={help}
           isSmall
           label={label}
         >

+ 7 - 1
packages/ui-app/src/Dropdown.tsx

@@ -15,14 +15,17 @@ import Labelled from './Labelled';
 type Props<Option> = BareProps & {
   defaultValue?: any,
   isPrimary?: boolean,
+  help?: React.ReactNode,
   isButton?: boolean,
   isDisabled?: boolean,
   isError?: boolean,
+  isMultiple?: boolean,
   label?: React.ReactNode,
   onChange?: (value: any) => void,
   onSearch?: (filteredOptions: Array<any>, query: string) => Array<Option>,
   options: Array<Option>,
   placeholder?: string,
+  renderLabel?: (item: any) => any,
   transform?: (value: any) => any,
   value?: any,
   withLabel?: boolean
@@ -55,7 +58,7 @@ export default class Dropdown<Option> extends React.PureComponent<Props<Option>>
   }
 
   render () {
-    const { className, defaultValue, isPrimary = true, isButton, isDisabled, isError, label, onSearch, options, placeholder, style, withLabel, value } = this.props;
+    const { className, defaultValue, isPrimary = true, help, isButton, isDisabled, isError, isMultiple, label, onSearch, options, placeholder, style, withLabel, renderLabel, value } = this.props;
     const dropdown = (
       <SUIDropdown
         button={isButton}
@@ -63,9 +66,11 @@ export default class Dropdown<Option> extends React.PureComponent<Props<Option>>
         disabled={isDisabled}
         error={isError}
         floating={isButton}
+        multiple={isMultiple}
         onChange={this.onChange}
         options={options}
         placeholder={placeholder}
+        renderLabel={renderLabel}
         search={onSearch}
         selection
         value={
@@ -85,6 +90,7 @@ export default class Dropdown<Option> extends React.PureComponent<Props<Option>>
       : (
         <Labelled
           className={classes('ui--Dropdown', className)}
+          help={help}
           label={label}
           style={style}
           withLabel={withLabel}

+ 36 - 11
packages/ui-app/src/IdentityIcon.tsx

@@ -8,8 +8,8 @@ import { QueueProps, QueueAction$Add } from './Status/types';
 import { I18nProps } from './types';
 
 import React from 'react';
-import { AccountId } from '@polkadot/types';
-import { withCall } from '@polkadot/ui-api/with';
+import { AccountId, Option } from '@polkadot/types';
+import { withCalls } from '@polkadot/ui-api/with';
 import BaseIdentityIcon from '@polkadot/ui-identicon';
 
 import { QueueConsumer } from './Status/Context';
@@ -20,7 +20,14 @@ type CopyProps = IdentityProps & I18nProps & {
 };
 
 type IconProps = ApiProps & IdentityProps & {
-  session_validators?: Array<AccountId>
+  session_validators?: Array<AccountId>,
+  staking_bonded?: Option<AccountId>
+};
+
+type Props = IconProps & IdentityProps;
+
+type State = {
+  isValidator: boolean
 };
 
 class CopyIcon extends React.PureComponent<CopyProps> {
@@ -53,20 +60,35 @@ class CopyIcon extends React.PureComponent<CopyProps> {
 
 const CopyIconI18N = translate(CopyIcon);
 
-class IdentityIcon extends React.PureComponent<IconProps & IdentityProps> {
-  render () {
-    const { session_validators = [], value } = this.props;
+class IdentityIcon extends React.PureComponent<Props, State> {
+  state: State = {
+    isValidator: false
+  };
 
-    const address = (value || '').toString();
-    const isValidator = (session_validators || []).find((validator) =>
-      validator.toString() === address
+  static getDerivedStateFromProps ({ session_validators = [], staking_bonded, value }: Props, prevState: State): State | null {
+    const address = value
+      ? value.toString()
+      : null;
+    const bonded = staking_bonded && staking_bonded.isSome
+      ? staking_bonded.unwrap().toString()
+      : null;
+    const isValidator = !!session_validators.find((validator) =>
+      [address, bonded].includes(validator.toString())
     );
 
+    return prevState.isValidator !== isValidator
+      ? { isValidator }
+      : null;
+  }
+
+  render () {
+    const { isValidator } = this.state;
+
     return (
       <QueueConsumer>
         {({ queueAction }: QueueProps) =>
           <CopyIconI18N
-            isHighlight={!!isValidator}
+            isHighlight={isValidator}
             {...this.props}
             queueAction={queueAction}
           />
@@ -76,4 +98,7 @@ class IdentityIcon extends React.PureComponent<IconProps & IdentityProps> {
   }
 }
 
-export default withCall('query.session.validators')(IdentityIcon);
+export default withCalls<Props>(
+  'query.session.validators',
+  ['query.staking.bonded', { paramName: 'value' }]
+)(IdentityIcon);

+ 22 - 2
packages/ui-app/src/Input.tsx

@@ -16,6 +16,7 @@ type Props = BareProps & {
   autoFocus?: boolean,
   children?: React.ReactNode,
   defaultValue?: any,
+  help?: React.ReactNode,
   icon?: React.ReactNode,
   isAction?: boolean,
   isDisabled?: boolean,
@@ -31,6 +32,8 @@ type Props = BareProps & {
   onBlur?: (event: React.KeyboardEvent<Element>) => void,
   onKeyDown?: (event: React.KeyboardEvent<Element>) => void,
   onKeyUp?: (event: React.KeyboardEvent<Element>) => void,
+  onKeyPress?: (event: React.KeyboardEvent<Element>) => void,
+  onPaste?: (event: React.ClipboardEvent<Element>) => void,
   placeholder?: string,
   tabIndex?: number,
   type?: Input$Type,
@@ -42,6 +45,12 @@ type State = {
   name: string;
 };
 
+// Find decimal separator used in current locale
+const getDecimalSeparator = (): string => Intl.NumberFormat()
+    .formatToParts(1.1)
+    .find(part => part.type === 'decimal')!
+    .value;
+
 // note: KeyboardEvent.keyCode and KeyboardEvent.which are deprecated
 const KEYS = {
   A: 'a',
@@ -57,7 +66,8 @@ const KEYS = {
   TAB: 'Tab',
   V: 'v',
   X: 'x',
-  ZERO: '0'
+  ZERO: '0',
+  DECIMAL: getDecimalSeparator()
 };
 
 const KEYS_PRE: Array<any> = [KEYS.ALT, KEYS.CMD, KEYS.CTRL];
@@ -83,11 +93,12 @@ export default class Input extends React.PureComponent<Props, State> {
   };
 
   render () {
-    const { autoFocus = false, children, className, defaultValue, icon, isEditable = false, isAction = false, isDisabled = false, isError = false, isHidden = false, label, max, maxLength, min, name, placeholder, style, tabIndex, type = 'text', value, withLabel } = this.props;
+    const { autoFocus = false, children, className, defaultValue, help, icon, isEditable = false, isAction = false, isDisabled = false, isError = false, isHidden = false, label, max, maxLength, min, name, placeholder, style, tabIndex, type = 'text', value, withLabel } = this.props;
 
     return (
       <Labelled
         className={className}
+        help={help}
         label={label}
         style={style}
         withLabel={withLabel}
@@ -132,6 +143,7 @@ export default class Input extends React.PureComponent<Props, State> {
                 ? 'new-password'
                 : 'off'
             }
+            onPaste={this.onPaste}
           />
           {
             isEditable
@@ -167,6 +179,14 @@ export default class Input extends React.PureComponent<Props, State> {
       onKeyUp(event);
     }
   }
+
+  private onPaste = (event: React.ClipboardEvent<Element>): void => {
+    const { onPaste } = this.props;
+
+    if (onPaste) {
+      onPaste(event);
+    }
+  }
 }
 
 export {

+ 64 - 16
packages/ui-app/src/InputAddress/index.tsx

@@ -22,17 +22,20 @@ import { Subscribe, Provider } from 'unstated';
 
 type Props = BareProps & {
   defaultValue?: string | null,
+  help?: React.ReactNode,
   hideAddress?: boolean;
   isDisabled?: boolean,
   isError?: boolean,
   isInput?: boolean,
+  isMultiple?: boolean,
   label?: string,
   onChange?: (value: string | null) => void,
-  addresses?: KeyringSectionOptions,
+  onChangeMulti?: (value: Array<string>) => void,
+  options?: Array<KeyringSectionOption>,
   optionsAll?: KeyringOptions,
   placeholder?: string,
   type?: KeyringOption$Type,
-  value?: string | Uint8Array,
+  value?: string | Uint8Array | Array<string>,
   withLabel?: boolean
 };
 
@@ -83,7 +86,10 @@ class InputAddress extends React.PureComponent<Props, State> {
   static getDerivedStateFromProps ({ value }: Props): State | null {
     try {
       return {
-        value: addressToAddress(value) || undefined
+        value: Array.isArray(value)
+          ? value.map(addressToAddress)
+          : (addressToAddress(value) || undefined)
+
       } as State;
     } catch (error) {
       return null;
@@ -108,14 +114,9 @@ class InputAddress extends React.PureComponent<Props, State> {
   }
 
   render () {
-    const { className, defaultValue, hideAddress = false, isDisabled = false, isError, label, addresses, optionsAll, type = DEFAULT_TYPE, style, withLabel } = this.props;
+    const { className, defaultValue, help, hideAddress = false, isDisabled = false, isError, isMultiple, label, options, optionsAll, placeholder, type = DEFAULT_TYPE, style, withLabel } = this.props;
     const { value } = this.state;
-
-    if (addresses && optionsAll) {
-      optionsAll.address = addresses;
-    }
-
-    const hasOptions = optionsAll && Object.keys(optionsAll[type]).length !== 0;
+    const hasOptions = (options && options.length !== 0) || (optionsAll && Object.keys(optionsAll[type]).length !== 0);
 
     if (!hasOptions && !isDisabled) {
       return null;
@@ -136,28 +137,63 @@ class InputAddress extends React.PureComponent<Props, State> {
       <Dropdown
         className={classes('ui--InputAddress', hideAddress ? 'flag--hideAddress' : '', className)}
         defaultValue={
-          value !== undefined
+          isMultiple || (value !== undefined)
             ? undefined
             : actualValue
         }
+        help={help}
         isDisabled={isDisabled}
         isError={isError}
+        isMultiple={isMultiple}
         label={label}
-        onChange={this.onChange(me)}
+        onChange={
+          isMultiple
+            ? this.onChangeMulti
+            : this.onChange
+        }
         onSearch={this.onSearch}
         options={
-          isDisabled && actualValue
-            ? [createOption(actualValue)]
-            : (optionsAll ? optionsAll[type] : [])
+          options
+            ? options
+            : (
+                isDisabled && actualValue
+                  ? [createOption(actualValue)]
+                  : (optionsAll ? optionsAll[type] : [])
+            )
+        }
+        placeholder={placeholder}
+        renderLabel={
+          isMultiple
+            ? this.renderLabel
+            : undefined
         }
         style={style}
-        value={value}
+        value={
+          isMultiple
+            ? undefined
+            : value
+        }
         withLabel={withLabel}
       />
       }</Subscribe></Provider>
     );
   }
 
+  private renderLabel = ({ value }: KeyringSectionOption): string | null => {
+    if (!value) {
+      return null;
+    }
+
+    const pair = keyring.getAccount(value).isValid()
+        ? keyring.getAccount(value)
+        : keyring.getAddress(value);
+    const name = pair.isValid()
+        ? pair.getMeta().name
+        : undefined;
+
+    return name || `${value.slice(0, 6)}…${value.slice(-6)}`;
+  }
+
   private getLastOptionValue (): KeyringSectionOption | undefined {
     const { optionsAll, type = DEFAULT_TYPE } = this.props;
 
@@ -199,6 +235,18 @@ class InputAddress extends React.PureComponent<Props, State> {
     };
   }
 
+  private onChangeMulti = (addresses: Array<string>) => {
+    const { onChangeMulti } = this.props;
+
+    if (onChangeMulti) {
+      onChangeMulti(
+        addresses
+          .map(transformToAccountId)
+          .filter((address) => address) as Array<string>
+      );
+    }
+  }
+
   private onSearch = (filteredOptions: KeyringSectionOptions, _query: string): KeyringSectionOptions => {
     const { isInput = true } = this.props;
     const query = _query.trim();

+ 3 - 1
packages/ui-app/src/InputBalance.tsx

@@ -12,6 +12,7 @@ import { InputNumber } from '@polkadot/ui-app';
 type Props = BareProps & {
   autoFocus?: boolean,
   defaultValue?: BN | string,
+  help?: React.ReactNode,
   isDisabled?: boolean,
   isError?: boolean,
   label?: any,
@@ -25,7 +26,7 @@ const DEFAULT_BITLENGTH = BitLengthOption.CHAIN_SPEC as BitLength;
 
 export default class InputBalance extends React.PureComponent<Props> {
   render () {
-    const { autoFocus, className, defaultValue, isDisabled, isError, label, onChange, placeholder, style, value, withLabel } = this.props;
+    const { autoFocus, className, defaultValue, help, isDisabled, isError, label, onChange, placeholder, style, value, withLabel } = this.props;
 
     return (
       <InputNumber
@@ -33,6 +34,7 @@ export default class InputBalance extends React.PureComponent<Props> {
         className={className}
         bitLength={DEFAULT_BITLENGTH}
         defaultValue={defaultValue}
+        help={help}
         isDisabled={isDisabled}
         isError={isError}
         isSi

+ 3 - 1
packages/ui-app/src/InputExtrinsic/index.tsx

@@ -21,6 +21,7 @@ import sectionOptions from './options/section';
 
 type Props = ApiProps & I18nProps & {
   defaultValue: MethodFunction,
+  help?: React.ReactNode,
   isDisabled?: boolean,
   isError?: boolean,
   isPrivate?: boolean,
@@ -54,7 +55,7 @@ class InputExtrinsic extends React.PureComponent<Props, State> {
   }
 
   render () {
-    const { api, className, label, style, withLabel } = this.props;
+    const { api, className, help, label, style, withLabel } = this.props;
     const { optionsMethod, optionsSection, value } = this.state;
 
     return (
@@ -63,6 +64,7 @@ class InputExtrinsic extends React.PureComponent<Props, State> {
         style={style}
       >
         <Labelled
+          help={help}
           label={label}
           withLabel={withLabel}
         >

+ 26 - 21
packages/ui-app/src/InputFile.tsx

@@ -17,6 +17,7 @@ type Props = BareProps & WithTranslation & {
   // i.e. MIME types: 'application/json, text/plain', or '.json, .txt'
   accept?: string,
   clearContent?: boolean,
+  help?: React.ReactNode,
   isDisabled?: boolean,
   isError?: boolean,
   label?: React.ReactNode,
@@ -42,30 +43,34 @@ class InputFile extends React.PureComponent<Props, State> {
   state: State = {};
 
   render () {
-    const { accept, className, clearContent, isDisabled, isError = false, label, placeholder, t, withLabel } = this.props;
+    const { accept, className, clearContent, help, isDisabled, isError = false, label, placeholder, t, withLabel } = this.props;
     const { file } = this.state;
 
-    const dropzone =
-      <Dropzone
-        accept={accept}
-        className={classes('ui--InputFile', isError ? 'error' : '', className)}
-        disabled={isDisabled}
-        multiple={false}
-        onDrop={this.onDrop}
+    return (
+      <Labelled
+        help={help}
+        label={label}
+        withLabel={withLabel}
       >
-        <div className='label'>
-        {!file || clearContent
-          ? placeholder || t('Drag and drop the file here')
-          : placeholder || t('{{name}} ({{size}} bytes)', {
-            replace: file
-          })
-        }
-        </div>
-      </Dropzone>;
-
-    return withLabel
-      ? <Labelled label={label}>{dropzone}</Labelled>
-      : dropzone;
+        <Dropzone
+          accept={accept}
+          className={classes('ui--InputFile', isError ? 'error' : '', className)}
+          disabled={isDisabled}
+          multiple={false}
+          onDrop={this.onDrop}
+        >
+          <div className='label'>
+            {
+              !file || clearContent
+                ? placeholder || t('drag and drop the file here')
+                : placeholder || t('{{name}} ({{size}} bytes)', {
+                  replace: file
+                })
+            }
+          </div>
+        </Dropzone>
+      </Labelled>
+    );
   }
 
   private onDrop = (files: Array<File>) => {

+ 100 - 99
packages/ui-app/src/InputNumber.tsx

@@ -11,16 +11,18 @@ import { formatBalance, isUndefined } from '@polkadot/util';
 import { classes } from './util';
 import { BitLengthOption } from './constants';
 import Dropdown from './Dropdown';
-import Input, { KEYS, KEYS_PRE, isCopy, isCut, isPaste, isSelectAll } from './Input';
+import Input, { KEYS, KEYS_PRE } from './Input';
 import translate from './translate';
 
 type Props = BareProps & I18nProps & {
   autoFocus?: boolean,
   bitLength?: BitLength,
   defaultValue?: BN | string,
+  help?: React.ReactNode,
   isDisabled?: boolean,
   isError?: boolean,
   isSi?: boolean,
+  isDecimal?: boolean,
   label?: any,
   maxLength?: number,
   onChange?: (value?: BN) => void,
@@ -30,16 +32,15 @@ type Props = BareProps & I18nProps & {
 };
 
 type State = {
-  defaultValue?: string,
   isPreKeyDown: boolean,
   isValid: boolean,
   siOptions: Array<{ value: string, text: string }>,
   siUnit: string,
+  value: string,
   valueBN: BN
 };
 
 const DEFAULT_BITLENGTH = BitLengthOption.NORMAL_NUMBERS as BitLength;
-const KEYS_ALLOWED: Array<any> = [KEYS.ARROW_LEFT, KEYS.ARROW_RIGHT, KEYS.BACKSPACE, KEYS.ENTER, KEYS.ESCAPE, KEYS.TAB];
 
 class InputNumber extends React.PureComponent<Props, State> {
   constructor (props: Props) {
@@ -47,10 +48,10 @@ class InputNumber extends React.PureComponent<Props, State> {
 
     const { defaultValue, isSi, value } = this.props;
     let valueBN = new BN(value || 0);
-    const si = formatBalance.calcSi(valueBN.toString());
+    const si = formatBalance.findSi('-');
 
     this.state = {
-      defaultValue: isSi
+      value: isSi
         ? new BN(defaultValue || valueBN).div(new BN(10).pow(new BN(si.power))).toString()
         : (defaultValue || valueBN).toString(),
       isPreKeyDown: false,
@@ -71,32 +72,27 @@ class InputNumber extends React.PureComponent<Props, State> {
     InputNumber.units = units;
   }
 
-  static getDerivedStateFromProps ({ isDisabled, isSi, defaultValue = '0' }: Props, state: State): Partial<State> | null {
+  static getDerivedStateFromProps ({ isDisabled, isSi, defaultValue = '0' }: Props): Partial<State> | null {
     if (!isDisabled || !isSi) {
       return null;
     }
 
     return {
-      defaultValue: formatBalance(defaultValue, false),
+      value: formatBalance(defaultValue, false),
       siUnit: formatBalance.calcSi(defaultValue.toString(), formatBalance.getDefaults().decimals).value
     };
   }
 
   render () {
-    const { bitLength = DEFAULT_BITLENGTH, className, defaultValue = '0', isSi, isDisabled, maxLength, style, t } = this.props;
-    const { isValid } = this.state;
+    const { bitLength = DEFAULT_BITLENGTH, className, help, isSi, isDisabled, maxLength, style, t } = this.props;
+    const { isValid, value } = this.state;
     const maxValueLength = this.maxValue(bitLength).toString().length - 1;
-    const value = this.state.defaultValue || defaultValue;
 
     return (
       <Input
         {...this.props}
         className={classes('ui--InputNumber', className)}
-        defaultValue={
-          isDisabled
-            ? undefined
-            : value
-        }
+        help={help}
         isAction={isSi}
         isDisabled={isDisabled}
         isError={!isValid}
@@ -104,28 +100,20 @@ class InputNumber extends React.PureComponent<Props, State> {
         onChange={this.onChange}
         onKeyDown={this.onKeyDown}
         onKeyUp={this.onKeyUp}
+        onPaste={this.onPaste}
         placeholder={t('Positive number')}
         style={style}
-        value={
-          isDisabled
-            ? value
-            : undefined
-        }
+        value={value}
         type='text'
       >
-        {this.renderSiDropdown()}
+        {isSi && this.renderSiDropdown()}
       </Input>
     );
   }
 
   private renderSiDropdown () {
-    const { isSi } = this.props;
     const { siOptions, siUnit } = this.state;
 
-    if (!isSi) {
-      return undefined;
-    }
-
     return (
       <Dropdown
         isPrimary={false}
@@ -145,51 +133,33 @@ class InputNumber extends React.PureComponent<Props, State> {
     return value.bitLength() <= (bitLength || DEFAULT_BITLENGTH);
   }
 
-  private isValidKey = (event: React.KeyboardEvent<Element>, isPreKeyDown: boolean): boolean => {
-    const { value: previousValue } = event.target as HTMLInputElement;
-    // prevents entry of zero if initial digit is zero
-    const isDuplicateZero = previousValue[0] === '0' && event.key === KEYS.ZERO;
-
-    if (isDuplicateZero) {
-      return false;
-    }
-
-    // allow cut/copy/paste combinations but not non-numeric letters (i.e. a, c, x, v) individually
-    if (
-      (isSelectAll(event.key, isPreKeyDown)) ||
-      (isCut(event.key, isPreKeyDown)) ||
-      (isCopy(event.key, isPreKeyDown)) ||
-      (isPaste(event.key, isPreKeyDown))
-    ) {
-      return true;
-    }
-
-    if (isNaN(Number(event.key)) && !KEYS_ALLOWED.includes(event.key)) {
-      return false;
-    }
-
-    return true;
-  }
-
   private isValidNumber (input: BN, bitLength: number = DEFAULT_BITLENGTH): boolean {
     const maxBN = this.maxValue(bitLength);
-
-    if (!input.lt(maxBN) || !this.isValidBitLength(input, bitLength)) {
+    if (input.lt(new BN(0)) || !input.lt(maxBN) || !this.isValidBitLength(input, bitLength)) {
       return false;
     }
 
     return true;
   }
 
+  private regex = (): RegExp => {
+    const { isDecimal, isSi } = this.props;
+    return new RegExp(
+      (isSi || isDecimal) ?
+        `^(0|[1-9]\\d*)(\\${KEYS.DECIMAL}\\d*)?$` :
+        `^(0|[1-9]\\d*)$`
+    );
+  }
+
   private onChange = (value: string): void => {
     const { bitLength, onChange } = this.props;
     const { siUnit } = this.state;
 
     try {
-      const valueBN = this.applySi(siUnit, new BN(value || 0));
+      const valueBN = this.inputValueToBn(value, siUnit);
       const isValid = this.isValidNumber(valueBN, bitLength);
 
-      this.setState({ isValid, valueBN });
+      this.setState({ isValid, value, valueBN });
 
       onChange && onChange(
         isValid
@@ -206,79 +176,110 @@ class InputNumber extends React.PureComponent<Props, State> {
 
     if (KEYS_PRE.includes(event.key)) {
       this.setState({ isPreKeyDown: true });
+      return;
     }
 
-    // restrict input of certain keys
-    const isValid = this.isValidKey(event, isPreKeyDown);
-
-    if (!isValid) {
-      event.preventDefault();
+    if (event.key.length === 1 && !isPreKeyDown) {
+      const { selectionStart: i, selectionEnd: j, value } = event.target as HTMLInputElement;
+      const newValue = `${
+        value.substring(0, i!)
+      }${
+        event.key
+      }${
+        value.substring(j!)
+      }`;
+
+      if (!this.regex().test(newValue)) {
+        event.preventDefault();
+      }
     }
   }
 
   private onKeyUp = (event: React.KeyboardEvent<Element>): void => {
-    const { value: newValue } = event.target as HTMLInputElement;
-    const isNewValueZero = new BN(newValue).isZero();
-
     if (KEYS_PRE.includes(event.key)) {
       this.setState({ isPreKeyDown: false });
     }
-
-    /* if new value equates to '0' in BN when but it's length is >=1 (i.e. '012', '00', etc)
-     * then replace the input value with just '0'.
-     * otherwise remove the preceding zeros from the new value (i.e. '0123' -> '123')
-     * note: edge case glitch occurs if existing value is '0' and you 'hold down' and keep
-     * pasting a value of '00' after it, then sometimes when you let go the
-     * remaining value shown as '000' or '00000' in the UI, but it's still ok because
-     * the actual BN if the user submitted would still be '0', and if they then press any key
-     * the UI input value resets to '0'
-     */
-    if (isNewValueZero && newValue.length >= 1) {
-      (event.target as HTMLInputElement).value = '0';
-    } else {
-      (event.target as HTMLInputElement).value = newValue.replace(/^0+/, '');
-    }
   }
 
-  private applySi (siUnit: string, value: BN): BN {
-    const { isSi } = this.props;
+  private onPaste = (event: React.ClipboardEvent<Element>): void => {
+    const { value: newValue } = event.target as HTMLInputElement;
 
-    if (!isSi) {
-      return value;
+    if (!this.regex().test(newValue)) {
+      event.preventDefault();
+      return;
     }
-
-    const si = formatBalance.findSi(siUnit);
-    const power = new BN(formatBalance.getDefaults().decimals + si.power);
-
-    return value.mul(new BN(10).pow(power));
-  }
-
-  private applyNewSi (oldSi: string, newSi: string, value: BN): BN {
-    const si = formatBalance.findSi(oldSi);
-    const power = new BN(formatBalance.getDefaults().decimals + si.power);
-
-    return this.applySi(newSi, value.div(new BN(10).pow(power)));
   }
 
   private selectSiUnit = (siUnit: string): void => {
     this.setState((prevState: State) => {
       const { bitLength, onChange } = this.props;
-      const valueBN = this.applyNewSi(prevState.siUnit, siUnit, prevState.valueBN);
-      const isValid = this.isValidNumber(valueBN, bitLength);
+      const isValid = this.isValidNumber(prevState.valueBN, bitLength);
+      const value = this.bnToInputValue(prevState.valueBN, siUnit);
 
       onChange && onChange(
         isValid
-          ? valueBN
+          ? prevState.valueBN
           : undefined
       );
 
       return {
         isValid,
         siUnit,
-        valueBN
+        value
       };
     });
   }
+
+  private inputValueToBn = (value: string, siUnit: string): BN => {
+    const { isSi } = this.props;
+    const basePower = isSi ? formatBalance.getDefaults().decimals : 0;
+    const siPower = isSi ? formatBalance.findSi(siUnit).power : 0;
+
+    const isDecimalValue = value.match(/^(\d+)\.(\d+)$/);
+
+    if (isDecimalValue) {
+      if (siPower - isDecimalValue[2].length < -basePower) {
+        return new BN(-1);
+      }
+
+      const div = new BN(value.replace(/\.\d*$/, ''));
+      const mod = new BN(value.replace(/^\d+\./, ''));
+
+      return div
+        .mul(new BN(10).pow(new BN(basePower + siPower)))
+        .add(mod.mul(new BN(10).pow((new BN(basePower + siPower - mod.toString().length)))));
+    } else {
+      return new BN(value.replace(/[^\d]/g, ''))
+        .mul(new BN(10).pow(new BN(basePower + siPower)));
+    }
+  }
+
+  private bnToInputValue = (bn: BN, siUnit: string): string => {
+    const { isSi } = this.props;
+
+    const basePower = isSi ? formatBalance.getDefaults().decimals : 0;
+    const siPower = isSi ? formatBalance.findSi(siUnit).power : 0;
+
+    const base = new BN(10).pow(new BN(basePower + siPower));
+    const zero = new BN(0);
+    const div = bn.div(base);
+    const mod = bn.mod(base);
+
+    return `${
+      div.gt(zero) ? div.toString() : '0'
+    }${
+      mod.gt(zero) ?
+        (() => {
+          const padding = Math.max(
+            mod.toString().length,
+            base.toString().length - div.toString().length,
+            bn.toString().length - div.toString().length
+          );
+          return `.${mod.toString(10, padding).replace(/0*$/, '')}`;
+        })() :
+        ''
+    }`;
+  }
 }
 
 export {

+ 3 - 1
packages/ui-app/src/InputRpc/index.tsx

@@ -22,6 +22,7 @@ import sectionOptions from './options/section';
 
 type Props = I18nProps & {
   defaultValue: RpcMethod,
+  help?: React.ReactNode,
   isError?: boolean,
   label: React.ReactNode,
   onChange?: (value: RpcMethod) => void,
@@ -50,7 +51,7 @@ class InputRpc extends React.PureComponent<Props, State> {
   }
 
   render () {
-    const { className, label, style, withLabel } = this.props;
+    const { className, help, label, style, withLabel } = this.props;
     const { optionsMethod, optionsSection, value } = this.state;
 
     return (
@@ -59,6 +60,7 @@ class InputRpc extends React.PureComponent<Props, State> {
         style={style}
       >
         <Labelled
+          help={help}
           label={label}
           withLabel={withLabel}
         >

+ 3 - 1
packages/ui-app/src/InputStorage/index.tsx

@@ -23,6 +23,7 @@ import sectionOptions from './options/section';
 
 type Props = ApiProps & I18nProps & {
   defaultValue: StorageFunction,
+  help?: React.ReactNode,
   isError?: boolean,
   label: React.ReactNode,
   onChange?: (value: StorageFunction) => void,
@@ -51,7 +52,7 @@ class InputStorage extends React.PureComponent<Props, State> {
   }
 
   render () {
-    const { className, label, style, withLabel } = this.props;
+    const { className, help, label, style, withLabel } = this.props;
     const { optionsMethod, optionsSection, value } = this.state;
 
     return (
@@ -60,6 +61,7 @@ class InputStorage extends React.PureComponent<Props, State> {
         style={style}
       >
         <Labelled
+          help={help}
           label={label}
           withLabel={withLabel}
         >

+ 65 - 4
packages/ui-app/src/Labelled.tsx

@@ -5,10 +5,13 @@
 import { BareProps } from './types';
 
 import React from 'react';
+import styled from 'styled-components';
 
+import Icon from './Icon';
 import { classes } from './util';
 
 type Props = BareProps & {
+  help?: React.ReactNode,
   isHidden?: boolean,
   isSmall?: boolean,
   label?: React.ReactNode,
@@ -20,9 +23,63 @@ const defaultLabel: any = (// node?
   <div>&nbsp;</div>
 );
 
+const Wrapper = styled.div`
+  align-items: center;
+  display: flex;
+  flex: 1 1;
+  text-align: left;
+
+  &.label-small {
+    display: block;
+
+    > label {
+      margin: 0;
+      min-width: 0;
+      padding-right: 0;
+    }
+  }
+
+  > .ui--Labelled-content {
+    box-sizing: border-box;
+    flex: 1 1;
+    min-width: 0;
+  }
+
+  > label {
+    flex: 0 0 15rem;
+    min-width: 15rem;
+    padding-right: 0.5rem;
+    position: relative;
+    text-align: right;
+    z-index: 1;
+
+    .help-hover {
+      background: #4e4e4e;
+      border-radius: 0.25rem;
+      color: #eee;
+      display: none;
+      padding: 0.5rem 1rem;
+      position: absolute;
+      text-align: left;
+      top: 0.5rem;
+      left: 2.5rem;
+      right: -5rem;
+      z-index: 10;
+    }
+
+    .icon.help {
+      margin-right: 0;
+    }
+
+    &.with-help:hover .help-hover {
+      display: block;
+    }
+  }
+`;
+
 export default class Labelled extends React.PureComponent<Props> {
   render () {
-    const { className, children, isSmall, isHidden, label = defaultLabel, style, withLabel = true } = this.props;
+    const { className, children, help, isSmall, isHidden, label = defaultLabel, style, withLabel = true } = this.props;
 
     if (isHidden) {
       return null;
@@ -32,16 +89,20 @@ export default class Labelled extends React.PureComponent<Props> {
       );
     }
 
+    const labelNode = help
+      ? <label className='with-help'>{label} <Icon name='help circle' /><div className='help-hover'>{help}</div></label>
+      : <label>{label}</label>;
+
     return (
-      <div
+      <Wrapper
         className={classes('ui--Labelled', isSmall ? 'label-small' : '', className)}
         style={style}
       >
-        <label>{label}</label>
+        {labelNode}
         <div className='ui--Labelled-content'>
           {children}
         </div>
-      </div>
+      </Wrapper>
     );
   }
 }

+ 3 - 1
packages/ui-app/src/Output.tsx

@@ -12,6 +12,7 @@ import { classes } from './util';
 
 type Props = BareProps & {
   children?: React.ReactNode,
+  help?: React.ReactNode,
   isError?: boolean,
   isHidden?: boolean,
   label?: any, // node?
@@ -22,11 +23,12 @@ type Props = BareProps & {
 
 export default class Output extends React.PureComponent<Props> {
   render () {
-    const { className, children, isError = false, isHidden, label, style, value, withCopy = false, withLabel } = this.props;
+    const { className, children, help, isError = false, isHidden, label, style, value, withCopy = false, withLabel } = this.props;
 
     return (
       <Labelled
         className={className}
+        help={help}
         isHidden={isHidden}
         label={label}
         style={style}

+ 3 - 1
packages/ui-app/src/Static.tsx

@@ -11,6 +11,7 @@ import Labelled from './Labelled';
 type Props = BareProps & {
   children?: React.ReactNode,
   defaultValue?: any,
+  help?: React.ReactNode,
   isDisabled?: boolean,
   isError?: boolean,
   isHidden?: boolean,
@@ -21,11 +22,12 @@ type Props = BareProps & {
 
 export default class Static extends React.PureComponent<Props> {
   render () {
-    const { className, children, defaultValue, isHidden, label, style, value, withLabel } = this.props;
+    const { className, children, defaultValue, help, isHidden, label, style, value, withLabel } = this.props;
 
     return (
       <Labelled
         className={className}
+        help={help}
         isHidden={isHidden}
         label={label}
         style={style}

+ 2 - 2
packages/ui-app/src/styles/app.css

@@ -61,8 +61,8 @@ main > section {
 }
 
 article {
-  background: white;
-  /*border: 1px solid #f2f2f2;
+  /*background: white;
+  border: 1px solid #f2f2f2;
   border-left-width: 0.25rem;*/
   border: 0 solid #fafafa;
   border-radius: 0.25rem;

+ 1 - 31
packages/ui-app/src/styles/components.css

@@ -253,36 +253,6 @@ header .ui--Button-Group {
   }
 }
 
-.ui--Labelled {
-  align-items: center;
-  display: flex;
-  flex: 1 1;
-  text-align: left;
-
-  &.label-small {
-    display: block;
-
-    > label {
-      margin: 0;
-      min-width: 0;
-      padding-right: 0;
-    }
-  }
-
-  > .ui--Labelled-content {
-    box-sizing: border-box;
-    flex: 1 1;
-    min-width: 0;
-  }
-
-  > label {
-    flex: 0 0 15rem;
-    min-width: 15rem;
-    padding-right: 0.5rem;
-    text-align: right;
-  }
-}
-
 .ui--Static {
   overflow: hidden;
   word-break: break-all;
@@ -366,4 +336,4 @@ header .ui--Button-Group {
 
 .ui.tiny.progress {
   font-size: .5rem;
-}
+}

+ 1 - 2
packages/ui-app/src/styles/media.css

@@ -13,7 +13,6 @@
 .ui--media-large,
 .ui--media-medium,
 .ui--media-small {
-  display: none;
   height: 0;
   visibility: hidden;
   width: 0;
@@ -95,4 +94,4 @@ th.ui--media-small {
       top: -2.9rem !important;
     }
   }
-}
+}

+ 2 - 2
packages/ui-params/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@polkadot/ui-params",
-  "version": "0.30.1",
+  "version": "0.31.0-beta.10",
   "main": "index.js",
   "repository": "https://github.com/polkadot-js/apps.git",
   "author": "Jaco Greeff <jacogr@gmail.com>",
@@ -11,6 +11,6 @@
   "license": "Apache-2.0",
   "dependencies": {
     "@babel/runtime": "^7.4.3",
-    "@polkadot/ui-app": "^0.30.1"
+    "@polkadot/ui-app": "^0.31.0-beta.10"
   }
 }

+ 1 - 1
packages/ui-reactive/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@polkadot/ui-reactive",
-  "version": "0.30.1",
+  "version": "0.31.0-beta.10",
   "description": "A collection of RxJs React components the Polkadot JS API",
   "main": "index.js",
   "keywords": [

+ 2 - 2
packages/ui-signer/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@polkadot/ui-signer",
-  "version": "0.30.1",
+  "version": "0.31.0-beta.10",
   "main": "index.js",
   "repository": "https://github.com/polkadot-js/apps.git",
   "author": "Jaco Greeff <jacogr@gmail.com>",
@@ -11,6 +11,6 @@
   "license": "Apache-2.0",
   "dependencies": {
     "@babel/runtime": "^7.4.3",
-    "@polkadot/ui-app": "^0.30.1"
+    "@polkadot/ui-app": "^0.31.0-beta.10"
   }
 }