Browse Source

Redo staking process and overview (#1268)

* remove filtering on actions tab

* remove unused balances on actions

* remove unused balances on actions

* only show stash

* show stashed including validators and nominators

* some buttons

* new stake button

* bond edit init

* BondEdit button

* combined session and validating button

* remove comments

* remove css hack

* phrasing

* validator prefs

* can edit validator prefs

* fix validatorPrefs undifined and nits

* validate unstakeThreshold (<11)

* with setting button noautoclose

* fix nominee styling

* change session and process feeling

* lint

* change nominees without defaultValue

* display and change reward destination

* change controller

* add balances on bond and unbond

* available balance, setting button right, nameOrAddress

* back to the old flow with set session key, cleanup logs

* balance off on chrome

* fix comments + a design hickup on chrome

* fix conflicts

* nits

* Various small UI fixes

* Types for getAddress defaults

* use name

* type props for Row

* lint
Thibaut Sardan 5 years ago
parent
commit
d868adae88
34 changed files with 1641 additions and 887 deletions
  1. 1 1
      packages/app-accounts/src/Account.tsx
  2. 0 1
      packages/app-accounts/src/modals/Transfer.tsx
  3. 1 1
      packages/app-address-book/src/Address.tsx
  4. 0 56
      packages/app-staking/src/Account/UnnominateButton.tsx
  5. 0 467
      packages/app-staking/src/Account/index.tsx
  6. 0 93
      packages/app-staking/src/Accounts.tsx
  7. 42 29
      packages/app-staking/src/Actions/Account/BondExtra.tsx
  8. 13 7
      packages/app-staking/src/Actions/Account/InputValidationController.tsx
  9. 1 1
      packages/app-staking/src/Actions/Account/InputValidationSession.tsx
  10. 64 0
      packages/app-staking/src/Actions/Account/InputValidationUnstakeThreshold.tsx
  11. 12 11
      packages/app-staking/src/Actions/Account/Nominate.tsx
  12. 139 0
      packages/app-staking/src/Actions/Account/SetControllerAccount.tsx
  13. 107 0
      packages/app-staking/src/Actions/Account/SetRewardDestination.tsx
  14. 32 31
      packages/app-staking/src/Actions/Account/SetSessionAccount.tsx
  15. 25 5
      packages/app-staking/src/Actions/Account/Unbond.tsx
  16. 51 25
      packages/app-staking/src/Actions/Account/Validate.tsx
  17. 655 0
      packages/app-staking/src/Actions/Account/index.tsx
  18. 104 0
      packages/app-staking/src/Actions/Accounts.tsx
  19. 35 96
      packages/app-staking/src/Actions/NewStake.tsx
  20. 9 0
      packages/app-staking/src/Actions/constants.tsx
  21. 1 1
      packages/app-staking/src/Overview/CurrentList.tsx
  22. 4 0
      packages/app-staking/src/Overview/index.tsx
  23. 1 1
      packages/app-staking/src/index.tsx
  24. 1 1
      packages/app-staking/src/types.ts
  25. 84 25
      packages/ui-app/src/AddressInfo.tsx
  26. 20 10
      packages/ui-app/src/AddressRow.tsx
  27. 18 2
      packages/ui-app/src/CodeRow.tsx
  28. 14 9
      packages/ui-app/src/InputAddress.tsx
  29. 161 0
      packages/ui-app/src/InputBalanceBonded.tsx
  30. 31 7
      packages/ui-app/src/Row.tsx
  31. 6 0
      packages/ui-app/src/constants.ts
  32. 1 0
      packages/ui-app/src/index.tsx
  33. 1 5
      packages/ui-app/src/styles/components.css
  34. 7 2
      packages/ui-signer/src/index.css

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

@@ -60,9 +60,9 @@ class Account extends React.PureComponent<Props> {
       >
         {this.renderModals()}
         <AddressInfo
+          address={address}
           withBalance
           withExtended
-          value={address}
         />
       </AddressCard>
     );

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

@@ -84,7 +84,6 @@ class Transfer extends React.PureComponent<Props> {
   componentDidUpdate (prevProps: Props, prevState: State) {
     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) ||

+ 1 - 1
packages/app-address-book/src/Address.tsx

@@ -57,9 +57,9 @@ class Address extends React.PureComponent<Props, State> {
       >
         {this.renderModals()}
         <AddressInfo
+          address={address}
           withBalance={{ available: true, free: true, total: true }}
           withExtended={{ nonce: true }}
-          value={address}
         />
       </AddressCard>
     );

+ 0 - 56
packages/app-staking/src/Account/UnnominateButton.tsx

@@ -1,56 +0,0 @@
-// 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/ui-app/types';
-
-import React from 'react';
-import { Button } from '@polkadot/ui-app';
-import { withCalls, withMulti } from '@polkadot/ui-api';
-import { AccountId, Vector } from '@polkadot/types';
-
-import translate from '../translate';
-
-type Props = I18nProps & {
-  accountId: string,
-  nominating: AccountId,
-  staking_nominatorsFor: Vector<AccountId>,
-  onClick: (index: number) => void
-};
-
-class UnnominateButton extends React.Component<Props> {
-  render () {
-    const { nominating, staking_nominatorsFor, style, t } = this.props;
-
-    if (!nominating || !staking_nominatorsFor) {
-      return null;
-    }
-
-    return (
-      <Button
-        className='staking--Account-buttons'
-        style={style}
-        isNegative
-        onClick={this.onClick}
-        label={t('Unnominate')}
-      />
-    );
-  }
-
-  onClick = () => {
-    const { accountId, staking_nominatorsFor, onClick } = this.props;
-
-    onClick(
-      staking_nominatorsFor
-        .map((accountId) => accountId.toString())
-        .indexOf(accountId));
-  }
-}
-
-export default withMulti(
-  UnnominateButton,
-  translate,
-  withCalls<Props>(
-    ['query.staking.nominatorsFor', { paramName: 'nominating' }]
-  )
-);

+ 0 - 467
packages/app-staking/src/Account/index.tsx

@@ -1,467 +0,0 @@
-// 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 { AccountFilter, RecentlyOfflineMap } from '../types';
-import { AccountId, Exposure, StakingLedger, ValidatorPrefs } from '@polkadot/types';
-import { ApiProps } from '@polkadot/ui-api/types';
-import { DerivedBalances, DerivedBalancesMap, DerivedStaking } from '@polkadot/api-derive/types';
-import { I18nProps } from '@polkadot/ui-app/types';
-import { KeyringSectionOption } from '@polkadot/ui-keyring/options/types';
-
-import React from 'react';
-import { AddressCard, AddressInfo, AddressMini, Button, RecentlyOffline, TxButton } from '@polkadot/ui-app';
-import { withCalls } from '@polkadot/ui-api';
-
-import Bond from './Bond';
-import BondExtra from './BondExtra';
-import Nominating from './Nominating';
-import SessionKey from './SessionKey';
-import translate from '../translate';
-import Unbond from './Unbond';
-import Validating from './Validating';
-
-type Props = ApiProps & I18nProps & {
-  accountId: string,
-  balances: DerivedBalancesMap,
-  filter: AccountFilter,
-  isValidator: boolean,
-  recentlyOffline: RecentlyOfflineMap,
-  balances_all?: DerivedBalances,
-  staking_info?: DerivedStaking,
-  stashOptions: Array<KeyringSectionOption>
-};
-
-type State = {
-  controllerId: string | null,
-  isActiveController: boolean,
-  isActiveSession: boolean,
-  isActiveStash: boolean,
-  isBondOpen: boolean,
-  isBondExtraOpen: boolean,
-  isNominateOpen: boolean,
-  isSessionKeyOpen: boolean,
-  isValidatingOpen: boolean,
-  isUnbondOpen: boolean,
-  nominators?: Array<AccountId>,
-  sessionId: string | null,
-  stakers?: Exposure,
-  stakingLedger?: StakingLedger,
-  stashId: string | null,
-  validatorPrefs?: ValidatorPrefs
-};
-
-function toIdString (id?: AccountId | null): string | null {
-  return id
-    ? id.toString()
-    : null;
-}
-
-class Account extends React.PureComponent<Props, State> {
-  state: State = {
-    isActiveController: false,
-    isActiveSession: false,
-    isActiveStash: false,
-    controllerId: null,
-    isBondOpen: false,
-    isBondExtraOpen: false,
-    isSessionKeyOpen: false,
-    isNominateOpen: false,
-    isValidatingOpen: false,
-    isUnbondOpen: false,
-    sessionId: null,
-    stashId: null
-  };
-
-  static getDerivedStateFromProps ({ staking_info }: Props): State | null {
-    if (!staking_info) {
-      return null;
-    }
-
-    const { accountId, controllerId, nextSessionId, nominators, stakers, stakingLedger, stashId, validatorPrefs } = staking_info;
-
-    return {
-      controllerId: toIdString(controllerId),
-      isActiveController: accountId.eq(controllerId),
-      isActiveSession: accountId.eq(nextSessionId),
-      isActiveStash: accountId.eq(stashId),
-      nominators,
-      sessionId: toIdString(nextSessionId),
-      stakers,
-      stakingLedger,
-      stashId: toIdString(stashId),
-      validatorPrefs
-    } as State;
-  }
-
-  render () {
-    const { accountId, filter } = this.props;
-    const { controllerId, isActiveController, isActiveStash, stashId } = this.state;
-
-    if ((filter === 'controller' && isActiveController) || (filter === 'stash' && isActiveStash) || (filter === 'unbonded' && (controllerId || stashId))) {
-      return null;
-    }
-
-    // Each component is rendered and gets a `is[Component]Openwill` passed in a `isOpen` props.
-    // These components will be loaded and return null at the first load (because is[Component]Open === false).
-    // This is deliberate in order to display the Component modals in a performant matter later on
-    // because their state will already be loaded.
-    return (
-      <AddressCard
-        buttons={this.renderButtons()}
-        type='account'
-        value={accountId}
-      >
-        {this.renderBond()}
-        {this.renderBondExtra()}
-        {this.renderNominating()}
-        {this.renderSessionKey()}
-        {this.renderUnbond()}
-        {this.renderValidating()}
-        <AddressInfo
-          withBalance
-          value={accountId}
-        >
-          <div className='staking--Account-links'>
-            {this.renderControllerId()}
-            {this.renderStashId()}
-            {this.renderSessionId()}
-            {this.renderNominee()}
-          </div>
-        </AddressInfo>
-      </AddressCard>
-    );
-  }
-
-  private renderBond () {
-    const { accountId } = this.props;
-    const { controllerId, isBondOpen } = this.state;
-
-    return (
-      <Bond
-        accountId={accountId}
-        controllerId={controllerId}
-        isOpen={isBondOpen}
-        onClose={this.toggleBond}
-      />
-    );
-  }
-
-  private renderBondExtra () {
-    const { accountId } = this.props;
-    const { controllerId, isBondExtraOpen } = this.state;
-
-    return (
-      <BondExtra
-        accountId={accountId}
-        controllerId={controllerId}
-        isOpen={isBondExtraOpen}
-        onClose={this.toggleBondExtra}
-      />
-    );
-  }
-
-  private renderUnbond () {
-    const { controllerId, isUnbondOpen } = this.state;
-
-    return (
-      <Unbond
-        controllerId={controllerId}
-        isOpen={isUnbondOpen}
-        onClose={this.toggleUnbond}
-      />
-    );
-  }
-
-  private renderValidating () {
-    const { accountId } = this.props;
-    const { isValidatingOpen, stashId, validatorPrefs } = this.state;
-
-    if (!validatorPrefs || !isValidatingOpen || !stashId) {
-      return null;
-    }
-
-    return (
-      <Validating
-        accountId={accountId}
-        isOpen
-        onClose={this.toggleValidating}
-        stashId={stashId}
-        validatorPrefs={validatorPrefs}
-      />
-    );
-  }
-
-  private renderSessionKey () {
-    const { accountId } = this.props;
-    const { isSessionKeyOpen, stashId } = this.state;
-
-    if (!stashId) {
-      return null;
-    }
-
-    return (
-      <SessionKey
-        accountId={accountId}
-        isOpen={isSessionKeyOpen}
-        onClose={this.toggleSessionKey}
-        stashId={stashId}
-      />
-    );
-  }
-
-  private renderNominee () {
-    const { t } = this.props;
-    const { nominators } = this.state;
-
-    if (!nominators || !nominators.length) {
-      return null;
-    }
-
-    return (
-      <div className='staking--Account-detail'>
-        <label className='staking--label'>{t('nominating')}</label>
-        {nominators.map((nomineeId, index) => (
-          <AddressMini
-            key={index}
-            iconInfo={this.renderOffline(nomineeId)}
-            value={nomineeId}
-            withBalance={false}
-            withBonded
-          />
-        ))}
-      </div>
-    );
-  }
-
-  private renderOffline (address: AccountId | string) {
-    const { recentlyOffline } = this.props;
-
-    return (
-      <RecentlyOffline
-        accountId={address}
-        offline={recentlyOffline[address.toString()]}
-        tooltip
-      />
-    );
-  }
-
-  private renderControllerId () {
-    const { t } = this.props;
-    const { controllerId, isActiveController } = this.state;
-
-    if (!controllerId || isActiveController) {
-      return null;
-    }
-
-    return (
-      <div className='staking--Account-detail'>
-        <label className='staking--label'>{t('controller')}</label>
-        <AddressMini
-          iconInfo={this.renderOffline(controllerId)}
-          value={controllerId}
-        />
-      </div>
-    );
-  }
-
-  private renderSessionId () {
-    const { t } = this.props;
-    const { isActiveSession, sessionId } = this.state;
-
-    if (!sessionId || isActiveSession) {
-      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 { isActiveStash, stashId } = this.state;
-
-    if (!stashId || isActiveStash) {
-      return null;
-    }
-
-    return (
-      <div className='staking--Account-detail'>
-        <label className='staking--label'>{t('stash')}</label>
-        <AddressMini
-          iconInfo={this.renderOffline(stashId)}
-          value={stashId}
-          withBalance={false}
-          withBonded
-        />
-      </div>
-    );
-  }
-
-  private renderNominating () {
-    const { accountId, stashOptions } = this.props;
-    const { isNominateOpen, stashId } = this.state;
-
-    if (!stashId) {
-      return null;
-    }
-
-    return (
-      <Nominating
-        accountId={accountId}
-        isOpen={isNominateOpen}
-        onClose={this.toggleNominate}
-        stashId={stashId}
-        stashOptions={stashOptions}
-      />
-    );
-  }
-
-  private renderButtons () {
-    const { accountId, balances_all, t } = this.props;
-    const { isActiveStash, isActiveController, nominators, sessionId, stakingLedger, validatorPrefs } = this.state;
-    const buttons = [];
-
-    if (isActiveStash) {
-      // only show a "Bond Additional" button if this stash account actually doesn't bond everything already
-      // staking_ledger.total gives the total amount that can be slashed (any active amount + what is being unlocked)
-      if (balances_all && stakingLedger && stakingLedger.total && (balances_all.freeBalance.gt(stakingLedger.total))) {
-        buttons.push(
-          <Button
-            isPrimary
-            key='bond'
-            onClick={this.toggleBondExtra}
-            label={t('Bond Additional')}
-          />
-        );
-      }
-
-      // don't show the `unbond` button if there's nothing to unbond
-      // staking_ledger.active gives the amount that can be unbonded (total - what's being unlocked).
-      if (stakingLedger && stakingLedger.active && stakingLedger.active.gtn(0)) {
-        buttons.length && buttons.push(<Button.Or key='bondAdditional.or' />);
-        buttons.push(
-          <Button
-            isNegative
-            key='unbond'
-            onClick={this.toggleUnbond}
-            label={t('Unbond')}
-          />
-        );
-      }
-    } else if (isActiveController) {
-      const isNominating = !!nominators && nominators.length;
-      const isValidating = !!validatorPrefs && !validatorPrefs.isEmpty;
-
-      // if we are validating/nominating show stop
-      if (isValidating || isNominating) {
-        buttons.push(
-          <TxButton
-            accountId={accountId}
-            isNegative
-            label={
-              isNominating
-                ? t('Stop Nominating')
-                : t('Stop Validating')
-            }
-            key='stop'
-            tx='staking.chill'
-          />
-        );
-      } else {
-        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
-            isPrimary
-            key='nominate'
-            onClick={this.toggleNominate}
-            label={t('Nominate')}
-          />
-        );
-      }
-    } else {
-      // we have nothing here, show the bond to get started
-      buttons.push(
-        <Button
-          isPrimary
-          key='bond'
-          onClick={this.toggleBond}
-          label={t('Bond Funds')}
-        />
-      );
-    }
-
-    return (
-      <Button.Group>
-        {buttons}
-      </Button.Group>
-    );
-  }
-
-  private toggleBond = () => {
-    this.setState(({ isBondOpen }) => ({
-      isBondOpen: !isBondOpen
-    }));
-  }
-
-  private toggleBondExtra = () => {
-    this.setState(({ isBondExtraOpen }) => ({
-      isBondExtraOpen: !isBondExtraOpen
-    }));
-  }
-
-  private toggleNominate = () => {
-    this.setState(({ isNominateOpen }) => ({
-      isNominateOpen: !isNominateOpen
-    }));
-  }
-
-  private toggleSessionKey = () => {
-    this.setState(({ isSessionKeyOpen }) => ({
-      isSessionKeyOpen: !isSessionKeyOpen
-    }));
-  }
-
-  private toggleUnbond = () => {
-    this.setState(({ isUnbondOpen }) => ({
-      isUnbondOpen: !isUnbondOpen
-    }));
-  }
-
-  private toggleValidating = () => {
-    this.setState(({ isValidatingOpen }) => ({
-      isValidatingOpen: !isValidatingOpen
-    }));
-  }
-}
-
-export default translate(
-  withCalls<Props>(
-    ['derive.staking.info', { paramName: 'accountId' }],
-    ['derive.balances.all', { paramName: 'accountId' }]
-  )(Account)
-);

+ 0 - 93
packages/app-staking/src/Accounts.tsx

@@ -1,93 +0,0 @@
-// 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/ui-app/types';
-import { AccountFilter, ComponentProps } from './types';
-
-import React from 'react';
-import { CardGrid, Dropdown, FilterOverlay } from '@polkadot/ui-app';
-import { getAddressName } from '@polkadot/ui-app/util';
-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;
-
-type State = {
-  filter: AccountFilter,
-  filterOptions: Array<{ text: React.ReactNode, value: AccountFilter }>
-};
-
-class Accounts extends React.PureComponent<Props, State> {
-  state: State;
-
-  constructor (props: Props) {
-    super(props);
-
-    const { t } = props;
-
-    this.state = {
-      filter: 'all',
-      filterOptions: [
-        { text: t('Show all accounts'), value: 'all' },
-        { text: t('Show all unbonded'), value: 'unbonded' },
-        { text: t('Show only stashes'), value: 'stash' },
-        { text: t('Show only controllers'), value: 'controller' }
-      ]
-    };
-  }
-
-  render () {
-    const { balances, recentlyOffline, t, validators } = this.props;
-    const { filter, filterOptions } = this.state;
-    const accounts = keyring.getAccounts();
-    const stashOptions = this.getStashOptions();
-
-    return (
-      <CardGrid>
-        <FilterOverlay>
-          <Dropdown
-            help={t('Select which types of accounts to display, either all, only the stash accounts or the controller accounts.')}
-            label={t('filter')}
-            onChange={this.onChangeFilter}
-            options={filterOptions}
-            value={filter}
-          />
-        </FilterOverlay>
-        {accounts.map((account) => {
-          const address = account.address();
-
-          return (
-            <Account
-              accountId={address}
-              balances={balances}
-              filter={filter}
-              isValidator={validators.includes(address)}
-              key={address}
-              recentlyOffline={recentlyOffline}
-              stashOptions={stashOptions}
-            />
-          );
-        })}
-      </CardGrid>
-    );
-  }
-
-  private getStashOptions (): Array<KeyringSectionOption> {
-    const { stashes } = this.props;
-
-    return stashes.map((stashId) =>
-      createOption(stashId, getAddressName(stashId, 'account'))
-    );
-  }
-
-  private onChangeFilter = (filter: AccountFilter): void => {
-    this.setState({ filter });
-  }
-}
-
-export default translate(Accounts);

+ 42 - 29
packages/app-staking/src/Account/BondExtra.tsx → packages/app-staking/src/Actions/Account/BondExtra.tsx

@@ -4,35 +4,44 @@
 
 import { I18nProps } from '@polkadot/ui-app/types';
 import { ApiProps } from '@polkadot/ui-api/types';
-import { CalculateBalanceProps } from '../types';
+import { CalculateBalanceProps } from '../../types';
 
 import BN from 'bn.js';
 import React from 'react';
-import { Button, InputAddress, InputBalance, Modal, TxButton, TxComponent } from '@polkadot/ui-app';
-import { Option, StakingLedger } from '@polkadot/types';
+import styled from 'styled-components';
+import { Button, InputAddress, InputBalance, Modal, TxButton, TxComponent, AddressInfo } from '@polkadot/ui-app';
+import { calcSignatureLength } from '@polkadot/ui-signer/Checks';
 import { SubmittableExtrinsic } from '@polkadot/api/promise/types';
 import { withCalls, withApi, withMulti } from '@polkadot/ui-api';
-import { calcSignatureLength } from '@polkadot/ui-signer/Checks';
 import { ZERO_BALANCE, ZERO_FEES } from '@polkadot/ui-signer/Checks/constants';
 
-import translate from '../translate';
+import translate from '../../translate';
 
 type Props = I18nProps & ApiProps & CalculateBalanceProps & {
-  accountId: string,
   controllerId: string,
   isOpen: boolean,
   onClose: () => void,
-  staking_ledger?: Option<StakingLedger>
+  stashId: string
 };
 
 type State = {
-  maxAdditional?: BN,
   extrinsic: SubmittableExtrinsic | null,
+  maxAdditional?: BN,
   maxBalance?: BN
 };
 
 const ZERO = new BN(0);
 
+const BalanceWrapper = styled.div`
+  & > div {
+    justify-content: flex-end;
+
+    & .column {
+      flex: 0;
+    }
+  }
+`;
+
 class BondExtra extends TxComponent<Props, State> {
   state: State = {
     extrinsic: null
@@ -52,7 +61,7 @@ class BondExtra extends TxComponent<Props, State> {
   }
 
   render () {
-    const { accountId, balances_all = ZERO_BALANCE, isOpen, onClose, t } = this.props;
+    const { balances_all = ZERO_BALANCE, isOpen, onClose, stashId, t } = this.props;
     const { extrinsic, maxAdditional, maxBalance = balances_all.availableBalance } = this.state;
     const canSubmit = !!maxAdditional && maxAdditional.gtn(0) && maxAdditional.lte(maxBalance);
 
@@ -77,10 +86,10 @@ class BondExtra extends TxComponent<Props, State> {
             />
             <Button.Or />
             <TxButton
-              accountId={accountId}
+              accountId={stashId}
               isDisabled={!canSubmit}
               isPrimary
-              label={t('Bond')}
+              label={t('Bond more')}
               onClick={onClose}
               extrinsic={extrinsic}
               ref={this.button}
@@ -92,26 +101,35 @@ class BondExtra extends TxComponent<Props, State> {
   }
 
   private renderContent () {
-    const { accountId, t } = this.props;
+    const { stashId, t } = this.props;
     const { maxBalance } = this.state;
 
     return (
       <>
         <Modal.Header>
-          {t('Bond Extra')}
+          {t('Bond more funds')}
         </Modal.Header>
         <Modal.Content className='ui--signer-Signer-Content'>
           <InputAddress
             className='medium'
-            defaultValue={accountId}
+            defaultValue={stashId}
             isDisabled
             label={t('stash account')}
           />
+          <BalanceWrapper>
+            <AddressInfo
+              address={stashId}
+              withBalance={{
+                available: true,
+                bonded: true
+              }}
+            />
+          </BalanceWrapper>
           <InputBalance
             autoFocus
             className='medium'
-            help={t('The maximum amount to increase the bonded value, this is adjusted using the available free funds on the account.')}
-            label={t('max additional value')}
+            help={t('Amount to add to the currently bonded funds. This is adjusted using the available funds on the account.')}
+            label={t('additionnal bonded funds')}
             maxValue={maxBalance}
             onChange={this.onChangeValue}
             onEnter={this.sendTx}
@@ -131,34 +149,29 @@ class BondExtra extends TxComponent<Props, State> {
         : null;
 
       return {
-        maxAdditional,
         extrinsic,
+        maxAdditional,
         maxBalance
       };
     });
   }
 
   private setMaxBalance = () => {
-    const { api, system_accountNonce = ZERO, balances_fees = ZERO_FEES, balances_all = ZERO_BALANCE, staking_ledger } = this.props;
+    const { api, system_accountNonce = ZERO, balances_fees = ZERO_FEES, balances_all = ZERO_BALANCE } = this.props;
     const { maxAdditional } = this.state;
 
     const { transactionBaseFee, transactionByteFee } = balances_fees;
-    const { freeBalance } = balances_all;
+    const { availableBalance } = balances_all;
 
     let prevMax = new BN(0);
     let maxBalance = new BN(1);
     let extrinsic;
 
-    let bonded = new BN(0);
-    if (staking_ledger && !staking_ledger.isNone) {
-      bonded = staking_ledger.unwrap().active;
-    }
-
     while (!prevMax.eq(maxBalance)) {
       prevMax = maxBalance;
 
       extrinsic = (maxAdditional && maxAdditional.gte(ZERO))
-        ? api.tx.staking.bondExtra(maxAdditional.sub(bonded))
+        ? api.tx.staking.bondExtra(maxAdditional)
         : null;
 
       const txLength = calcSignatureLength(extrinsic, system_accountNonce);
@@ -166,11 +179,12 @@ class BondExtra extends TxComponent<Props, State> {
       const fees = transactionBaseFee
         .add(transactionByteFee.muln(txLength));
 
-      maxBalance = new BN(freeBalance).sub(fees).sub(bonded);
+      maxBalance = availableBalance.sub(fees);
     }
 
     this.nextState({
       extrinsic,
+      maxAdditional,
       maxBalance
     });
   }
@@ -186,8 +200,7 @@ export default withMulti(
   withApi,
   withCalls<Props>(
     'derive.balances.fees',
-    ['derive.balances.all', { paramName: 'accountId' }],
-    ['query.system.accountNonce', { paramName: 'accountId' }],
-    ['query.staking.ledger', { paramName: 'controllerId' }]
+    ['derive.balances.all', { paramName: 'stashId' }],
+    ['query.system.accountNonce', { paramName: 'stashId' }]
   )
 );

+ 13 - 7
packages/app-staking/src/Account/ValidateController.tsx → packages/app-staking/src/Actions/Account/InputValidationController.tsx

@@ -9,12 +9,13 @@ import { Icon } from '@polkadot/ui-app';
 import { AccountId, Option, StakingLedger } from '@polkadot/types';
 import { withCalls } from '@polkadot/ui-api';
 
-import translate from '../translate';
+import translate from '../../translate';
 
 type Props = I18nProps & {
-  accountId: string,
+  accountId: string | null,
   bondedId?: string | null,
-  controllerId: string,
+  controllerId: string | null,
+  defaultController?: string,
   onError: (error: string | null) => void,
   stashId?: string | null
 };
@@ -28,10 +29,14 @@ class ValidateController extends React.PureComponent<Props, State> {
     error: null
   };
 
-  static getDerivedStateFromProps ({ accountId, bondedId, controllerId, onError, stashId, t }: Props, prevState: State): State {
+  static getDerivedStateFromProps ({ accountId, bondedId, controllerId, defaultController, onError, stashId, t }: Props, prevState: State): State {
     const error = (() => {
-      if (controllerId === accountId) {
-        return t('A controller account which is not the same as your selected account is required');
+      if (defaultController === controllerId) {
+        // don't show an error if the selected controller is the default
+        // this applies when changing controller
+        return null;
+      } else if (controllerId === accountId) {
+        return t('Please select distinct stash and controller accounts');
       } else if (bondedId) {
         return t('A controller account should not map to another stash. This selected controller is a stash, controlled by {{bondedId}}', { replace: { bondedId } });
       } else if (stashId) {
@@ -51,9 +56,10 @@ class ValidateController extends React.PureComponent<Props, State> {
   }
 
   render () {
+    const { accountId } = this.props;
     const { error } = this.state;
 
-    if (!error) {
+    if (!error || !accountId) {
       return null;
     }
 

+ 1 - 1
packages/app-staking/src/Account/ValidateSession.tsx → packages/app-staking/src/Actions/Account/InputValidationSession.tsx

@@ -8,7 +8,7 @@ import React from 'react';
 import { Icon } from '@polkadot/ui-app';
 import keyring from '@polkadot/ui-keyring';
 
-import translate from '../translate';
+import translate from '../../translate';
 
 type Props = I18nProps & {
   controllerId: string,

+ 64 - 0
packages/app-staking/src/Actions/Account/InputValidationUnstakeThreshold.tsx

@@ -0,0 +1,64 @@
+// Copyright 2017-2019 @polkadot/ui-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/ui-app/types';
+
+import BN from 'bn.js';
+import React from 'react';
+import { Icon } from '@polkadot/ui-app';
+
+import translate from '../../translate';
+
+type Props = I18nProps & {
+  unstakeThreshold: BN | undefined,
+  onError: (error: string | null) => void
+};
+
+type State = {
+  error: string | null
+};
+
+class InputValidationUnstakeThreshold extends React.PureComponent<Props, State> {
+  state: State = {
+    error: null
+  };
+
+  static getDerivedStateFromProps ({ onError, t, unstakeThreshold }: Props, prevState: State): State | null {
+    let error = null;
+
+    if (!unstakeThreshold) {
+      return null;
+    }
+
+    if (unstakeThreshold.ltn(0)) {
+      error = t('The Threshold must be a positive number');
+    } else if (unstakeThreshold.gtn(10)) {
+      error = t('The Threshold must lower than 11');
+    }
+
+    if (error === prevState.error || !unstakeThreshold) {
+      return null;
+    }
+
+    onError(error);
+
+    return { error };
+  }
+
+  render () {
+    const { error } = this.state;
+
+    if (!error) {
+      return null;
+    }
+
+    return (
+      <article className='warning'>
+        <div><Icon name='warning sign' />{error}</div>
+      </article>
+    );
+  }
+}
+
+export default translate(InputValidationUnstakeThreshold);

+ 12 - 11
packages/app-staking/src/Account/Nominating.tsx → packages/app-staking/src/Actions/Account/Nominate.tsx

@@ -8,23 +8,24 @@ import { KeyringSectionOption } from '@polkadot/ui-keyring/options/types';
 import React from 'react';
 import { Button, InputAddress, Modal, TxButton } from '@polkadot/ui-app';
 
-import translate from '../translate';
+import translate from '../../translate';
 
 type Props = I18nProps & {
-  accountId: string,
+  controllerId: string,
   isOpen: boolean,
+  nominees?: Array<string>,
   onClose: () => void,
   stashId: string,
   stashOptions: Array<KeyringSectionOption>
 };
 
 type State = {
-  nominees: Array<string>
+  nominees: Array<string> | undefined
 };
 
-class Nominating extends React.PureComponent<Props, State> {
+class Nominate extends React.PureComponent<Props, State> {
   state: State = {
-    nominees: []
+    nominees: this.props.nominees
   };
 
   render () {
@@ -48,7 +49,7 @@ class Nominating extends React.PureComponent<Props, State> {
   }
 
   renderButtons () {
-    const { accountId, onClose, t } = this.props;
+    const { controllerId, onClose, t } = this.props;
     const { nominees } = this.state;
 
     return (
@@ -61,8 +62,8 @@ class Nominating extends React.PureComponent<Props, State> {
           />
           <Button.Or />
           <TxButton
-            accountId={accountId}
-            isDisabled={nominees.length === 0}
+            accountId={controllerId}
+            isDisabled={!nominees || nominees.length === 0}
             isPrimary
             onClick={onClose}
             params={[nominees]}
@@ -75,7 +76,7 @@ class Nominating extends React.PureComponent<Props, State> {
   }
 
   renderContent () {
-    const { accountId, stashId, stashOptions, t } = this.props;
+    const { controllerId, stashId, stashOptions, t } = this.props;
 
     return (
       <>
@@ -85,7 +86,7 @@ class Nominating extends React.PureComponent<Props, State> {
         <Modal.Content className='ui--signer-Signer-Content'>
           <InputAddress
             className='medium'
-            defaultValue={accountId}
+            defaultValue={controllerId}
             isDisabled
             label={t('controller account')}
           />
@@ -115,4 +116,4 @@ class Nominating extends React.PureComponent<Props, State> {
   }
 }
 
-export default translate(Nominating);
+export default translate(Nominate);

+ 139 - 0
packages/app-staking/src/Actions/Account/SetControllerAccount.tsx

@@ -0,0 +1,139 @@
+// Copyright 2017-2019 @polkadot/ui-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/ui-app/types';
+
+import React from 'react';
+import { Button, Icon, InputAddress, Modal, TxButton, TxComponent } from '@polkadot/ui-app';
+import { withMulti } from '@polkadot/ui-api';
+
+import translate from '../../translate';
+import InputValidationController from '../Account/InputValidationController';
+
+type Props = I18nProps & {
+  defaultControllerId: string,
+  isValidating?: boolean,
+  onClose: () => void,
+  stashId: string
+};
+
+type State = {
+  controllerError: string | null,
+  controllerId: string | null
+};
+
+class SetControllerAccount extends TxComponent<Props, State> {
+  constructor (props: Props) {
+    super(props);
+
+    this.state = {
+      controllerError: null,
+      controllerId: null
+    };
+  }
+
+  render () {
+    const { defaultControllerId, onClose, stashId, t } = this.props;
+    const { controllerError, controllerId } = this.state;
+    const canSubmit = !controllerError && !!controllerId && (defaultControllerId !== controllerId);
+
+    return (
+      <Modal
+        className='staking--SetControllerAccount'
+        dimmer='inverted'
+        open
+        size='small'
+      >
+        {this.renderContent()}
+        <Modal.Actions>
+          <Button.Group>
+            <Button
+              isNegative
+              onClick={onClose}
+              label={t('Cancel')}
+            />
+            <Button.Or />
+            <TxButton
+              accountId={stashId}
+              isDisabled={!canSubmit}
+              isPrimary
+              label={t('Set controller')}
+              onClick={onClose}
+              params={[controllerId]}
+              tx='staking.setController'
+              ref={this.button}
+            />
+          </Button.Group>
+        </Modal.Actions>
+      </Modal>
+    );
+  }
+
+  private renderContent () {
+    const { defaultControllerId, stashId, t } = this.props;
+    const { controllerId, controllerError } = this.state;
+
+    return (
+      <>
+        <Modal.Header>
+          {t('Change controller account')}
+        </Modal.Header>
+        <Modal.Content className='ui--signer-Signer-Content'>
+          {this.renderSessionAccountWarning()}
+          <InputAddress
+            className='medium'
+            isDisabled
+            label={t('stash account')}
+            value={stashId}
+          />
+          <InputAddress
+            className='medium'
+            defaultValue={defaultControllerId}
+            help={t('The controller is the account that will be used to control any nominating or validating actions. Should not match another stash or controller.')}
+            isError={!!controllerError}
+            label={t('controller account')}
+            onChange={this.onChangeController}
+            type='account'
+            value={controllerId}
+          />
+          <InputValidationController
+            accountId={stashId}
+            defaultController={defaultControllerId}
+            controllerId={controllerId}
+            onError={this.onControllerError}
+          />
+        </Modal.Content>
+      </>
+    );
+  }
+
+  private renderSessionAccountWarning () {
+    const { isValidating = false, t } = this.props;
+
+    if (!isValidating) {
+      return null;
+    }
+
+    return (
+      <article className='warning'>
+        <div className='warning'>
+          <Icon name='warning sign' />
+          {t('Warning - Changing the controller while validating will modify the associated session account. It is advised to stop validating before changing the controller account.')}
+        </div>
+      </article>
+    );
+  }
+  private onChangeController = (controllerId: string) => {
+    this.setState({ controllerId });
+  }
+
+  private onControllerError = (controllerError: string | null) => {
+    this.setState({ controllerError });
+  }
+}
+
+export default withMulti(
+  SetControllerAccount,
+  translate
+);

+ 107 - 0
packages/app-staking/src/Actions/Account/SetRewardDestination.tsx

@@ -0,0 +1,107 @@
+// Copyright 2017-2019 @polkadot/ui-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/ui-app/types';
+
+import React from 'react';
+import { Button, Dropdown, InputAddress, Modal, TxButton, TxComponent } from '@polkadot/ui-app';
+import { withMulti } from '@polkadot/ui-api';
+
+import translate from '../../translate';
+import { rewardDestinationOptions } from '../constants';
+
+type Props = I18nProps & {
+  defaultDestination?: number,
+  controllerId: string,
+  onClose: () => void
+};
+
+type State = {
+  destination: number
+};
+
+class SetRewardDestination extends TxComponent<Props, State> {
+  constructor (props: Props) {
+    super(props);
+
+    this.state = {
+      destination: 0
+    };
+  }
+
+  render () {
+    const { controllerId, onClose, t } = this.props;
+    const { destination } = this.state;
+    const canSubmit = !!controllerId;
+
+    return (
+      <Modal
+        className='staking--Bonding'
+        dimmer='inverted'
+        open
+        size='small'
+      >
+        {this.renderContent()}
+        <Modal.Actions>
+          <Button.Group>
+            <Button
+              isNegative
+              onClick={onClose}
+              label={t('Cancel')}
+            />
+            <Button.Or />
+            <TxButton
+              accountId={controllerId}
+              isDisabled={!canSubmit}
+              isPrimary
+              label={t('Set reward destination')}
+              onClick={onClose}
+              params={[destination]}
+              tx={'staking.setPayee'}
+              ref={this.button}
+            />
+          </Button.Group>
+        </Modal.Actions>
+      </Modal>
+    );
+  }
+
+  private renderContent () {
+    const { controllerId, defaultDestination, t } = this.props;
+
+    return (
+      <>
+        <Modal.Header>
+          {t('Bonding Preferences')}
+        </Modal.Header>
+        <Modal.Content className='ui--signer-Signer-Content'>
+          <InputAddress
+            className='medium'
+            isDisabled
+            defaultValue={controllerId}
+            help={t('The controller is the account that is be used to control any nominating or validating actions. I will sign this transaction.')}
+            label={t('controller account')}
+          />
+          <Dropdown
+            className='medium'
+            defaultValue={defaultDestination}
+            help={t('The destination account for any payments as either a nominator or validator')}
+            label={t('payment destination')}
+            onChange={this.onChangeDestination}
+            options={rewardDestinationOptions}
+          />
+        </Modal.Content>
+      </>
+    );
+  }
+
+  private onChangeDestination = (destination: number) => {
+    this.setState({ destination });
+  }
+}
+
+export default withMulti(
+  SetRewardDestination,
+  translate
+);

+ 32 - 31
packages/app-staking/src/Account/SessionKey.tsx → packages/app-staking/src/Actions/Account/SetSessionAccount.tsx

@@ -7,13 +7,14 @@ import { I18nProps } from '@polkadot/ui-app/types';
 import React from 'react';
 import { Button, InputAddress, Modal, TxButton } from '@polkadot/ui-app';
 
-import ValidateSession from './ValidateSession';
-import translate from '../translate';
+import ValidateSession from './InputValidationSession';
+import translate from '../../translate';
 
 type Props = I18nProps & {
-  accountId: string,
+  controllerId: string,
   isOpen: boolean,
   onClose: () => void,
+  sessionId?: string | null,
   stashId: string
 };
 
@@ -22,7 +23,7 @@ type State = {
   sessionId: string
 };
 
-class Key extends React.PureComponent<Props, State> {
+class SetSessionKey extends React.PureComponent<Props, State> {
   state: State;
 
   constructor (props: Props) {
@@ -30,12 +31,12 @@ class Key extends React.PureComponent<Props, State> {
 
     this.state = {
       sessionError: null,
-      sessionId: props.accountId
+      sessionId: props.sessionId || props.controllerId
     };
   }
 
   render () {
-    const { accountId, isOpen, onClose, t } = this.props;
+    const { controllerId, isOpen, onClose, t } = this.props;
     const { sessionError, sessionId } = this.state;
 
     if (!isOpen) {
@@ -44,61 +45,61 @@ class Key extends React.PureComponent<Props, State> {
 
     return (
       <Modal
-        className='staking--Stash'
+        className='staking--SetSessionAccount'
         dimmer='inverted'
         open
         size='small'
       >
         {this.renderContent()}
         <Modal.Actions>
-        <Button.Group>
-          <Button
-            isNegative
-            onClick={onClose}
-            label={t('Cancel')}
-          />
-          <Button.Or />
-          <TxButton
-            accountId={accountId}
-            isDisabled={!sessionId || !!sessionError}
-            isPrimary
-            label={t('Set Session Key')}
-            onClick={onClose}
-            params={[sessionId]}
-            tx='session.setKey'
-          />
-        </Button.Group>
-      </Modal.Actions>
+          <Button.Group>
+            <Button
+              isNegative
+              onClick={onClose}
+              label={t('Cancel')}
+            />
+            <Button.Or />
+            <TxButton
+              accountId={controllerId}
+              isDisabled={!sessionId || !!sessionError}
+              isPrimary
+              label={t('Set Session Key')}
+              onClick={ onClose }
+              params={[sessionId]}
+              tx='session.setKey'
+            />
+          </Button.Group>
+        </Modal.Actions>
       </Modal>
     );
   }
 
   private renderContent () {
-    const { accountId, stashId, t } = this.props;
+    const { controllerId, stashId, t } = this.props;
     const { sessionId } = this.state;
 
     return (
       <>
         <Modal.Header>
-          {t('Session Key')}
+          {t('Set Session Key')}
         </Modal.Header>
         <Modal.Content className='ui--signer-Signer-Content'>
           <InputAddress
             className='medium'
-            defaultValue={accountId}
+            defaultValue={controllerId}
             isDisabled
             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.')}
+            help={t('Changing the key only takes effect at the start of the next session. If validating, it must be an ed25519 key.')}
             label={t('session key')}
             onChange={this.onChangeSession}
             type='account'
             value={sessionId}
           />
           <ValidateSession
-            controllerId={accountId}
+            controllerId={controllerId}
             onError={this.onSessionError}
             sessionId={sessionId}
             stashId={stashId}
@@ -117,4 +118,4 @@ class Key extends React.PureComponent<Props, State> {
   }
 }
 
-export default translate(Key);
+export default translate(SetSessionKey);

+ 25 - 5
packages/app-staking/src/Account/Unbond.tsx → packages/app-staking/src/Actions/Account/Unbond.tsx

@@ -7,16 +7,18 @@ import { ApiProps } from '@polkadot/ui-api/types';
 
 import BN from 'bn.js';
 import React from 'react';
+import styled from 'styled-components';
 import { AccountId, Option, StakingLedger } from '@polkadot/types';
-import { Button, InputAddress, InputBalance, Modal, TxButton, TxComponent } from '@polkadot/ui-app';
+import { AddressInfo, Button, InputAddress, InputBalance, Modal, TxButton, TxComponent } from '@polkadot/ui-app';
 import { withCalls, withApi, withMulti } from '@polkadot/ui-api';
 
-import translate from '../translate';
+import translate from '../../translate';
 
 type Props = I18nProps & ApiProps & {
   controllerId?: AccountId | null,
   isOpen: boolean,
   onClose: () => void,
+  stashId: string,
   staking_ledger?: Option<StakingLedger>
 };
 
@@ -25,6 +27,16 @@ type State = {
   maxUnbond?: BN
 };
 
+const BalanceWrapper = styled.div`
+  & > div {
+    justify-content: flex-end;
+
+    & .column {
+      flex: 0;
+    }
+  }
+`;
+
 class Unbond extends TxComponent<Props, State> {
   state: State = {};
 
@@ -78,13 +90,13 @@ class Unbond extends TxComponent<Props, State> {
   }
 
   private renderContent () {
-    const { controllerId, t } = this.props;
+    const { controllerId, stashId, t } = this.props;
     const { maxBalance } = this.state;
 
     return (
       <>
         <Modal.Header>
-          {t('Unbond')}
+          {t('Unbond funds')}
         </Modal.Header>
         <Modal.Content className='ui--signer-Signer-Content'>
           <InputAddress
@@ -93,10 +105,18 @@ class Unbond extends TxComponent<Props, State> {
             isDisabled
             label={t('controller account')}
           />
+          <BalanceWrapper>
+            <AddressInfo
+              accountId={stashId}
+              withBalance={{
+                bonded: true
+              }}
+            />
+          </BalanceWrapper>
           <InputBalance
             autoFocus
             className='medium'
-            help={t('The maximum amount to unbond, this is adjusted using the bonded funds on the account.')}
+            help={t('The amount of funds to unbond, this is adjusted using the bonded funds on the stash account.')}
             label={t('unbond amount')}
             maxValue={maxBalance}
             onChange={this.onChangeValue}

+ 51 - 25
packages/app-staking/src/Account/Validating.tsx → packages/app-staking/src/Actions/Account/Validate.tsx

@@ -9,39 +9,51 @@ import React from 'react';
 import { ValidatorPrefs } from '@polkadot/types';
 import { Button, InputAddress, InputBalance, InputNumber, Modal, TxButton, TxComponent } from '@polkadot/ui-app';
 
-import translate from '../translate';
+import InputValidationUnstakeThreshold from './InputValidationUnstakeThreshold';
+import translate from '../../translate';
 
 type Props = I18nProps & {
-  accountId: string,
+  controllerId: string,
   isOpen: boolean,
   onClose: () => void,
   stashId: string,
-  validatorPrefs: ValidatorPrefs
+  validatorPrefs?: ValidatorPrefs
 };
 
 type State = {
   unstakeThreshold?: BN,
+  unstakeThresholdError: string | null,
   validatorPayment?: BN
 };
 
-class Staking extends TxComponent<Props, State> {
+class Validate extends TxComponent<Props, State> {
   state: State = {
     unstakeThreshold: new BN(3),
+    unstakeThresholdError: null,
     validatorPayment: new BN(0)
   };
 
-  // inject the preferences are returned via RPC once into the state (from this
+  // inject the preferences returned via RPC once into the state (from this
   // point forward it will be entirely managed by the actual inputs)
   static getDerivedStateFromProps (props: Props, state: State): State | null {
-    if (state.unstakeThreshold) {
+    if (state.unstakeThreshold && state.validatorPayment) {
       return null;
     }
 
-    const { unstakeThreshold, validatorPayment } = props.validatorPrefs;
+    if (props.validatorPrefs) {
+      const { unstakeThreshold, validatorPayment } = props.validatorPrefs;
+
+      return {
+        unstakeThreshold: unstakeThreshold.toBn(),
+        unstakeThresholdError: null,
+        validatorPayment: validatorPayment.toBn()
+      };
+    }
 
     return {
-      unstakeThreshold: unstakeThreshold.toBn(),
-      validatorPayment: validatorPayment.toBn()
+      unstakeThreshold: undefined,
+      unstakeThresholdError: null,
+      validatorPayment: undefined
     };
   }
 
@@ -66,8 +78,9 @@ class Staking extends TxComponent<Props, State> {
   }
 
   private renderButtons () {
-    const { accountId, onClose, t } = this.props;
-    const { unstakeThreshold, validatorPayment } = this.state;
+    const { controllerId, onClose, t, validatorPrefs } = this.props;
+    const { unstakeThreshold, unstakeThresholdError, validatorPayment } = this.state;
+    const isChangingPrefs = validatorPrefs && !!validatorPrefs.unstakeThreshold;
 
     return (
       <Modal.Actions>
@@ -79,9 +92,10 @@ class Staking extends TxComponent<Props, State> {
           />
           <Button.Or />
           <TxButton
-            accountId={accountId}
+            accountId={controllerId}
+            isDisabled={!!unstakeThresholdError}
             isPrimary
-            label={t('Validate')}
+            label={isChangingPrefs ? t('Set validator preferences') : t('Validate')}
             onClick={onClose}
             params={[{
               unstakeThreshold,
@@ -96,33 +110,36 @@ class Staking extends TxComponent<Props, State> {
   }
 
   private renderContent () {
-    const { accountId, stashId, t } = this.props;
-    const { unstakeThreshold, validatorPayment } = this.state;
+    const { controllerId, stashId, t, validatorPrefs } = this.props;
+    const { unstakeThreshold, unstakeThresholdError, validatorPayment } = this.state;
+    const defaultValue = validatorPrefs && validatorPrefs.unstakeThreshold && validatorPrefs.unstakeThreshold.toBn();
 
     return (
       <>
         <Modal.Header>
-          {t('Validating')}
+          {t('Set validator preferences')}
         </Modal.Header>
         <Modal.Content className='ui--signer-Signer-Content'>
           <InputAddress
             className='medium'
-            defaultValue={accountId}
+            defaultValue={stashId.toString()}
             isDisabled
-            label={t('controller account')}
+            label={t('stash account')}
           />
           <InputAddress
             className='medium'
-            defaultValue={stashId.toString()}
+            defaultValue={controllerId}
             isDisabled
-            label={t('stash account')}
+            label={t('controller account')}
           />
           <InputNumber
             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')}
+            defaultValue={defaultValue}
+            help={t('The number of time this validator can get slashed before being automatically unstaked (maximum of 10 allowed)')}
+            isError={!!unstakeThresholdError}
+            label={t('automatic unstake threshold')}
             onChange={this.onChangeThreshold}
             onEnter={this.sendTx}
             value={
@@ -131,10 +148,15 @@ class Staking extends TxComponent<Props, State> {
                 : '3'
             }
           />
+          <InputValidationUnstakeThreshold
+            onError={this.onUnstakeThresholdError}
+            unstakeThreshold={unstakeThreshold}
+          />
           <InputBalance
             className='medium'
-            help={t('Reward that validator takes up-front, the remainder is split between themselves and nominators')}
-            label={t('payment preferences')}
+            defaultValue={validatorPrefs && validatorPrefs.validatorPayment && validatorPrefs.validatorPayment.toBn()}
+            help={t('Amount taken up-front from the reward by the validator before spliting the remainder between themselves and the nominators')}
+            label={t('reward commission')}
             onChange={this.onChangePayment}
             onEnter={this.sendTx}
             value={
@@ -159,6 +181,10 @@ class Staking extends TxComponent<Props, State> {
       this.setState({ unstakeThreshold });
     }
   }
+
+  private onUnstakeThresholdError = (unstakeThresholdError: string | null) => {
+    this.setState({ unstakeThresholdError });
+  }
 }
 
-export default translate(Staking);
+export default translate(Validate);

+ 655 - 0
packages/app-staking/src/Actions/Account/index.tsx

@@ -0,0 +1,655 @@
+// 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 { RecentlyOfflineMap } from '../../types';
+import { AccountId, Exposure, StakingLedger, ValidatorPrefs } from '@polkadot/types';
+import { ApiProps } from '@polkadot/ui-api/types';
+import { DerivedBalances, DerivedStaking } from '@polkadot/api-derive/types';
+import { I18nProps } from '@polkadot/ui-app/types';
+import { KeyringSectionOption } from '@polkadot/ui-keyring/options/types';
+
+import { Popup } from 'semantic-ui-react';
+import React from 'react';
+import styled from 'styled-components';
+import { AddressInfo, AddressMini, AddressRow, Button, Card, Menu, RecentlyOffline, TxButton } from '@polkadot/ui-app';
+import { withCalls } from '@polkadot/ui-api';
+
+import BondExtra from './BondExtra';
+import Nominate from './Nominate';
+import SetControllerAccount from './SetControllerAccount';
+import SetRewardDestination from './SetRewardDestination';
+import SetSessionAccount from './SetSessionAccount';
+import translate from '../../translate';
+import Unbond from './Unbond';
+import Validate from './Validate';
+
+type Props = ApiProps & I18nProps & {
+  accountId: string,
+  recentlyOffline: RecentlyOfflineMap,
+  balances_all?: DerivedBalances,
+  staking_info?: DerivedStaking,
+  stashOptions: Array<KeyringSectionOption>
+};
+
+type State = {
+  controllerId: string | null,
+  destination: number,
+  isActiveStash: boolean,
+  isBondExtraOpen: boolean,
+  isNominateOpen: boolean,
+  isSetControllerAccountOpen: boolean,
+  isSetRewardDestinationOpen: boolean,
+  isSetSessionAccountOpen: boolean,
+  isSettingPopupOpen: boolean,
+  isStashNominating: boolean,
+  isStashValidating: boolean,
+  isUnbondOpen: boolean,
+  isValidateOpen: boolean,
+  nominees?: Array<string>,
+  sessionId: string | null,
+  stakers?: Exposure,
+  stakingLedger?: StakingLedger,
+  stashId: string | null,
+  validatorPrefs?: ValidatorPrefs
+};
+
+const Wrapper = styled.div`
+  display: flex;
+
+  .ui.button.tiny {
+    visibility: visible
+  }
+
+  .staking--Accounts {
+    flex: 2;
+  }
+
+  .staking--Account-detail.actions{
+    display: inline-block;
+    vertical-align: top;
+
+    .staking--label {
+      margin: .5rem 1.75rem -0.5rem 4.5rem;
+      text-align: left;
+      }
+    }
+  }
+
+  .staking--Actions-Infos {
+    flex: 3;
+    display: flex;
+    flex-direction: column;
+
+    .buttons {
+      margin-bottom: 1rem;
+      flex: 0;
+
+      button {
+        margin-right: .25rem;
+      }
+    }
+
+    .staking--balances {
+      div {
+        justify-content: flex-end;
+      }
+
+      .column {
+        flex:0;
+      }
+    }
+
+    .staking--Account-Nominee {
+      display: flex;
+      flex-direction: column;
+      justify-content: flex-end;
+      padding-top: 1em;
+      flex: 1;
+    }
+  }
+
+  .staking--Account-Nominee {
+    text-align: right;
+
+    .staking--label {
+      margin: 0 2.25rem -.75rem 0;
+    }
+  }
+`;
+
+function toIdString (id?: AccountId | null): string | null {
+  return id
+    ? id.toString()
+    : null;
+}
+
+class Account extends React.PureComponent<Props, State> {
+  state: State = {
+    controllerId: null,
+    destination: 0,
+    isActiveStash: false,
+    isBondExtraOpen: false,
+    isNominateOpen: false,
+    isSetControllerAccountOpen: false,
+    isSettingPopupOpen: false,
+    isSetRewardDestinationOpen: false,
+    isSetSessionAccountOpen: false,
+    isStashNominating: false,
+    isStashValidating: false,
+    isUnbondOpen: false,
+    isValidateOpen: false,
+    sessionId: null,
+    stashId: null
+  };
+
+  static getDerivedStateFromProps ({ staking_info }: Props): State | null {
+    if (!staking_info) {
+      return null;
+    }
+
+    const { accountId, controllerId, nextSessionId, nominators, rewardDestination, stakers, stakingLedger, stashId, validatorPrefs } = staking_info;
+    const isStashNominating = nominators && nominators.length !== 0;
+    const isStashValidating = !!validatorPrefs && !validatorPrefs.isEmpty && !isStashNominating;
+
+    return {
+      controllerId: toIdString(controllerId),
+      destination: rewardDestination && rewardDestination.toNumber(),
+      isActiveStash: accountId.eq(stashId),
+      isStashNominating,
+      isStashValidating,
+      nominees: nominators && nominators.map(toIdString),
+      sessionId: toIdString(nextSessionId),
+      stakers,
+      stakingLedger,
+      stashId: toIdString(stashId),
+      validatorPrefs
+    } as any as State;
+  }
+
+  render () {
+    const { isActiveStash } = this.state;
+
+    if (!isActiveStash) {
+      return null;
+    }
+
+    // Each component is rendered and gets a `is[Component]Open` passed in a `isOpen` props.
+    // These components will be loaded and return null at the first load (because is[Component]Open === false).
+    // This is deliberate in order to display the Component modals in a performant matter later on
+    // because their state will already be loaded.
+    return (
+      <Card>
+        {this.renderBondExtra()}
+        {this.renderSetValidatorPrefs()}
+        {this.renderNominate()}
+        {this.renderSetControllerAccount()}
+        {this.renderSetRewardDestination()}
+        {this.renderSetSessionAccount()}
+        {this.renderUnbond()}
+        {this.renderValidate()}
+        <Wrapper>
+          <div className='staking--Accounts'>
+            {this.renderStashAccount()}
+            {this.renderControllerAccount()}
+            {this.renderSessionAccount()}
+          </div>
+          <div className='staking--Actions-Infos'>
+            <div className='buttons'>
+              {this.renderButtons()}
+            </div>
+            <div className='staking--balances'>
+              {this.renderInfos()}
+            </div>
+            {this.renderNominee()}
+          </div>
+        </Wrapper>
+      </Card>
+    );
+  }
+
+  private renderBondExtra () {
+    const { controllerId, isBondExtraOpen, stashId } = this.state;
+
+    return (
+      <BondExtra
+        controllerId={controllerId}
+        isOpen={isBondExtraOpen}
+        onClose={this.toggleBondExtra}
+        stashId={stashId}
+      />
+    );
+  }
+
+  private renderUnbond () {
+    const { controllerId, isUnbondOpen, stashId } = this.state;
+
+    return (
+      <Unbond
+        controllerId={controllerId}
+        isOpen={isUnbondOpen}
+        onClose={this.toggleUnbond}
+        stashId={stashId}
+      />
+    );
+  }
+
+  private renderInfos () {
+    const { stashId } = this.state;
+
+    return (
+      <AddressInfo
+        address={stashId}
+        withBalance={{
+          available: false,
+          bonded: true,
+          free: false,
+          redeemable: true,
+          unlocking: true
+        }}
+        withRewardDestination
+        withValidatorPrefs
+      />
+    );
+  }
+
+  private renderNominee () {
+    const { t } = this.props;
+    const { nominees } = this.state;
+
+    if (!nominees || !nominees.length) {
+      return null;
+    }
+
+    return (
+      <div className='staking--Account-Nominee'>
+        <label className='staking--label'>{t('nominating')}</label>
+        {nominees.map((nomineeId, index) => (
+          <AddressMini
+            key={index}
+            iconInfo={this.renderOffline(nomineeId)}
+            value={nomineeId}
+            withBalance={false}
+            withBonded
+          />
+        ))}
+      </div>
+    );
+  }
+
+  private renderOffline (address: AccountId | string) {
+    const { recentlyOffline } = this.props;
+
+    return (
+      <RecentlyOffline
+        accountId={address}
+        offline={recentlyOffline[address.toString()]}
+        tooltip
+      />
+    );
+  }
+
+  private renderControllerAccount () {
+    const { t } = this.props;
+    const { controllerId } = this.state;
+
+    if (!controllerId) {
+      return null;
+    }
+
+    return (
+      <div className='staking--Account-detail actions'>
+        <label className='staking--label'>{t('controller')}</label>
+        <AddressRow
+          value={controllerId}
+          iconInfo={this.renderOffline(controllerId)}
+          withAddressOrName
+          withBalance={{
+            available: true,
+            bonded: false,
+            free: false,
+            redeemable: false,
+            unlocking: false
+          }}
+        />
+      </div>
+
+    );
+  }
+
+  private renderSessionAccount () {
+    const { t } = this.props;
+    const { sessionId } = this.state;
+
+    if (!sessionId) {
+      return null;
+    }
+
+    return (
+      <div className='staking--Account-detail actions'>
+        <label className='staking--label'>{t('session')}</label>
+        <AddressRow
+          value={sessionId}
+          withAddressOrName
+          withBalance={{
+            available: true,
+            bonded: false,
+            free: false,
+            redeemable: false,
+            unlocking: false
+          }}
+        />
+      </div>
+    );
+  }
+
+  private renderStashAccount () {
+    const { accountId, t } = this.props;
+
+    return (
+      <div className='staking--Account-detail actions'>
+        <label className='staking--label'>{t('stash')}</label>
+        <AddressRow
+          value={accountId}
+          iconInfo={this.renderOffline(accountId)}
+          withAddressOrName
+          withBalance={{
+            available: true,
+            bonded: false,
+            free: false,
+            redeemable: false,
+            unlocking: false
+          }}
+        />
+      </div>
+    );
+  }
+
+  private renderNominate () {
+    const { stashOptions } = this.props;
+    const { controllerId, isNominateOpen, nominees, stashId } = this.state;
+
+    if (!stashId || !controllerId) {
+      return null;
+    }
+
+    return (
+      <Nominate
+        controllerId={controllerId}
+        isOpen={isNominateOpen}
+        nominees={nominees}
+        onClose={this.toggleNominate}
+        stashId={stashId}
+        stashOptions={stashOptions}
+      />
+    );
+  }
+
+  private renderValidate () {
+    const { controllerId, isValidateOpen, stashId, validatorPrefs } = this.state;
+
+    if (!stashId || !controllerId) {
+      return null;
+    }
+
+    return (
+      <Validate
+        controllerId={controllerId}
+        isOpen={isValidateOpen}
+        onClose={this.toggleValidate}
+        stashId={stashId}
+        validatorPrefs={validatorPrefs}
+      />
+    );
+  }
+
+  private renderButtons () {
+    const { t } = this.props;
+    const { controllerId, isSettingPopupOpen, isStashNominating, isStashValidating, sessionId } = this.state;
+    const buttons = [];
+
+    // if we are validating/nominating show stop
+    if (isStashNominating || isStashValidating) {
+      buttons.push(
+        <TxButton
+          accountId={controllerId}
+          isNegative
+          label={
+            isStashNominating
+              ? t('Stop Nominating')
+              : t('Stop Validating')
+          }
+          key='stop'
+          tx='staking.chill'
+        />
+      );
+    } else {
+      if (!sessionId) {
+        buttons.push(
+          <Button
+            isPrimary
+            key='set'
+            onClick={this.toggleSetSessionAccount}
+            label={t('Set Session Key')}
+          />
+        );
+      } else {
+        buttons.push(
+          <Button
+            isPrimary
+            key='validate'
+            onClick={this.toggleValidate}
+            label={t('Validate')}
+          />
+        );
+      }
+
+      buttons.push(<Button.Or key='nominate.or' />);
+      buttons.push(
+        <Button
+          isPrimary
+          key='nominate'
+          onClick={this.toggleNominate}
+          label={t('Nominate')}
+        />
+      );
+    }
+
+    buttons.push(
+      <Popup
+        key='settings'
+        onClose={this.toggleSettingPopup}
+        open={isSettingPopupOpen}
+        position='bottom left'
+        trigger={
+          <Button
+            icon='setting'
+            onClick={this.toggleSettingPopup}
+            size='tiny'
+          />
+        }
+      >
+        {this.renderPopupMenu()}
+      </Popup>
+    );
+
+    return (
+      <>
+        <Button.Group>
+          {buttons}
+        </Button.Group>
+      </>
+    );
+  }
+
+  private renderPopupMenu () {
+    const { balances_all, t } = this.props;
+    const { isStashNominating, isStashValidating, sessionId } = this.state;
+
+    // only show a "Bond Additional" button if this stash account actually doesn't bond everything already
+    // staking_ledger.total gives the total amount that can be slashed (any active amount + what is being unlocked)
+    const canBondExtra = balances_all && balances_all.availableBalance.gtn(0);
+
+    return (
+      <Menu
+        vertical
+        text
+        onClick={this.toggleSettingPopup}
+      >
+        {canBondExtra &&
+          <Menu.Item onClick={this.toggleBondExtra}>
+            {t('Bond more funds')}
+          </Menu.Item>
+        }
+        <Menu.Item onClick={this.toggleUnbond}>
+          {t('Unbond funds')}
+        </Menu.Item>
+        <Menu.Item onClick={this.toggleSetControllerAccount}>
+          {t('Change controller account')}
+        </Menu.Item>
+        <Menu.Item onClick={this.toggleSetRewardDestination}>
+          {t('Change reward destination')}
+        </Menu.Item>
+        {isStashValidating &&
+          <Menu.Item onClick={this.toggleValidate}>
+            {t('Change validator preferences')}
+          </Menu.Item>
+        }
+        {sessionId &&
+          <Menu.Item onClick={this.toggleSetSessionAccount}>
+            {t('Change session account')}
+          </Menu.Item>
+        }
+        {isStashNominating &&
+          <Menu.Item onClick={this.toggleNominate}>
+            {t('Change nominee(s)')}
+          </Menu.Item>
+        }
+      </Menu>
+    );
+  }
+
+  private renderSetValidatorPrefs () {
+    const { controllerId, isValidateOpen, stashId, validatorPrefs } = this.state;
+
+    if (!controllerId || !validatorPrefs || !stashId) {
+      return null;
+    }
+
+    return (
+      <Validate
+        controllerId={controllerId}
+        isOpen={isValidateOpen}
+        onClose={this.toggleValidate}
+        stashId={stashId}
+        validatorPrefs={validatorPrefs}
+      />
+    );
+  }
+
+  private renderSetControllerAccount () {
+    const { controllerId, isSetControllerAccountOpen, isStashValidating, stashId } = this.state;
+
+    if (!isSetControllerAccountOpen || !stashId) {
+      return null;
+    }
+
+    return (
+      <SetControllerAccount
+        defaultControllerId={controllerId}
+        isValidating={isStashValidating}
+        onClose={this.toggleSetControllerAccount}
+        stashId={stashId}
+      />
+    );
+  }
+
+  private renderSetRewardDestination () {
+    const { controllerId, destination, isSetRewardDestinationOpen } = this.state;
+
+    if (!isSetRewardDestinationOpen || !controllerId) {
+      return null;
+    }
+
+    return (
+      <SetRewardDestination
+        controllerId={controllerId}
+        defaultDestination={destination}
+        onClose={this.toggleSetRewardDestination}
+      />
+    );
+  }
+
+  private renderSetSessionAccount () {
+    const { controllerId, isSetSessionAccountOpen, stashId, sessionId } = this.state;
+
+    if (!controllerId || !stashId) {
+      return null;
+    }
+
+    return (
+      <SetSessionAccount
+        controllerId={controllerId}
+        isOpen={isSetSessionAccountOpen}
+        onClose={this.toggleSetSessionAccount}
+        sessionId={sessionId}
+        stashId={stashId}
+      />
+    );
+  }
+
+  private toggleBondExtra = () => {
+    this.setState(({ isBondExtraOpen }) => ({
+      isBondExtraOpen: !isBondExtraOpen
+    }));
+  }
+
+  private toggleNominate = () => {
+    this.setState(({ isNominateOpen }) => ({
+      isNominateOpen: !isNominateOpen
+    }));
+  }
+
+  private toggleSetControllerAccount = () => {
+    this.setState(({ isSetControllerAccountOpen }) => ({
+      isSetControllerAccountOpen: !isSetControllerAccountOpen
+    }));
+  }
+
+  private toggleSetRewardDestination = () => {
+    this.setState(({ isSetRewardDestinationOpen }) => ({
+      isSetRewardDestinationOpen: !isSetRewardDestinationOpen
+    }));
+  }
+
+  private toggleSetSessionAccount = () => {
+    this.setState(({ isSetSessionAccountOpen }) => ({
+      isSetSessionAccountOpen: !isSetSessionAccountOpen
+    }));
+  }
+
+  private toggleSettingPopup = () => {
+    this.setState(({ isSettingPopupOpen }) => ({
+      isSettingPopupOpen: !isSettingPopupOpen
+    }));
+  }
+
+  private toggleUnbond = () => {
+    this.setState(({ isUnbondOpen }) => ({
+      isUnbondOpen: !isUnbondOpen
+    }));
+  }
+
+  private toggleValidate = () => {
+    this.setState(({ isValidateOpen }) => ({
+      isValidateOpen: !isValidateOpen
+    }));
+  }
+}
+
+export default translate(
+  withCalls<Props>(
+    ['derive.staking.info', { paramName: 'accountId' }],
+    ['derive.balances.all', { paramName: 'accountId' }]
+  )(Account)
+);

+ 104 - 0
packages/app-staking/src/Actions/Accounts.tsx

@@ -0,0 +1,104 @@
+// Copyright 2017-2019 @polkadot/app-staking authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+import { I18nProps } from '@polkadot/ui-app/types';
+import { ComponentProps } from '../types';
+
+import React from 'react';
+import { Button, CardGrid, Icon } from '@polkadot/ui-app';
+import createOption from '@polkadot/ui-keyring/options/item';
+import { getAddressName } from '@polkadot/ui-app/util';
+import keyring from '@polkadot/ui-keyring';
+import styled from 'styled-components';
+
+import Account from './Account';
+import { KeyringSectionOption } from '@polkadot/ui-keyring/options/types';
+import StartStaking from './NewStake';
+import translate from '../translate';
+
+type Props = I18nProps & ComponentProps;
+
+type State = {
+  isNewStakeOpen: boolean
+};
+
+const Wrapper = styled(CardGrid) `
+.ui--CardGrid-buttons {
+  text-align: right;
+}
+`;
+
+class Accounts extends React.PureComponent<Props,State> {
+  state: State = {
+    isNewStakeOpen: false
+  };
+
+  render () {
+    const { recentlyOffline, t } = this.props;
+    const accounts = keyring.getAccounts();
+    const stashOptions = this.getStashOptions();
+
+    return (
+      <Wrapper
+        buttons={
+          <Button
+            isPrimary
+            key='new-stake'
+            label={
+              <>
+                <Icon name='add'/>
+                {t('New stake')}
+              </>
+          }
+            onClick={this.toggleNewStake}
+          />
+        }
+      >
+        {this.renderNewStake()}
+        {accounts.map((account, index) => {
+          const address = account.address();
+
+          return (
+            <Account
+              accountId={address}
+              key={index}
+              recentlyOffline={recentlyOffline}
+              stashOptions={stashOptions}
+            />
+          );
+        })}
+      </Wrapper>
+    );
+  }
+
+  private renderNewStake () {
+    const { isNewStakeOpen } = this.state;
+
+    if (!isNewStakeOpen) {
+      return null;
+    }
+
+    return (
+      <StartStaking
+        onClose={this.toggleNewStake}
+      />
+    );
+  }
+
+  private toggleNewStake = (): void => {
+    this.setState(({ isNewStakeOpen }) => ({
+      isNewStakeOpen: !isNewStakeOpen
+    }));
+  }
+
+  private getStashOptions (): Array<KeyringSectionOption> {
+    const { stashes } = this.props;
+
+    return stashes.map((stashId) =>
+      createOption(stashId, getAddressName(stashId, 'account'))
+    );
+  }
+}
+
+export default translate(Accounts);

+ 35 - 96
packages/app-staking/src/Account/Bond.tsx → packages/app-staking/src/Actions/NewStake.tsx

@@ -9,79 +9,47 @@ import { CalculateBalanceProps } from '../types';
 import BN from 'bn.js';
 import React from 'react';
 import { SubmittableExtrinsic } from '@polkadot/api/promise/types';
-import { Button, Dropdown, InputAddress, InputBalance, Modal, TxButton, TxComponent } from '@polkadot/ui-app';
-import { withCalls, withApi, withMulti } from '@polkadot/ui-api';
-import { calcSignatureLength } from '@polkadot/ui-signer/Checks';
-import { ZERO_BALANCE, ZERO_FEES } from '@polkadot/ui-signer/Checks/constants';
+import { Button, Dropdown, InputAddress, InputBalanceBonded, Modal, TxButton, TxComponent } from '@polkadot/ui-app';
+import { withApi, withMulti } from '@polkadot/ui-api';
 
 import translate from '../translate';
-import ValidateController from './ValidateController';
+import { rewardDestinationOptions } from './constants';
+import InputValidationController from './Account/InputValidationController';
 
 type Props = I18nProps & ApiProps & CalculateBalanceProps & {
-  accountId: string,
-  controllerId?: string | null,
-  isOpen: boolean,
   onClose: () => void
 };
 
 type State = {
   bondValue?: BN,
   controllerError: string | null,
-  controllerId: string,
+  controllerId: string | null,
   destination: number,
   extrinsic: SubmittableExtrinsic | null,
-  maxBalance?: BN
+  stashId: string | null
 };
 
-const stashOptions = [
-  { text: 'Stash account (increase the amount at stake)', value: 0 },
-  { text: 'Stash account (do not increase the amount at stake)', value: 1 },
-  { text: 'Controller account', value: 2 }
-];
-
-const ZERO = new BN(0);
-
-class Bond extends TxComponent<Props, State> {
+class NewStake extends TxComponent<Props, State> {
   state: State;
 
   constructor (props: Props) {
     super(props);
 
-    const { accountId, controllerId } = this.props;
-
     this.state = {
       controllerError: null,
-      controllerId: controllerId ? controllerId.toString() : accountId,
+      controllerId: null,
       destination: 0,
-      extrinsic: null
+      extrinsic: null,
+      stashId: null
     };
   }
 
-  componentDidUpdate (prevProps: Props, prevState: State) {
-    const { balances_fees } = this.props;
-    const { controllerId, destination, extrinsic } = this.state;
-
-    const hasLengthChanged = ((extrinsic && extrinsic.encodedLength) || 0) !== ((prevState.extrinsic && prevState.extrinsic.encodedLength) || 0);
-
-    if ((controllerId && prevState.controllerId !== controllerId) ||
-      (prevState.destination !== destination) ||
-      (balances_fees !== prevProps.balances_fees) ||
-      hasLengthChanged
-    ) {
-      this.setMaxBalance();
-    }
-  }
-
   render () {
-    const { accountId, isOpen, onClose, t } = this.props;
-    const { bondValue, controllerError, controllerId, extrinsic, maxBalance } = this.state;
-    const hasValue = !!bondValue && bondValue.gtn(0) && (!maxBalance || bondValue.lte(maxBalance));
+    const { onClose, t } = this.props;
+    const { bondValue, controllerError, controllerId, extrinsic, stashId } = this.state;
+    const hasValue = !!bondValue && bondValue.gtn(0);
     const canSubmit = hasValue && !controllerError && !!controllerId;
 
-    if (!isOpen) {
-      return null;
-    }
-
     return (
       <Modal
         className='staking--Bonding'
@@ -99,7 +67,7 @@ class Bond extends TxComponent<Props, State> {
             />
             <Button.Or />
             <TxButton
-              accountId={accountId}
+              accountId={stashId}
               isDisabled={!canSubmit}
               isPrimary
               label={t('Bond')}
@@ -114,8 +82,8 @@ class Bond extends TxComponent<Props, State> {
   }
 
   private renderContent () {
-    const { accountId, t } = this.props;
-    const { controllerId, controllerError, bondValue, destination, maxBalance } = this.state;
+    const { t } = this.props;
+    const { controllerId, controllerError, bondValue, destination, stashId } = this.state;
     const hasValue = !!bondValue && bondValue.gtn(0);
 
     return (
@@ -126,9 +94,10 @@ class Bond extends TxComponent<Props, State> {
         <Modal.Content className='ui--signer-Signer-Content'>
           <InputAddress
             className='medium'
-            defaultValue={accountId}
-            isDisabled
             label={t('stash account')}
+            onChange={this.onChangeStash}
+            type='account'
+            value={stashId}
           />
           <InputAddress
             className='medium'
@@ -139,20 +108,23 @@ class Bond extends TxComponent<Props, State> {
             type='account'
             value={controllerId}
           />
-          <ValidateController
-            accountId={accountId}
+          <InputValidationController
+            accountId={stashId}
             controllerId={controllerId}
             onError={this.onControllerError}
           />
-          <InputBalance
+          <InputBalanceBonded
             autoFocus
             className='medium'
+            controllerId={controllerId}
+            destination={destination}
+            extrinsicProp={'staking.bond'}
             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)')}
             isError={!hasValue}
             label={t('value bonded')}
-            maxValue={maxBalance}
             onChange={this.onChangeValue}
             onEnter={this.sendTx}
+            stashId={stashId}
             withMax
           />
           <Dropdown
@@ -161,7 +133,7 @@ class Bond extends TxComponent<Props, State> {
             help={t('The destination account for any payments as either a nominator or validator')}
             label={t('payment destination')}
             onChange={this.onChangeDestination}
-            options={stashOptions}
+            options={rewardDestinationOptions}
             value={destination}
           />
         </Modal.Content>
@@ -172,7 +144,7 @@ class Bond extends TxComponent<Props, State> {
   private nextState (newState: Partial<State>): void {
     this.setState((prevState: State): State => {
       const { api } = this.props;
-      const { bondValue = prevState.bondValue, controllerError = prevState.controllerError, controllerId = prevState.controllerId, destination = prevState.destination, maxBalance = prevState.maxBalance } = newState;
+      const { bondValue = prevState.bondValue, controllerError = prevState.controllerError, controllerId = prevState.controllerId, destination = prevState.destination, stashId = prevState.stashId } = newState;
       const extrinsic = (bondValue && controllerId)
         ? api.tx.staking.bond(controllerId, bondValue, destination)
         : null;
@@ -183,43 +155,11 @@ class Bond extends TxComponent<Props, State> {
         controllerId,
         destination,
         extrinsic,
-        maxBalance
+        stashId
       };
     });
   }
 
-  private setMaxBalance = () => {
-    const { api, system_accountNonce = ZERO, balances_fees = ZERO_FEES, balances_all = ZERO_BALANCE } = this.props;
-    const { controllerId, destination } = this.state;
-
-    const { transactionBaseFee, transactionByteFee } = balances_fees;
-    const { freeBalance } = balances_all;
-
-    let prevMax = new BN(0);
-    let maxBalance = new BN(1);
-    let extrinsic;
-
-    while (!prevMax.eq(maxBalance)) {
-      prevMax = maxBalance;
-
-      extrinsic = controllerId && destination
-        ? api.tx.staking.bond(controllerId, prevMax, destination)
-        : null;
-
-      const txLength = calcSignatureLength(extrinsic, system_accountNonce);
-
-      const fees = transactionBaseFee
-        .add(transactionByteFee.muln(txLength));
-
-      maxBalance = new BN(freeBalance).sub(fees);
-    }
-
-    this.nextState({
-      extrinsic,
-      maxBalance
-    });
-  }
-
   private onChangeController = (controllerId: string) => {
     this.nextState({ controllerId });
   }
@@ -228,6 +168,10 @@ class Bond extends TxComponent<Props, State> {
     this.nextState({ destination });
   }
 
+  private onChangeStash = (stashId: string) => {
+    this.nextState({ stashId });
+  }
+
   private onChangeValue = (bondValue?: BN) => {
     this.nextState({ bondValue });
   }
@@ -238,12 +182,7 @@ class Bond extends TxComponent<Props, State> {
 }
 
 export default withMulti(
-  Bond,
+  NewStake,
   translate,
-  withApi,
-  withCalls<Props>(
-    'derive.balances.fees',
-    ['derive.balances.all', { paramName: 'accountId' }],
-    ['query.system.accountNonce', { paramName: 'accountId' }]
-  )
+  withApi
 );

+ 9 - 0
packages/app-staking/src/Actions/constants.tsx

@@ -0,0 +1,9 @@
+// Copyright 2017-2019 @polkadot/ui-app authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+export const rewardDestinationOptions = [
+    { text: 'Stash account (increase the amount at stake)', value: 0 },
+    { text: 'Stash account (do not increase the amount at stake)', value: 1 },
+    { text: 'Controller account', value: 2 }
+];

+ 1 - 1
packages/app-staking/src/Overview/CurrentList.tsx

@@ -13,7 +13,7 @@ import translate from '../translate';
 import Address from './Address';
 
 type Props = I18nProps & {
-  balances: DerivedBalancesMap,
+  balances?: DerivedBalancesMap,
   current: Array<string>,
   lastAuthor?: string,
   lastBlock: string,

+ 4 - 0
packages/app-staking/src/Overview/index.tsx

@@ -62,6 +62,10 @@ class Overview extends React.PureComponent<Props> {
   private sortByBalance (list: Array<string>): Array<string> {
     const { balances } = this.props;
 
+    if (!balances) {
+      return [];
+    }
+
     return list.sort((a, b) => {
       const balanceA = balances[a] || { freeBalance: ZERO };
       const balanceB = balances[b] || { freeBalance: ZERO };

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

@@ -19,7 +19,7 @@ import accountObservable from '@polkadot/ui-keyring/observable/accounts';
 import './index.css';
 
 import basicMd from './md/basic.md';
-import Accounts from './Accounts';
+import Accounts from './Actions/Accounts';
 import Overview from './Overview';
 import translate from './translate';
 

+ 1 - 1
packages/app-staking/src/types.ts

@@ -12,7 +12,7 @@ export type Nominators = {
 };
 
 export type ComponentProps = {
-  balances: DerivedBalancesMap,
+  balances?: DerivedBalancesMap,
   controllers: Array<string>,
   recentlyOffline: RecentlyOfflineMap,
   stashes: Array<string>,

+ 84 - 25
packages/ui-app/src/AddressInfo.tsx

@@ -12,9 +12,9 @@ import { formatBalance, formatNumber } from '@polkadot/util';
 import { Icon, Tooltip, TxButton } from '@polkadot/ui-app';
 import { withCalls, withMulti } from '@polkadot/ui-api';
 
-import translate from './translate';
 import CryptoType from './CryptoType';
 import Label from './Label';
+import translate from './translate';
 
 // true to display, or (for bonded) provided values [own, ...all extras]
 export type BalanceActiveType = {
@@ -30,31 +30,34 @@ export type CryptoActiveType = {
   nonce?: boolean
 };
 
+export type ValidatorPrefsType = {
+  unstakeThreshold?: boolean,
+  validatorPayment?: boolean
+};
+
 type Props = BareProps & I18nProps & {
+  address: string,
   balances_all?: DerivedBalances,
   children?: React.ReactNode,
   staking_info?: DerivedStaking,
-  value: string,
   withBalance?: boolean | BalanceActiveType,
-  withExtended?: boolean | CryptoActiveType
+  withRewardDestination?: boolean,
+  withExtended?: boolean | CryptoActiveType,
+  withValidatorPrefs?: boolean | ValidatorPrefsType
 };
 
-// <AddressInfo
-//   withBalance // default
-//   withExtended={true} // optional
-//   value={address}
-// >{children></AddressInfo>
-//
-// Additionally to tweak the display, i.e. only available
-//
-// <AddressInfo withBalance={{ available: true }} />
 class AddressInfo extends React.PureComponent<Props> {
   render () {
     const { children, className } = this.props;
 
     return (
       <div className={className}>
-        {this.renderBalances()}
+        <div className='column'>
+          {this.renderBalances()}
+          {this.renderValidatorPrefs()}
+          {this.renderRewardDestination()}
+        </div>
+
         {this.renderExtended()}
         {children && (
           <div className='column'>
@@ -78,7 +81,7 @@ class AddressInfo extends React.PureComponent<Props> {
     }
 
     return (
-      <div className='column'>
+      <>
         {balanceDisplay.free && (
           <>
             <Label label={t('total')} />
@@ -109,17 +112,17 @@ class AddressInfo extends React.PureComponent<Props> {
             </div>
           </>
         )}
-      </div>
+    </>
     );
   }
 
-  // either true (filtered above already) or [own, ...all extras]
+  // either true (filtered above already) or [own, ...all extras bonded funds from nominators]
   private renderBonded (bonded: true | Array<BN>) {
     const { staking_info, t } = this.props;
     let value = undefined;
 
     if (Array.isArray(bonded)) {
-      // Get the sum of all extra values (if available)
+      // Get the sum of funds bonded by any nominator (if available)
       const extras = bonded.filter((value, index) => index !== 0);
       const extra = extras.reduce((total, value) => total.add(value), new BN(0)).gtn(0)
         ? `(+${extras.map((bonded) => formatBalance(bonded)).join(', ')})`
@@ -141,7 +144,7 @@ class AddressInfo extends React.PureComponent<Props> {
   }
 
   private renderExtended () {
-    const { balances_all, t, value, withExtended } = this.props;
+    const { balances_all, t, address, withExtended } = this.props;
     const extendedDisplay = withExtended === true
       ? { crypto: true, nonce: true }
       : withExtended
@@ -164,7 +167,7 @@ class AddressInfo extends React.PureComponent<Props> {
           <>
             <Label label={t('crypto type')} />
             <CryptoType
-              accountId={value}
+              accountId={address}
               className='result'
             />
           </>
@@ -173,6 +176,23 @@ class AddressInfo extends React.PureComponent<Props> {
     );
   }
 
+  private renderRewardDestination () {
+    const { staking_info, withRewardDestination, t } = this.props;
+
+    if (!withRewardDestination) {
+      return null;
+    }
+
+    return (
+      staking_info &&
+      staking_info.rewardDestination &&
+      <>
+        <Label label={t('reward destination')} />
+        <div className='result'>{staking_info.rewardDestination.toString().toLowerCase()}</div>
+      </>
+    );
+  }
+
   private renderRedeemButton () {
     const { staking_info, t } = this.props;
 
@@ -213,6 +233,38 @@ class AddressInfo extends React.PureComponent<Props> {
       ))
     );
   }
+
+  private renderValidatorPrefs () {
+    const { staking_info, t, withValidatorPrefs = false } = this.props;
+    const validatorPrefsDisplay = withValidatorPrefs === true
+    ? { unstakeThreshold: true, validatorPayment: true }
+    : withValidatorPrefs;
+
+    if (!validatorPrefsDisplay || !staking_info || !staking_info.validatorPrefs) {
+      return null;
+    }
+
+    // start with a spacer
+    return (
+      <>
+        <div className='spacer'></div>
+        {validatorPrefsDisplay.unstakeThreshold && staking_info.validatorPrefs.unstakeThreshold && (
+          <>
+            <Label label={t('unstake threshold')} />
+            <div className='result'>{staking_info.validatorPrefs.unstakeThreshold.toString()}
+            </div>
+          </>
+        )}
+        {validatorPrefsDisplay.validatorPayment && staking_info.validatorPrefs.validatorPayment && (
+          <>
+            <Label label={t('commision')} />
+            <div className='result'>{formatBalance(staking_info.validatorPrefs.validatorPayment.toBn())}
+            </div>
+          </>
+        )}
+      </>
+    );
+  }
 }
 
 export default withMulti(
@@ -221,6 +273,7 @@ export default withMulti(
     display: flex;
     flex: 1;
     justify-content: center;
+    white-space: nowrap;
 
     .column {
       flex: 1;
@@ -240,19 +293,25 @@ export default withMulti(
       .result {
         grid-column:  2;
 
-        .iconButton {
-          padding-left: 0!important;
+        .icon {
+          margin-left: .3em;
+          margin-right: 0;
+          padding-right: 0 !important;
         }
 
-        i.info.circle.icon {
-          margin-left: .3em;
+        button.ui.icon.primary.button.iconButton {
+          background: white !important;
         }
       }
+
+      .spacer {
+        margin-top: 1rem;
+      }
     }
   `,
   translate,
   withCalls<Props>(
-    ['derive.balances.all', { paramName: 'value' }],
-    ['derive.staking.info', { paramName: 'value' }]
+    ['derive.balances.all', { paramName: 'address' }],
+    ['derive.staking.info', { paramName: 'address' }]
   )
 );

+ 20 - 10
packages/ui-app/src/AddressRow.tsx

@@ -4,7 +4,6 @@
 
 import { I18nProps } from '@polkadot/ui-app/types';
 import { AccountId, AccountIndex, Address } from '@polkadot/types';
-import { KeyringItemType } from '@polkadot/ui-keyring/types';
 
 import BN from 'bn.js';
 import React from 'react';
@@ -14,26 +13,23 @@ import BaseIdentityIcon from '@polkadot/ui-identicon';
 import keyring from '@polkadot/ui-keyring';
 
 import AddressInfo, { BalanceActiveType } from './AddressInfo';
+import { classes, getAddressName, getAddressTags, toShortAddress } from './util';
 import CopyButton from './CopyButton';
 import IdentityIcon from './IdentityIcon';
 import Row, { RowProps, RowState, styles } from './Row';
 import translate from './translate';
-import { classes, getAddressName, getAddressTags, toShortAddress } from './util';
 
 export type Props = I18nProps & RowProps & {
-  accounts_idAndIndex?: [AccountId?, AccountIndex?]
   bonded?: BN | Array<BN>,
   isContract?: boolean,
   isValid?: boolean,
-  type: KeyringItemType,
   value: AccountId | AccountIndex | Address | string | null,
+  withAddressOrName?: boolean,
   withBalance?: boolean | BalanceActiveType,
   withIndex?: boolean
 };
 
-type State = RowState & {
-  address: string
-};
+type State = RowState;
 
 const DEFAULT_ADDR = '5'.padEnd(16, 'x');
 const ICON_SIZE = 48;
@@ -86,8 +82,7 @@ class AddressRow extends Row<Props, State> {
         <div className='ui--Row-base'>
           {this.renderIcon()}
           <div className='ui--Row-details'>
-            {this.renderName()}
-            {this.renderAddress()}
+            {this.renderAddressAndName()}
             {this.renderAccountIndex()}
             {!isContract && this.renderBalances()}
             {this.renderTags()}
@@ -117,6 +112,21 @@ class AddressRow extends Row<Props, State> {
     };
   }
 
+  protected renderAddressAndName () {
+    const { withAddressOrName = false } = this.props;
+
+    if (withAddressOrName) {
+      return this.renderName(true);
+    } else {
+      return (
+        <>
+          {this.renderName()}
+          {this.renderAddress()}
+        </>
+      );
+    }
+  }
+
   private renderAddress () {
     const { address } = this.state;
 
@@ -158,7 +168,7 @@ class AddressRow extends Row<Props, State> {
     return (
       <div className='ui--Row-balances'>
         <AddressInfo
-          value={accountId}
+          address={accountId}
           withBalance={withBalance}
         />
       </div>

+ 18 - 2
packages/ui-app/src/CodeRow.tsx

@@ -9,10 +9,13 @@ import { CodeStored } from '@polkadot/app-contracts/types';
 import React from 'react';
 import styled from 'styled-components';
 import { withMulti } from '@polkadot/ui-api';
-import { CopyButton, Icon, Messages } from '@polkadot/ui-app';
 import { classes, toShortAddress } from '@polkadot/ui-app/util';
 import contracts from '@polkadot/app-contracts/store';
 
+import CopyButton from './CopyButton';
+import Icon from './Icon';
+import Messages from './Messages';
+
 import Row, { RowProps, RowState, styles } from './Row';
 import translate from './translate';
 
@@ -42,6 +45,8 @@ const CodeIcon = styled.div`
   }
 `;
 
+const DEFAULT_ADDR = '5'.padEnd(16, 'x');
+
 class CodeRow extends Row<Props, State> {
   state: State;
 
@@ -51,10 +56,15 @@ class CodeRow extends Row<Props, State> {
     this.state = this.createState();
   }
 
-  static getDerivedStateFromProps ({ code: { json } }: Props, prevState: State): State | null {
+  static getDerivedStateFromProps ({ code: { json }, accounts_idAndIndex = [] }: Props, prevState: State): State | null {
     const codeHash = json.codeHash || DEFAULT_HASH;
     const name = json.name || DEFAULT_NAME;
     const tags = json.tags || [];
+    const [_accountId] = accounts_idAndIndex;
+    const accountId = _accountId;
+    const address = accountId
+      ? accountId.toString()
+      : DEFAULT_ADDR;
 
     const state = { tags } as State;
     let hasChanged = false;
@@ -69,6 +79,11 @@ class CodeRow extends Row<Props, State> {
       hasChanged = true;
     }
 
+    if (address !== prevState.address) {
+      state.address = address;
+      hasChanged = true;
+    }
+
     return hasChanged
       ? state
       : null;
@@ -101,6 +116,7 @@ class CodeRow extends Row<Props, State> {
     const { code: { json: { codeHash = DEFAULT_HASH, name = DEFAULT_NAME, tags = [] } } } = this.props;
 
     return {
+      address: DEFAULT_ADDR,
       codeHash,
       isEditingName: false,
       isEditingTags: false,

+ 14 - 9
packages/ui-app/src/InputAddress.tsx

@@ -2,20 +2,20 @@
 // This software may be modified and distributed under the terms
 // of the Apache-2.0 license. See the LICENSE file for details.
 
-import { KeyringOptions, KeyringSectionOption, KeyringSectionOptions, KeyringOption$Type } from '@polkadot/ui-keyring/options/types';
 import { BareProps } from './types';
+import { KeyringOptions, KeyringSectionOption, KeyringSectionOptions, KeyringOption$Type } from '@polkadot/ui-keyring/options/types';
 
 import React from 'react';
 import store from 'store';
 import styled from 'styled-components';
+import createItem from '@polkadot/ui-keyring/options/item';
 import keyring from '@polkadot/ui-keyring';
 import keyringOption from '@polkadot/ui-keyring/options';
-import createItem from '@polkadot/ui-keyring/options/item';
 import { withMulti, withObservable } from '@polkadot/ui-api';
 
-import Dropdown from './Dropdown';
 import { classes, getAddressName } from './util';
 import addressToAddress from './util/toAddress';
+import Dropdown from './Dropdown';
 
 type Props = BareProps & {
   defaultValue?: string | null,
@@ -147,15 +147,20 @@ class InputAddress extends React.PureComponent<Props, State> {
             ? [createOption(actualValue)]
             : (optionsAll ? optionsAll[type] : [])
       );
+    let _defaultValue;
+
+    if (value !== undefined) {
+      _defaultValue = undefined;
+    } else if (isMultiple) {
+      _defaultValue = undefined;
+    } else {
+      _defaultValue = actualValue;
+    }
 
     return (
       <Dropdown
         className={classes('ui--InputAddress', hideAddress && 'hideAddress', className)}
-        defaultValue={
-          isMultiple || (value !== undefined)
-            ? undefined
-            : actualValue
-        }
+        defaultValue={_defaultValue}
         help={help}
         isDisabled={isDisabled}
         isError={isError}
@@ -263,7 +268,7 @@ class InputAddress extends React.PureComponent<Props, State> {
       if (accountId) {
         matches.push(
           keyring.saveRecent(
-            accountId
+            accountId.toString()
           ).option
         );
       }

+ 161 - 0
packages/ui-app/src/InputBalanceBonded.tsx

@@ -0,0 +1,161 @@
+// Copyright 2017-2019 @polkadot/ui-app 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, BitLength } from './types';
+import { DerivedFees, DerivedBalances } from '@polkadot/api-derive/types';
+
+import BN from 'bn.js';
+import React from 'react';
+import { ApiProps } from '@polkadot/ui-api/types';
+import { BitLengthOption } from '@polkadot/ui-app/constants';
+import { calcSignatureLength } from '@polkadot/ui-signer/Checks';
+import { InputNumber } from '@polkadot/ui-app';
+import { SubmittableExtrinsic } from '@polkadot/api/promise/types';
+import { withCalls, withMulti, withApi } from '@polkadot/ui-api';
+import { ZERO_BALANCE, ZERO_FEES } from '@polkadot/ui-signer/Checks/constants';
+
+type Props = BareProps & ApiProps & {
+  autoFocus?: boolean,
+  balances_fees?: DerivedFees,
+  balances_all?: DerivedBalances,
+  controllerId: string,
+  defaultValue?: BN | string,
+  destination?: number,
+  extrinsicProp: 'staking.bond' | 'staking.bondExtra' | 'staking.unbond',
+  help?: React.ReactNode,
+  isDisabled?: boolean,
+  isError?: boolean,
+  label?: any,
+  onChange?: (value?: BN) => void,
+  onEnter?: () => void,
+  placeholder?: string,
+  stashId: string,
+  system_accountNonce?: BN,
+  value?: BN | string,
+  withEllipsis?: boolean,
+  withLabel?: boolean,
+  withMax?: boolean
+};
+
+type State = {
+  maxBalance?: BN,
+  extrinsic: SubmittableExtrinsic | null
+};
+
+const ZERO = new BN(0);
+const DEFAULT_BITLENGTH = BitLengthOption.CHAIN_SPEC as BitLength;
+
+class InputBalanceBonded extends React.PureComponent<Props, State> {
+  state: State;
+
+  constructor (props: Props) {
+    super(props);
+
+    this.state = {
+      extrinsic: null,
+      maxBalance: ZERO
+    };
+  }
+  render () {
+    const { autoFocus, className, defaultValue, help, isDisabled, isError, label, onChange, onEnter, placeholder,style, value, withEllipsis, withLabel, withMax } = this.props;
+    const { maxBalance } = this.state;
+
+    return (
+      <InputNumber
+        autoFocus={autoFocus}
+        className={className}
+        bitLength={DEFAULT_BITLENGTH}
+        defaultValue={defaultValue}
+        help={help}
+        isDisabled={isDisabled}
+        isError={isError}
+        isSi
+        label={label}
+        maxValue={maxBalance}
+        onChange={onChange}
+        onEnter={onEnter}
+        placeholder={placeholder}
+        style={style}
+        value={value}
+        withEllipsis={withEllipsis}
+        withLabel={withLabel}
+        withMax={withMax}
+      />
+    );
+  }
+
+  componentDidUpdate (prevProps: Props, prevState: State) {
+    const { balances_all, balances_fees, controllerId, destination } = this.props;
+    const { extrinsic } = this.state;
+    const hasLengthChanged = ((extrinsic && extrinsic.encodedLength) || 0) !== ((prevState.extrinsic && prevState.extrinsic.encodedLength) || 0);
+
+    if ((controllerId && prevProps.controllerId !== controllerId) ||
+      (prevProps.destination !== destination) ||
+      (balances_fees !== prevProps.balances_fees) ||
+      (balances_all !== prevProps.balances_all) ||
+      hasLengthChanged
+    ) {
+      this.setMaxBalance();
+    }
+  }
+
+  private setMaxBalance = () => {
+    const { api, balances_fees = ZERO_FEES, balances_all = ZERO_BALANCE, controllerId, destination, extrinsicProp, system_accountNonce = ZERO } = this.props;
+    const { transactionBaseFee, transactionByteFee } = balances_fees;
+    const { freeBalance } = balances_all;
+    let prevMax = new BN(0);
+    let maxBalance = new BN(1);
+    let extrinsic;
+
+    while (!prevMax.eq(maxBalance)) {
+      prevMax = maxBalance;
+
+      if (extrinsicProp === 'staking.bond') {
+        extrinsic = controllerId && (destination || destination === 0)
+        ? api.tx.staking.bond(controllerId, prevMax, destination)
+        : null;
+      } else if (extrinsicProp === 'staking.unbond') {
+        extrinsic = api.tx.staking.unbond(prevMax);
+      } else if (extrinsicProp === 'staking.bondExtra') {
+        extrinsic = api.tx.staking.bonExtra(prevMax);
+      }
+
+      const txLength = calcSignatureLength(extrinsic, system_accountNonce);
+      const fees = transactionBaseFee
+        .add(transactionByteFee.muln(txLength));
+
+      maxBalance = new BN(freeBalance).sub(fees);
+    }
+
+    this.nextState({
+      extrinsic,
+      maxBalance
+    });
+  }
+
+  private nextState (newState: Partial<State>): void {
+    this.setState((prevState: State): State => {
+      const { api, controllerId, destination, value } = this.props;
+      const { maxBalance = prevState.maxBalance } = newState;
+      const extrinsic = (value && controllerId && destination)
+        ? api.tx.staking.bond(controllerId, value, destination)
+        : null;
+
+      return {
+        extrinsic,
+        maxBalance
+      };
+    });
+  }
+}
+
+export default withMulti(
+  InputBalanceBonded,
+  withApi,
+  withCalls<Props>(
+    'derive.balances.fees',
+    ['derive.balances.all', { paramName: 'stashId' }],
+    ['query.system.accountNonce', { paramName: 'stashId' }]
+  )
+);

+ 31 - 7
packages/ui-app/src/Row.tsx

@@ -2,11 +2,17 @@
 // This software may be modified and distributed under the terms
 // of the Apache-2.0 license. See the LICENSE file for details.
 
+import { AccountId, AccountIndex } from '@polkadot/types';
+import { KeyringItemType } from '@polkadot/ui-keyring/types';
+
 import { Label } from 'semantic-ui-react';
 import React from 'react';
-import { Button, Input, InputTags } from '@polkadot/ui-app';
 
-import { classes } from './util';
+import Button from './Button';
+import { classes, getAddressName } from './util';
+import CopyButton from './CopyButton';
+import Input from './Input';
+import InputTags from './InputTags';
 
 export const styles = `
   text-align: left;
@@ -150,6 +156,7 @@ export const styles = `
 `;
 
 export type RowProps = {
+  accounts_idAndIndex?: [AccountId?, AccountIndex?]
   buttons?: React.ReactNode,
   children?: React.ReactNode,
   className?: string,
@@ -158,11 +165,13 @@ export type RowProps = {
   iconInfo?: React.ReactNode,
   isEditable?: boolean,
   isInline?: boolean,
+  type?: KeyringItemType,
   withIcon?: boolean,
   withTags?: boolean
 };
 
 export type RowState = {
+  address: string
   isEditingName: boolean,
   isEditingTags: boolean,
   name: string,
@@ -230,10 +239,11 @@ class Row<P extends RowProps, S extends RowState> extends React.PureComponent<P,
     );
   }
 
-  protected renderName () {
-    const { isEditable } = this.props;
-    const { isEditingName, name } = this.state;
+  protected renderName (withCopy: boolean = false) {
+    const { isEditable, type } = this.props;
+    const { address, isEditingName, name } = this.state;
 
+    // can't be both editable and copiable
     return isEditingName
       ? (
         <Input
@@ -251,8 +261,22 @@ class Row<P extends RowProps, S extends RowState> extends React.PureComponent<P,
           className={classes('ui--Row-name', isEditable && 'editable')}
           onClick={isEditable ? this.toggleNameEditor : undefined}
         >
-          {name}
-          {isEditable && this.renderEditIcon()}
+          {withCopy && !isEditable
+            ? (
+              <CopyButton
+                isAddress
+                value={address}
+              >
+                {getAddressName(address, type, true)}
+              </CopyButton>
+            )
+            : (
+              <>
+                {name}
+                {isEditable && this.renderEditIcon()}
+              </>
+            )
+          }
         </div>
       );
   }

+ 6 - 0
packages/ui-app/src/constants.ts

@@ -12,3 +12,9 @@ export enum ScreenSizes {
   TABLET = 768,
   PHONE = 576
 }
+
+export const rewardDestinationOptions = [
+  { text: 'Stash account (increase the amount at stake)', value: 0 },
+  { text: 'Stash account (do not increase the amount at stake)', value: 1 },
+  { text: 'Controller account', value: 2 }
+];

+ 1 - 0
packages/ui-app/src/index.tsx

@@ -34,6 +34,7 @@ export { default as InfoForInput } from './InfoForInput';
 export { default as Input } from './Input';
 export { default as InputAddress } from './InputAddress';
 export { default as InputBalance } from './InputBalance';
+export { default as InputBalanceBonded } from './InputBalanceBonded';
 export { default as InputError } from './InputError';
 export { default as InputExtrinsic } from './InputExtrinsic';
 export { default as InputFile } from './InputFile';

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

@@ -44,13 +44,9 @@ header .ui--Button-Group {
 }
 
 button.ui.icon.iconButton {
-  padding: 0em .3em .3em .3em !important;
+  padding: 0em 0em .3em .3em !important;
   color: #2e86ab  !important;
   background: none  !important;
-  /*trick to let the button in the flow but keep the content centered regardless*/
-  margin-left: -2em  !important;
-  position: relative  !important;
-  right: -2.3em  !important;
 }
 
 .editable {

+ 7 - 2
packages/ui-signer/src/index.css

@@ -11,8 +11,13 @@
   line-height: 1.5em;
 }
 
-.ui--signer-Signer-Content .expanded {
-  h3 {
+.ui--signer-Signer-Content {
+  .modal-Text {
+    margin-bottom: 1em;
+    padding: 1em;
+  }
+
+  .expanded h3 {
     margin-bottom: 0.75rem;
   }