From 3da6f3fbcfc44a7fa3a625e34650faf5d4858907 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Mon, 23 Mar 2020 16:56:49 +0100 Subject: [PATCH] Staking Address component breakdown (#2445) * Staking Address component breakdown * Share Favorite component --- .../src/Overview/Address/Favorite.tsx | 31 +++ .../src/Overview/Address/StakeOther.tsx | 48 ++++ .../src/Overview/Address/Status.tsx | 49 +++++ .../{Address.tsx => Address/index.tsx} | 80 ++----- .../page-staking/src/Query/ChartPoints.tsx | 75 +++++++ .../page-staking/src/Query/ChartRewards.tsx | 96 ++++++++ .../page-staking/src/Query/ChartStake.tsx | 88 ++++++++ packages/page-staking/src/Query/Validator.tsx | 206 +----------------- packages/page-staking/src/Query/types.ts | 19 ++ packages/page-staking/src/Query/util.ts | 9 + .../page-staking/src/Targets/Validator.tsx | 17 +- 11 files changed, 450 insertions(+), 268 deletions(-) create mode 100644 packages/page-staking/src/Overview/Address/Favorite.tsx create mode 100644 packages/page-staking/src/Overview/Address/StakeOther.tsx create mode 100644 packages/page-staking/src/Overview/Address/Status.tsx rename packages/page-staking/src/Overview/{Address.tsx => Address/index.tsx} (74%) create mode 100644 packages/page-staking/src/Query/ChartPoints.tsx create mode 100644 packages/page-staking/src/Query/ChartRewards.tsx create mode 100644 packages/page-staking/src/Query/ChartStake.tsx create mode 100644 packages/page-staking/src/Query/types.ts create mode 100644 packages/page-staking/src/Query/util.ts diff --git a/packages/page-staking/src/Overview/Address/Favorite.tsx b/packages/page-staking/src/Overview/Address/Favorite.tsx new file mode 100644 index 0000000000..58df79817f --- /dev/null +++ b/packages/page-staking/src/Overview/Address/Favorite.tsx @@ -0,0 +1,31 @@ +// Copyright 2017-2020 @polkadot/app-staking authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import React, { useCallback } from 'react'; +import { Icon } from '@polkadot/react-components'; + +interface Props { + address: string; + isFavorite: boolean; + toggleFavorite: (accountId: string) => void; +} + +function Favorite ({ address, isFavorite, toggleFavorite }: Props): React.ReactElement { + const _onFavorite = useCallback( + (): void => toggleFavorite(address), + [address] + ); + + return ( + + + + ); +} + +export default React.memo(Favorite); diff --git a/packages/page-staking/src/Overview/Address/StakeOther.tsx b/packages/page-staking/src/Overview/Address/StakeOther.tsx new file mode 100644 index 0000000000..7db0dbfa6e --- /dev/null +++ b/packages/page-staking/src/Overview/Address/StakeOther.tsx @@ -0,0 +1,48 @@ +// Copyright 2017-2020 @polkadot/app-staking authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import { AccountId, Balance } from '@polkadot/types/interfaces'; + +import BN from 'bn.js'; +import React from 'react'; +import { AddressMini, Expander } from '@polkadot/react-components'; +import { FormatBalance } from '@polkadot/react-query'; +import { formatNumber } from '@polkadot/util'; + +import { useTranslation } from '../../translate'; + +interface Props { + stakeOther?: BN; + nominators: [AccountId, Balance][]; +} + +function StakeOther ({ nominators, stakeOther }: Props): React.ReactElement { + const { t } = useTranslation(); + + return ( + + {stakeOther?.gtn(0) && ( + <> + + +  ({formatNumber(nominators.length)}) + + }> + {nominators.map(([who, bonded]): React.ReactNode => + + )} + + + )} + + ); +} + +export default React.memo(StakeOther); diff --git a/packages/page-staking/src/Overview/Address/Status.tsx b/packages/page-staking/src/Overview/Address/Status.tsx new file mode 100644 index 0000000000..751b99d514 --- /dev/null +++ b/packages/page-staking/src/Overview/Address/Status.tsx @@ -0,0 +1,49 @@ +// Copyright 2017-2020 @polkadot/app-staking authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import React from 'react'; +import { Badge, Icon } from '@polkadot/react-components'; +import { formatNumber } from '@polkadot/util'; + +import { useTranslation } from '../../translate'; + +interface Props { + isElected: boolean; + onlineCount?: false | number; + onlineMessage?: boolean; +} + +function Status ({ isElected, onlineCount, onlineMessage }: Props): React.ReactElement { + const { t } = useTranslation(); + + return ( + + {isElected && ( + } + isInline + isTooltip + type='next' + /> + )} + {(!!onlineCount || onlineMessage) && ( + } + isInline + isTooltip + type='online' + /> + )} + + ); +} + +export default React.memo(Status); diff --git a/packages/page-staking/src/Overview/Address.tsx b/packages/page-staking/src/Overview/Address/index.tsx similarity index 74% rename from packages/page-staking/src/Overview/Address.tsx rename to packages/page-staking/src/Overview/Address/index.tsx index 3d290d8804..1edf54b2d5 100644 --- a/packages/page-staking/src/Overview/Address.tsx +++ b/packages/page-staking/src/Overview/Address/index.tsx @@ -8,13 +8,16 @@ import { DeriveAccountInfo, DerivedStakingQuery } from '@polkadot/api-derive/typ import BN from 'bn.js'; import React, { useCallback, useEffect, useState } from 'react'; import ApiPromise from '@polkadot/api/promise'; -import { AddressMini, AddressSmall, Badge, Expander, Icon } from '@polkadot/react-components'; +import { AddressSmall, Icon } from '@polkadot/react-components'; import { useApi, useCall } from '@polkadot/react-hooks'; import { FormatBalance } from '@polkadot/react-query'; import keyring from '@polkadot/ui-keyring'; import { formatNumber } from '@polkadot/util'; -import { useTranslation } from '../translate'; +import { useTranslation } from '../../translate'; +import Favorite from './Favorite'; +import Status from './Status'; +import StakeOther from './StakeOther'; interface Props { address: string; @@ -121,10 +124,6 @@ function Address ({ address, className, filterName, hasQueries, isAuthor, isElec ); }, [address, filterName, info]); - const _onFavorite = useCallback( - (): void => toggleFavorite(address), - [address] - ); const _onQueryStats = useCallback( (): void => { window.location.hash = `/staking/query/${address}`; @@ -134,38 +133,16 @@ function Address ({ address, className, filterName, hasQueries, isAuthor, isElec return ( - - - - - {isElected && ( - } - isInline - isTooltip - type='next' - /> - )} - {(!!onlineCount || onlineMessage) && ( - } - isInline - isTooltip - type='online' - /> - )} - + + @@ -177,27 +154,10 @@ function Address ({ address, className, filterName, hasQueries, isAuthor, isElec /> )} - - {stakeOther?.gtn(0) && ( - <> - - -  ({formatNumber(nominators.length)}) - - }> - {nominators.map(([who, bonded]): React.ReactNode => - - )} - - - )} - + {commission && ( <>{commission} @@ -214,7 +174,7 @@ function Address ({ address, className, filterName, hasQueries, isAuthor, isElec )} - {hasQueries && api.query.imOnline?.authoredBlocks && ( + {hasQueries && ( { + total += points.toNumber(); + labels.push(era.toHuman()); + + if (points.gtn(0)) { + avgCount++; + } + + avgSet.push((avgCount ? Math.ceil(total * 100 / avgCount) : 0) / 100); + idxSet.push(points); + }); + + return { + chart: [idxSet, avgSet], + labels + }; +} + +function ChartPoints ({ validatorId }: Props): React.ReactElement { + const { t } = useTranslation(); + const { api } = useApi(); + const [{ chart, labels }, setChart] = useState({ chart: [], labels: [] }); + const stakerPoints = useCall(api.derive.staking.stakerPoints as any, [validatorId, true]); + const legends = useMemo(() => [ + t('points'), + t('average') + ], [t]); + + useEffect((): void => { + stakerPoints && setChart( + extractPoints(stakerPoints) + ); + }, [stakerPoints]); + + return ( +
+

{t('era points')}

+ {chart.length + ? ( + + ) + : + } +
+ ); +} + +export default React.memo(ChartPoints); diff --git a/packages/page-staking/src/Query/ChartRewards.tsx b/packages/page-staking/src/Query/ChartRewards.tsx new file mode 100644 index 0000000000..3cdf777f32 --- /dev/null +++ b/packages/page-staking/src/Query/ChartRewards.tsx @@ -0,0 +1,96 @@ +// Copyright 2017-2020 @polkadot/app-staking authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import { DeriveEraRewards, DeriveOwnSlashes, DeriveStakerPoints } from '@polkadot/api-derive/types'; +import { ChartInfo, LineDataEntry, Props } from './types'; + +import BN from 'bn.js'; +import React, { useEffect, useMemo, useState } from 'react'; +import { Chart, Spinner } from '@polkadot/react-components'; +import { useApi, useCall } from '@polkadot/react-hooks'; +import { formatBalance } from '@polkadot/util'; + +import { useTranslation } from '../translate'; +import { balanceToNumber } from './util'; + +const COLORS_REWARD = ['#8c2200', '#008c22', '#acacac']; + +function extractRewards (erasRewards: DeriveEraRewards[], ownSlashes: DeriveOwnSlashes[], allPoints: DeriveStakerPoints[], divisor: BN): ChartInfo { + const labels: string[] = []; + const slashSet: LineDataEntry = []; + const rewardSet: LineDataEntry = []; + const avgSet: LineDataEntry = []; + let avgCount = 0; + let total = 0; + + erasRewards.forEach(({ era, eraReward }): void => { + const points = allPoints.find((points) => points.era.eq(era)); + const slashed = ownSlashes.find((slash) => slash.era.eq(era)); + const reward = points?.eraPoints.gtn(0) + ? balanceToNumber(points.points.mul(eraReward).div(points.eraPoints), divisor) + : 0; + const slash = slashed + ? balanceToNumber(slashed.total, divisor) + : 0; + + total += reward; + + if (reward > 0) { + avgCount++; + } + + labels.push(era.toHuman()); + rewardSet.push(reward); + avgSet.push((avgCount ? Math.ceil(total * 100 / avgCount) : 0) / 100); + slashSet.push(slash); + }); + + return { + chart: [slashSet, rewardSet, avgSet], + labels + }; +} + +function ChartRewards ({ validatorId }: Props): React.ReactElement { + const { t } = useTranslation(); + const { api } = useApi(); + const [{ chart, labels }, setChart] = useState({ chart: [], labels: [] }); + const ownSlashes = useCall(api.derive.staking.ownSlashes as any, [validatorId, true]); + const erasRewards = useCall(api.derive.staking.erasRewards as any, []); + const stakerPoints = useCall(api.derive.staking.stakerPoints as any, [validatorId, true]); + const { currency, divisor } = useMemo((): { currency: string; divisor: BN } => ({ + currency: formatBalance.getDefaults().unit, + divisor: new BN('1'.padEnd(formatBalance.getDefaults().decimals + 1, '0')) + }), [formatBalance]); + const legends = useMemo(() => [ + t('{{currency}} slashed', { replace: { currency } }), + t('{{currency}} rewards', { replace: { currency } }), + t('{{currency}} average', { replace: { currency } }) + ], [currency, t]); + + useEffect((): void => { + erasRewards && ownSlashes && stakerPoints && setChart( + extractRewards(erasRewards, ownSlashes, stakerPoints, divisor) + ); + }, [erasRewards, stakerPoints]); + + return ( +
+

{t('rewards & slashes')}

+ {chart.length + ? ( + + ) + : + } +
+ ); +} + +export default React.memo(ChartRewards); diff --git a/packages/page-staking/src/Query/ChartStake.tsx b/packages/page-staking/src/Query/ChartStake.tsx new file mode 100644 index 0000000000..abe8f9e960 --- /dev/null +++ b/packages/page-staking/src/Query/ChartStake.tsx @@ -0,0 +1,88 @@ +// Copyright 2017-2020 @polkadot/app-staking authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import { DeriveOwnExposure } from '@polkadot/api-derive/types'; +import { ChartInfo, LineDataEntry, Props } from './types'; + +import BN from 'bn.js'; +import React, { useEffect, useMemo, useState } from 'react'; +import { Chart, Spinner } from '@polkadot/react-components'; +import { useApi, useCall } from '@polkadot/react-hooks'; +import { formatBalance } from '@polkadot/util'; + +import { useTranslation } from '../translate'; +import { balanceToNumber } from './util'; + +const COLORS_STAKE = [undefined, '#8c2200', '#acacac']; + +function extractStake (exposures: DeriveOwnExposure[], divisor: BN): ChartInfo { + const labels: string[] = []; + const cliSet: LineDataEntry = []; + const expSet: LineDataEntry = []; + const avgSet: LineDataEntry = []; + let avgCount = 0; + let total = 0; + + exposures.forEach(({ clipped, era, exposure }): void => { + const cli = balanceToNumber(clipped.total.unwrap(), divisor); + const exp = balanceToNumber(exposure.total.unwrap(), divisor); + + total += cli; + + if (cli > 0) { + avgCount++; + } + + avgSet.push((avgCount ? Math.ceil(total * 100 / avgCount) : 0) / 100); + labels.push(era.toHuman()); + cliSet.push(cli); + expSet.push(exp); + }); + + return { + chart: [cliSet, expSet, avgSet], + labels + }; +} + +function ChartStake ({ validatorId }: Props): React.ReactElement { + const { t } = useTranslation(); + const { api } = useApi(); + const [{ chart, labels }, setChart] = useState({ chart: [], labels: [] }); + const ownExposure = useCall(api.derive.staking.ownExposure as any, [validatorId, true]); + const { currency, divisor } = useMemo((): { currency: string; divisor: BN } => ({ + currency: formatBalance.getDefaults().unit, + divisor: new BN('1'.padEnd(formatBalance.getDefaults().decimals + 1, '0')) + }), [formatBalance]); + const legends = useMemo(() => [ + t('{{currency}} clipped', { replace: { currency } }), + t('{{currency}} total', { replace: { currency } }), + t('{{currency}} average', { replace: { currency } }) + ], [currency, t]); + + useEffect((): void => { + ownExposure && setChart( + extractStake(ownExposure, divisor) + ); + }, [ownExposure]); + + return ( +
+

{t('elected stake')}

+ {chart && !!chart[0]?.length + ? ( + + ) + : + } +
+ ); +} + +export default React.memo(ChartStake); diff --git a/packages/page-staking/src/Query/Validator.tsx b/packages/page-staking/src/Query/Validator.tsx index cff49fc929..d7e0ca1be8 100644 --- a/packages/page-staking/src/Query/Validator.tsx +++ b/packages/page-staking/src/Query/Validator.tsx @@ -2,212 +2,24 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. -import { DeriveEraRewards, DeriveOwnExposure, DeriveOwnSlashes, DeriveStakerPoints } from '@polkadot/api-derive/types'; +import { Props } from './types'; -import BN from 'bn.js'; -import React, { useEffect, useMemo, useState } from 'react'; -import { Chart, Columar, Column, Spinner } from '@polkadot/react-components'; -import { useApi, useCall } from '@polkadot/react-hooks'; -import { formatBalance } from '@polkadot/util'; +import React from 'react'; +import { Columar, Column } from '@polkadot/react-components'; -import { useTranslation } from '../translate'; - -interface Props { - className?: string; - validatorId: string; -} - -type LineDataEntry = (BN | number)[]; - -type LineData = LineDataEntry[]; - -interface ChartInfo { - chart: LineData; - labels: string[]; -} - -const COLORS_REWARD = ['#8c2200', '#008c22', '#acacac']; -const COLORS_POINTS = [undefined, '#acacac']; -const COLORS_STAKE = [undefined, '#8c2200', '#acacac']; - -function balanceToNumber (amount: BN, divisor: BN): number { - return amount.muln(1000).div(divisor).toNumber() / 1000; -} - -function extractPoints (points: DeriveStakerPoints[]): ChartInfo { - const labels: string[] = []; - const avgSet: LineDataEntry = []; - const idxSet: LineDataEntry = []; - let avgCount = 0; - let total = 0; - - points.forEach(({ era, points }): void => { - total += points.toNumber(); - labels.push(era.toHuman()); - - if (points.gtn(0)) { - avgCount++; - } - - avgSet.push((avgCount ? Math.ceil(total * 100 / avgCount) : 0) / 100); - idxSet.push(points); - }); - - return { - chart: [idxSet, avgSet], - labels - }; -} - -function extractRewards (erasRewards: DeriveEraRewards[], ownSlashes: DeriveOwnSlashes[], allPoints: DeriveStakerPoints[], divisor: BN): ChartInfo { - const labels: string[] = []; - const slashSet: LineDataEntry = []; - const rewardSet: LineDataEntry = []; - const avgSet: LineDataEntry = []; - let avgCount = 0; - let total = 0; - - erasRewards.forEach(({ era, eraReward }): void => { - const points = allPoints.find((points) => points.era.eq(era)); - const slashed = ownSlashes.find((slash) => slash.era.eq(era)); - const reward = points?.eraPoints.gtn(0) - ? balanceToNumber(points.points.mul(eraReward).div(points.eraPoints), divisor) - : 0; - const slash = slashed - ? balanceToNumber(slashed.total, divisor) - : 0; - - total += reward; - - if (reward > 0) { - avgCount++; - } - - labels.push(era.toHuman()); - rewardSet.push(reward); - avgSet.push((avgCount ? Math.ceil(total * 100 / avgCount) : 0) / 100); - slashSet.push(slash); - }); - - return { - chart: [slashSet, rewardSet, avgSet], - labels - }; -} - -function extractStake (exposures: DeriveOwnExposure[], divisor: BN): ChartInfo { - const labels: string[] = []; - const cliSet: LineDataEntry = []; - const expSet: LineDataEntry = []; - const avgSet: LineDataEntry = []; - let avgCount = 0; - let total = 0; - - exposures.forEach(({ clipped, era, exposure }): void => { - const cli = balanceToNumber(clipped.total.unwrap(), divisor); - const exp = balanceToNumber(exposure.total.unwrap(), divisor); - - total += cli; - - if (cli > 0) { - avgCount++; - } - - avgSet.push((avgCount ? Math.ceil(total * 100 / avgCount) : 0) / 100); - labels.push(era.toHuman()); - cliSet.push(cli); - expSet.push(exp); - }); - - return { - chart: [cliSet, expSet, avgSet], - labels - }; -} +import ChartPoints from './ChartPoints'; +import ChartRewards from './ChartRewards'; +import ChartStake from './ChartStake'; function Validator ({ className, validatorId }: Props): React.ReactElement { - const { t } = useTranslation(); - const { api } = useApi(); - const [{ chart: pointChart, labels: pointLabels }, setPointChart] = useState({ chart: [], labels: [] }); - const [{ chart: rewardChart, labels: rewardLabels }, setRewardChart] = useState({ chart: [], labels: [] }); - const [{ chart: stakeChart, labels: stakeLabels }, setStakeCharts] = useState({ chart: [], labels: [] }); - const ownExposure = useCall(api.derive.staking.ownExposure as any, [validatorId, true]); - const ownSlashes = useCall(api.derive.staking.ownSlashes as any, [validatorId, true]); - const erasRewards = useCall(api.derive.staking.erasRewards as any, []); - const stakerPoints = useCall(api.derive.staking.stakerPoints as any, [validatorId, true]); - const { currency, divisor } = useMemo((): { currency: string; divisor: BN } => ({ - currency: formatBalance.getDefaults().unit, - divisor: new BN('1'.padEnd(formatBalance.getDefaults().decimals + 1, '0')) - }), [formatBalance]); - const legExpose = useMemo(() => [t('{{currency}} clipped', { replace: { currency } }), t('{{currency}} total', { replace: { currency } }), t('{{currency}} average', { replace: { currency } })], [currency, t]); - const legPoints = useMemo(() => [t('points'), t('average')], [t]); - const legReward = useMemo(() => [t('{{currency}} slashed', { replace: { currency } }), t('{{currency}} rewards', { replace: { currency } }), t('{{currency}} average', { replace: { currency } })], [currency, t]); - - useEffect((): void => { - stakerPoints && setPointChart( - extractPoints(stakerPoints) - ); - }, [stakerPoints]); - - useEffect((): void => { - erasRewards && ownSlashes && stakerPoints && setRewardChart( - extractRewards(erasRewards, ownSlashes, stakerPoints, divisor) - ); - }, [erasRewards, stakerPoints]); - - useEffect((): void => { - ownExposure && setStakeCharts( - extractStake(ownExposure, divisor) - ); - }, [ownExposure]); - return ( -
-

{t('era points')}

- {pointChart.length - ? ( - - ) - : - } -
-
-

{t('rewards & slashes')}

- {rewardChart.length - ? ( - - ) - : - } -
+ +
-
-

{t('elected stake')}

- {stakeChart && !!stakeChart[0]?.length - ? ( - - ) - : - } -
+
); diff --git a/packages/page-staking/src/Query/types.ts b/packages/page-staking/src/Query/types.ts new file mode 100644 index 0000000000..a46f67e61c --- /dev/null +++ b/packages/page-staking/src/Query/types.ts @@ -0,0 +1,19 @@ +// Copyright 2017-2020 @polkadot/app-staking authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import BN from 'bn.js'; + +export interface Props { + className?: string; + validatorId: string; +} + +export type LineDataEntry = (BN | number)[]; + +export type LineData = LineDataEntry[]; + +export interface ChartInfo { + chart: LineData; + labels: string[]; +} diff --git a/packages/page-staking/src/Query/util.ts b/packages/page-staking/src/Query/util.ts new file mode 100644 index 0000000000..9b525397a3 --- /dev/null +++ b/packages/page-staking/src/Query/util.ts @@ -0,0 +1,9 @@ +// Copyright 2017-2020 @polkadot/app-staking authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import BN from 'bn.js'; + +export function balanceToNumber (amount: BN, divisor: BN): number { + return amount.muln(1000).div(divisor).toNumber() / 1000; +} diff --git a/packages/page-staking/src/Targets/Validator.tsx b/packages/page-staking/src/Targets/Validator.tsx index 4b95783eca..50e103cbe0 100644 --- a/packages/page-staking/src/Targets/Validator.tsx +++ b/packages/page-staking/src/Targets/Validator.tsx @@ -10,6 +10,7 @@ import { FormatBalance } from '@polkadot/react-query'; import { formatNumber } from '@polkadot/util'; import { useTranslation } from '../translate'; +import Favorite from '../Overview/Address/Favorite'; interface Props { info: ValidatorInfo; @@ -19,10 +20,6 @@ interface Props { function Validator ({ info: { accountId, bondOther, bondOwn, bondTotal, commissionPer, isCommission, isFavorite, isNominating, key, numNominators, rankOverall, rewardPayout, validatorPayment }, toggleFavorite }: Props): React.ReactElement { const { t } = useTranslation(); - const _onFavorite = useCallback( - (): void => toggleFavorite(key), - [key] - ); const _onQueryStats = useCallback( (): void => { window.location.hash = `/staking/query/${key}`; @@ -32,13 +29,11 @@ function Validator ({ info: { accountId, bondOther, bondOwn, bondTotal, commissi return ( - - - + {formatNumber(rankOverall)}