diff --git a/packages/page-staking/src/Actions/Account/BondExtra.tsx b/packages/page-staking/src/Actions/Account/BondExtra.tsx index f28581f308..e951f59e5f 100644 --- a/packages/page-staking/src/Actions/Account/BondExtra.tsx +++ b/packages/page-staking/src/Actions/Account/BondExtra.tsx @@ -2,12 +2,13 @@ // 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 { DeriveBalancesAll, DeriveStakingAccount } from '@polkadot/api-derive/types'; import { AmountValidateState } from '../types'; import BN from 'bn.js'; -import React, { useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { InputAddress, InputBalance, Modal, TxButton } from '@polkadot/react-components'; +import { useApi, useCall } from '@polkadot/react-hooks'; import { BalanceFree } from '@polkadot/react-query'; import { BN_ZERO } from '@polkadot/util'; @@ -15,21 +16,37 @@ import { useTranslation } from '../../translate'; import ValidateAmount from './InputValidateAmount'; interface Props { + controllerId: string | null; onClose: () => void; stakingInfo?: DeriveStakingAccount; stashId: string; } -function BondExtra ({ onClose, stakingInfo, stashId }: Props): React.ReactElement { +function BondExtra ({ controllerId, onClose, stakingInfo, stashId }: Props): React.ReactElement { const { t } = useTranslation(); + const { api } = useApi(); const [amountError, setAmountError] = useState(null); const [maxAdditional, setMaxAdditional] = useState(); const [maxBalance] = useState(); + const [startBalance, setStartBalance] = useState(null); + const stashBalance = useCall(api.derive.balances.all, [stashId]); const currentAmount = useMemo( () => stakingInfo && stakingInfo.stakingLedger?.active.unwrap(), [stakingInfo] ); + useEffect((): void => { + if (stakingInfo && stakingInfo.stakingLedger && stashBalance) { + const available = stashBalance.freeBalance.sub(stakingInfo.stakingLedger.active.unwrap()); + + setStartBalance( + available.gt(api.consts.balances.existentialDeposit) + ? available.sub(api.consts.balances.existentialDeposit) + : BN_ZERO + ); + } + }, [api, stakingInfo, stashBalance]); + return ( {t('Since this transaction deals with funding, the stash account will be used.')}

- - - ('Amount to add to the currently bonded funds. This is adjusted using the available funds on the account.')} - isError={!!amountError?.error || !maxAdditional || maxAdditional.eqn(0)} - isWarning={!!amountError?.warning} - label={t('additional bonded funds')} - labelExtra={ - {t('balance')}} - params={stashId} - /> - } - maxValue={maxBalance} - onChange={setMaxAdditional} - /> - - - -

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

-
-
+ {startBalance && ( + + + ('Amount to add to the currently bonded funds. This is adjusted using the available funds on the account.')} + isError={!!amountError?.error || !maxAdditional || maxAdditional.eqn(0)} + label={t('additional bonded funds')} + labelExtra={ + {t('balance')}} + params={stashId} + /> + } + maxValue={maxBalance} + onChange={setMaxAdditional} + /> + + + +

{t('The amount placed at-stake should allow some free funds for future transactions.')}

+
+
+ )} void; + stashId: string | null; value?: BN | null; } -function ValidateAmount ({ accountId, currentAmount, onError, value }: Props): React.ReactElement | null { +function formatExistential (value: BN): string { + let fmt = (value.mul(BN_THOUSAND).div(BN_TEN.pow(new BN(formatBalance.getDefaults().decimals))).toNumber() / 1000).toFixed(3); + + while (fmt.length !== 1 && ['.', '0'].includes(fmt[fmt.length - 1])) { + const isLast = fmt.endsWith('.'); + + fmt = fmt.substr(0, fmt.length - 1); + + if (isLast) { + break; + } + } + + return fmt; +} + +function ValidateAmount ({ currentAmount, onError, stashId, value }: Props): React.ReactElement | null { const { t } = useTranslation(); const { api } = useApi(); - const allBalances = useCall(api.derive.balances.all, [accountId]); + const stashBalance = useCall(api.derive.balances.all, [stashId]); 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; + if (stashBalance && value) { + // also used in bond extra, take check against total of current bonded and new const check = value.add(currentAmount || BN_ZERO); + const existentialDeposit = api.consts.balances.existentialDeposit; + const maxBond = stashBalance.freeBalance.sub(existentialDeposit.divn(2)); + let newError: string | null = null; - 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.'); + if (check.gte(maxBond)) { + newError = t('The specified value is too large and does not allow funds to pay future transaction fees.'); + } else if (check.lt(existentialDeposit)) { + newError = t('The bonded amount is less than the minimum bond amount of {{existentialDeposit}}', { + replace: { existentialDeposit: formatExistential(existentialDeposit) } + }); } setResult((state): AmountValidateState => { const error = state.error !== newError ? newError : state.error; - const warning = state.warning !== newWarning ? newWarning : state.warning; + const warning = state.warning; onError((error || warning) ? { error, warning } : null); return { error, warning }; }); } - }, [allBalances, currentAmount, onError, t, value]); + }, [api, currentAmount, onError, stashBalance, t, value]); if (error || warning) { return ( diff --git a/packages/page-staking/src/Actions/Account/index.tsx b/packages/page-staking/src/Actions/Account/index.tsx index 80ddd42ea7..a2a0482c4e 100644 --- a/packages/page-staking/src/Actions/Account/index.tsx +++ b/packages/page-staking/src/Actions/Account/index.tsx @@ -55,6 +55,7 @@ function Account ({ className = '', info: { controllerId, destination, destinati {isBondExtraOpen && ( { const [controllerId, setControllerId] = useState(null); const [destination, setDestination] = useState(0); const [stashId, setStashId] = useState(null); + const [startBalance, setStartBalance] = useState(null); + const stashBalance = useCall(api.derive.balances.all, [stashId]); const bondedBlocks = useUnbondDuration(); const _setError = useCallback( @@ -39,6 +43,18 @@ function Bond ({ className = '', onChange }: Props): React.ReactElement { [] ); + useEffect((): void => { + stashBalance && setStartBalance( + stashBalance.freeBalance.gt(api.consts.balances.existentialDeposit) + ? stashBalance.freeBalance.sub(api.consts.balances.existentialDeposit) + : BN_ZERO + ); + }, [api, stashBalance]); + + useEffect((): void => { + setStartBalance(null); + }, [stashId]); + useEffect((): void => { onChange( (amount && amount.gtn(0) && !amountError?.error && !controllerError && controllerId && stashId) @@ -89,41 +105,44 @@ function Bond ({ className = '', onChange }: Props): React.ReactElement {

{t('To ensure optimal fund security using the same stash/controller is strongly discouraged, but not forbidden.')}

- - - ('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?.error} - isWarning={!!amountError?.warning} - label={t('value bonded')} - labelExtra={ - {t('balance')}} - params={stashId} - /> - } - onChange={setAmount} - /> - - {bondedBlocks?.gtn(0) && ( - ('The bonding duration for any staked funds. Needs to be unlocked and withdrawn to become available.')} - label={t('on-chain bonding duration')} - > - - - )} - - -

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

-

{t('Once bonded, it wil need to be unlocked/withdrawn and will be locked for at least the bonding duration.')}

-
-
+ {startBalance && ( + + + ('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?.error} + label={t('value bonded')} + labelExtra={ + {t('balance')}} + params={stashId} + /> + } + onChange={setAmount} + /> + + {bondedBlocks?.gtn(0) && ( + ('The bonding duration for any staked funds. Needs to be unlocked and withdrawn to become available.')} + label={t('on-chain bonding duration')} + > + + + )} + + +

{t('The amount placed at-stake should not be your full available available amount to allow for transaction fees.')}

+

{t('Once bonded, it wil need to be unlocked/withdrawn and will be locked for at least the bonding duration.')}

+
+
+ )} { +function InputBalance ({ autoFocus, children, 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 => { @@ -88,7 +88,9 @@ function InputBalance ({ autoFocus, className = '', defaultValue: inDefault, hel withEllipsis={withEllipsis} withLabel={withLabel} withMax={withMax} - /> + > + {children} + ); }