Extend fund bonding warnings (#2980)

* Extend fund bonding warnings

* Fix warning display
This commit is contained in:
Jaco Greeff 2020-06-18 14:28:16 +02:00 committed by GitHub
parent d345d3be36
commit 5460da3a48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 92 additions and 44 deletions

View File

@ -2,8 +2,11 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { DeriveStakingAccount } from '@polkadot/api-derive/types';
import { AmountValidateState } from '../types';
import BN from 'bn.js';
import React, { useState } from 'react';
import React, { useMemo, useState } from 'react';
import { InputAddress, InputBalance, Modal, TxButton } from '@polkadot/react-components';
import { BalanceFree } from '@polkadot/react-query';
import { BN_ZERO } from '@polkadot/util';
@ -13,14 +16,19 @@ import ValidateAmount from './InputValidateAmount';
interface Props {
onClose: () => void;
stakingInfo?: DeriveStakingAccount;
stashId: string;
}
function BondExtra ({ onClose, stashId }: Props): React.ReactElement<Props> {
function BondExtra ({ onClose, stakingInfo, stashId }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [amountError, setAmountError] = useState<string | null>(null);
const [amountError, setAmountError] = useState<AmountValidateState | null>(null);
const [maxAdditional, setMaxAdditional] = useState<BN | undefined>();
const [maxBalance] = useState<BN | undefined>();
const currentAmount = useMemo(
() => stakingInfo && stakingInfo.stakingLedger?.active.unwrap(),
[stakingInfo]
);
return (
<Modal
@ -46,7 +54,8 @@ function BondExtra ({ onClose, stashId }: Props): React.ReactElement<Props> {
<InputBalance
autoFocus
help={t<string>('Amount to add to the currently bonded funds. This is adjusted using the available funds on the account.')}
isError={!!amountError || !maxAdditional || maxAdditional.eqn(0)}
isError={!!amountError?.error || !maxAdditional || maxAdditional.eqn(0)}
isWarning={!!amountError?.warning}
label={t<string>('additional bonded funds')}
labelExtra={
<BalanceFree
@ -59,12 +68,13 @@ function BondExtra ({ onClose, stashId }: Props): React.ReactElement<Props> {
/>
<ValidateAmount
accountId={stashId}
currentAmount={currentAmount}
onError={setAmountError}
value={maxAdditional}
/>
</Modal.Column>
<Modal.Column>
<p>{t<string>('Ensure that not all funds are locked, funds need to be available for fees.')}</p>
<p>{t<string>('The amount placed at-stake should be no more that 95% of your available amount to protect against slashing events.')}</p>
</Modal.Column>
</Modal.Columns>
</Modal.Content>
@ -72,7 +82,7 @@ function BondExtra ({ onClose, stashId }: Props): React.ReactElement<Props> {
<TxButton
accountId={stashId}
icon='sign-in'
isDisabled={!maxAdditional?.gt(BN_ZERO)}
isDisabled={!maxAdditional?.gt(BN_ZERO) || !!amountError?.error}
isPrimary
label={t<string>('Bond more')}
onStart={onClose}

View File

@ -3,50 +3,63 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import { DeriveBalancesAll } from '@polkadot/api-derive/types';
import { AmountValidateState } from '../types';
import BN from 'bn.js';
import React, { useEffect, useState } from 'react';
import { Icon } from '@polkadot/react-components';
import { useApi, useCall } from '@polkadot/react-hooks';
import { BN_ZERO } from '@polkadot/util';
import { useTranslation } from '../../translate';
interface Props {
accountId: string | null;
onError: (error: string | null) => void;
currentAmount?: BN | null;
onError: (state: AmountValidateState | null) => void;
value?: BN | null;
}
function ValidateAmount ({ accountId, onError, value }: Props): React.ReactElement<Props> | null {
function ValidateAmount ({ accountId, currentAmount, onError, value }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { api } = useApi();
const allBalances = useCall<DeriveBalancesAll>(api.derive.balances.all, [accountId]);
const [error, setError] = useState<string | null>(null);
const [{ error, warning }, setResult] = useState<AmountValidateState>({ error: null, warning: null });
useEffect((): void => {
// don't show an error if the selected controller is the default
// this applies when changing controller
if (allBalances && value) {
let newError: string | null = null;
let newWarning: string | null = null;
const check = value.add(currentAmount || BN_ZERO);
if (value.gt(allBalances.freeBalance)) {
if (check.gte(allBalances.freeBalance)) {
newError = t<string>('The specified value is greater than your free balance. The node will bond the maximum amount available.');
} else if (check.gt(allBalances.freeBalance.muln(95).divn(100))) {
newWarning = t<string>('The specified value is greater than the recommended amount. You may not be adequately protected against slashing events or have funds for future fees.');
}
onError(newError);
setError((error) => error !== newError ? newError : error);
}
}, [allBalances, onError, t, value]);
setResult((state): AmountValidateState => {
const error = state.error !== newError ? newError : state.error;
const warning = state.warning !== newWarning ? newWarning : state.warning;
if (!error) {
return null;
onError((error || warning) ? { error, warning } : null);
return { error, warning };
});
}
}, [allBalances, currentAmount, onError, t, value]);
if (error || warning) {
return (
<article className={error ? 'error' : 'warning'}>
<div><Icon name='warning sign' />{error || warning}</div>
</article>
);
}
return (
<article className='warning'>
<div><Icon name='warning sign' />{error}</div>
</article>
);
return null;
}
export default React.memo(ValidateAmount);

View File

@ -56,6 +56,7 @@ function Account ({ className = '', info: { controllerId, destination, destinati
{isBondExtraOpen && (
<BondExtra
onClose={toggleBondExtra}
stakingInfo={stakingAccount}
stashId={stashId}
/>
)}

View File

@ -2,6 +2,7 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { AmountValidateState } from '../types';
import { BondInfo } from './types';
import BN from 'bn.js';
@ -25,7 +26,7 @@ function Bond ({ className = '', onChange }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const [amount, setAmount] = useState<BN | undefined>();
const [amountError, setAmountError] = useState<string | null>(null);
const [amountError, setAmountError] = useState<AmountValidateState | null>(null);
const [controllerError, setControllerError] = useState<boolean>(false);
const [controllerId, setControllerId] = useState<string | null>(null);
const [destination, setDestination] = useState(0);
@ -40,7 +41,7 @@ function Bond ({ className = '', onChange }: Props): React.ReactElement<Props> {
useEffect((): void => {
onChange(
(amount && amount.gtn(0) && !controllerError && controllerId && stashId)
(amount && amount.gtn(0) && !amountError?.error && !controllerError && controllerId && stashId)
? {
bondOwnTx: api.tx.staking.bond(stashId, amount, destination),
bondTx: api.tx.staking.bond(controllerId, amount, destination),
@ -56,7 +57,7 @@ function Bond ({ className = '', onChange }: Props): React.ReactElement<Props> {
stashId: null
}
);
}, [api, amount, controllerError, controllerId, destination, stashId, onChange]);
}, [api, amount, amountError, controllerError, controllerId, destination, stashId, onChange]);
const hasValue = !!amount?.gtn(0);
@ -93,7 +94,8 @@ function Bond ({ className = '', onChange }: Props): React.ReactElement<Props> {
<InputBalance
autoFocus
help={t<string>('The total amount of the stash balance that will be at stake in any forthcoming rounds (should be less than the free amount available)')}
isError={!hasValue || !!amountError}
isError={!hasValue || !!amountError?.error}
isWarning={!!amountError?.warning}
label={t<string>('value bonded')}
labelExtra={
<BalanceFree
@ -103,6 +105,11 @@ function Bond ({ className = '', onChange }: Props): React.ReactElement<Props> {
}
onChange={setAmount}
/>
<InputValidateAmount
accountId={stashId}
onError={setAmountError}
value={amount}
/>
{bondedBlocks?.gtn(0) && (
<Static
help={t<string>('The bonding duration for any staked funds. Needs to be unlocked and withdrawn to become available.')}
@ -111,11 +118,6 @@ function Bond ({ className = '', onChange }: Props): React.ReactElement<Props> {
<BlockToTime blocks={bondedBlocks} />
</Static>
)}
<InputValidateAmount
accountId={stashId}
onError={setAmountError}
value={amount}
/>
</Modal.Column>
<Modal.Column>
<p>{t<string>('The amount placed at-stake should be no more that 95% of your available amount to protect against slashing events.')}</p>

View File

@ -0,0 +1,8 @@
// Copyright 2017-2020 @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.
export interface AmountValidateState {
error: string | null;
warning: string | null;
}

View File

@ -28,6 +28,7 @@ interface Props extends BareProps {
isHidden?: boolean;
isInPlaceEditor?: boolean;
isReadOnly?: boolean;
isWarning?: boolean;
label?: React.ReactNode;
labelExtra?: React.ReactNode;
max?: number;
@ -91,7 +92,7 @@ const isSelectAll = (key: string, isPreKeyDown: boolean): boolean =>
let counter = 0;
function Input ({ autoFocus = false, children, className, defaultValue, help, icon, inputClassName, isAction = false, isDisabled = false, isDisabledError = false, isEditable = false, isError = false, isFull = false, isHidden = false, isInPlaceEditor = false, isReadOnly = false, label, labelExtra, max, maxLength, min, name, onBlur, onChange, onEnter, onEscape, onKeyDown, onKeyUp, onPaste, placeholder, tabIndex, type = 'text', value, withEllipsis, withLabel }: Props): React.ReactElement<Props> {
function Input ({ autoFocus = false, children, className, defaultValue, help, icon, inputClassName, isAction = false, isDisabled = false, isDisabledError = false, isEditable = false, isError = false, isFull = false, isHidden = false, isInPlaceEditor = false, isReadOnly = false, isWarning = false, label, labelExtra, max, maxLength, min, name, onBlur, onChange, onEnter, onEscape, onKeyDown, onKeyUp, onPaste, placeholder, tabIndex, type = 'text', value, withEllipsis, withLabel }: Props): React.ReactElement<Props> {
const [stateName] = useState(`in_${counter++}_at_${Date.now()}`);
const _onBlur = useCallback(
@ -147,17 +148,18 @@ function Input ({ autoFocus = false, children, className, defaultValue, help, ic
<SUIInput
action={isAction}
autoFocus={autoFocus}
className={
[
isEditable
? 'ui--Input edit icon'
: 'ui--Input',
isInPlaceEditor
? 'inPlaceEditor'
: '',
inputClassName || ''
].join(' ')
}
className={[
isEditable
? 'ui--Input edit icon'
: 'ui--Input',
isInPlaceEditor
? 'inPlaceEditor'
: '',
inputClassName || '',
isWarning && !isError
? 'isWarning'
: ''
].join(' ')}
defaultValue={
isUndefined(value)
? (defaultValue || '')

View File

@ -18,6 +18,7 @@ interface Props extends BareProps {
isDisabled?: boolean;
isError?: boolean;
isFull?: boolean;
isWarning?: boolean;
isZeroable?: boolean;
label?: React.ReactNode;
labelExtra?: React.ReactNode;
@ -54,7 +55,7 @@ function reformat (value: string | BN, isDisabled?: boolean): string {
return formatBalance(value, { forceUnit: '-', withSi: false }).replace(',', isDisabled ? ',' : '');
}
function InputBalance ({ autoFocus, className = '', defaultValue: inDefault, help, isDisabled, isError, isFull, isZeroable, label, labelExtra, maxValue, onChange, onEnter, onEscape, placeholder, value, withEllipsis, withLabel, withMax }: Props): React.ReactElement<Props> {
function InputBalance ({ autoFocus, className = '', defaultValue: inDefault, help, isDisabled, isError, isFull, isWarning, isZeroable, label, labelExtra, maxValue, onChange, onEnter, onEscape, placeholder, value, withEllipsis, withLabel, withMax }: Props): React.ReactElement<Props> {
const [defaultValue, setDefaultValue] = useState<string | undefined>();
useEffect((): void => {
@ -74,6 +75,7 @@ function InputBalance ({ autoFocus, className = '', defaultValue: inDefault, hel
isError={isError}
isFull={isFull}
isSi
isWarning={isWarning}
isZeroable={isZeroable}
label={label}
labelExtra={labelExtra}

View File

@ -25,6 +25,7 @@ interface Props extends BareProps {
isFull?: boolean;
isSi?: boolean;
isDecimal?: boolean;
isWarning?: boolean;
isZeroable?: boolean;
label?: React.ReactNode;
labelExtra?: React.ReactNode;
@ -159,7 +160,7 @@ function getValues (value: BN | string = BN_ZERO, si: SiDef | null, bitLength: B
: getValuesFromString(value, si, bitLength, isZeroable, maxValue);
}
function InputNumber ({ autoFocus, bitLength = DEFAULT_BITLENGTH, children, className = '', defaultValue, help, isDecimal, isFull, isSi, isDisabled, isError = false, isZeroable = true, label, labelExtra, maxLength, maxValue, onChange, onEnter, onEscape, placeholder, value: propsValue }: Props): React.ReactElement<Props> {
function InputNumber ({ autoFocus, bitLength = DEFAULT_BITLENGTH, children, className = '', defaultValue, help, isDecimal, isFull, isSi, isDisabled, isError = false, isWarning, isZeroable = true, label, labelExtra, maxLength, maxValue, onChange, onEnter, onEscape, placeholder, value: propsValue }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [si, setSi] = useState<SiDef | null>(isSi ? formatBalance.findSi('-') : null);
const [isPreKeyDown, setIsPreKeyDown] = useState(false);
@ -248,6 +249,7 @@ function InputNumber ({ autoFocus, bitLength = DEFAULT_BITLENGTH, children, clas
isDisabled={isDisabled}
isError={!isValid || isError}
isFull={isFull}
isWarning={isWarning}
label={label}
labelExtra={labelExtra}
maxLength={maxLength || maxValueLength}

View File

@ -94,6 +94,14 @@ export default css`
}
}
&.isWarning.ui.input {
> input,
input:focus {
background: #ffffe0;
border-color: #eeeeae;
}
}
.ui--SiDropdown {
width: 6rem;
text-align: center;