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;
-}