Allow to withdrawUnbonded + additional info (#1070)

* skeleton for Available and Unlocking

* available (with bug) and unlocking balance

* implement available according to the rust code

* lint

* unlock modal components

* use tx button directly instead of useless modal withdrawUnbonded

* use BN and move max to util

* Sum unlockables

* show unlockableSum and refactor

* add unlockable button

* lint

* smaller padding around unlock button

* cleanup and address comment

* use labels directly instead of passing in props

* function ordering and make private

* fixme for vesting, function ordering

* css in components

* lint
This commit is contained in:
Thibaut Sardan 2019-05-08 11:06:54 +02:00 committed by Jaco Greeff
parent 48e6107a4b
commit 4d39523daf
12 changed files with 338 additions and 32 deletions

View File

@ -80,9 +80,11 @@ class Account extends React.PureComponent<Props, State> {
name={name}
value={accountId}
identIconSize={96}
withAvailable
withBonded
withIndex={false}
withNonce={false}
withUnlocking
>
<div className='staking--Account-expand'>
{this.renderButtons()}
@ -171,24 +173,6 @@ class Account extends React.PureComponent<Props, State> {
);
}
// private renderUnbond () {
// const { accountId } = this.props;
// const { controllerId, isBondingOpen } = this.state;
// if (!controllerId) {
// return null;
// }
// return (
// <UnBond
// accountId={accountId}
// controllerId={controllerId}
// isOpen={isBondingOpen}
// onClose={this.toggleBonding}
// />
// );
// }
private renderNominee () {
const { recentlyOffline, staking_nominators, t } = this.props;

View File

@ -11,11 +11,13 @@ import { Nonce } from '@polkadot/ui-reactive';
import { withCalls } from '@polkadot/ui-api';
import BaseIdentityIcon from '@polkadot/ui-identicon';
import AvailableDisplay from './Available';
import { classes, toShortAddress } from './util';
import BalanceDisplay from './Balance';
import BondedDisplay from './Bonded';
import IdentityIcon from './IdentityIcon';
import translate from './translate';
import UnlockingDisplay from './Unlocking';
export type Props = I18nProps & {
accounts_idAndIndex?: [AccountId?, AccountIndex?],
@ -23,17 +25,19 @@ export type Props = I18nProps & {
bonded?: BN | Array<BN>,
children?: React.ReactNode,
extraInfo?: React.ReactNode,
name?: string,
value: AccountId | AccountIndex | Address | string | null,
withBalance?: boolean,
withBonded?: boolean,
withIndex?: boolean,
identIconSize?: number,
isShort?: boolean,
name?: string,
session_validators?: Array<AccountId>,
value: AccountId | AccountIndex | Address | string | null,
withAvailable?: boolean,
withBalance?: boolean,
withBonded?: boolean,
withCopy?: boolean,
withIcon?: boolean,
withNonce?: boolean
withIndex?: boolean,
withNonce?: boolean,
withUnlocking?: boolean
};
const DEFAULT_ADDR = '5'.padEnd(16, 'x');
@ -53,9 +57,11 @@ class AddressSummary extends React.PureComponent<Props> {
{this.renderIcon()}
{this.renderAccountId()}
{this.renderAccountIndex()}
{this.renderAvailable()}
{this.renderBalance()}
{this.renderBonded()}
{this.renderNonce()}
{this.renderUnlocking()}
</div>
{this.renderChildren()}
</div>
@ -174,6 +180,41 @@ class AddressSummary extends React.PureComponent<Props> {
);
}
protected renderAvailable () {
const { accounts_idAndIndex = [], t, value, withAvailable } = this.props;
const [_accountId] = accounts_idAndIndex;
const accountId = _accountId || value;
if (!withAvailable || !accountId) {
return null;
}
return (
<AvailableDisplay
className='ui--AddressSummary-available'
label={t('available ')}
params={accountId}
/>
);
}
protected renderUnlocking () {
const { accounts_idAndIndex = [], value, withUnlocking } = this.props;
const [_accountId] = accounts_idAndIndex;
const accountId = _accountId || value;
if (!withUnlocking || !accountId) {
return null;
}
return (
<UnlockingDisplay
className='ui--AddressSummary-available'
params={accountId}
/>
);
}
protected renderIcon (className: string = 'ui--AddressSummary-icon', size?: number) {
const { accounts_idAndIndex = [], identIconSize = 96, value, withIcon = true } = this.props;

View File

@ -0,0 +1,36 @@
// 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 } from './types';
import React from 'react';
import { AccountId, AccountIndex, Address } from '@polkadot/types';
import { Available } from '@polkadot/ui-reactive';
import { classes } from './util';
export type Props = BareProps & {
label?: string,
params?: AccountId | AccountIndex | Address | string | Uint8Array | null,
withLabel?: boolean
};
export default class AvailableDisplay extends React.PureComponent<Props> {
render () {
const { params, className, label, style } = this.props;
if (!params) {
return null;
}
return (
<Available
className={classes('ui--Available', className)}
label={label}
params={params}
style={style}
/>
);
}
}

View File

@ -2,19 +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 { SubmittableExtrinsic } from '@polkadot/api/promise/types';
import { ApiProps } from '@polkadot/ui-api/types';
import { assert, isFunction, isUndefined } from '@polkadot/util';
import { Index } from '@polkadot/types';
import { IExtrinsic } from '@polkadot/types/types';
import { ApiProps } from '@polkadot/ui-api/types';
import { SubmittableExtrinsic } from '@polkadot/api/promise/types';
import { QueueTx, QueueTx$ExtrinsicAdd, TxCallback } from './Status/types';
import React from 'react';
import { SubmittableResult } from '@polkadot/api';
import { withApi } from '@polkadot/ui-api';
import { assert, isFunction, isUndefined } from '@polkadot/util';
import { QueueConsumer } from './Status/Context';
import Button from './Button';
import { Button$Sizes } from './Button/types';
import { QueueConsumer } from './Status/Context';
type ConstructFn = () => Array<any>;
@ -26,17 +27,20 @@ type InjectedProps = {
type Props = ApiProps & {
accountId?: string,
accountNonce?: Index,
isPrimary?: boolean,
className?: string,
extrinsic?: IExtrinsic | SubmittableExtrinsic,
icon?: string,
iconSize?: Button$Sizes,
isDisabled?: boolean,
isNegative?: boolean,
isPrimary?: boolean,
label: React.ReactNode,
onClick?: () => any,
onFailed?: TxCallback,
onSuccess?: TxCallback,
onUpdate?: TxCallback,
params?: Array<any> | ConstructFn,
tx?: string,
extrinsic?: IExtrinsic | SubmittableExtrinsic
tx?: string
};
type InnerProps = Props & InjectedProps;
@ -52,17 +56,20 @@ class TxButtonInner extends React.PureComponent<InnerProps> {
} as State;
render () {
const { accountId, isDisabled, isNegative, isPrimary, label } = this.props;
const { accountId, className, icon, iconSize , isDisabled, isNegative, isPrimary, label } = this.props;
const { isSending } = this.state;
return (
<Button
className={className}
icon={icon}
isDisabled={isSending || isDisabled || !accountId}
isLoading={isSending}
isNegative={isNegative}
isPrimary={isUndefined(isPrimary) ? !isNegative : isPrimary}
label={label}
onClick={this.send}
size={iconSize}
/>
);
}

View File

@ -0,0 +1,31 @@
// 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 } from './types';
import BN from 'bn.js';
import React from 'react';
import { AccountId, AccountIndex, Address } from '@polkadot/types';
import { Unlocking } from '@polkadot/ui-reactive';
import { classes } from './util';
export type Props = BareProps & {
bonded?: BN | Array<BN>,
params?: AccountId | AccountIndex | Address | string | Uint8Array | null,
withLabel?: boolean
};
export default class UnlockingDisplay extends React.PureComponent<Props> {
render () {
const { params, className, style } = this.props;
return (
<Unlocking
className={classes('ui--Unlocking', className)}
params={params}
style={style}
/>
);
}
}

View File

@ -98,6 +98,11 @@ header .ui--Button-Group {
white-space: nowrap
}
.ui--AddressSummary-available > button.ui.icon.withDrawUnbonded {
padding: 0.3em;
margin-left: 0.4em;
}
.ui--AddressSummary-base {
padding: 0 1em;
text-align: center;

View File

@ -194,3 +194,4 @@
.ui.segment {
background: transparent;
}

View File

@ -4,5 +4,6 @@
export { default as classes } from './classes';
export { default as getAddrName } from './getAddrName';
export { default as max } from './max';
export { default as toAddress } from './toAddress';
export { default as toShortAddress } from './toShortAddress';

View File

@ -0,0 +1,11 @@
// 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 BN from 'bn.js';
export default function max (items: BN[]) {
return items.reduce((acc: BN, val: BN) => {
return (val.gt(acc) ? val : acc);
}, new BN(0));
}

View File

@ -0,0 +1,57 @@
// Copyright 2017-2019 @polkadot/ui-reactive 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, CallProps } from '@polkadot/ui-api/types';
import { AccountId, AccountIndex, Address, Balance, BalanceLock, BlockNumber } from '@polkadot/types';
import BN from 'bn.js';
import { formatBalance } from '@polkadot/util';
import { max } from '@polkadot/ui-app/util';
import React from 'react';
import { withCalls } from '@polkadot/ui-api';
type Props = BareProps & CallProps & {
balances_freeBalance?: Balance,
balances_locks?: Array<BalanceLock>,
chain_bestNumber?: BlockNumber,
children?: React.ReactNode,
label?: string,
params?: AccountId | AccountIndex | Address | string | Uint8Array | null
};
export class AvailableDisplay extends React.PureComponent<Props> {
render () {
const { balances_freeBalance, balances_locks, chain_bestNumber, children, className, label = '', style } = this.props;
let maxLock = new BN(0);
if (Array.isArray(balances_locks)) {
// only get the locks that are valid until passed the current block
const totals = balances_locks.filter((value) => chain_bestNumber && value.until.gt(chain_bestNumber));
// get the maximum of the locks according to https://crates.parity.io/srml_balances/index.html#terminology
maxLock = max(totals.map(({ amount }) => amount));
}
// FIXME it needs to take into account the balances.vesting()
const available = maxLock.eq(new BlockNumber('0xffffffffffffffffffffffffffffffff')) ? new Balance(0) : balances_freeBalance && balances_freeBalance.sub(maxLock) ;
return (
<div
className={className}
style={style}
>
{label}{
available
? formatBalance(available)
: formatBalance(balances_freeBalance)
}{children}
</div>
);
}
}
export default withCalls<Props>(
['query.balances.freeBalance', { paramName: 'params' }],
['query.balances.locks', { paramName: 'params' }],
'derive.chain.bestNumber'
)(AvailableDisplay);

View File

@ -0,0 +1,130 @@
// Copyright 2017-2019 @polkadot/ui-reactive authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { AccountId, AccountIndex, Address, Option, StakingLedger, UnlockChunk } from '@polkadot/types';
import { BareProps, CallProps } from '@polkadot/ui-api/types';
import BN from 'bn.js';
import { formatBalance } from '@polkadot/util';
import { I18nProps } from '@polkadot/ui-app/types';
import React from 'react';
import translate from '@polkadot/ui-app/translate';
import { TxButton } from '@polkadot/ui-app';
import { withCalls } from '@polkadot/ui-api';
type Props = BareProps & CallProps & I18nProps & {
balances_freeBalance?: BN,
chain_bestNumber?: BN,
controllerId?: AccountId,
params?: AccountId | AccountIndex | Address | string | Uint8Array | null,
remainingLabel?: string,
staking_ledger?: StakingLedger,
session_eraLength?: BN,
unlockings?: Array<UnlockChunk>
};
export class UnlockingDisplay extends React.PureComponent<Props> {
render () {
const { unlockings } = this.props;
return unlockings ?
<>
{this.renderUnlockableSum()}
{this.renderLocked()}
</>
: null ;
}
private remainingBlocks (era: BN) {
const { chain_bestNumber, session_eraLength } = this.props;
if (!chain_bestNumber || !session_eraLength || era.lten(0)) {
return new BN(0);
} else {
const remaining = session_eraLength.mul(era).sub(chain_bestNumber);
return remaining.lten(0) ? new BN(0) : remaining;
}
}
private renderLocked () {
const { className, controllerId, style, t, unlockings } = this.props;
if (!unlockings || !controllerId) return null;
const labels: {locked: string, remaining: string } = {
locked: t('locked '),
remaining: t(' blocks left')
};
const locked = unlockings.filter((chunk) => this.remainingBlocks(chunk.era).gtn(0));
return (
<div>
{locked.map((unlocking,index) => (
<div
className={className}
style={style}
key={index}
>
{labels.locked}{formatBalance(unlocking.value)} ({this.remainingBlocks(unlocking.era).toString()}{labels.remaining})
</div>
))}
</div>
);
}
private renderUnlockableSum () {
const { className, controllerId, style, t, unlockings } = this.props;
if (!unlockings || !unlockings[0] || !controllerId) return null;
const labelUnlockable = t('unlockable ');
const unlockable = unlockings.filter((chunk) => this.remainingBlocks(chunk.era).eqn(0));
const unlockableSum = unlockable.reduce(
(curr, prev) => {
return new UnlockChunk({ value: curr.value.add(prev.value) });
}, new UnlockChunk({ value: new BN(0), era: new BN(0) })
).value;
return (unlockableSum.gtn(0) ?
<div
className={className}
style={style}
key='unlockable'
>
{labelUnlockable}{formatBalance(unlockableSum)}
<TxButton
accountId={controllerId.toString()}
className='withDrawUnbonded'
icon='lock'
size='small'
isPrimary
key='unlock'
params={[]}
tx='staking.withdrawUnbonded'
/>
</div>
: null
);
}
}
export default translate(
withCalls<Props>(
['query.balances.freeBalance', { paramName: 'params' }],
['query.staking.bonded', {
paramName: 'params',
propName: 'controllerId',
transform: (value) =>
value.unwrapOr(null)
}],
['query.staking.ledger', {
paramName: 'controllerId',
propName: 'unlockings',
transform: (ledger: Option<StakingLedger>) =>
ledger.unwrapOr({ unlocking: null }).unlocking
}],
'derive.session.eraLength',
'derive.chain.bestNumber'
)(UnlockingDisplay)
);

View File

@ -3,6 +3,7 @@
// of the Apache-2.0 license. See the LICENSE file for details.
export { default as AccountIndex } from './AccountIndex';
export { default as Available } from './Available';
export { default as Balance } from './Balance';
export { default as Bonded } from './Bonded';
export { default as BestFinalized } from './BestFinalized';
@ -14,3 +15,4 @@ export { default as NodeVersion } from './NodeVersion';
export { default as Nonce } from './Nonce';
export { default as TimeNow } from './TimeNow';
export { default as TimePeriod } from './TimePeriod';
export { default as Unlocking } from './Unlocking';