Add slashing graph (#1886)
This commit is contained in:
parent
c5d1245ad3
commit
03a3336584
@ -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')}>
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -4,3 +4,4 @@
|
||||
|
||||
export { default as useDebounce } from './debounce';
|
||||
export { default as useFavorites } from './favorites';
|
||||
export { default as useSessionSlashes } from './sessionSlashes';
|
||||
|
||||
163
packages/react-hooks/src/sessionSlashes.tsx
Normal file
163
packages/react-hooks/src/sessionSlashes.tsx
Normal 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;
|
||||
}
|
||||
19
packages/react-hooks/src/types.ts
Normal file
19
packages/react-hooks/src/types.ts
Normal 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[];
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user