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:
parent
48e6107a4b
commit
4d39523daf
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
36
packages/ui-app/src/Available.tsx
Normal file
36
packages/ui-app/src/Available.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
31
packages/ui-app/src/Unlocking.tsx
Normal file
31
packages/ui-app/src/Unlocking.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -194,3 +194,4 @@
|
||||
.ui.segment {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
11
packages/ui-app/src/util/max.ts
Normal file
11
packages/ui-app/src/util/max.ts
Normal 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));
|
||||
}
|
||||
57
packages/ui-reactive/src/Available.tsx
Normal file
57
packages/ui-reactive/src/Available.tsx
Normal 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);
|
||||
130
packages/ui-reactive/src/Unlocking.tsx
Normal file
130
packages/ui-reactive/src/Unlocking.tsx
Normal 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)
|
||||
);
|
||||
@ -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';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user