From 118cc51dc347eca36e11caf7ffd72e1c0c7e71d7 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Mon, 30 Nov 2020 00:47:24 +0100 Subject: [PATCH] Human-readable payout (target) descriptions (#4107) --- .../page-staking/src/Targets/Validator.tsx | 15 ++++++- packages/page-staking/src/Targets/index.tsx | 2 +- packages/page-staking/src/useSortedTargets.ts | 43 +++++++++++++------ packages/react-hooks/src/useBlockTime.ts | 10 +++-- 4 files changed, 52 insertions(+), 18 deletions(-) diff --git a/packages/page-staking/src/Targets/Validator.tsx b/packages/page-staking/src/Targets/Validator.tsx index 6ddfb29e7c..09de9f05c7 100644 --- a/packages/page-staking/src/Targets/Validator.tsx +++ b/packages/page-staking/src/Targets/Validator.tsx @@ -10,7 +10,7 @@ import React, { useCallback, useMemo } from 'react'; import { AddressSmall, Badge, Checkbox, Icon } from '@polkadot/react-components'; import { checkVisibility } from '@polkadot/react-components/util'; import { FormatBalance } from '@polkadot/react-query'; -import { useApi, useCall } from '@polkadot/react-hooks'; +import { useApi, useBlockTime, useCall } from '@polkadot/react-hooks'; import { formatNumber } from '@polkadot/util'; import MaxBadge from '../MaxBadge'; @@ -32,6 +32,7 @@ function Validator ({ allSlashes, canSelect, filterName, info, isNominated, isSe const { t } = useTranslation(); const { api } = useApi(); const accountInfo = useCall(api.derive.accounts.info, [info.accountId]); + const [,, time] = useBlockTime(info.lastPayout); const isVisible = useMemo( () => accountInfo @@ -108,7 +109,17 @@ function Validator ({ allSlashes, canSelect, filterName, info, isNominated, isSe - {lastPayout && formatNumber(lastPayout)} + + {lastPayout && ( + api.consts.babe + ? time.days + ? time.days === 1 + ? t('yesterday') + : t('{{days}} days', { replace: { days: time.days } }) + : t('recently') + : formatNumber(lastPayout) + )} + {numNominators || ''} { diff --git a/packages/page-staking/src/Targets/index.tsx b/packages/page-staking/src/Targets/index.tsx index 6888479cf7..b899453261 100644 --- a/packages/page-staking/src/Targets/index.tsx +++ b/packages/page-staking/src/Targets/index.tsx @@ -221,7 +221,7 @@ function Targets ({ className = '', isInElection, ownStashes, targets: { avgStak const header = useMemo(() => [ [t('validators'), 'start', 3], - [t('paid'), 'media--1400'], + [t('payout'), 'media--1500'], ...(SORT_KEYS as (keyof typeof labelsRef.current)[]).map((header) => [ <>{labelsRef.current[header]}, `${sorted ? `isClickable ${sortBy === header ? 'highlight--border' : ''} number` : 'number'} ${CLASSES[header] || ''}`, diff --git a/packages/page-staking/src/useSortedTargets.ts b/packages/page-staking/src/useSortedTargets.ts index b55ee1c947..4680bbd371 100644 --- a/packages/page-staking/src/useSortedTargets.ts +++ b/packages/page-staking/src/useSortedTargets.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import type { ApiPromise } from '@polkadot/api'; -import type { DeriveSessionIndexes, DeriveStakingElected, DeriveStakingWaiting } from '@polkadot/api-derive/types'; +import type { DeriveSessionInfo, DeriveStakingElected, DeriveStakingWaiting } from '@polkadot/api-derive/types'; import type { Option } from '@polkadot/types'; import type { Balance, ValidatorPrefsTo196 } from '@polkadot/types/interfaces'; import type { SortedTargets, TargetSortBy, ValidatorInfo } from './types'; @@ -13,6 +13,13 @@ import { registry } from '@polkadot/react-api'; import { calcInflation, useAccounts, useApi, useCall } from '@polkadot/react-hooks'; import { BN_ONE, BN_ZERO } from '@polkadot/util'; +interface LastEra { + eraLength: BN; + lastEra: BN; + lastEraEnd: BN; + sessionLength: BN; +} + const PERBILL = new BN(1_000_000_000); const EMPTY_PARTIAL = {}; @@ -77,7 +84,7 @@ function sortValidators (list: ValidatorInfo[]): ValidatorInfo[] { ); } -function extractSingle (allAccounts: string[], derive: DeriveStakingElected | DeriveStakingWaiting, favorites: string[], perValidatorReward: BN): [ValidatorInfo[], string[]] { +function extractSingle (allAccounts: string[], derive: DeriveStakingElected | DeriveStakingWaiting, favorites: string[], perValidatorReward: BN, { eraLength, lastEra, sessionLength }: LastEra): [ValidatorInfo[], string[]] { const nominators: Record = {}; const emptyExposure = registry.createType('Exposure'); const info = derive.info; @@ -104,7 +111,14 @@ function extractSingle (allAccounts: string[], derive: DeriveStakingElected | De return isNominating || allAccounts.includes(nominator); }, allAccounts.includes(key)); const isElected = !isWaitingDerive(derive) && derive.nextElected.some((e) => e.eq(accountId)); - const lastPayout = stakingLedger.claimedRewards[stakingLedger.claimedRewards.length - 1]; + const lastEraPayout = !lastEra.isZero() + ? stakingLedger.claimedRewards[stakingLedger.claimedRewards.length - 1] + : undefined; + let lastPayout: BN | undefined = lastEraPayout; + + if (lastPayout && !sessionLength.eq(BN_ONE)) { + lastPayout = lastEra.sub(lastPayout).mul(eraLength); + } return { accountId, @@ -142,10 +156,10 @@ function extractSingle (allAccounts: string[], derive: DeriveStakingElected | De return [list, Object.keys(nominators)]; } -function extractInfo (api: ApiPromise, allAccounts: string[], electedDerive: DeriveStakingElected, waitingDerive: DeriveStakingWaiting, favorites: string[], totalIssuance: BN, lastReward = BN_ONE): Partial { +function extractInfo (api: ApiPromise, allAccounts: string[], electedDerive: DeriveStakingElected, waitingDerive: DeriveStakingWaiting, favorites: string[], totalIssuance: BN, lastEraInfo: LastEra, lastReward: BN): Partial { const perValidatorReward = lastReward.divn(electedDerive.info.length); - const [elected, nominators] = extractSingle(allAccounts, electedDerive, favorites, perValidatorReward); - const [waiting] = extractSingle(allAccounts, waitingDerive, favorites, perValidatorReward); + const [elected, nominators] = extractSingle(allAccounts, electedDerive, favorites, perValidatorReward, lastEraInfo); + const [waiting] = extractSingle(allAccounts, waitingDerive, favorites, perValidatorReward, lastEraInfo); const activeTotals = elected .filter(({ isActive }) => isActive) .map(({ bondTotal }) => bondTotal) @@ -177,7 +191,12 @@ function extractInfo (api: ApiPromise, allAccounts: string[], electedDerive: Der } const transformEra = { - transform: ({ activeEra }: DeriveSessionIndexes) => activeEra.gtn(0) ? activeEra.subn(1) : BN_ZERO + transform: ({ activeEra, activeEraStart, eraLength, sessionLength }: DeriveSessionInfo): LastEra => ({ + eraLength, + lastEra: activeEra.gtn(0) ? activeEra.subn(1) : BN_ZERO, + lastEraEnd: activeEra.gtn(0) && activeEraStart.isSome ? activeEraStart.unwrap() : BN_ZERO, + sessionLength + }) }; const transformReward = { @@ -190,14 +209,14 @@ export default function useSortedTargets (favorites: string[]): SortedTargets { const totalIssuance = useCall(api.query.balances.totalIssuance); const electedInfo = useCall(api.derive.staking.electedInfo); const waitingInfo = useCall(api.derive.staking.waitingInfo); - const lastEra = useCall(api.derive.session.indexes, undefined, transformEra); - const lastReward = useCall(lastEra && api.query.staking.erasValidatorReward, [lastEra], transformReward); + const lastEraInfo = useCall(api.derive.session.info, undefined, transformEra); + const lastReward = useCall(lastEraInfo && api.query.staking.erasValidatorReward, [lastEraInfo?.lastEra], transformReward); const partial = useMemo( - () => electedInfo && totalIssuance && waitingInfo - ? extractInfo(api, allAccounts, electedInfo, waitingInfo, favorites, totalIssuance, lastReward) + () => electedInfo && lastEraInfo && lastReward && totalIssuance && waitingInfo + ? extractInfo(api, allAccounts, electedInfo, waitingInfo, favorites, totalIssuance, lastEraInfo, lastReward) : EMPTY_PARTIAL, - [api, allAccounts, electedInfo, favorites, lastReward, totalIssuance, waitingInfo] + [api, allAccounts, electedInfo, favorites, lastEraInfo, lastReward, totalIssuance, waitingInfo] ); return { inflation: { inflation: 0, stakedReturn: 0 }, ...partial, lastReward }; diff --git a/packages/react-hooks/src/useBlockTime.ts b/packages/react-hooks/src/useBlockTime.ts index e391e42f7f..e0f88ddc9d 100644 --- a/packages/react-hooks/src/useBlockTime.ts +++ b/packages/react-hooks/src/useBlockTime.ts @@ -1,6 +1,8 @@ // Copyright 2017-2020 @polkadot/app-democracy authors & contributors // SPDX-License-Identifier: Apache-2.0 +import type { Time } from '@polkadot/util/types'; + import BN from 'bn.js'; import { useMemo } from 'react'; import { useApi } from '@polkadot/react-hooks'; @@ -8,7 +10,7 @@ import { BN_ONE, extractTime } from '@polkadot/util'; import { useTranslation } from './translate'; -type Result = [number, string]; +type Result = [number, string, Time]; const DEFAULT_TIME = new BN(6000); @@ -24,7 +26,8 @@ export function useBlockTime (blocks = BN_ONE): Result { api.consts.timestamp?.minimumPeriod.muln(2) || DEFAULT_TIME ); - const { days, hours, minutes, seconds } = extractTime(blockTime.mul(blocks).toNumber()); + const time = extractTime(blockTime.mul(blocks).toNumber()); + const { days, hours, minutes, seconds } = time; const timeStr = [ days ? (days > 1) ? t('{{days}} days', { replace: { days } }) : t('1 day') : null, hours ? (hours > 1) ? t('{{hours}} hrs', { replace: { hours } }) : t('1 hr') : null, @@ -37,7 +40,8 @@ export function useBlockTime (blocks = BN_ONE): Result { return [ blockTime.toNumber(), - timeStr + timeStr, + time ]; }, [api, blocks, t]