Suggest max values in bonding (leaving existential) (#2985)

This commit is contained in:
Jaco Greeff 2020-06-19 11:48:57 +02:00 committed by GitHub
parent 348a3625ba
commit 16eaca36f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 146 additions and 85 deletions

View File

@ -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

View File

@ -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 (

View File

@ -55,6 +55,7 @@ function Account ({ className = '', info: { controllerId, destination, destinati
<AddressSmall value={stashId} />
{isBondExtraOpen && (
<BondExtra
controllerId={controllerId}
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 { 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

View File

@ -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;

View File

@ -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>
);
}