diff --git a/packages/app-staking/src/Query/Validator.tsx b/packages/app-staking/src/Query/Validator.tsx index 929ce64cc48f..37e595bd8c05 100644 --- a/packages/app-staking/src/Query/Validator.tsx +++ b/packages/app-staking/src/Query/Validator.tsx @@ -3,19 +3,19 @@ // of the Apache-2.0 license. See the LICENSE file for details. import { I18nProps } from '@polkadot/react-components/types'; -import { Balance, Hash, Exposure, SessionIndex } from '@polkadot/types/interfaces'; -import { SessionRewards, Slash } from '@polkadot/react-hooks/types'; +import { Balance, Hash, Exposure } from '@polkadot/types/interfaces'; +import { SessionRewards, Slash } from '../types'; import BN from 'bn.js'; import React, { useEffect, useState } from 'react'; import { Chart, Columar, Column } from '@polkadot/react-components'; import { toShortAddress } from '@polkadot/react-components/util'; import { getHistoric } from '@polkadot/react-api/util'; -import { trackStream, useApi } from '@polkadot/react-hooks'; -import { u32 } from '@polkadot/types'; +import { useApi } from '@polkadot/react-hooks'; import { formatBalance, formatNumber } from '@polkadot/util'; import translate from '../translate'; +import useBlockCounts from '../useBlockCounts'; interface Props extends I18nProps { className?: string; @@ -92,11 +92,7 @@ function extractEraSlash (validatorId: string, slashes: Slash[]): BN { function Validator ({ className, sessionRewards, t, validatorId }: Props): React.ReactElement { const { api } = useApi(); - // 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 blockCounts = useBlockCounts(validatorId, sessionRewards); const [blocksLabels, setBlocksLabels] = useState([]); const [blocksChart, setBlocksChart] = useState(null); const [{ currency, divisor }] = useState<{ currency: string; divisor: BN }>({ diff --git a/packages/app-staking/src/types.ts b/packages/app-staking/src/types.ts index 919148f1cffe..53f61837f5d0 100644 --- a/packages/app-staking/src/types.ts +++ b/packages/app-staking/src/types.ts @@ -3,8 +3,7 @@ // of the Apache-2.0 license. See the LICENSE file for details. import { DerivedFees, DerivedBalances, DerivedHeartbeats, DerivedStakingOverview } from '@polkadot/api-derive/types'; -import { BlockNumber } from '@polkadot/types/interfaces'; -import { SessionRewards } from '@polkadot/react-hooks/types'; +import { AccountId, Balance, BlockNumber, Hash, SessionIndex } from '@polkadot/types/interfaces'; export type Nominators = Record; @@ -29,3 +28,19 @@ export interface CalculateBalanceProps { export type AccountFilter = 'all' | 'controller' | 'session' | 'stash' | 'unbonded'; export type ValidatorFilter = 'all' | 'hasNominators' | 'noNominators' | 'hasWarnings' | 'noWarnings' | 'iNominated' | 'nextSet'; + +export interface Slash { + accountId: AccountId; + amount: Balance; +} + +export interface SessionRewards { + blockHash: Hash; + blockNumber: BlockNumber; + isEventsEmpty: boolean; + parentHash: Hash; + reward: Balance; + sessionIndex: SessionIndex; + slashes: Slash[]; + treasury: Balance; +} diff --git a/packages/app-staking/src/useBlockCounts.tsx b/packages/app-staking/src/useBlockCounts.tsx new file mode 100644 index 000000000000..bffc50067517 --- /dev/null +++ b/packages/app-staking/src/useBlockCounts.tsx @@ -0,0 +1,38 @@ +// Copyright 2017-2019 @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 { SessionIndex } from '@polkadot/types/interfaces'; +import { SessionRewards } from './types'; + +import { useEffect, useState } from 'react'; +import { useApi, trackStream } from '@polkadot/react-hooks'; +import { u32 } from '@polkadot/types'; + +export default function useBlockCounts (accountId: string, sessionRewards: SessionRewards[]): u32[] { + const { api } = useApi(); + const [counts, setCounts] = useState([]); + const [historic, setHistoric] = useState([]); + const sessionIndex = trackStream(api.query.session.currentIndex, []); + const current = trackStream(api.query.imOnline?.authoredBlocks, [sessionIndex, accountId]); + + useEffect((): void => { + if (api.query.imOnline?.authoredBlocks && sessionRewards && sessionRewards.length) { + const filtered = sessionRewards.filter(({ sessionIndex }): boolean => sessionIndex.gtn(0)); + + if (filtered.length) { + Promise + .all(filtered.map(({ parentHash, sessionIndex }): Promise => + api.query.imOnline.authoredBlocks.at(parentHash, sessionIndex.subn(1), accountId) as Promise + )) + .then(setHistoric); + } + } + }, [accountId, sessionRewards]); + + useEffect((): void => { + setCounts([...historic, current || api.createType('u32')]); + }, [current, historic]); + + return counts; +} diff --git a/packages/app-staking/src/useSessionRewards.tsx b/packages/app-staking/src/useSessionRewards.tsx index 9f715871307d..f2ef3f753673 100644 --- a/packages/app-staking/src/useSessionRewards.tsx +++ b/packages/app-staking/src/useSessionRewards.tsx @@ -3,7 +3,7 @@ // of the Apache-2.0 license. See the LICENSE file for details. import { Balance, EventRecord, Hash, Header, StorageChangeSet } from '@polkadot/types/interfaces'; -import { Slash, SessionRewards } from '@polkadot/react-hooks/types'; +import { Slash, SessionRewards } from './types'; import BN from 'bn.js'; import { useEffect, useState } from 'react'; @@ -22,9 +22,11 @@ interface Serialized { blockHash: string; blockNumber: string; isEventsEmpty: boolean; + parentHash: string; reward: string; sessionIndex: string; slashes: SerializedSlash[]; + treasury: string; } const MAX_BLOCKS = 2500; @@ -34,17 +36,20 @@ function fromJSON (sessions: Serialized[]): SessionRewards[] { let keepAll = true; return sessions - .map(({ blockHash, blockNumber, isEventsEmpty, reward, sessionIndex, slashes }): SessionRewards => ({ + .map(({ blockHash, blockNumber, isEventsEmpty, parentHash, reward, sessionIndex, slashes, treasury }): SessionRewards => ({ blockHash: createType(registry, 'Hash', blockHash), blockNumber: createType(registry, 'BlockNumber', blockNumber), isEventsEmpty, + parentHash: createType(registry, 'Hash', parentHash), reward: createType(registry, 'Balance', reward), sessionIndex: createType(registry, 'SessionIndex', sessionIndex), slashes: slashes.map(({ accountId, amount }): Slash => ({ accountId: createType(registry, 'AccountId', accountId), amount: createType(registry, 'Balance', amount) - })) + })), + treasury: createType(registry, 'Balance', treasury) })) + .filter(({ parentHash }): boolean => !parentHash.isEmpty) .filter(({ isEventsEmpty }): boolean => { if (!isEventsEmpty) { // we first see if we have some data up to this point (we may not, i.e. non-archive) @@ -59,16 +64,18 @@ function fromJSON (sessions: Serialized[]): SessionRewards[] { } function toJSON (sessions: SessionRewards[], maxSessions: number): Serialized[] { - return sessions.map(({ blockHash, blockNumber, isEventsEmpty, reward, sessionIndex, slashes }): Serialized => ({ + return sessions.map(({ blockHash, blockNumber, isEventsEmpty, parentHash, reward, sessionIndex, slashes, treasury }): Serialized => ({ blockHash: blockHash.toHex(), blockNumber: blockNumber.toHex(), isEventsEmpty, + parentHash: parentHash.toHex(), reward: reward.toHex(), sessionIndex: sessionIndex.toHex(), slashes: slashes.map(({ accountId, amount }): SerializedSlash => ({ accountId: accountId.toString(), amount: amount.toHex() - })) + })), + treasury: treasury.toHex() })).slice(-maxSessions); } @@ -108,10 +115,10 @@ async function loadSome (api: ApiPromise, fromHash: Hash, toHash: Hash): Promise amount: amount as any })) ); - const rewards: (Balance | undefined)[] = events.map((info): Balance | undefined => { + const rewards: [Balance | undefined, Balance | undefined][] = events.map((info): [Balance | undefined, Balance | undefined] => { const rewards = info.filter(({ event: { method } }): boolean => method === 'Reward'); - return rewards[0]?.event?.data[0] as Balance; + return [rewards[0]?.event?.data[0] as Balance, rewards[0]?.event?.data[1] as Balance]; }); // For old v1, the query results have empty spots (subsequently fixed in v2), @@ -122,11 +129,13 @@ async function loadSome (api: ApiPromise, fromHash: Hash, toHash: Hash): Promise blockHash: headers[index].hash, blockNumber: headers[index].number.unwrap(), isEventsEmpty: events[index].length === 0, - reward: rewards[index] || createType(registry, 'Balance'), + parentHash: headers[index].parentHash, + reward: rewards[index][0] || createType(registry, 'Balance'), sessionIndex: createType(registry, 'SessionIndex', u8aToU8a( value.isSome ? value.unwrap() : new Uint8Array([]) )), - slashes: slashes[index] + slashes: slashes[index], + treasury: rewards[index][1] || createType(registry, 'Balance') })); } diff --git a/packages/react-hooks/src/track/trackStream.tsx b/packages/react-hooks/src/track/trackStream.tsx index f99fd48d344a..45e7860c7dfc 100644 --- a/packages/react-hooks/src/track/trackStream.tsx +++ b/packages/react-hooks/src/track/trackStream.tsx @@ -6,6 +6,7 @@ import { Codec } from '@polkadot/types/types'; import { Options, Param, Params } from './types'; import { useEffect, useRef, useState } from 'react'; +import { isUndefined } from '@polkadot/util'; import { dummyPromise, extractParams, transformIdentity } from './util'; @@ -20,6 +21,11 @@ interface TrackFn { (a: Param, b: Param, cb: TrackFnCallback): TrackFnResult; (a: Param, cb: TrackFnCallback): TrackFnResult; (cb: TrackFnCallback): TrackFnResult; + meta?: { + type: { + isDoubleMap: boolean; + }; + }; } // tracks a stream, typically an api.* call (derive, rpc, query) that @@ -35,10 +41,12 @@ export default function trackStream (fn: TrackFn | undefined, params: Params tracker.current.subscriber = dummyPromise; }; const _subscribe = (params: Params): void => { + const validParams = params.filter((p): boolean => !isUndefined(p)); + _unsubscribe(); setImmediate((): void => { - tracker.current.subscriber = fn + tracker.current.subscriber = fn && (!fn.meta || !fn.meta.type?.isDoubleMap || validParams.length === 2) // eslint-disable-next-line @typescript-eslint/ban-ts-ignore // @ts-ignore We tried to get the typings right, close but no cigar... ? fn(...params, (value: any): void => setValue(transform(value))) diff --git a/packages/react-hooks/src/types.ts b/packages/react-hooks/src/types.ts deleted file mode 100644 index d210e98fca6e..000000000000 --- a/packages/react-hooks/src/types.ts +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2017-2019 @polkadot/react-components 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, BlockNumber, Hash, SessionIndex } from '@polkadot/types/interfaces'; - -export interface Slash { - accountId: AccountId; - amount: Balance; -} - -export interface SessionRewards { - blockHash: Hash; - blockNumber: BlockNumber; - isEventsEmpty: boolean; - reward: Balance; - sessionIndex: SessionIndex; - slashes: Slash[]; -}