From 5460da3a481328511b9348bf152fcf803931cd76 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Thu, 18 Jun 2020 14:28:16 +0200 Subject: [PATCH] Extend fund bonding warnings (#2980) * Extend fund bonding warnings * Fix warning display --- .../src/Actions/Account/BondExtra.tsx | 22 +++++++--- .../Actions/Account/InputValidateAmount.tsx | 43 ++++++++++++------- .../src/Actions/Account/index.tsx | 1 + .../src/Actions/partials/Bond.tsx | 20 +++++---- packages/page-staking/src/Actions/types.ts | 8 ++++ packages/react-components/src/Input.tsx | 26 +++++------ .../react-components/src/InputBalance.tsx | 4 +- packages/react-components/src/InputNumber.tsx | 4 +- .../react-components/src/styles/components.ts | 8 ++++ 9 files changed, 92 insertions(+), 44 deletions(-) create mode 100644 packages/page-staking/src/Actions/types.ts diff --git a/packages/page-staking/src/Actions/Account/BondExtra.tsx b/packages/page-staking/src/Actions/Account/BondExtra.tsx index 0fc4d365ae..f28581f308 100644 --- a/packages/page-staking/src/Actions/Account/BondExtra.tsx +++ b/packages/page-staking/src/Actions/Account/BondExtra.tsx @@ -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 { +function BondExtra ({ onClose, stakingInfo, stashId }: Props): React.ReactElement { const { t } = useTranslation(); - const [amountError, setAmountError] = useState(null); + const [amountError, setAmountError] = useState(null); const [maxAdditional, setMaxAdditional] = useState(); const [maxBalance] = useState(); + const currentAmount = useMemo( + () => stakingInfo && stakingInfo.stakingLedger?.active.unwrap(), + [stakingInfo] + ); return ( { ('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('additional bonded funds')} labelExtra={ { /> -

{t('Ensure that not all funds are locked, funds need to be available for fees.')}

+

{t('The amount placed at-stake should be no more that 95% of your available amount to protect against slashing events.')}

@@ -72,7 +82,7 @@ function BondExtra ({ onClose, stashId }: Props): React.ReactElement { ('Bond more')} onStart={onClose} diff --git a/packages/page-staking/src/Actions/Account/InputValidateAmount.tsx b/packages/page-staking/src/Actions/Account/InputValidateAmount.tsx index 24c431b0d0..69b0b89be6 100644 --- a/packages/page-staking/src/Actions/Account/InputValidateAmount.tsx +++ b/packages/page-staking/src/Actions/Account/InputValidateAmount.tsx @@ -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 | null { +function ValidateAmount ({ accountId, currentAmount, onError, value }: Props): React.ReactElement | null { const { t } = useTranslation(); const { api } = useApi(); const allBalances = useCall(api.derive.balances.all, [accountId]); - const [error, setError] = useState(null); + const [{ error, warning }, setResult] = useState({ 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('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('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 ( +
+
{error || warning}
+
+ ); } - return ( -
-
{error}
-
- ); + return null; } export default React.memo(ValidateAmount); diff --git a/packages/page-staking/src/Actions/Account/index.tsx b/packages/page-staking/src/Actions/Account/index.tsx index db7ead3fd9..80ddd42ea7 100644 --- a/packages/page-staking/src/Actions/Account/index.tsx +++ b/packages/page-staking/src/Actions/Account/index.tsx @@ -56,6 +56,7 @@ function Account ({ className = '', info: { controllerId, destination, destinati {isBondExtraOpen && ( )} diff --git a/packages/page-staking/src/Actions/partials/Bond.tsx b/packages/page-staking/src/Actions/partials/Bond.tsx index f357270131..0f24ee5f32 100644 --- a/packages/page-staking/src/Actions/partials/Bond.tsx +++ b/packages/page-staking/src/Actions/partials/Bond.tsx @@ -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 { const { t } = useTranslation(); const { api } = useApi(); const [amount, setAmount] = useState(); - const [amountError, setAmountError] = useState(null); + const [amountError, setAmountError] = useState(null); const [controllerError, setControllerError] = useState(false); const [controllerId, setControllerId] = useState(null); const [destination, setDestination] = useState(0); @@ -40,7 +41,7 @@ function Bond ({ className = '', onChange }: Props): React.ReactElement { 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 { 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 { ('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('value bonded')} labelExtra={ { } onChange={setAmount} /> + {bondedBlocks?.gtn(0) && ( ('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 { )} -

{t('The amount placed at-stake should be no more that 95% of your available amount to protect against slashing events.')}

diff --git a/packages/page-staking/src/Actions/types.ts b/packages/page-staking/src/Actions/types.ts new file mode 100644 index 0000000000..d2170723ec --- /dev/null +++ b/packages/page-staking/src/Actions/types.ts @@ -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; +} diff --git a/packages/react-components/src/Input.tsx b/packages/react-components/src/Input.tsx index 67243f8191..b397c547f7 100644 --- a/packages/react-components/src/Input.tsx +++ b/packages/react-components/src/Input.tsx @@ -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 { +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 { const [stateName] = useState(`in_${counter++}_at_${Date.now()}`); const _onBlur = useCallback( @@ -147,17 +148,18 @@ function Input ({ autoFocus = false, children, className, defaultValue, help, ic { +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 { const [defaultValue, setDefaultValue] = useState(); 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} diff --git a/packages/react-components/src/InputNumber.tsx b/packages/react-components/src/InputNumber.tsx index 7b66f5359f..0ae3e0caf3 100644 --- a/packages/react-components/src/InputNumber.tsx +++ b/packages/react-components/src/InputNumber.tsx @@ -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 { +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 { const { t } = useTranslation(); const [si, setSi] = useState(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} diff --git a/packages/react-components/src/styles/components.ts b/packages/react-components/src/styles/components.ts index cb0c3b9f23..7ffa7c5f5f 100644 --- a/packages/react-components/src/styles/components.ts +++ b/packages/react-components/src/styles/components.ts @@ -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;