Extend fund bonding warnings (#2980)
* Extend fund bonding warnings * Fix warning display
This commit is contained in:
parent
d345d3be36
commit
5460da3a48
@ -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}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -56,6 +56,7 @@ function Account ({ className = '', info: { controllerId, destination, destinati
|
||||
{isBondExtraOpen && (
|
||||
<BondExtra
|
||||
onClose={toggleBondExtra}
|
||||
stakingInfo={stakingAccount}
|
||||
stashId={stashId}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -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>
|
||||
|
||||
8
packages/page-staking/src/Actions/types.ts
Normal file
8
packages/page-staking/src/Actions/types.ts
Normal 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;
|
||||
}
|
||||
@ -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 || '')
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user