diff --git a/packages/page-staking/src/Actions/Account/InputValidateAmount.tsx b/packages/page-staking/src/Actions/Account/InputValidateAmount.tsx index ffd495b3e5..53cda24adf 100644 --- a/packages/page-staking/src/Actions/Account/InputValidateAmount.tsx +++ b/packages/page-staking/src/Actions/Account/InputValidateAmount.tsx @@ -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 | null { +function ValidateAmount ({ currentAmount, isNominating, minNominated, minNominatorBond, minValidatorBond, onError, stashId, value }: Props): React.ReactElement | null { const { t } = useTranslation(); const { api } = useApi(); const stashBalance = useCall(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 ; diff --git a/packages/page-staking/src/Actions/Account/ListNominees.tsx b/packages/page-staking/src/Actions/Account/ListNominees.tsx index 1b69fa1cfe..9552b12370 100644 --- a/packages/page-staking/src/Actions/Account/ListNominees.tsx +++ b/packages/page-staking/src/Actions/Account/ListNominees.tsx @@ -70,6 +70,7 @@ function ListNominees ({ nominating, stashId }: Props): React.ReactElement(api.query.staking && api.derive.session?.indexes); const eraExposure = useCall(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] diff --git a/packages/page-staking/src/Actions/Account/WarnBond.tsx b/packages/page-staking/src/Actions/Account/WarnBond.tsx new file mode 100644 index 0000000000..fa22f34c28 --- /dev/null +++ b/packages/page-staking/src/Actions/Account/WarnBond.tsx @@ -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 | null { + const { t } = useTranslation(); + + const isBelow = useMemo( + () => minBond && stakingInfo && stakingInfo.stakingLedger.active.unwrap().lt(minBond), + [minBond, stakingInfo] + ); + + return isBelow + ? ('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); diff --git a/packages/page-staking/src/Actions/Account/index.tsx b/packages/page-staking/src/Actions/Account/index.tsx index b044840897..61d044584d 100644 --- a/packages/page-staking/src/Actions/Account/index.tsx +++ b/packages/page-staking/src/Actions/Account/index.tsx @@ -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 /> + ) : ( {isStashNominating && ( - + <> + + + )} ) diff --git a/packages/page-staking/src/Actions/NewNominator.tsx b/packages/page-staking/src/Actions/NewNominator.tsx index 5dfad03483..d1ac6bc326 100644 --- a/packages/page-staking/src/Actions/NewNominator.tsx +++ b/packages/page-staking/src/Actions/NewNominator.tsx @@ -74,7 +74,8 @@ function NewNominator ({ isInElection, targets }: Props): React.ReactElement )} diff --git a/packages/page-staking/src/Actions/NewValidator.tsx b/packages/page-staking/src/Actions/NewValidator.tsx index e21acfe555..ef6e91158e 100644 --- a/packages/page-staking/src/Actions/NewValidator.tsx +++ b/packages/page-staking/src/Actions/NewValidator.tsx @@ -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 { +function NewValidator ({ isInElection, targets }: Props): React.ReactElement { const { t } = useTranslation(); const { api } = useApi(); const [isVisible, toggleVisible] = useToggle(); @@ -72,7 +74,10 @@ function NewValidator ({ isInElection }: Props): React.ReactElement { > {step === 1 && ( - + )} {controllerId && stashId && step === 2 && ( <> diff --git a/packages/page-staking/src/Actions/index.tsx b/packages/page-staking/src/Actions/index.tsx index 5f0ac3596e..160020bb6c 100644 --- a/packages/page-staking/src/Actions/index.tsx +++ b/packages/page-staking/src/Actions/index.tsx @@ -160,7 +160,10 @@ function Actions ({ className = '', isInElection, ownStashes, targets }: Props): isInElection={isInElection} targets={targets} /> - + diff --git a/packages/page-staking/src/Actions/partials/Bond.tsx b/packages/page-staking/src/Actions/partials/Bond.tsx index 8cf7a2368c..0ea2f9ce05 100644 --- a/packages/page-staking/src/Actions/partials/Bond.tsx +++ b/packages/page-staking/src/Actions/partials/Bond.tsx @@ -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 { +function Bond ({ className = '', isNominating, minNominated, minNominatorBond, minValidatorBond, onChange }: Props): React.ReactElement { const { t } = useTranslation(); const { api } = useApi(); const [amount, setAmount] = useState(); @@ -144,7 +146,9 @@ function Bond ({ className = '', isNominating, minNomination, onChange }: Props) activeEra.gt(BN_ZERO) ? activeEra.sub(BN_ONE) : undefined }; -function Summary ({ avgStaked, lowStaked, minNominated, stakedReturn, totalIssuance, totalStaked }: Props): React.ReactElement { +function Summary ({ avgStaked, lowStaked, minNominated, minNominatorBond, stakedReturn, totalIssuance, totalStaked }: Props): React.ReactElement { const { t } = useTranslation(); const { api } = useApi(); const lastEra = useCall(api.derive.session.indexes, undefined, transformEra); @@ -107,12 +108,25 @@ function Summary ({ avgStaked, lowStaked, minNominated, stakedReturn, totalIssua {minNominated?.gt(BN_ZERO) && ( ('min nominated')} + label={ + minNominatorBond + ? t('min nominated / threshold') + : t('min nominated')} > + {minNominatorBond && ( + <> +  /  + + + )} )} diff --git a/packages/page-staking/src/Targets/index.tsx b/packages/page-staking/src/Targets/index.tsx index 77d08f9907..f495b3ee0c 100644 --- a/packages/page-staking/src/Targets/index.tsx +++ b/packages/page-staking/src/Targets/index.tsx @@ -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 { +function Targets ({ className = '', isInElection, ownStashes, targets: { avgStaked, inflation: { stakedReturn }, lowStaked, medianComm, minNominated, minNominatorBond, nominators, totalIssuance, totalStaked, validatorIds, validators }, toggleFavorite, toggleLedger }: Props): React.ReactElement { 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} diff --git a/packages/page-staking/src/index.tsx b/packages/page-staking/src/index.tsx index 757836a230..4007f7b51f 100644 --- a/packages/page-staking/src/index.tsx +++ b/packages/page-staking/src/index.tsx @@ -81,7 +81,7 @@ function StakingApp ({ basePath, className = '' }: Props): React.ReactElement('Account actions') }, - isFunction(api.query.staking.activeEra) && { + isFunction(api.query.staking.activeEra) && hasAccounts && ownStashes && (ownStashes.length !== 0) && { name: 'payout', text: t('Payouts') }, @@ -104,7 +104,7 @@ function StakingApp ({ basePath, className = '' }: Props): React.ReactElement('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 (
diff --git a/packages/page-staking/src/types.ts b/packages/page-staking/src/types.ts index 508e04f3f8..d9f5d115f5 100644 --- a/packages/page-staking/src/types.ts +++ b/packages/page-staking/src/types.ts @@ -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; diff --git a/packages/page-staking/src/useSortedTargets.ts b/packages/page-staking/src/useSortedTargets.ts index 1973248c55..efadee17ff 100644 --- a/packages/page-staking/src/useSortedTargets.ts +++ b/packages/page-staking/src/useSortedTargets.ts @@ -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 = {}; 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?, Option?, 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(api.query.staking.historyDepth); - const totalIssuance = useCall(api.query.balances?.totalIssuance); + const { counterForNominators, counterForValidators, historyDepth, maxNominatorsCount, maxValidatorsCount, minNominatorBond, minValidatorBond, totalIssuance } = useCallMulti([ + 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(api.derive.staking.electedInfo, [{ ...DEFAULT_FLAGS_ELECTED, withLedger }]); const waitingInfo = useCall(api.derive.staking.waitingInfo, [{ ...DEFAULT_FLAGS_WAITING, withLedger }]); const lastEraInfo = useCall(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 + }; }