diff --git a/packages/app-staking/src/Overview/Address.tsx b/packages/app-staking/src/Overview/Address.tsx index eda08efea284..f2d921eb2053 100644 --- a/packages/app-staking/src/Overview/Address.tsx +++ b/packages/app-staking/src/Overview/Address.tsx @@ -10,10 +10,9 @@ import { ValidatorFilter } from '../types'; import BN from 'bn.js'; import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; -import { withCalls, withMulti } from '@polkadot/react-api'; import { AddressCard, AddressMini, Badge, Expander, Icon } from '@polkadot/react-components'; import { classes } from '@polkadot/react-components/util'; -import { useApiContext } from '@polkadot/react-hooks'; +import { trackStream, useApiContext } from '@polkadot/react-hooks'; import { formatNumber } from '@polkadot/util'; import translate from '../translate'; @@ -24,13 +23,13 @@ interface Props extends I18nProps { className?: string; defaultName: string; filter: ValidatorFilter; + hasQueries: boolean; isElected: boolean; isFavorite: boolean; lastAuthors?: string[]; myAccounts: string[]; points?: Points; recentlyOnline?: DerivedHeartbeats; - stakingInfo?: DerivedStaking; toggleFavorite: (accountId: string) => void; withNominations?: boolean; } @@ -47,8 +46,10 @@ interface StakingState { const WITH_VALIDATOR_PREFS = { validatorPayment: true }; -function Address ({ address, authorsMap, className, defaultName, filter, isElected, isFavorite, lastAuthors, myAccounts, points, recentlyOnline, stakingInfo, t, toggleFavorite, withNominations = true }: Props): React.ReactElement | null { +function Address ({ address, authorsMap, className, defaultName, filter, hasQueries, isElected, isFavorite, lastAuthors, myAccounts, points, recentlyOnline, t, toggleFavorite, withNominations = true }: Props): React.ReactElement | null { const { api, isSubstrateV2 } = useApiContext(); + // FIXME Any horrors, caused by trackStream + const stakingInfo = trackStream(api.derive.staking.info as any, [address]); const [extraInfo, setExtraInfo] = useState<[React.ReactNode, React.ReactNode][] | undefined>(); const [hasActivity, setHasActivity] = useState(true); const [{ balanceOpts, controllerId, hasNominators, isNominatorMe, nominators, sessionId, stashId }, setStakingState] = useState({ @@ -188,7 +189,7 @@ function Address ({ address, authorsMap, className, defaultName, filter, isElect } isDisabled={isSubstrateV2 && !hasActivity} overlay={ - api.query.imOnline?.authoredBlocks && ( + hasQueries && api.query.imOnline?.authoredBlocks && ( ( - ['derive.staking.info', { - paramName: 'address', - propName: 'stakingInfo' - }] - ) + ` ); diff --git a/packages/app-staking/src/Overview/CurrentList.tsx b/packages/app-staking/src/Overview/CurrentList.tsx index 44e6a8216eaa..6f3fccd9e9fc 100644 --- a/packages/app-staking/src/Overview/CurrentList.tsx +++ b/packages/app-staking/src/Overview/CurrentList.tsx @@ -18,6 +18,7 @@ import Address from './Address'; interface Props extends I18nProps { authorsMap: Record; + hasQueries: boolean; lastAuthors?: string[]; next: string[]; recentlyOnline?: DerivedHeartbeats; @@ -55,7 +56,7 @@ function accountsToString (accounts: AccountId[]): string[] { return accounts.map((accountId): string => accountId.toString()); } -function CurrentList ({ authorsMap, lastAuthors, next, recentlyOnline, stakingOverview, t }: Props): React.ReactElement { +function CurrentList ({ authorsMap, hasQueries, lastAuthors, next, recentlyOnline, stakingOverview, t }: Props): React.ReactElement { const { isSubstrateV2 } = useApiContext(); const [favorites, toggleFavorite] = useFavorites(STORE_FAVS_BASE); const [filter, setFilter] = useState('all'); @@ -84,6 +85,7 @@ function CurrentList ({ authorsMap, lastAuthors, next, recentlyOnline, stakingOv authorsMap={authorsMap} defaultName={defaultName} filter={filter} + hasQueries={hasQueries} isElected={isElected} isFavorite={isFavorite} lastAuthors={lastAuthors} diff --git a/packages/app-staking/src/Overview/index.tsx b/packages/app-staking/src/Overview/index.tsx index b7efc4f5a6fd..be59b4eafec5 100644 --- a/packages/app-staking/src/Overview/index.tsx +++ b/packages/app-staking/src/Overview/index.tsx @@ -14,7 +14,7 @@ import Summary from './Summary'; interface Props extends BareProps, ComponentProps {} -export default function Overview ({ allControllers, allStashes, className, recentlyOnline, stakingOverview }: Props): React.ReactElement { +export default function Overview ({ allControllers, hasQueries, allStashes, className, recentlyOnline, stakingOverview }: Props): React.ReactElement { const { isSubstrateV2 } = useApiContext(); const { byAuthor, lastBlockAuthors, lastBlockNumber } = useContext(BlockAuthorsContext); const [next, setNext] = useState([]); @@ -41,6 +41,7 @@ export default function Overview ({ allControllers, allStashes, className, recen /> formatNumber(bn)), - [ - values.map(([,, { total }]): BN => - total.unwrap().div(divisor)) - // exposures.map(({ own }): BN => - // own.unwrap().div(divisor)), - // exposures.map(({ others }): BN => - // others.reduce((total, { value }): BN => total.add(value.unwrap()), new BN(0)).div(divisor)) - ] + values.map(([, { total }]): BN => + total.unwrap().div(divisor)) + // exposures.map(({ own }): BN => + // own.unwrap().div(divisor)), + // exposures.map(({ others }): BN => + // others.reduce((total, { value }): BN => total.add(value.unwrap()), new BN(0)).div(divisor)) ]; } -function extractSplit (values: [BN, Hash, Exposure][], validatorId: string): SplitData | null { - const last = values[values.length - 1][2]; +function extractSplit (values: [Hash, Exposure][], validatorId: string): SplitData | null { + const last = values[values.length - 1][1]; const total = last.total.unwrap(); if (total.eqn(0)) { @@ -96,8 +79,17 @@ function extractEraSlash (validatorId: string, slashes: Slash[]): BN { }, new BN(0)); } -function Validator ({ blockCounts, className, currentIndex, sessionRewards, startNumber, t, validatorId }: Props): React.ReactElement { +function balanceToNumber (amount: BN, divisor: BN): number { + return amount.muln(1000).div(divisor).toNumber() / 1000; +} + +function Validator ({ className, sessionRewards, t, validatorId }: Props): React.ReactElement { const { api } = useApiContext(); + // FIXME There is something seriously wrong in these two with "any" horrors + const blockCounts = trackStream(api.query.imOnline?.authoredBlocks?.multi as any, [sessionRewards, validatorId], { + paramMap: ([sessionRewards, validatorId]: [SessionRewards[], string]): any => + [sessionRewards.map(({ sessionIndex }): [SessionIndex, string] => [sessionIndex, validatorId])] + }); const [blocksLabels, setBlocksLabels] = useState([]); const [blocksChart, setBlocksChart] = useState(null); const [{ rewardsChart, rewardsLabels }, setRewardsInfo] = useState<{ rewardsChart: LineData | null; rewardsLabels: string[] }>({ rewardsChart: null, rewardsLabels: [] }); @@ -106,53 +98,65 @@ function Validator ({ blockCounts, className, currentIndex, sessionRewards, star const divisor = new BN('1'.padEnd(formatBalance.getDefaults().decimals + 1, '0')); useEffect((): void => { - api.isReady.then(async (): Promise => { - const values = await getHistoric(api, 'staking.stakers', [validatorId], { - interval: (api.consts.babe?.epochDuration as BlockNumber || new BN(500)).muln(2).divn(3), - max: MAX_SESSIONS, - startNumber + if (!splitChart) { + const hashes = sessionRewards.map(({ blockHash }): Hash => blockHash); + const stakeLabels = sessionRewards.map(({ sessionIndex }): string => formatNumber(sessionIndex)); + + api.isReady.then(async (): Promise => { + const values = await getHistoric(api, 'staking.stakers', [validatorId], hashes); + const stakeChart = extractStake(values, divisor); + const splitChart = extractSplit(values, validatorId); + const splitMax = splitChart ? Math.min(Math.ceil(splitChart[0].value), 100) : 100; + + setStakeInfo({ stakeChart, stakeLabels }); + setSplitInfo({ splitChart, splitMax }); }); - const [stakeLabels, stakeChart] = extractStake(values, divisor); - const splitChart = extractSplit(values, validatorId); - const splitMax = splitChart ? Math.min(Math.ceil(splitChart[0].value), 100) : 100; - - setStakeInfo({ stakeChart, stakeLabels }); - setSplitInfo({ splitChart, splitMax }); - }); - }, []); + } + }, [sessionRewards, splitChart]); useEffect((): void => { - const rewardsLabels: string[] = []; - const rewardsChart: LineData = [[]]; - - sessionRewards.forEach(({ sessionIndex, slashes }): void => { - // this shows the start of the new era, however rewards are for previous - rewardsLabels.push(formatNumber(sessionIndex.subn(1))); - - // calculate and format to 3 decimals - rewardsChart[0].push( - extractEraSlash(validatorId, slashes).muln(1000).div(divisor).toNumber() / 1000 - ); - }); + if (blockCounts) { + const rewardsLabels: string[] = []; + const rewardsChart: LineData = [[], [], []]; + let total = new BN(0); + + sessionRewards.forEach(({ blockNumber, reward, sessionIndex, slashes }, index): void => { + // this shows the start of the new era, however rewards are for previous + rewardsLabels.push(formatNumber(sessionIndex.subn(1))); + + const neg = extractEraSlash(validatorId, slashes); + const pos = index + ? reward.mul(blockCounts[index - 1]).div(blockNumber.sub(sessionRewards[index - 1].blockNumber)) + : new BN(0); + + // add this to the total + total = total.add(neg).add(pos); + + // calculate and format to 3 decimals + rewardsChart[0].push(balanceToNumber(neg, divisor)); + rewardsChart[1].push(balanceToNumber(pos, divisor)); + rewardsChart[2].push(balanceToNumber(total.divn(index), divisor)); + }); - setRewardsInfo({ rewardsChart, rewardsLabels }); - }, [sessionRewards, validatorId]); + setRewardsInfo({ rewardsChart, rewardsLabels }); + } + }, [blockCounts, sessionRewards, validatorId]); useEffect((): void => { setBlocksLabels( - getIndexRange(currentIndex).map((index): string => formatNumber(index)) + sessionRewards.map(({ sessionIndex }): string => formatNumber(sessionIndex)) ); - }, [currentIndex]); + }, [sessionRewards]); useEffect((): void => { if (blockCounts) { const avgSet: number[] = []; const idxSet: BN[] = []; - blockCounts.reduce((total: BN, value, index): BN => { + blockCounts.reduce((total: BN, value: u32, index: number): BN => { total = total.add(value); - avgSet.push(total.toNumber() / (index + 1)); + avgSet.push(total.muln(100).divn(index + 1).toNumber() / 100); idxSet.push(value); return total; @@ -160,7 +164,7 @@ function Validator ({ blockCounts, className, currentIndex, sessionRewards, star setBlocksChart([idxSet, avgSet]); } - }, [blockCounts, blocksLabels]); + }, [blockCounts]); return ( @@ -169,7 +173,7 @@ function Validator ({ blockCounts, className, currentIndex, sessionRewards, star <> {blocksChart && (
-

{t('blocks per session')}

+

{t('blocks produced')}

-

{t('slashed per session')}

+

{t('rewards & slashes')}

@@ -222,13 +226,4 @@ function Validator ({ blockCounts, className, currentIndex, sessionRewards, star ); } -export default translate( - withCalls( - ['query.imOnline.authoredBlocks', { - isMulti: true, - propName: 'blockCounts', - paramPick: ({ currentIndex, validatorId }: Props): [BN, string][] => - getIndexRange(currentIndex).map((index): [BN, string] => [index, validatorId]) - }] - )(Validator) -); +export default translate(Validator); diff --git a/packages/app-staking/src/Query/index.tsx b/packages/app-staking/src/Query/index.tsx index 12c410d9affb..93cae7326ec9 100644 --- a/packages/app-staking/src/Query/index.tsx +++ b/packages/app-staking/src/Query/index.tsx @@ -3,10 +3,9 @@ // of the Apache-2.0 license. See the LICENSE file for details. import { I18nProps } from '@polkadot/react-components/types'; -import { BlockNumber } from '@polkadot/types/interfaces'; import { ComponentProps } from '../types'; -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { withRouter, RouteComponentProps } from 'react-router-dom'; import { Button, InputAddressSimple } from '@polkadot/react-components'; @@ -47,16 +46,9 @@ interface Props extends I18nProps, ComponentProps, RouteComponentProps<{}> { // /> // -function Query ({ bestNumber, className, sessionRewards, stakingOverview, match: { params: { value } }, t }: Props): React.ReactElement { - const [startNumber, setStartNumber] = useState(); +function Query ({ className, sessionRewards, match: { params: { value } }, t }: Props): React.ReactElement { const [validatorId, setValidatorId] = useState(value || null); - useEffect((): void => { - if (bestNumber && !startNumber) { - setStartNumber(bestNumber); - } - }, [bestNumber, startNumber]); - const _onQuery = (): void => { if (validatorId) { window.location.hash = `/staking/query/${validatorId}`; @@ -79,11 +71,9 @@ function Query ({ bestNumber, className, sessionRewards, stakingOverview, match: onClick={_onQuery} /> - {value && startNumber && stakingOverview && ( + {value && ( )} diff --git a/packages/app-staking/src/index.tsx b/packages/app-staking/src/index.tsx index c43758e69f07..955e26b72e54 100644 --- a/packages/app-staking/src/index.tsx +++ b/packages/app-staking/src/index.tsx @@ -16,8 +16,8 @@ import styled from 'styled-components'; import { Option } from '@polkadot/types'; import { HelpOverlay } from '@polkadot/react-components'; import Tabs from '@polkadot/react-components/Tabs'; -import { withCalls, withMulti, withObservable } from '@polkadot/react-api'; -import { useApiContext, useSessionRewards } from '@polkadot/react-hooks'; +import { withMulti, withObservable } from '@polkadot/react-api'; +import { trackStream, useApiContext, useSessionRewards } from '@polkadot/react-hooks'; import accountObservable from '@polkadot/ui-keyring/observable/accounts'; import Accounts from './Actions/Accounts'; @@ -29,19 +29,32 @@ import translate from './translate'; interface Props extends AppProps, ApiProps, I18nProps { allAccounts?: SubjectInfo; - allStashesAndControllers?: [string[], string[]]; - bestNumber?: BlockNumber; - recentlyOnline?: DerivedHeartbeats; - stakingOverview?: DerivedStakingOverview; } const EMPY_ACCOUNTS: string[] = []; const EMPTY_ALL: [string[], string[]] = [EMPY_ACCOUNTS, EMPY_ACCOUNTS]; -function App ({ allAccounts, allStashesAndControllers: [allStashes, allControllers] = EMPTY_ALL, basePath, bestNumber, className, recentlyOnline, stakingOverview, t }: Props): React.ReactElement { +function transformStakingControllers ([stashes, controllers]: [AccountId[], Option[]]): [string[], string[]] { + return [ + stashes.map((accountId): string => accountId.toString()), + controllers + .filter((optId): boolean => optId.isSome) + .map((accountId): string => accountId.unwrap().toString()) + ]; +} + +function App ({ allAccounts, basePath, className, t }: Props): React.ReactElement { const { api } = useApiContext(); + const stakingControllers = trackStream<[string[], string[]]>(api.derive.staking.controllers, [], { transform: transformStakingControllers }); + const bestNumber = trackStream(api.derive.chain.bestNumber, []); + const recentlyOnline = trackStream(api.derive.imOnline.receivedHeartbeats, []); + const stakingOverview = trackStream(api.derive.staking.overview, []); const sessionRewards = useSessionRewards(MAX_SESSIONS); const routeMatch = useRouteMatch({ path: basePath, strict: true }); + + const hasAccounts = !!allAccounts && Object.keys(allAccounts).length !== 0; + const hasQueries = hasAccounts && !!(api.query.imOnline?.authoredBlocks); + const [allStashes, allControllers] = stakingControllers || EMPTY_ALL; const _renderComponent = (Component: React.ComponentType, className?: string): () => React.ReactNode => { // eslint-disable-next-line react/display-name return (): React.ReactNode => { @@ -56,6 +69,8 @@ function App ({ allAccounts, allStashesAndControllers: [allStashes, allControlle allStashes={allStashes} bestNumber={bestNumber} className={className} + hasAccounts={hasAccounts} + hasQueries={hasQueries} recentlyOnline={recentlyOnline} sessionRewards={sessionRewards} stakingOverview={stakingOverview} @@ -71,11 +86,11 @@ function App ({ allAccounts, allStashesAndControllers: [allStashes, allControlle