Swap rewards from historic queries (#2352)

* Swap rewrds from historic queries

* Headers with key
This commit is contained in:
Jaco Greeff 2020-03-09 07:07:36 +01:00 committed by GitHub
parent f6691fdcf8
commit a77bd85f27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 26 additions and 229 deletions

View File

@ -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 (
<Dropdown.Header content={withI18n ? t(text) : text} />
<Dropdown.Header
key={text as string}
content={withI18n ? t(text) : text}
/>
);
}

View File

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

View File

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

View File

@ -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<Props> {
export default function Query ({ className }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { value } = useParams();
const [validatorId, setValidatorId] = useState<string | null>(value || null);
@ -44,10 +41,7 @@ export default function Query ({ className, sessionRewards }: Props): React.Reac
/>
</InputAddressSimple>
{value && (
<Validator
sessionRewards={sessionRewards}
validatorId={value}
/>
<Validator validatorId={value} />
)}
</div>
);

View File

@ -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';

View File

@ -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<Props> {
function Targets ({ className }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const { allAccounts } = useAccounts();
const lastEra = useCall<BN>(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<BN>(api.query.staking.erasValidatorReward, [lastEra], {
defaultValue: new BN(1),
transform: (optBalance: Option<Balance>) =>
optBalance.unwrapOrDefault()
}) || new BN(1);
const [_amount, setAmount] = useState<BN | undefined>(new BN(1000));
const electedInfo = useCall<DerivedStakingElected>(api.derive.staking.electedInfo, []);
const [favorites, toggleFavorite] = useFavorites(STORE_FAVS_BASE);
const [lastReward, setLastReward] = useState(new BN(0));
const [{ nominators, validators, totalStaked }, setWorkable] = useState<AllInfo>({ 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<Prop
});
};
useEffect((): void => {
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);

View File

@ -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);

View File

@ -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<Props>
}) as string[];
const recentlyOnline = useCall<DerivedHeartbeats>(api.derive.imOnline?.receivedHeartbeats, []);
const stakingOverview = useCall<DerivedStakingOverview>(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<Props>
return hasAccounts
? hasQueries
? []
: ['query']
: ['returns', 'query']
: ['actions', 'query'];
}, [hasAccounts, hasQueries]);
@ -102,10 +99,10 @@ function StakingApp ({ basePath, className }: Props): React.ReactElement<Props>
/>
<Switch>
<Route path={[`${basePath}/query/:value`, `${basePath}/query`]}>
<Query sessionRewards={sessionRewards} />
<Query />
</Route>
<Route path={`${basePath}/returns`}>
<Targets sessionRewards={sessionRewards} />
<Targets />
</Route>
</Switch>
<Actions

View File

@ -1,187 +0,0 @@
// Copyright 2017-2020 @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 { Balance, EventRecord, Hash, Header, StorageChangeSet } from '@polkadot/types/interfaces';
import { Slash, SessionRewards } from './types';
import BN from 'bn.js';
import { useEffect, useState } from 'react';
import { ApiPromise } from '@polkadot/api';
import { registry } from '@polkadot/react-api';
import { useApi, useCacheKey, useIsMountedRef } from '@polkadot/react-hooks';
import { createType } from '@polkadot/types';
import { bnMax, u8aToU8a } from '@polkadot/util';
interface SerializedSlash {
accountId: string;
amount: string;
}
interface Serialized {
blockHash: string;
blockNumber: string;
isEventsEmpty: boolean;
parentHash: string;
reward: string;
sessionIndex: string;
slashes: SerializedSlash[];
treasury: string;
}
const MAX_BLOCKS = 250;
function fromJSON (sessions: Serialized[]): SessionRewards[] {
let keepAll = false;
return sessions
.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)
.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<SessionRewards[]> {
// 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<Header> => api.rpc.chain.getHeader(block))
);
const events: EventRecord[][] = await Promise.all(
results.map(({ block }): Promise<EventRecord[]> =>
(api.query.system.events.at(block) as unknown as Promise<EventRecord[]>)
.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<Serialized[]>('hooks:sessionSlashes');
const [filtered, setFiltered] = useState<SessionRewards[]>([]);
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<void> => {
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;
}