Suggest max values in bonding (leaving existential) (#2985)
This commit is contained in:
parent
348a3625ba
commit
16eaca36f6
@ -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<Props> {
|
||||
function BondExtra ({ controllerId, onClose, stakingInfo, stashId }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [amountError, setAmountError] = useState<AmountValidateState | null>(null);
|
||||
const [maxAdditional, setMaxAdditional] = useState<BN | undefined>();
|
||||
const [maxBalance] = useState<BN | undefined>();
|
||||
const [startBalance, setStartBalance] = useState<BN | null>(null);
|
||||
const stashBalance = useCall<DeriveBalancesAll>(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 (
|
||||
<Modal
|
||||
className='staking--BondExtra'
|
||||
@ -49,34 +66,37 @@ function BondExtra ({ onClose, stakingInfo, stashId }: Props): React.ReactElemen
|
||||
<p>{t<string>('Since this transaction deals with funding, the stash account will be used.')}</p>
|
||||
</Modal.Column>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns>
|
||||
<Modal.Column>
|
||||
<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?.error || !maxAdditional || maxAdditional.eqn(0)}
|
||||
isWarning={!!amountError?.warning}
|
||||
label={t<string>('additional bonded funds')}
|
||||
labelExtra={
|
||||
<BalanceFree
|
||||
label={<span className='label'>{t<string>('balance')}</span>}
|
||||
params={stashId}
|
||||
/>
|
||||
}
|
||||
maxValue={maxBalance}
|
||||
onChange={setMaxAdditional}
|
||||
/>
|
||||
<ValidateAmount
|
||||
accountId={stashId}
|
||||
currentAmount={currentAmount}
|
||||
onError={setAmountError}
|
||||
value={maxAdditional}
|
||||
/>
|
||||
</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>
|
||||
</Modal.Column>
|
||||
</Modal.Columns>
|
||||
{startBalance && (
|
||||
<Modal.Columns>
|
||||
<Modal.Column>
|
||||
<InputBalance
|
||||
autoFocus
|
||||
defaultValue={startBalance}
|
||||
help={t<string>('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<string>('additional bonded funds')}
|
||||
labelExtra={
|
||||
<BalanceFree
|
||||
label={<span className='label'>{t<string>('balance')}</span>}
|
||||
params={stashId}
|
||||
/>
|
||||
}
|
||||
maxValue={maxBalance}
|
||||
onChange={setMaxAdditional}
|
||||
/>
|
||||
<ValidateAmount
|
||||
controllerId={controllerId}
|
||||
currentAmount={currentAmount}
|
||||
onError={setAmountError}
|
||||
stashId={stashId}
|
||||
value={maxAdditional}
|
||||
/>
|
||||
</Modal.Column>
|
||||
<Modal.Column>
|
||||
<p>{t<string>('The amount placed at-stake should allow some free funds for future transactions.')}</p>
|
||||
</Modal.Column>
|
||||
</Modal.Columns>
|
||||
)}
|
||||
</Modal.Content>
|
||||
<Modal.Actions onCancel={onClose}>
|
||||
<TxButton
|
||||
|
||||
@ -9,47 +9,66 @@ 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 { BN_TEN, BN_THOUSAND, BN_ZERO, formatBalance } from '@polkadot/util';
|
||||
|
||||
import { useTranslation } from '../../translate';
|
||||
|
||||
interface Props {
|
||||
accountId: string | null;
|
||||
controllerId: string | null;
|
||||
currentAmount?: BN | null;
|
||||
onError: (state: AmountValidateState | null) => void;
|
||||
stashId: string | null;
|
||||
value?: BN | null;
|
||||
}
|
||||
|
||||
function ValidateAmount ({ accountId, currentAmount, onError, value }: Props): React.ReactElement<Props> | 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<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const allBalances = useCall<DeriveBalancesAll>(api.derive.balances.all, [accountId]);
|
||||
const stashBalance = useCall<DeriveBalancesAll>(api.derive.balances.all, [stashId]);
|
||||
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;
|
||||
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<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.');
|
||||
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 (
|
||||
|
||||
@ -55,6 +55,7 @@ function Account ({ className = '', info: { controllerId, destination, destinati
|
||||
<AddressSmall value={stashId} />
|
||||
{isBondExtraOpen && (
|
||||
<BondExtra
|
||||
controllerId={controllerId}
|
||||
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 { DeriveBalancesAll } from '@polkadot/api-derive/types';
|
||||
import { AmountValidateState } from '../types';
|
||||
import { BondInfo } from './types';
|
||||
|
||||
@ -9,7 +10,8 @@ import BN from 'bn.js';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Dropdown, InputAddress, InputBalance, Modal, Static } from '@polkadot/react-components';
|
||||
import { BalanceFree, BlockToTime } from '@polkadot/react-query';
|
||||
import { useApi } from '@polkadot/react-hooks';
|
||||
import { useApi, useCall } from '@polkadot/react-hooks';
|
||||
import { BN_ZERO } from '@polkadot/util';
|
||||
|
||||
import { useTranslation } from '../../translate';
|
||||
import InputValidateAmount from '../Account/InputValidateAmount';
|
||||
@ -31,6 +33,8 @@ function Bond ({ className = '', onChange }: Props): React.ReactElement<Props> {
|
||||
const [controllerId, setControllerId] = useState<string | null>(null);
|
||||
const [destination, setDestination] = useState(0);
|
||||
const [stashId, setStashId] = useState<string | null>(null);
|
||||
const [startBalance, setStartBalance] = useState<BN | null>(null);
|
||||
const stashBalance = useCall<DeriveBalancesAll>(api.derive.balances.all, [stashId]);
|
||||
const bondedBlocks = useUnbondDuration();
|
||||
|
||||
const _setError = useCallback(
|
||||
@ -39,6 +43,18 @@ function Bond ({ className = '', onChange }: Props): React.ReactElement<Props> {
|
||||
[]
|
||||
);
|
||||
|
||||
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<Props> {
|
||||
<p>{t<string>('To ensure optimal fund security using the same stash/controller is strongly discouraged, but not forbidden.')}</p>
|
||||
</Modal.Column>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns>
|
||||
<Modal.Column>
|
||||
<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?.error}
|
||||
isWarning={!!amountError?.warning}
|
||||
label={t<string>('value bonded')}
|
||||
labelExtra={
|
||||
<BalanceFree
|
||||
label={<span className='label'>{t<string>('balance')}</span>}
|
||||
params={stashId}
|
||||
/>
|
||||
}
|
||||
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.')}
|
||||
label={t<string>('on-chain bonding duration')}
|
||||
>
|
||||
<BlockToTime blocks={bondedBlocks} />
|
||||
</Static>
|
||||
)}
|
||||
</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>
|
||||
<p>{t<string>('Once bonded, it wil need to be unlocked/withdrawn and will be locked for at least the bonding duration.')}</p>
|
||||
</Modal.Column>
|
||||
</Modal.Columns>
|
||||
{startBalance && (
|
||||
<Modal.Columns>
|
||||
<Modal.Column>
|
||||
<InputBalance
|
||||
autoFocus
|
||||
defaultValue={startBalance}
|
||||
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?.error}
|
||||
label={t<string>('value bonded')}
|
||||
labelExtra={
|
||||
<BalanceFree
|
||||
label={<span className='label'>{t<string>('balance')}</span>}
|
||||
params={stashId}
|
||||
/>
|
||||
}
|
||||
onChange={setAmount}
|
||||
/>
|
||||
<InputValidateAmount
|
||||
controllerId={controllerId}
|
||||
onError={setAmountError}
|
||||
stashId={stashId}
|
||||
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.')}
|
||||
label={t<string>('on-chain bonding duration')}
|
||||
>
|
||||
<BlockToTime blocks={bondedBlocks} />
|
||||
</Static>
|
||||
)}
|
||||
</Modal.Column>
|
||||
<Modal.Column>
|
||||
<p>{t<string>('The amount placed at-stake should not be your full available available amount to allow for transaction fees.')}</p>
|
||||
<p>{t<string>('Once bonded, it wil need to be unlocked/withdrawn and will be locked for at least the bonding duration.')}</p>
|
||||
</Modal.Column>
|
||||
</Modal.Columns>
|
||||
)}
|
||||
<Modal.Columns>
|
||||
<Modal.Column>
|
||||
<Dropdown
|
||||
|
||||
@ -56,7 +56,7 @@ interface ChainData {
|
||||
// });
|
||||
// });
|
||||
|
||||
const DEFAULT_DECIMALS = registry.createType('u32', 12);
|
||||
const DEFAULT_DECIMALS = registry.createType('u32', 15);
|
||||
const DEFAULT_SS58 = registry.createType('u32', addressDefaults.prefix);
|
||||
const injectedPromise = web3Enable('polkadot-js/apps');
|
||||
let api: ApiPromise;
|
||||
|
||||
@ -55,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, isWarning, isZeroable, label, labelExtra, maxValue, onChange, onEnter, onEscape, placeholder, value, withEllipsis, withLabel, withMax }: Props): React.ReactElement<Props> {
|
||||
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<Props> {
|
||||
const [defaultValue, setDefaultValue] = useState<string | undefined>();
|
||||
|
||||
useEffect((): void => {
|
||||
@ -88,7 +88,9 @@ function InputBalance ({ autoFocus, className = '', defaultValue: inDefault, hel
|
||||
withEllipsis={withEllipsis}
|
||||
withLabel={withLabel}
|
||||
withMax={withMax}
|
||||
/>
|
||||
>
|
||||
{children}
|
||||
</InputNumber>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user