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
This commit is contained in:
Thibaut Sardan 2019-06-13 12:04:01 +08:00 committed by Jaco Greeff
parent db4d7441d9
commit d868adae88
34 changed files with 1643 additions and 889 deletions

View File

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

View File

@ -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) ||

View File

@ -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>
);

View File

@ -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' }]
)
);

View File

@ -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)
);

View File

@ -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);

View File

@ -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' }]
)
);

View File

@ -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;
}

View File

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

View File

@ -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);

View File

@ -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);

View File

@ -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
);

View File

@ -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
);

View File

@ -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);

View File

@ -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}

View File

@ -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}
isDisabled
label={t('controller account')}
/>
<InputAddress
className='medium'
defaultValue={stashId.toString()}
isDisabled
label={t('stash account')}
/>
<InputAddress
className='medium'
defaultValue={controllerId}
isDisabled
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);

View File

@ -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)
);

View File

@ -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);

View File

@ -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
);

View File

@ -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 }
];

View File

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

View File

@ -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 };

View File

@ -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';

View File

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

View File

@ -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' }]
)
);

View File

@ -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>

View File

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

View File

@ -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
);
}

View File

@ -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' }]
)
);

View File

@ -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>
);
}

View File

@ -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 }
];

View File

@ -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';

View File

@ -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 {

View File

@ -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;
}