From a77bd85f278f387b0e3042c7ed049d12184361dc Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Mon, 9 Mar 2020 07:07:36 +0100 Subject: [PATCH] Swap rewards from historic queries (#2352) * Swap rewrds from historic queries * Headers with key --- packages/page-settings/src/util.tsx | 5 +- packages/page-staking/src/Query/Validator.tsx | 2 - .../page-staking/src/Query/ValidatorPrev.tsx | 2 +- packages/page-staking/src/Query/index.tsx | 10 +- .../src/{ => Query}/useBlockCounts.tsx | 2 +- packages/page-staking/src/Targets/index.tsx | 35 ++-- packages/page-staking/src/constants.ts | 3 - packages/page-staking/src/index.tsx | 9 +- .../page-staking/src/useSessionRewards.tsx | 187 ------------------ 9 files changed, 26 insertions(+), 229 deletions(-) rename packages/page-staking/src/{ => Query}/useBlockCounts.tsx (97%) delete mode 100644 packages/page-staking/src/useSessionRewards.tsx diff --git a/packages/page-settings/src/util.tsx b/packages/page-settings/src/util.tsx index 56c9295008..9e4667d18f 100644 --- a/packages/page-settings/src/util.tsx +++ b/packages/page-settings/src/util.tsx @@ -12,7 +12,10 @@ import uiSettings from '@polkadot/ui-settings'; export function createOption (t: (input: any) => string, { info, isHeader, text, value, withI18n }: Option, overrides: string[] = [], override = 'empty'): Option | React.ReactNode { if (isHeader) { return ( - + ); } diff --git a/packages/page-staking/src/Query/Validator.tsx b/packages/page-staking/src/Query/Validator.tsx index 83e845ed65..383f07f809 100644 --- a/packages/page-staking/src/Query/Validator.tsx +++ b/packages/page-staking/src/Query/Validator.tsx @@ -3,7 +3,6 @@ // of the Apache-2.0 license. See the LICENSE file for details. import { DeriveStakerReward } from '@polkadot/api-derive/types'; -import { SessionRewards } from '../types'; import BN from 'bn.js'; import React, { useEffect, useMemo, useState } from 'react'; @@ -15,7 +14,6 @@ import { useTranslation } from '../translate'; interface Props { className?: string; - sessionRewards: SessionRewards[]; validatorId: string; } diff --git a/packages/page-staking/src/Query/ValidatorPrev.tsx b/packages/page-staking/src/Query/ValidatorPrev.tsx index 5f39b80283..428b644c70 100644 --- a/packages/page-staking/src/Query/ValidatorPrev.tsx +++ b/packages/page-staking/src/Query/ValidatorPrev.tsx @@ -14,7 +14,7 @@ import { useApi } from '@polkadot/react-hooks'; import { formatBalance, formatNumber } from '@polkadot/util'; import { useTranslation } from '../translate'; -import useBlockCounts from '../useBlockCounts'; +import useBlockCounts from './useBlockCounts'; interface Props { className?: string; diff --git a/packages/page-staking/src/Query/index.tsx b/packages/page-staking/src/Query/index.tsx index ea3d2ba360..6115b10c35 100644 --- a/packages/page-staking/src/Query/index.tsx +++ b/packages/page-staking/src/Query/index.tsx @@ -2,8 +2,6 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. -import { SessionRewards } from '../types'; - import React, { useState } from 'react'; import { useParams } from 'react-router-dom'; import { Button, InputAddressSimple } from '@polkadot/react-components'; @@ -13,10 +11,9 @@ import Validator from './Validator'; interface Props { className?: string; - sessionRewards: SessionRewards[]; } -export default function Query ({ className, sessionRewards }: Props): React.ReactElement { +export default function Query ({ className }: Props): React.ReactElement { const { t } = useTranslation(); const { value } = useParams(); const [validatorId, setValidatorId] = useState(value || null); @@ -44,10 +41,7 @@ export default function Query ({ className, sessionRewards }: Props): React.Reac /> {value && ( - + )} ); diff --git a/packages/page-staking/src/useBlockCounts.tsx b/packages/page-staking/src/Query/useBlockCounts.tsx similarity index 97% rename from packages/page-staking/src/useBlockCounts.tsx rename to packages/page-staking/src/Query/useBlockCounts.tsx index 623f275c93..ef9d2f2f37 100644 --- a/packages/page-staking/src/useBlockCounts.tsx +++ b/packages/page-staking/src/Query/useBlockCounts.tsx @@ -3,7 +3,7 @@ // of the Apache-2.0 license. See the LICENSE file for details. import { DeriveSessionIndexes } from '@polkadot/api-derive/types'; -import { SessionRewards } from './types'; +import { SessionRewards } from '../types'; import { useEffect, useState } from 'react'; import { useApi, useCall, useIsMountedRef } from '@polkadot/react-hooks'; diff --git a/packages/page-staking/src/Targets/index.tsx b/packages/page-staking/src/Targets/index.tsx index b874225cc8..9aad5873ed 100644 --- a/packages/page-staking/src/Targets/index.tsx +++ b/packages/page-staking/src/Targets/index.tsx @@ -2,9 +2,8 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. -import { DerivedStakingElected } from '@polkadot/api-derive/types'; -import { ValidatorPrefs, ValidatorPrefsTo196 } from '@polkadot/types/interfaces'; -import { SessionRewards } from '../types'; +import { DerivedStakingElected, DeriveSessionIndexes } from '@polkadot/api-derive/types'; +import { Balance, ValidatorPrefs, ValidatorPrefsTo196 } from '@polkadot/types/interfaces'; import { ValidatorInfo } from './types'; import BN from 'bn.js'; @@ -12,8 +11,8 @@ import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; import { registry } from '@polkadot/react-api'; import { Icon, InputBalance, Table } from '@polkadot/react-components'; -import { useAccounts, useApi, useDebounce, useFavorites, useCall } from '@polkadot/react-hooks'; -import { createType } from '@polkadot/types'; +import { useAccounts, useApi, useCall, useDebounce, useFavorites } from '@polkadot/react-hooks'; +import { createType, Option } from '@polkadot/types'; import { STORE_FAVS_BASE } from '../constants'; import { useTranslation } from '../translate'; @@ -24,7 +23,6 @@ const PERBILL = new BN(1000000000); interface Props { className?: string; - sessionRewards: SessionRewards[]; } interface AllInfo { @@ -156,14 +154,23 @@ function extractInfo (allAccounts: string[], amount: BN = new BN(0), electedInfo return { nominators, totalStaked, validators }; } -function Targets ({ className, sessionRewards }: Props): React.ReactElement { +function Targets ({ className }: Props): React.ReactElement { const { t } = useTranslation(); const { api } = useApi(); const { allAccounts } = useAccounts(); + const lastEra = useCall(api.derive.session.indexes as any, [], { + defaultValue: new BN(0), + transform: ({ activeEra }: DeriveSessionIndexes) => + activeEra.gtn(0) ? activeEra.subn(1) : new BN(0) + }) || new BN(0); + const lastReward = useCall(api.query.staking.erasValidatorReward, [lastEra], { + defaultValue: new BN(1), + transform: (optBalance: Option) => + optBalance.unwrapOrDefault() + }) || new BN(1); const [_amount, setAmount] = useState(new BN(1000)); const electedInfo = useCall(api.derive.staking.electedInfo, []); const [favorites, toggleFavorite] = useFavorites(STORE_FAVS_BASE); - const [lastReward, setLastReward] = useState(new BN(0)); const [{ nominators, validators, totalStaked }, setWorkable] = useState({ nominators: [], totalStaked: new BN(0), validators: [] }); const [{ sorted, sortBy, sortFromMax }, setSorted] = useState<{ sorted: ValidatorInfo[]; sortBy: SortBy; sortFromMax: boolean }>({ sorted: [], sortBy: 'rankOverall', sortFromMax: true }); const amount = useDebounce(_amount); @@ -190,18 +197,6 @@ function Targets ({ className, sessionRewards }: Props): React.ReactElement { - if (sessionRewards && sessionRewards.length) { - const lastRewardSession = sessionRewards.filter(({ reward }): boolean => reward.gtn(0)); - - setLastReward( - lastRewardSession.length - ? lastRewardSession[lastRewardSession.length - 1].reward - : new BN(0) - ); - } - }, [sessionRewards]); - useEffect((): void => { if (electedInfo) { const { nominators, totalStaked, validators } = extractInfo(allAccounts, amount, electedInfo, favorites, lastReward); diff --git a/packages/page-staking/src/constants.ts b/packages/page-staking/src/constants.ts index 4ba56dfe46..f29cd4b640 100644 --- a/packages/page-staking/src/constants.ts +++ b/packages/page-staking/src/constants.ts @@ -3,6 +3,3 @@ // of the Apache-2.0 license. See the LICENSE file for details. export const STORE_FAVS_BASE = 'staking:favorites'; - -// assuming 4 hrs sessions, we grab results for 10 days -export const MAX_SESSIONS = 10 * (24 / 4); diff --git a/packages/page-staking/src/index.tsx b/packages/page-staking/src/index.tsx index e491479477..7b3a0b246e 100644 --- a/packages/page-staking/src/index.tsx +++ b/packages/page-staking/src/index.tsx @@ -20,9 +20,7 @@ import Overview from './Overview'; import Summary from './Overview/Summary'; import Query from './Query'; import Targets from './Targets'; -import { MAX_SESSIONS } from './constants'; import { useTranslation } from './translate'; -import useSessionRewards from './useSessionRewards'; function reduceNominators (nominators: string[], additional: string[]): string[] { return nominators.concat(...additional.filter((nominator): boolean => !nominators.includes(nominator))); @@ -41,7 +39,6 @@ function StakingApp ({ basePath, className }: Props): React.ReactElement }) as string[]; const recentlyOnline = useCall(api.derive.imOnline?.receivedHeartbeats, []); const stakingOverview = useCall(api.derive.staking.overview, []); - const sessionRewards = useSessionRewards(MAX_SESSIONS); const [nominators, dispatchNominators] = useReducer(reduceNominators, [] as string[]); const hasQueries = useMemo((): boolean => { return hasAccounts && !!(api.query.imOnline?.authoredBlocks) && !!(api.query.staking.activeEra); @@ -74,7 +71,7 @@ function StakingApp ({ basePath, className }: Props): React.ReactElement return hasAccounts ? hasQueries ? [] - : ['query'] + : ['returns', 'query'] : ['actions', 'query']; }, [hasAccounts, hasQueries]); @@ -102,10 +99,10 @@ function StakingApp ({ basePath, className }: Props): React.ReactElement /> - + - + ({ - 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) - .reverse() - // we drop everything before the last reward - .filter(({ reward }): boolean => { - if (reward.gtn(0)) { - keepAll = true; - } - - return keepAll; - }) - .reverse(); -} - -function toJSON (sessions: SessionRewards[], maxSessions: number): 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); -} - -function mergeResults (sessions: SessionRewards[], newSessions: SessionRewards[]): SessionRewards[] { - const tmp = sessions - .concat(newSessions) - .sort((a, b): number => a.blockNumber.cmp(b.blockNumber)); - - // for the first, always use it, otherwise ignore on same sessionIndex - return tmp.filter(({ sessionIndex }, index): boolean => - index === 0 || !tmp[index - 1].sessionIndex.eq(sessionIndex) - ); -} - -async function loadSome (api: ApiPromise, fromHash: Hash, toHash: Hash): Promise { - // query a range of blocks - on non-archive nodes this will fail, so return an empty set - const results = await api.rpc.state - .queryStorage([api.query.session.currentIndex.key()], fromHash, toHash) - .catch((): StorageChangeSet[] => []); - const headers = await Promise.all( - results.map(({ block }): Promise
=> api.rpc.chain.getHeader(block)) - ); - const events: EventRecord[][] = await Promise.all( - results.map(({ block }): Promise => - (api.query.system.events.at(block) as unknown as Promise) - .then((records): EventRecord[] => - records.filter(({ event: { section } }): boolean => section === 'staking') - ) - .catch((): EventRecord[] => []) // may throw, update metadata for old - ) - ); - const slashes: Slash[][] = events.map((info): Slash[] => - info - .filter(({ event: { method } }): boolean => method === 'Slash') - .map(({ event: { data: [accountId, amount] } }): Slash => ({ - accountId: accountId as any, - amount: amount as any - })) - ); - 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, rewards[0]?.event?.data[1] as Balance]; - }); - - // For old v1, the query results have empty spots (subsequently fixed in v2), - // filter these before trying to extract the results - return results - .filter(({ changes }): boolean => !!(changes && changes.length)) - .map(({ changes: [[, value]] }, index): SessionRewards => ({ - blockHash: headers[index].hash, - blockNumber: headers[index].number.unwrap(), - isEventsEmpty: events[index].length === 0, - 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], - treasury: rewards[index][1] || createType(registry, 'Balance') - })); -} - -export default function useSessionRewards (maxSessions: number): SessionRewards[] { - const { api } = useApi(); - const mounted = useIsMountedRef(); - const [getCache, setCache] = useCacheKey('hooks:sessionSlashes'); - const [filtered, setFiltered] = useState([]); - - useEffect((): void => { - let workQueue = fromJSON(getCache() || []); - const savedNumber = workQueue[workQueue.length - 1] - ? workQueue[workQueue.length - 1].blockNumber - : undefined; - - setImmediate((): void => { - api.isReady.then(async (): Promise => { - const maxSessionsStore = maxSessions + 1; // assuming first is a bust - const bestHeader = await api.rpc.chain.getHeader(); - let toHash = bestHeader.hash; - let toNumber = bestHeader.number.unwrap().toBn(); - let fromNumber = bnMax(toNumber.subn(MAX_BLOCKS), new BN(1)); - - while (true) { - // console.log(`Updating rewards cache, #${fromNumber} -> #${toNumber}`); - - const fromHash = await api.rpc.chain.getBlockHash(fromNumber as any); - const newQueue = await loadSome(api, fromHash, toHash); - - workQueue = mergeResults(workQueue, newQueue); - toHash = fromHash; - toNumber = fromNumber; - fromNumber = bnMax(toNumber.subn(MAX_BLOCKS), new BN(1)); - - if (mounted.current) { - setCache(toJSON(workQueue, maxSessionsStore)); - setFiltered(workQueue.slice(-maxSessions)); - } - - const lastNumber = workQueue[workQueue.length - 1]?.blockNumber; - - if (!mounted.current || !lastNumber || fromNumber.eqn(1) || ((workQueue.length >= maxSessionsStore) && fromNumber.lt(savedNumber || lastNumber))) { - break; - } - } - }); - }); - }, []); - - return filtered; -}