Swap rewards from historic queries (#2352)
* Swap rewrds from historic queries * Headers with key
This commit is contained in:
parent
f6691fdcf8
commit
a77bd85f27
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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';
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user