Warn on staking min bond (#5674)
* Warn on staking min bond * Warn on min validator amount
This commit is contained in:
parent
5170f480a9
commit
d91bdcd191
@ -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} />;
|
||||
|
||||
@ -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]
|
||||
|
||||
32
packages/page-staking/src/Actions/Account/WarnBond.tsx
Normal file
32
packages/page-staking/src/Actions/Account/WarnBond.tsx
Normal 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);
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -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 && (
|
||||
<>
|
||||
|
||||
@ -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} />
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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 && (
|
||||
<>
|
||||
/
|
||||
<FormatBalance
|
||||
value={minNominatorBond}
|
||||
withSi
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</CardSummary>
|
||||
)}
|
||||
</section>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}`}>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user