Warn on staking min bond (#5674)

* Warn on staking min bond

* Warn on min validator amount
This commit is contained in:
Jaco 2021-06-24 14:46:09 +03:00 committed by GitHub
parent 5170f480a9
commit d91bdcd191
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 179 additions and 29 deletions

View File

@ -17,14 +17,21 @@ interface Props {
controllerId: string | null;
currentAmount?: BN | null;
isNominating?: boolean;
minNomination?: BN;
minNominated?: BN;
minNominatorBond?: BN;
minValidatorBond?: BN;
onError: (state: AmountValidateState | null) => void;
stashId: string | null;
value?: BN | 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);
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('.');
@ -39,7 +46,7 @@ function formatExistential (value: BN): string {
return fmt;
}
function ValidateAmount ({ currentAmount, isNominating, minNomination, onError, stashId, value }: Props): React.ReactElement<Props> | null {
function ValidateAmount ({ currentAmount, isNominating, minNominated, minNominatorBond, minValidatorBond, onError, stashId, value }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { api } = useApi();
const stashBalance = useCall<DeriveBalancesAll>(api.derive.balances?.all, [stashId]);
@ -60,22 +67,38 @@ function ValidateAmount ({ currentAmount, isNominating, minNomination, onError,
newError = t('The bonded amount is less than the minimum bond amount of {{existentialDeposit}}', {
replace: { existentialDeposit: formatExistential(existentialDeposit) }
});
} else if (isNominating && minNomination && check.lte(minNomination)) {
newWarning = t('The bonded amount is less than the current active minimum nominated amount of {{minNomination}} and depending on the network state, may not be selected to participate', {
replace: { minNomination: formatBalance(minNomination) }
});
} else if (isNominating) {
if (minNominatorBond && check.lt(minNominatorBond)) {
newError = t('The bonded amount is less than the minimum threshold of {{minBond}} for nominators', {
replace: { minBond: formatBalance(minNominatorBond) }
});
} else if (minNominated && check.lt(minNominated)) {
newWarning = t('The bonded amount is less than the current active minimum nominated amount of {{minNomination}} and depending on the network state, may not be selected to participate', {
replace: { minNomination: formatBalance(minNominated) }
});
}
} else {
if (minValidatorBond && check.lt(minValidatorBond)) {
newError = t('The bonded amount is less than the minimum threshold of {{minBond}} for validators', {
replace: { minBond: formatBalance(minValidatorBond) }
});
}
}
setResult((state): AmountValidateState => {
const error = state.error !== newError ? newError : state.error;
const warning = state.warning !== newWarning ? newWarning : state.warning;
onError((error || warning) ? { error, warning } : null);
onError(
(error || warning)
? { error, warning }
: null
);
return { error, warning };
});
}
}, [api, currentAmount, isNominating, minNomination, onError, stashBalance, t, value]);
}, [api, currentAmount, isNominating, minNominated, minNominatorBond, minValidatorBond, onError, stashBalance, t, value]);
if (error) {
return <MarkError content={error} />;

View File

@ -70,6 +70,7 @@ function ListNominees ({ nominating, stashId }: Props): React.ReactElement<Props
const { nomsActive, nomsChilled, nomsInactive, nomsOver, nomsWaiting } = useInactives(stashId, nominating);
const sessionInfo = useCall<DeriveSessionIndexes>(api.query.staking && api.derive.session?.indexes);
const eraExposure = useCall<DeriveEraExposure>(isFunction(api.query.staking.erasStakers) && api.derive.staking.eraExposure, [sessionInfo?.activeEra]);
const [renActive, renChilled, renInactive, renOver, renWaiting] = useMemo(
() => [renderNominators(stashId, nomsActive, eraExposure), renderNominators(stashId, nomsChilled), renderNominators(stashId, nomsInactive), renderNominators(stashId, nomsOver), renderNominators(stashId, nomsWaiting)],
[eraExposure, nomsActive, nomsChilled, nomsInactive, nomsOver, nomsWaiting, stashId]

View File

@ -0,0 +1,32 @@
// Copyright 2017-2021 @polkadot/app-staking authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type BN from 'bn.js';
import type { DeriveStakingAccount } from '@polkadot/api-derive/types';
import React, { useMemo } from 'react';
import { MarkWarning } from '@polkadot/react-components';
import { formatBalance } from '@polkadot/util';
import { useTranslation } from '../../translate';
interface Props {
minBond?: BN;
stakingInfo?: DeriveStakingAccount;
}
function WarnBond ({ minBond, stakingInfo }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const isBelow = useMemo(
() => minBond && stakingInfo && stakingInfo.stakingLedger.active.unwrap().lt(minBond),
[minBond, stakingInfo]
);
return isBelow
? <MarkWarning content={t<string>('Your bonded amount is below the on-chain minimum threshold of {{minBond}} and may be chilled. Bond extra funds to increase the bonded amount.', { replace: { minBond: formatBalance(minBond) } })} />
: null;
}
export default React.memo(WarnBond);

View File

@ -28,6 +28,7 @@ import SetRewardDestination from './SetRewardDestination';
import SetSessionKey from './SetSessionKey';
import Unbond from './Unbond';
import Validate from './Validate';
import WarnBond from './WarnBond';
interface Props {
allSlashes?: [BN, UnappliedSlash[]][];
@ -209,15 +210,25 @@ function Account ({ allSlashes, className = '', info: { controllerId, destinatio
withHexSessionId={hexSessionIdNext !== '0x' && [hexSessionIdQueue, hexSessionIdNext]}
withValidatorPrefs
/>
<WarnBond
minBond={targets.minValidatorBond}
stakingInfo={stakingAccount}
/>
</td>
)
: (
<td className='all expand left'>
{isStashNominating && (
<ListNominees
nominating={nominating}
stashId={stashId}
/>
<>
<ListNominees
nominating={nominating}
stashId={stashId}
/>
<WarnBond
minBond={targets.minNominatorBond}
stakingInfo={stakingAccount}
/>
</>
)}
</td>
)

View File

@ -74,7 +74,8 @@ function NewNominator ({ isInElection, targets }: Props): React.ReactElement<Pro
{step === 1 && (
<BondPartial
isNominating
minNomination={targets.minNominated}
minNominated={targets.minNominated}
minNominatorBond={targets.minNominatorBond}
onChange={setBondInfo}
/>
)}

View File

@ -1,6 +1,7 @@
// Copyright 2017-2021 @polkadot/app-staking authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { SortedTargets } from '../types';
import type { BondInfo, SessionInfo, ValidateInfo } from './partials/types';
import React, { useCallback, useState } from 'react';
@ -16,11 +17,12 @@ import ValidatePartial from './partials/Validate';
interface Props {
isInElection?: boolean;
targets: SortedTargets;
}
const NUM_STEPS = 2;
function NewValidator ({ isInElection }: Props): React.ReactElement<Props> {
function NewValidator ({ isInElection, targets }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const [isVisible, toggleVisible] = useToggle();
@ -72,7 +74,10 @@ function NewValidator ({ isInElection }: Props): React.ReactElement<Props> {
>
<Modal.Content>
{step === 1 && (
<BondPartial onChange={setBondInfo} />
<BondPartial
minValidatorBond={targets.minValidatorBond}
onChange={setBondInfo}
/>
)}
{controllerId && stashId && step === 2 && (
<>

View File

@ -160,7 +160,10 @@ function Actions ({ className = '', isInElection, ownStashes, targets }: Props):
isInElection={isInElection}
targets={targets}
/>
<NewValidator isInElection={isInElection} />
<NewValidator
isInElection={isInElection}
targets={targets}
/>
<NewStash />
</Button.Group>
<ElectionBanner isInElection={isInElection} />

View File

@ -22,7 +22,9 @@ import useUnbondDuration from '../useUnbondDuration';
interface Props {
className?: string;
isNominating?: boolean;
minNomination?: BN;
minNominated?: BN;
minNominatorBond?: BN;
minValidatorBond?: BN;
onChange: (info: BondInfo) => void;
}
@ -34,7 +36,7 @@ const EMPTY_INFO = {
stashId: null
};
function Bond ({ className = '', isNominating, minNomination, onChange }: Props): React.ReactElement<Props> {
function Bond ({ className = '', isNominating, minNominated, minNominatorBond, minValidatorBond, onChange }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const [amount, setAmount] = useState<BN | undefined>();
@ -144,7 +146,9 @@ function Bond ({ className = '', isNominating, minNomination, onChange }: Props)
<InputValidateAmount
controllerId={controllerId}
isNominating={isNominating}
minNomination={minNomination}
minNominated={minNominated}
minNominatorBond={minNominatorBond}
minValidatorBond={minValidatorBond}
onError={setAmountError}
stashId={stashId}
value={amount}

View File

@ -19,6 +19,7 @@ interface Props {
avgStaked?: BN;
lowStaked?: BN;
minNominated?: BN;
minNominatorBond?: BN;
numNominators?: number;
numValidators?: number;
stakedReturn: number;
@ -34,7 +35,7 @@ const transformEra = {
transform: ({ activeEra }: DeriveSessionIndexes) => activeEra.gt(BN_ZERO) ? activeEra.sub(BN_ONE) : undefined
};
function Summary ({ avgStaked, lowStaked, minNominated, stakedReturn, totalIssuance, totalStaked }: Props): React.ReactElement<Props> {
function Summary ({ avgStaked, lowStaked, minNominated, minNominatorBond, stakedReturn, totalIssuance, totalStaked }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const lastEra = useCall<BN | undefined>(api.derive.session.indexes, undefined, transformEra);
@ -107,12 +108,25 @@ function Summary ({ avgStaked, lowStaked, minNominated, stakedReturn, totalIssua
{minNominated?.gt(BN_ZERO) && (
<CardSummary
className='media--1600'
label={t<string>('min nominated')}
label={
minNominatorBond
? t<string>('min nominated / threshold')
: t<string>('min nominated')}
>
<FormatBalance
value={minNominated}
withCurrency={!minNominatorBond}
withSi
/>
{minNominatorBond && (
<>
&nbsp;/&nbsp;
<FormatBalance
value={minNominatorBond}
withSi
/>
</>
)}
</CardSummary>
)}
</section>

View File

@ -188,7 +188,7 @@ const DEFAULT_NAME = { isQueryFiltered: false, nameFilter: '' };
const DEFAULT_SORT: SortState = { sortBy: 'rankOverall', sortFromMax: true };
function Targets ({ className = '', isInElection, ownStashes, targets: { avgStaked, inflation: { stakedReturn }, lowStaked, medianComm, minNominated, nominators, totalIssuance, totalStaked, validatorIds, validators }, toggleFavorite, toggleLedger }: Props): React.ReactElement<Props> {
function Targets ({ className = '', isInElection, ownStashes, targets: { avgStaked, inflation: { stakedReturn }, lowStaked, medianComm, minNominated, minNominatorBond, nominators, totalIssuance, totalStaked, validatorIds, validators }, toggleFavorite, toggleLedger }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const allSlashes = useAvailableSlashes();
@ -361,6 +361,7 @@ function Targets ({ className = '', isInElection, ownStashes, targets: { avgStak
avgStaked={avgStaked}
lowStaked={lowStaked}
minNominated={minNominated}
minNominatorBond={minNominatorBond}
numNominators={nominators?.length}
numValidators={validators?.length}
stakedReturn={stakedReturn}

View File

@ -81,7 +81,7 @@ function StakingApp ({ basePath, className = '' }: Props): React.ReactElement<Pr
name: 'actions',
text: t<string>('Account actions')
},
isFunction(api.query.staking.activeEra) && {
isFunction(api.query.staking.activeEra) && hasAccounts && ownStashes && (ownStashes.length !== 0) && {
name: 'payout',
text: t<string>('Payouts')
},
@ -104,7 +104,7 @@ function StakingApp ({ basePath, className = '' }: Props): React.ReactElement<Pr
name: 'query',
text: t<string>('Validator stats')
}
].filter((q): q is { name: string; text: string } => !!q), [api, slashes, t]);
].filter((q): q is { name: string; text: string } => !!q), [api, hasAccounts, ownStashes, slashes, t]);
return (
<main className={`staking--App ${className}`}>

View File

@ -72,11 +72,17 @@ export type TargetSortBy = keyof ValidatorInfoRank;
export interface SortedTargets {
avgStaked?: BN;
counterForNominators?: BN;
counterForValidators?: BN;
electedIds?: string[];
inflation: Inflation;
lowStaked?: BN;
medianComm: number;
maxNominatorsCount?: BN;
maxValidatorsCount?: BN;
minNominated: BN;
minNominatorBond?: BN;
minValidatorBond?: BN;
nominators?: string[];
nominateIds?: string[];
totalStaked?: BN;

View File

@ -4,12 +4,13 @@
import type { ApiPromise } from '@polkadot/api';
import type { DeriveSessionInfo, DeriveStakingElected, DeriveStakingWaiting } from '@polkadot/api-derive/types';
import type { Inflation } from '@polkadot/react-hooks/types';
import type { Option, u32 } from '@polkadot/types';
import type { SortedTargets, TargetSortBy, ValidatorInfo } from './types';
import BN from 'bn.js';
import { useMemo } from 'react';
import { useAccounts, useApi, useCall, useInflation } from '@polkadot/react-hooks';
import { useAccounts, useApi, useCall, useCallMulti, useInflation } from '@polkadot/react-hooks';
import { arrayFlatten, BN_HUNDRED, BN_MAX_INTEGER, BN_ONE, BN_ZERO } from '@polkadot/util';
interface LastEra {
@ -19,6 +20,17 @@ interface LastEra {
sessionLength: BN;
}
interface MultiResult {
counterForNominators?: BN;
counterForValidators?: BN;
historyDepth?: BN;
maxNominatorsCount?: BN;
maxValidatorsCount?: BN;
minNominatorBond?: BN;
minValidatorBond?: BN;
totalIssuance?: BN;
}
const EMPTY_PARTIAL: Partial<SortedTargets> = {};
const DEFAULT_FLAGS_ELECTED = { withController: true, withExposure: true, withPrefs: true };
const DEFAULT_FLAGS_WAITING = { withController: true, withPrefs: true };
@ -242,11 +254,37 @@ const transformEra = {
})
};
const transformMulti = {
defaultValue: {},
transform: ([historyDepth, counterForNominators, counterForValidators, optMaxNominatorsCount, optMaxValidatorsCount, minNominatorBond, minValidatorBond, totalIssuance]: [BN, BN?, BN?, Option<u32>?, Option<u32>?, BN?, BN?, BN?]): MultiResult => ({
counterForNominators,
counterForValidators,
historyDepth,
maxNominatorsCount: optMaxNominatorsCount && optMaxNominatorsCount.isSome
? optMaxNominatorsCount.unwrap()
: undefined,
maxValidatorsCount: optMaxValidatorsCount && optMaxValidatorsCount.isSome
? optMaxValidatorsCount.unwrap()
: undefined,
minNominatorBond,
minValidatorBond,
totalIssuance
})
};
export default function useSortedTargets (favorites: string[], withLedger: boolean): SortedTargets {
const { api } = useApi();
const { allAccounts } = useAccounts();
const historyDepth = useCall<BN>(api.query.staking.historyDepth);
const totalIssuance = useCall<BN>(api.query.balances?.totalIssuance);
const { counterForNominators, counterForValidators, historyDepth, maxNominatorsCount, maxValidatorsCount, minNominatorBond, minValidatorBond, totalIssuance } = useCallMulti<MultiResult>([
api.query.staking.historyDepth,
api.query.staking.counterForNominators,
api.query.staking.counterForValidators,
api.query.staking.maxNominatorsCount,
api.query.staking.maxValidatorsCount,
api.query.staking.minNominatorBond,
api.query.staking.minValidatorBond,
api.query.balances?.totalIssuance
], transformMulti);
const electedInfo = useCall<DeriveStakingElected>(api.derive.staking.electedInfo, [{ ...DEFAULT_FLAGS_ELECTED, withLedger }]);
const waitingInfo = useCall<DeriveStakingWaiting>(api.derive.staking.waitingInfo, [{ ...DEFAULT_FLAGS_WAITING, withLedger }]);
const lastEraInfo = useCall<LastEra>(api.derive.session.info, undefined, transformEra);
@ -267,5 +305,16 @@ export default function useSortedTargets (favorites: string[], withLedger: boole
[baseInfo, inflation]
);
return { inflation, medianComm: 0, minNominated: BN_ZERO, ...partial };
return {
counterForNominators,
counterForValidators,
inflation,
maxNominatorsCount,
maxValidatorsCount,
medianComm: 0,
minNominated: BN_ZERO,
minNominatorBond,
minValidatorBond,
...partial
};
}