Add slashing graph (#1886)

This commit is contained in:
Jaco Greeff 2019-11-14 23:31:26 +01:00 committed by GitHub
parent c5d1245ad3
commit 03a3336584
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 238 additions and 13 deletions

View File

@ -4,6 +4,7 @@
import { I18nProps } from '@polkadot/react-components/types';
import { Balance, BlockNumber, Hash, Exposure, SessionIndex } from '@polkadot/types/interfaces';
import { SessionRewards } from '@polkadot/react-hooks/types';
import BN from 'bn.js';
import React, { useContext, useEffect, useState } from 'react';
@ -19,6 +20,7 @@ interface Props extends I18nProps {
blockCounts?: BN[];
className?: string;
currentIndex: SessionIndex;
stakingRewards: SessionRewards[];
startNumber: BlockNumber;
validatorId: string;
}
@ -87,16 +89,17 @@ function extractSplit (values: [BN, Hash, Exposure][], validatorId: string): Spl
}));
}
function Validator ({ blockCounts, className, currentIndex, startNumber, t, validatorId }: Props): React.ReactElement<Props> {
function Validator ({ blockCounts, className, currentIndex, stakingRewards, startNumber, t, validatorId }: Props): React.ReactElement<Props> {
const { api } = useContext(ApiContext);
const [blocksLabels, setBlocksLabels] = useState<string[]>([]);
const [blocksChart, setBlocksChart] = useState<LineData | null>(null);
const [{ rewardsChart, rewardsLabels }, setRewardsInfo] = useState<{ rewardsChart: LineData | null; rewardsLabels: string[] }>({ rewardsChart: null, rewardsLabels: [] });
const [splitChart, setSplitInfo] = useState<SplitData | null>(null);
const [{ stakeChart, stakeLabels }, setStakeInfo] = useState<{ stakeChart: LineData | null; stakeLabels: string[]}>({ stakeChart: null, stakeLabels: [] });
const divisor = new BN('1'.padEnd(formatBalance.getDefaults().decimals + 1, '0'));
useEffect((): void => {
api.isReady.then(async (): Promise<void> => {
const divisor = new BN('1'.padEnd(formatBalance.getDefaults().decimals + 1, '0'));
const values = await getHistoric<Exposure>(api, 'staking.stakers', [validatorId], {
interval: (api.consts.babe?.epochDuration as BlockNumber || new BN(500)).muln(2).divn(3),
max: SESSIONS,
@ -110,6 +113,24 @@ function Validator ({ blockCounts, className, currentIndex, startNumber, t, vali
});
}, []);
useEffect((): void => {
const rewardsLabels: string[] = [];
const rewardsChart: LineData = [[]];
stakingRewards.forEach(({ sessionIndex, slashes }): void => {
rewardsLabels.push(formatNumber(sessionIndex));
rewardsChart[0].push(
slashes.reduce((total: BN, { accountId, amount }): BN => {
return accountId.eq(validatorId)
? total.sub(amount)
: total;
}, new BN(0)).muln(1000).div(divisor).toNumber() / 1000
);
});
setRewardsInfo({ rewardsChart, rewardsLabels });
}, [stakingRewards]);
useEffect((): void => {
setBlocksLabels(
getIndexRange(currentIndex).map((index): string => formatNumber(index))
@ -137,16 +158,31 @@ function Validator ({ blockCounts, className, currentIndex, startNumber, t, vali
return (
<Columar className={className}>
<Column emptyText={t('Loading block data')}>
{blocksChart && (
<div className='staking--Chart'>
<h1>{t('blocks per session')}</h1>
<Chart.Line
colors={COLORS_BLOCKS}
labels={blocksLabels}
legends={[t('blocks'), t('average')]}
values={blocksChart}
/>
</div>
{(rewardsChart || blocksChart) && (
<>
{blocksChart && (
<div className='staking--Chart'>
<h1>{t('blocks per session')}</h1>
<Chart.Line
colors={COLORS_BLOCKS}
labels={blocksLabels}
legends={[t('blocks'), t('average')]}
values={blocksChart}
/>
</div>
)}
{rewardsChart && (
<div className='staking--Chart'>
<h1>{t('slashed per session')}</h1>
<Chart.Line
colors={COLORS_BLOCKS}
labels={rewardsLabels}
legends={[t('slashed'), t('rewarded')]}
values={rewardsChart}
/>
</div>
)}
</>
)}
</Column>
<Column emptyText={t('Loading staker data')}>

View File

@ -47,7 +47,7 @@ interface Props extends I18nProps, ComponentProps, RouteComponentProps<{}> {
// />
// </Input>
function Query ({ bestNumber, className, stakingOverview, match: { params: { value } }, t }: Props): React.ReactElement<Props> {
function Query ({ bestNumber, className, stakingRewards, stakingOverview, match: { params: { value } }, t }: Props): React.ReactElement<Props> {
const [startNumber, setStartNumber] = useState<BlockNumber | undefined>();
const [validatorId, setValidatorId] = useState<string | null>(value || null);
@ -82,6 +82,7 @@ function Query ({ bestNumber, className, stakingOverview, match: { params: { val
{value && startNumber && stakingOverview && (
<Validator
currentIndex={stakingOverview.currentIndex}
stakingRewards={stakingRewards}
startNumber={startNumber}
validatorId={value}
/>

View File

@ -17,6 +17,7 @@ import { Option } from '@polkadot/types';
import { HelpOverlay } from '@polkadot/react-components';
import Tabs from '@polkadot/react-components/Tabs';
import { ApiContext, withCalls, withMulti, withObservable } from '@polkadot/react-api';
import { useSessionSlashes } from '@polkadot/react-hooks';
import accountObservable from '@polkadot/ui-keyring/observable/accounts';
import Accounts from './Actions/Accounts';
@ -38,6 +39,7 @@ const EMPTY_ALL: [string[], string[]] = [EMPY_ACCOUNTS, EMPY_ACCOUNTS];
function App ({ allAccounts, allStashesAndControllers: [allStashes, allControllers] = EMPTY_ALL, basePath, bestNumber, className, recentlyOnline, stakingOverview, t }: Props): React.ReactElement<Props> {
const { api } = useContext(ApiContext);
const stakingRewards = useSessionSlashes();
const routeMatch = useRouteMatch({ path: basePath, strict: true });
const _renderComponent = (Component: React.ComponentType<ComponentProps>, className?: string): () => React.ReactNode => {
// eslint-disable-next-line react/display-name
@ -54,6 +56,7 @@ function App ({ allAccounts, allStashesAndControllers: [allStashes, allControlle
bestNumber={bestNumber}
className={className}
recentlyOnline={recentlyOnline}
stakingRewards={stakingRewards}
stakingOverview={stakingOverview}
/>
);

View File

@ -4,6 +4,7 @@
import { DerivedFees, DerivedBalances, DerivedHeartbeats, DerivedStakingOverview } from '@polkadot/api-derive/types';
import { BlockNumber } from '@polkadot/types/interfaces';
import { SessionRewards } from '@polkadot/react-hooks/types';
import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
export type Nominators = Record<string, string[]>;
@ -15,6 +16,7 @@ export interface ComponentProps {
bestNumber?: BlockNumber;
className?: string;
recentlyOnline?: DerivedHeartbeats;
stakingRewards: SessionRewards[];
stakingOverview?: DerivedStakingOverview;
}

View File

@ -4,3 +4,4 @@
export { default as useDebounce } from './debounce';
export { default as useFavorites } from './favorites';
export { default as useSessionSlashes } from './sessionSlashes';

View File

@ -0,0 +1,163 @@
// 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 { Balance, BlockNumber, EventRecord, Hash, Header } from '@polkadot/types/interfaces';
import { Slash, SessionRewards } from './types';
import BN from 'bn.js';
import { useContext, useEffect, useState } from 'react';
import store from 'store';
import { ApiPromise } from '@polkadot/api';
import { ApiContext } from '@polkadot/react-api';
import { createType } from '@polkadot/types';
import { bnMax, u8aToU8a } from '@polkadot/util';
interface SlashSer {
accountId: string;
amount: string;
}
interface SessionResultSer {
blockHash: string;
blockNumber: string;
isEventsEmpty: boolean;
reward: string;
sessionIndex: string;
slashes: SlashSer[];
}
// assuming 4 hrs sessions, we grab results for 10 days (+2 for at-start throw-away)
const MAX_SESSIONS = 10 * (24 / 4) + 2;
function getStorage (storageKey: string): SessionRewards[] {
const sessions: SessionResultSer[] = store.get(storageKey, []);
return sessions.map(({ blockHash, blockNumber, isEventsEmpty, reward, sessionIndex, slashes }): SessionRewards => ({
blockHash: createType('Hash', blockHash),
blockNumber: createType('BlockNumber', blockNumber),
isEventsEmpty,
reward: createType('Balance', reward),
sessionIndex: createType('SessionIndex', sessionIndex),
slashes: slashes.map(({ accountId, amount }): Slash => ({
accountId: createType('AccountId', accountId),
amount: createType('Balance', amount)
}))
}));
}
function setStorage (storageKey: string, sessions: SessionRewards[]): SessionResultSer[] {
return store.set(
storageKey,
sessions
.map(({ blockHash, blockNumber, isEventsEmpty, reward, sessionIndex, slashes }): SessionResultSer => ({
blockHash: blockHash.toHex(),
blockNumber: blockNumber.toHex(),
isEventsEmpty,
reward: reward.toHex(),
sessionIndex: sessionIndex.toHex(),
slashes: slashes.map(({ accountId, amount }): SlashSer => ({
accountId: accountId.toString(),
amount: amount.toHex()
}))
}))
.slice(0, MAX_SESSIONS + 1)
);
}
function mergeResults (sessions: SessionRewards[], newSessions: SessionRewards[]): SessionRewards[] {
const tmp = sessions
.concat(newSessions)
.sort((a, b): number => a.blockNumber.cmp(b.blockNumber));
return tmp.filter(({ sessionIndex }, index): boolean =>
index === 0
// for the first, always use it
? true
// if the prev has the same sessionIndex, ignore this one
: !tmp[index - 1].sessionIndex.eq(sessionIndex)
);
}
async function loadSome (api: ApiPromise, fromHash: Hash, toHash: Hash): Promise<SessionRewards[]> {
const results = await api.rpc.state.queryStorage([api.query.session.currentIndex.key()], fromHash, toHash);
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[] => []) // undecodable may throw
)
);
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)[] = events.map((info): Balance | undefined => {
const rewards = info.filter(({ event: { method } }): boolean => method === 'Reward');
return rewards[0]?.event?.data[0] as Balance;
});
return results.map(({ changes: [[, value]] }, index): SessionRewards => ({
blockHash: headers[index].hash,
blockNumber: headers[index].number.unwrap(),
isEventsEmpty: events[index].length === 0,
reward: rewards[index] || createType('Balance'),
sessionIndex: createType('SessionIndex', u8aToU8a(value.unwrap())),
slashes: slashes[index]
}));
}
export default function useSessionSlashes (maxSessions = MAX_SESSIONS): SessionRewards[] {
const { api } = useContext(ApiContext);
const STORAGE_KEY = `hooks:sessionSlashes:${api.genesisHash}`;
const [results, setResults] = useState<SessionRewards[]>(getStorage(STORAGE_KEY));
const [filtered, setFiltered] = useState<SessionRewards[]>([]);
useEffect((): void => {
let workQueue = results;
api.isReady.then(async (): Promise<void> => {
const sessionLength = (api.consts.babe?.epochDuration as BlockNumber || new BN(500));
const count = sessionLength.muln(maxSessions).divn(10).toNumber();
const bestHeader = await api.rpc.chain.getHeader();
let toHash = bestHeader.hash;
let toNumber = bestHeader.number.unwrap().toBn();
let fromHash = api.genesisHash;
let fromNumber = bnMax(toNumber.subn(count), new BN(1));
while (true) {
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(count), new BN(1));
setStorage(STORAGE_KEY, workQueue);
setResults(workQueue);
if (fromNumber.eqn(1) || (workQueue.length > maxSessions)) {
break;
}
}
});
}, []);
useEffect((): void => {
setFiltered(results.filter(({ isEventsEmpty }): boolean => !isEventsEmpty));
}, [results]);
return filtered;
}

View File

@ -0,0 +1,19 @@
// 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[];
}