diff --git a/packages/apps/public/locales/en/app-staking.json b/packages/apps/public/locales/en/app-staking.json index bf58d6f8536b..c161373c0fbd 100644 --- a/packages/apps/public/locales/en/app-staking.json +++ b/packages/apps/public/locales/en/app-staking.json @@ -57,6 +57,8 @@ "Join nomination pool": "Join nomination pool", "Keys from rotateKeys": "Keys from rotateKeys", "Max, {{eras}} eras": "Max, {{eras}} eras", + "Minimum stake among the active nominators": "Minimum stake among the active nominators", + "Minimum (total) stake among the active validators.": "Minimum (total) stake among the active validators.", "Members ({{count}})": "Members ({{count}})", "Most profitable": "Most profitable", "Move up {{jumpCount}}": "Move up {{jumpCount}}", @@ -278,6 +280,7 @@ "member account": "member account", "members": "members", "min": "min", + "min active stake": "min active stake", "min nominated": "min nominated", "min nominated / threshold": "min nominated / threshold", "my nodes": "my nodes", @@ -352,4 +355,4 @@ "{{currency}} slashed": "{{currency}} slashed", "{{currency}} total": "{{currency}} total", "{{days}} days": "{{days}} days" -} \ No newline at end of file +} diff --git a/packages/apps/public/locales/it/translation.json b/packages/apps/public/locales/it/translation.json index 1b999051beee..6687372f9376 100644 --- a/packages/apps/public/locales/it/translation.json +++ b/packages/apps/public/locales/it/translation.json @@ -320,6 +320,7 @@ "Image": "Carica codice", "Important notice": "Notifica importante", "In calculating the election outcome, this prioritized vote ordering will be used to determine the final score for the candidates.": "In fase di calcolo dell'esito dell'elezione, la priorità delle preferenze assegnata sarà usata per determinare il punteggio finale di ogni candidato", + "In order to receive staking rewards, and be exposed with an active validator to slash, a nominator needs to be 'active'. Compare the below thresholds with your active stake to ensure you are in the correct set between 'active' and 'intention'.": "", "Inactive": "Inattivo", "Inactive nominations ({{count}})": "Nomine inattive ({{count}})", "Include an optional tip for faster processing": "Includi una mancia per velocizzare la finalizzazione", @@ -1466,6 +1467,7 @@ "https://example.com": "https://example.com", "id": "id", "ideal staked": "", + "min active stake": "", "identity": "identità", "imminent preimage (proposal already passed)": "proposta imminente (già approvate)", "immortal": "perpetua", @@ -2134,4 +2136,4 @@ "{{threshold}}, passing": "{{threshold}}, approvato", "{{type}} copied": "{{type}} copiato", "{{value}}x voting balance, locked for {{lock}}x duration ({{period}} days)": "{{value}} x moltiplicatore di voto, fondi bloccati per {{lock}}x = {{period}} giorni" -} \ No newline at end of file +} diff --git a/packages/apps/public/locales/ru/translation.json b/packages/apps/public/locales/ru/translation.json index 44681680dcb6..cfd6034529cf 100644 --- a/packages/apps/public/locales/ru/translation.json +++ b/packages/apps/public/locales/ru/translation.json @@ -318,6 +318,7 @@ "Image": "", "Important notice": "Важное уведомление", "In calculating the election outcome, this prioritized vote ordering will be used to determine the final score for the candidates.": "Для подсчета результатов голосования будет использовано приоритезированное голосование", + "In order to receive staking rewards, and be exposed with an active validator to slash, a nominator needs to be 'active'. Compare the below thresholds with your active stake to ensure you are in the correct set between 'active' and 'intention'.": "", "Inactive": "", "Inactive nominations ({{count}})": "Неактивные номинации ({{count}})", "Include an optional tip for faster processing": "Добавить чаевые для приоритетной обработки", @@ -1492,6 +1493,7 @@ "https://example.com": "https://primer.com", "id": "", "ideal staked": "", + "min active stake": "", "identity": "личность", "imminent preimage (proposal already passed)": "неизбежный прообраз (предложение уже одобрено)", "immortal": "бессмертный", @@ -2209,4 +2211,4 @@ "{{threshold}}, passing": "{{threshold}}, проходит", "{{type}} copied": "", "{{value}}x voting balance, locked for {{lock}}x duration ({{period}} days)": "{{value}}x баланс голосования, заблокировано на {{lock}}x ({{period}} дней)" -} \ No newline at end of file +} diff --git a/packages/page-staking/src/Bags/index.tsx b/packages/page-staking/src/Bags/index.tsx index eac9c2a6b7d7..4d19e8ea1ade 100644 --- a/packages/page-staking/src/Bags/index.tsx +++ b/packages/page-staking/src/Bags/index.tsx @@ -10,7 +10,6 @@ import { Button, MarkWarning, Table, ToggleGroup } from '@polkadot/react-compone import { useTranslation } from '../translate'; import Bag from './Bag'; -import Summary from './Summary'; import useBagsList from './useBagsList'; import useBagsNodes from './useBagsNodes'; @@ -61,11 +60,7 @@ function Bags ({ className, ownStashes }: Props): React.ReactElement { ); return ( -
- + <> { /> ))} -
+ ); } diff --git a/packages/page-staking/src/Overview/SpinnerWrap.tsx b/packages/page-staking/src/Overview/SpinnerWrap.tsx new file mode 100644 index 000000000000..4d262ca5e10b --- /dev/null +++ b/packages/page-staking/src/Overview/SpinnerWrap.tsx @@ -0,0 +1,22 @@ +// Copyright 2017-2022 @polkadot/app-staking authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import React, { JSXElementConstructor, ReactElement } from 'react'; +import styled from 'styled-components'; + +import { Spinner } from '@polkadot/react-components'; + +interface Props { + check: boolean + children: ReactElement> +} + +function SpinnerWrap ({ check, children }: Props): React.ReactElement { + return check + ? children + : (); +} + +export default React.memo(styled(SpinnerWrap)` + margin-top: 1rem; +`); diff --git a/packages/page-staking/src/Overview/SummaryBags.tsx b/packages/page-staking/src/Overview/SummaryBags.tsx new file mode 100644 index 000000000000..16b017fd084b --- /dev/null +++ b/packages/page-staking/src/Overview/SummaryBags.tsx @@ -0,0 +1,60 @@ +// Copyright 2017-2022 @polkadot/app-staking authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { BN } from '@polkadot/util'; + +import React, { useMemo } from 'react'; + +import { Card, CardSummary, SummaryBox } from '@polkadot/react-components'; +import { useCall } from '@polkadot/react-hooks'; +import { StakerState } from '@polkadot/react-hooks/types'; +import { formatNumber } from '@polkadot/util'; + +import useBagsList from '../Bags/useBagsList'; +import useBagsNodes from '../Bags/useBagsNodes'; +import useQueryModule from '../Bags/useQueryModule'; +import { useTranslation } from '../translate'; +import { Section, Title } from './index'; +import SpinnerWrap from './SpinnerWrap'; + +interface Props { + ownStashes?: StakerState[]; +} + +function SummaryBags ({ ownStashes }: Props) { + const { t } = useTranslation(); + const mod = useQueryModule(); + const stashIds = useMemo( + () => ownStashes + ? ownStashes.map(({ stashId }) => stashId) + : [], + [ownStashes] + ); + const mapOwn = useBagsNodes(stashIds); + const bags = useBagsList(); + const total = useCall(mod.counterForListNodes); + + return ( + + {t<string>('bags')} + +
+ ('total bags')}> + + {formatNumber(bags?.length)} + + +
+
+ ('total nodes')}> + + {formatNumber(total)} + + +
+
+
+ ); +} + +export default SummaryBags; diff --git a/packages/page-staking/src/Overview/SummaryGeneral.tsx b/packages/page-staking/src/Overview/SummaryGeneral.tsx new file mode 100644 index 000000000000..feee43ef8a35 --- /dev/null +++ b/packages/page-staking/src/Overview/SummaryGeneral.tsx @@ -0,0 +1,105 @@ +// Copyright 2017-2022 @polkadot/app-staking authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { BN } from '@polkadot/util'; + +import React, { useMemo } from 'react'; + +import SummarySession from '@polkadot/app-explorer/SummarySession'; +import { Card, CardSummary, SummaryBox } from '@polkadot/react-components'; +import { FormatBalance } from '@polkadot/react-query'; + +import { useTranslation } from '../translate'; +import { SortedTargets } from '../types'; +import { Section } from './index'; +import SpinnerWrap from './SpinnerWrap'; + +interface Props { + targets: SortedTargets; +} + +interface ProgressInfo { + hideValue: true; + total: BN; + value: BN; +} + +function getProgressInfo (value?: BN, total?: BN): ProgressInfo | undefined { + return value && total && !total.isZero() + ? { + hideValue: true, + total, + value + } + : undefined; +} + +function SummaryGeneral ({ targets: { inflation: { idealStake, + inflation, + stakedReturn }, +totalIssuance, +totalStaked } }: Props) { + const { t } = useTranslation(); + + const progressStake = useMemo( + () => getProgressInfo(totalStaked, totalIssuance), + [totalIssuance, totalStaked] + ); + + const returnsCheck: string = useMemo( + () => { + if (totalIssuance && stakedReturn > 0 && Number.isFinite(stakedReturn)) { + return (stakedReturn.toFixed(1) + '%'); + } + + return '0%'; + }, [totalIssuance, stakedReturn]); + + return ( + + +
+ +
+
+ ('total staked')} + progress={progressStake} + > + + + + +
+
+ ('ideal staked')} + > + 0) && Number.isFinite(idealStake)} + > + {(idealStake * 100).toFixed(1)}% + + + ('inflation')} + > + 0) && Number.isFinite(inflation)} + >{inflation.toFixed(1)}% + + ('returns')}> + {returnsCheck} + +
+
+
+ ); +} + +export default SummaryGeneral; diff --git a/packages/page-staking/src/Overview/SummaryNominators.tsx b/packages/page-staking/src/Overview/SummaryNominators.tsx new file mode 100644 index 000000000000..87f945beb4fd --- /dev/null +++ b/packages/page-staking/src/Overview/SummaryNominators.tsx @@ -0,0 +1,124 @@ +// Copyright 2017-2022 @polkadot/app-staking authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import React from 'react'; + +import { Card, CardSummary, MarkWarning, SummaryBox } from '@polkadot/react-components'; +import { useApi } from '@polkadot/react-hooks'; +import { FormatBalance } from '@polkadot/react-query'; +import { formatNumber } from '@polkadot/util'; + +import { useTranslation } from '../translate'; +import { SortedTargets } from '../types'; +import { Section, Title } from './index'; +import SpinnerWrap from './SpinnerWrap'; + +interface Props { + targets: SortedTargets; +} + +function SummaryNominators ({ targets: { maxNominatorsCount, + minNominatorBond, + nominatorActiveCount, + nominatorMaxElectingCount, + nominatorMinActiveThreshold } }: Props) { + const { t } = useTranslation(); + + const { api } = useApi(); + + const maxElectingVotersDefined = !!api.consts.electionProviderMultiPhase?.maxElectingVoters; + const maxNominatorDefined = !!api.query.staking.maxNominatorsCount && maxNominatorsCount !== undefined; + const minNominatorBondDefined = !!api.query.staking.minNominatorBond && minNominatorBond !== undefined; + const nominatorMinActiveThresholdDefined = !!api.query.staking.erasStakers && nominatorMinActiveThreshold !== undefined; + const nominatorActiveCountDefined = !!api.query.erasStakers && nominatorActiveCount !== undefined; + + return ( + <> + + {t<string>('nominators')} + +
+ ('Maximum number of nominator intentions.')} + label={t('maximum')} + > + {maxNominatorDefined + ? + {formatNumber(maxNominatorsCount?.toNumber())} + + : '-'} + +
+
+ ('Number of electing nominators.')} + label={t('electing')} + > + {maxElectingVotersDefined + ? + {formatNumber(nominatorMaxElectingCount)} + + : '-'} + +
+
+ ('Number of nominators backing active validators in the current era.')} + label={t('active')} + > + {nominatorActiveCountDefined + ? + {formatNumber(nominatorActiveCount)} + + : '-'} + +
+
+ +
+ ('Threshold stake to intend nomination.')} + label={t('intention thrs')} + > + {minNominatorBondDefined + ? + + + : '-'} + +
+
+ ('Minimum threshold stake among active nominators.')} + label={t('min active thrs')} + > + {nominatorMinActiveThresholdDefined + ? + {nominatorMinActiveThreshold} + + : '-'} + +
+
+ {/** Average Stake of Active Nominators? */} +
+
+ ('In order to receive staking rewards, and be exposed with an active validator to slash, a nominator needs to be "active". Compare the below thresholds with your active stake to ensure you are in the correct set between "active" and "intention".')} + withIcon={false} + > + Learn More + +
+ + ); +} + +export default SummaryNominators; diff --git a/packages/page-staking/src/Overview/SummaryValidators.tsx b/packages/page-staking/src/Overview/SummaryValidators.tsx new file mode 100644 index 000000000000..69d3aa7afc05 --- /dev/null +++ b/packages/page-staking/src/Overview/SummaryValidators.tsx @@ -0,0 +1,120 @@ +// Copyright 2017-2022 @polkadot/app-staking authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import React from 'react'; + +import { Card, CardSummary, SummaryBox } from '@polkadot/react-components'; +import { useApi } from '@polkadot/react-hooks'; +import { FormatBalance } from '@polkadot/react-query'; +import { formatNumber } from '@polkadot/util'; + +import { useTranslation } from '../translate'; +import { SortedTargets } from '../types'; +import { Section, Title } from './index'; +import SpinnerWrap from './SpinnerWrap'; + +interface Props { + targets: SortedTargets; +} + +function SummaryValidators ({ targets: + { avgStaked, + maxValidatorsCount, + minValidatorBond, + validatorActiveCount, + validatorMinActiveThreshold, + waitingIds } }: Props) { + const { t } = useTranslation(); + const { api } = useApi(); + + const maxValidatorDefined = !!api.query.staking.maxValidatorsCount && maxValidatorsCount !== undefined; + const minValidatorBondDefined = !!api.query.staking.minValidatorBond && minValidatorBond !== undefined; + const validatorMinActiveThresholdDefined = !!api.query.staking.erasStakers && validatorMinActiveThreshold !== undefined; + const validatorActiveCountDefined = !!api.query.staking.erasStakers && validatorActiveCount !== undefined; + + return ( + + {t<string>('validators')} + +
+ ('Maximum number of validator intentions.')} + label={t('max intention')} + > + {maxValidatorDefined + ? + {maxValidatorsCount?.toNumber()} + + : '-'} + +
+
+ ('Count of waiting validators.')} + label={t('waiting')} + > + + {formatNumber(waitingIds?.length)} + + +
+
+ ('Count of active validators.')} + label={t('active')} + > + {validatorActiveCountDefined + ? + {validatorActiveCount} + + : '-'} + +
+
+ +
+ ('Threshold stake among intended validators.')} + label={t('intention thrs')} + > + {minValidatorBondDefined + ? + + + : '-'} + +
+
+ ('Minimum threshold stake among active validators.')} + label={t('min active thrs')} + > + {validatorMinActiveThresholdDefined + ? + {validatorMinActiveThreshold} + + : '-'} + +
+
+ ('Average stake among active validators.')} + label={t('average active stake')} + > + + + + +
+
+
+ ); +} + +export default SummaryValidators; diff --git a/packages/page-staking/src/Overview/index.tsx b/packages/page-staking/src/Overview/index.tsx new file mode 100644 index 000000000000..4d160d7208d6 --- /dev/null +++ b/packages/page-staking/src/Overview/index.tsx @@ -0,0 +1,73 @@ +// Copyright 2017-2022 @polkadot/app-staking authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { DeriveStakingOverview } from '@polkadot/api-derive/types'; +import type { StakerState } from '@polkadot/react-hooks/types'; +import type { SortedTargets } from '../types'; + +import React from 'react'; +import styled from 'styled-components'; + +import { useApi } from '@polkadot/react-hooks'; +import { isFunction } from '@polkadot/util'; + +import SummaryBags from './SummaryBags'; +import SummaryGeneral from './SummaryGeneral'; +import SummaryNominators from './SummaryNominators'; +import SummaryValidators from './SummaryValidators'; + +interface Props { + className?: string; + nominators?: string[]; + ownStashes?: StakerState[]; + stakingOverview?: DeriveStakingOverview; + targets: SortedTargets; + hasStashes?: boolean +} + +function Overview ({ className = '', + hasStashes, + ownStashes, + targets }: Props): React.ReactElement { + const { api } = useApi(); + + return ( +
+ + + + {hasStashes && isFunction(api.query.bagsList?.counterForListNodes) && } +
+ ); +} + +export const Title = styled.div` + text-align: left; + text-transform: lowercase; + margin: 0 0 20px; + font-weight: 400; + font-size: 15px; +`; + +export const Section = styled.section` + width: 33%; + justify-content: center; +`; + +export default React.memo(styled(Overview)` + article { + justify-content: center; + } + .validator--Account-block-icon { + display: inline-block; + margin-right: 0.75rem; + margin-top: -0.25rem; + vertical-align: middle; + } + + .validator--Summary-authors { + .validator--Account-block-icon+.validator--Account-block-icon { + margin-left: -1.5rem; + } + } +`); diff --git a/packages/page-staking/src/Targets/Summary.tsx b/packages/page-staking/src/Targets/Summary.tsx deleted file mode 100644 index 3cfbe7c6d907..000000000000 --- a/packages/page-staking/src/Targets/Summary.tsx +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright 2017-2022 @polkadot/app-staking authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import type { Option } from '@polkadot/types'; -import type { Balance } from '@polkadot/types/interfaces'; -import type { BN } from '@polkadot/util'; - -import React, { useMemo } from 'react'; - -import { CardSummary, SummaryBox } from '@polkadot/react-components'; -import { useApi, useCall } from '@polkadot/react-hooks'; -import { FormatBalance } from '@polkadot/react-query'; -import { BN_ZERO } from '@polkadot/util'; - -import { useTranslation } from '../translate'; - -interface Props { - avgStaked?: BN; - lastEra?: BN; - lowStaked?: BN; - minNominated?: BN; - minNominatorBond?: BN; - numNominators?: number; - numValidators?: number; - stakedReturn: number; - totalIssuance?: BN; - totalStaked?: BN; -} - -interface ProgressInfo { - hideValue: true; - total: BN; - value: BN; -} - -const OPT_REWARD = { - transform: (optBalance: Option) => - optBalance.unwrapOrDefault() -}; - -function getProgressInfo (value?: BN, total?: BN): ProgressInfo | undefined { - return value && total && !total.isZero() - ? { - hideValue: true, - total, - value - } - : undefined; -} - -function Summary ({ avgStaked, lastEra, lowStaked, minNominated, minNominatorBond, stakedReturn, totalIssuance, totalStaked }: Props): React.ReactElement { - const { t } = useTranslation(); - const { api } = useApi(); - const lastReward = useCall(lastEra && api.query.staking.erasValidatorReward, [lastEra], OPT_REWARD); - - const progressStake = useMemo( - () => getProgressInfo(totalStaked, totalIssuance), - [totalIssuance, totalStaked] - ); - - const progressAvg = useMemo( - () => getProgressInfo(lowStaked, avgStaked), - [avgStaked, lowStaked] - ); - - return ( - -
- {progressStake && ( - ('total staked')} - progress={progressStake} - > - - - )} -
-
- {totalIssuance && (stakedReturn > 0) && Number.isFinite(stakedReturn) && ( - ('returns')}> - {stakedReturn.toFixed(1)}% - - )} -
-
- {progressAvg && ( - ('lowest / avg staked')}`} - progress={progressAvg} - > - -  /  - - - )} -
-
- {minNominated?.gt(BN_ZERO) && ( - ('min nominated / threshold') - : t('min nominated')} - > - - {minNominatorBond && ( - <> -  /  - - - )} - - )} -
-
- {lastReward?.gt(BN_ZERO) && ( - ('last reward')}> - - - )} -
-
- ); -} - -export default React.memo(Summary); diff --git a/packages/page-staking/src/Targets/index.tsx b/packages/page-staking/src/Targets/index.tsx index 2f90ff20ca42..89ac73d8ca30 100644 --- a/packages/page-staking/src/Targets/index.tsx +++ b/packages/page-staking/src/Targets/index.tsx @@ -21,7 +21,6 @@ import Legend from '../Legend'; import { useTranslation } from '../translate'; import useIdentities from '../useIdentities'; import Nominate from './Nominate'; -import Summary from './Summary'; import useOwnNominators from './useOwnNominators'; import Validator from './Validator'; @@ -194,7 +193,7 @@ const DEFAULT_NAME = { isQueryFiltered: false, nameFilter: '' }; const DEFAULT_SORT: SortState = { sortBy: 'rankOverall', sortFromMax: true }; -function Targets ({ className = '', isInElection, nominatedBy, ownStashes, targets: { avgStaked, inflation: { stakedReturn }, lastEra, lowStaked, medianComm, minNominated, minNominatorBond, nominators, totalIssuance, totalStaked, validatorIds, validators }, toggleFavorite, toggleLedger, toggleNominatedBy }: Props): React.ReactElement { +function Targets ({ className = '', isInElection, nominatedBy, ownStashes, targets: { medianComm, validatorIds, validators }, toggleFavorite, toggleLedger, toggleNominatedBy }: Props): React.ReactElement { const { t } = useTranslation(); const { api } = useApi(); const allSlashes = useAvailableSlashes(); @@ -363,18 +362,6 @@ function Targets ({ className = '', isInElection, nominatedBy, ownStashes, targe return (
-