Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 8 additions & 14 deletions packages/app-staking/src/Overview/Address.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@ import { ValidatorFilter } from '../types';
import BN from 'bn.js';
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import { withCalls, withMulti } from '@polkadot/react-api';
import { AddressCard, AddressMini, Badge, Expander, Icon } from '@polkadot/react-components';
import { classes } from '@polkadot/react-components/util';
import { useApiContext } from '@polkadot/react-hooks';
import { trackStream, useApiContext } from '@polkadot/react-hooks';
import { formatNumber } from '@polkadot/util';

import translate from '../translate';
Expand All @@ -24,13 +23,13 @@ interface Props extends I18nProps {
className?: string;
defaultName: string;
filter: ValidatorFilter;
hasQueries: boolean;
isElected: boolean;
isFavorite: boolean;
lastAuthors?: string[];
myAccounts: string[];
points?: Points;
recentlyOnline?: DerivedHeartbeats;
stakingInfo?: DerivedStaking;
toggleFavorite: (accountId: string) => void;
withNominations?: boolean;
}
Expand All @@ -47,8 +46,10 @@ interface StakingState {

const WITH_VALIDATOR_PREFS = { validatorPayment: true };

function Address ({ address, authorsMap, className, defaultName, filter, isElected, isFavorite, lastAuthors, myAccounts, points, recentlyOnline, stakingInfo, t, toggleFavorite, withNominations = true }: Props): React.ReactElement<Props> | null {
function Address ({ address, authorsMap, className, defaultName, filter, hasQueries, isElected, isFavorite, lastAuthors, myAccounts, points, recentlyOnline, t, toggleFavorite, withNominations = true }: Props): React.ReactElement<Props> | null {
const { api, isSubstrateV2 } = useApiContext();
// FIXME Any horrors, caused by trackStream
const stakingInfo = trackStream<DerivedStaking>(api.derive.staking.info as any, [address]);
const [extraInfo, setExtraInfo] = useState<[React.ReactNode, React.ReactNode][] | undefined>();
const [hasActivity, setHasActivity] = useState(true);
const [{ balanceOpts, controllerId, hasNominators, isNominatorMe, nominators, sessionId, stashId }, setStakingState] = useState<StakingState>({
Expand Down Expand Up @@ -188,7 +189,7 @@ function Address ({ address, authorsMap, className, defaultName, filter, isElect
}
isDisabled={isSubstrateV2 && !hasActivity}
overlay={
api.query.imOnline?.authoredBlocks && (
hasQueries && api.query.imOnline?.authoredBlocks && (
<Icon
className='staking--stats'
name='line graph'
Expand Down Expand Up @@ -223,7 +224,7 @@ function Address ({ address, authorsMap, className, defaultName, filter, isElect
);
}

export default withMulti(
export default translate(
styled(Address)`
.extras {
display: inline-block;
Expand Down Expand Up @@ -295,12 +296,5 @@ export default withMulti(
position: absolute;
right: 0.5rem;
}
`,
translate,
withCalls<Props>(
['derive.staking.info', {
paramName: 'address',
propName: 'stakingInfo'
}]
)
`
);
4 changes: 3 additions & 1 deletion packages/app-staking/src/Overview/CurrentList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import Address from './Address';

interface Props extends I18nProps {
authorsMap: Record<string, string>;
hasQueries: boolean;
lastAuthors?: string[];
next: string[];
recentlyOnline?: DerivedHeartbeats;
Expand Down Expand Up @@ -55,7 +56,7 @@ function accountsToString (accounts: AccountId[]): string[] {
return accounts.map((accountId): string => accountId.toString());
}

function CurrentList ({ authorsMap, lastAuthors, next, recentlyOnline, stakingOverview, t }: Props): React.ReactElement<Props> {
function CurrentList ({ authorsMap, hasQueries, lastAuthors, next, recentlyOnline, stakingOverview, t }: Props): React.ReactElement<Props> {
const { isSubstrateV2 } = useApiContext();
const [favorites, toggleFavorite] = useFavorites(STORE_FAVS_BASE);
const [filter, setFilter] = useState<ValidatorFilter>('all');
Expand Down Expand Up @@ -84,6 +85,7 @@ function CurrentList ({ authorsMap, lastAuthors, next, recentlyOnline, stakingOv
authorsMap={authorsMap}
defaultName={defaultName}
filter={filter}
hasQueries={hasQueries}
isElected={isElected}
isFavorite={isFavorite}
lastAuthors={lastAuthors}
Expand Down
3 changes: 2 additions & 1 deletion packages/app-staking/src/Overview/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import Summary from './Summary';

interface Props extends BareProps, ComponentProps {}

export default function Overview ({ allControllers, allStashes, className, recentlyOnline, stakingOverview }: Props): React.ReactElement<Props> {
export default function Overview ({ allControllers, hasQueries, allStashes, className, recentlyOnline, stakingOverview }: Props): React.ReactElement<Props> {
const { isSubstrateV2 } = useApiContext();
const { byAuthor, lastBlockAuthors, lastBlockNumber } = useContext(BlockAuthorsContext);
const [next, setNext] = useState<string[]>([]);
Expand All @@ -41,6 +41,7 @@ export default function Overview ({ allControllers, allStashes, className, recen
/>
<CurrentList
authorsMap={byAuthor}
hasQueries={hasQueries}
lastAuthors={lastBlockAuthors}
next={next}
recentlyOnline={recentlyOnline}
Expand Down
155 changes: 75 additions & 80 deletions packages/app-staking/src/Query/Validator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,29 @@
// of the Apache-2.0 license. See the LICENSE file for details.

import { I18nProps } from '@polkadot/react-components/types';
import { Balance, BlockNumber, Hash, Exposure, SessionIndex } from '@polkadot/types/interfaces';
import { Balance, Hash, Exposure, SessionIndex } from '@polkadot/types/interfaces';
import { SessionRewards, Slash } from '@polkadot/react-hooks/types';

import BN from 'bn.js';
import React, { useEffect, useState } from 'react';
import { Chart, Columar, Column } from '@polkadot/react-components';
import { toShortAddress } from '@polkadot/react-components/util';
import { withCalls } from '@polkadot/react-api';
import { getHistoric } from '@polkadot/react-api/util';
import { useApiContext } from '@polkadot/react-hooks';
import { trackStream, useApiContext } from '@polkadot/react-hooks';
import { u32 } from '@polkadot/types';
import { formatBalance, formatNumber } from '@polkadot/util';

import { MAX_SESSIONS } from '../constants';
import translate from '../translate';

interface Props extends I18nProps {
blockCounts?: BN[];
className?: string;
currentIndex: SessionIndex;
sessionRewards: SessionRewards[];
startNumber: BlockNumber;
validatorId: string;
}

type LineData = (BN | number)[][];
type LineDataEntry = (BN | number)[];

type LineData = LineDataEntry[];

interface SplitEntry {
colors: string[];
Expand All @@ -39,37 +37,22 @@ type SplitData = SplitEntry[];

const COLORS_MINE = ['#ff8c00'];
const COLORS_OTHER = ['#acacac'];
const COLORS_REWARD = ['#8c2200', '#008c22', '#acacac'];
const COLORS_BLOCKS = [undefined, '#acacac'];

function getIndexRange (currentIndex: SessionIndex): BN[] {
const range: BN[] = [];
let thisIndex: BN = currentIndex;

while (thisIndex.gtn(0) && range.length < MAX_SESSIONS) {
range.push(thisIndex);

thisIndex = thisIndex.subn(1);
}

return range.reverse();
}

function extractStake (values: [BN, Hash, Exposure][], divisor: BN): [string[], LineData] {
function extractStake (values: [Hash, Exposure][], divisor: BN): LineData {
return [
values.map(([bn]): string => formatNumber(bn)),
[
values.map(([,, { total }]): BN =>
total.unwrap().div(divisor))
// exposures.map(({ own }): BN =>
// own.unwrap().div(divisor)),
// exposures.map(({ others }): BN =>
// others.reduce((total, { value }): BN => total.add(value.unwrap()), new BN(0)).div(divisor))
]
values.map(([, { total }]): BN =>
total.unwrap().div(divisor))
// exposures.map(({ own }): BN =>
// own.unwrap().div(divisor)),
// exposures.map(({ others }): BN =>
// others.reduce((total, { value }): BN => total.add(value.unwrap()), new BN(0)).div(divisor))
];
}

function extractSplit (values: [BN, Hash, Exposure][], validatorId: string): SplitData | null {
const last = values[values.length - 1][2];
function extractSplit (values: [Hash, Exposure][], validatorId: string): SplitData | null {
const last = values[values.length - 1][1];
const total = last.total.unwrap();

if (total.eqn(0)) {
Expand All @@ -96,8 +79,17 @@ function extractEraSlash (validatorId: string, slashes: Slash[]): BN {
}, new BN(0));
}

function Validator ({ blockCounts, className, currentIndex, sessionRewards, startNumber, t, validatorId }: Props): React.ReactElement<Props> {
function balanceToNumber (amount: BN, divisor: BN): number {
return amount.muln(1000).div(divisor).toNumber() / 1000;
}

function Validator ({ className, sessionRewards, t, validatorId }: Props): React.ReactElement<Props> {
const { api } = useApiContext();
// FIXME There is something seriously wrong in these two with "any" horrors
const blockCounts = trackStream<u32[]>(api.query.imOnline?.authoredBlocks?.multi as any, [sessionRewards, validatorId], {
paramMap: ([sessionRewards, validatorId]: [SessionRewards[], string]): any =>
[sessionRewards.map(({ sessionIndex }): [SessionIndex, string] => [sessionIndex, validatorId])]
});
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: [] });
Expand All @@ -106,61 +98,73 @@ function Validator ({ blockCounts, className, currentIndex, sessionRewards, star
const divisor = new BN('1'.padEnd(formatBalance.getDefaults().decimals + 1, '0'));

useEffect((): void => {
api.isReady.then(async (): Promise<void> => {
const values = await getHistoric<Exposure>(api, 'staking.stakers', [validatorId], {
interval: (api.consts.babe?.epochDuration as BlockNumber || new BN(500)).muln(2).divn(3),
max: MAX_SESSIONS,
startNumber
if (!splitChart) {
const hashes = sessionRewards.map(({ blockHash }): Hash => blockHash);
const stakeLabels = sessionRewards.map(({ sessionIndex }): string => formatNumber(sessionIndex));

api.isReady.then(async (): Promise<void> => {
const values = await getHistoric<Exposure>(api, 'staking.stakers', [validatorId], hashes);
const stakeChart = extractStake(values, divisor);
const splitChart = extractSplit(values, validatorId);
const splitMax = splitChart ? Math.min(Math.ceil(splitChart[0].value), 100) : 100;

setStakeInfo({ stakeChart, stakeLabels });
setSplitInfo({ splitChart, splitMax });
});
const [stakeLabels, stakeChart] = extractStake(values, divisor);
const splitChart = extractSplit(values, validatorId);
const splitMax = splitChart ? Math.min(Math.ceil(splitChart[0].value), 100) : 100;

setStakeInfo({ stakeChart, stakeLabels });
setSplitInfo({ splitChart, splitMax });
});
}, []);
}
}, [sessionRewards, splitChart]);

useEffect((): void => {
const rewardsLabels: string[] = [];
const rewardsChart: LineData = [[]];

sessionRewards.forEach(({ sessionIndex, slashes }): void => {
// this shows the start of the new era, however rewards are for previous
rewardsLabels.push(formatNumber(sessionIndex.subn(1)));

// calculate and format to 3 decimals
rewardsChart[0].push(
extractEraSlash(validatorId, slashes).muln(1000).div(divisor).toNumber() / 1000
);
});
if (blockCounts) {
const rewardsLabels: string[] = [];
const rewardsChart: LineData = [[], [], []];
let total = new BN(0);

sessionRewards.forEach(({ blockNumber, reward, sessionIndex, slashes }, index): void => {
// this shows the start of the new era, however rewards are for previous
rewardsLabels.push(formatNumber(sessionIndex.subn(1)));

const neg = extractEraSlash(validatorId, slashes);
const pos = index
? reward.mul(blockCounts[index - 1]).div(blockNumber.sub(sessionRewards[index - 1].blockNumber))
: new BN(0);

// add this to the total
total = total.add(neg).add(pos);

// calculate and format to 3 decimals
rewardsChart[0].push(balanceToNumber(neg, divisor));
rewardsChart[1].push(balanceToNumber(pos, divisor));
rewardsChart[2].push(balanceToNumber(total.divn(index), divisor));
});

setRewardsInfo({ rewardsChart, rewardsLabels });
}, [sessionRewards, validatorId]);
setRewardsInfo({ rewardsChart, rewardsLabels });
}
}, [blockCounts, sessionRewards, validatorId]);

useEffect((): void => {
setBlocksLabels(
getIndexRange(currentIndex).map((index): string => formatNumber(index))
sessionRewards.map(({ sessionIndex }): string => formatNumber(sessionIndex))
);
}, [currentIndex]);
}, [sessionRewards]);

useEffect((): void => {
if (blockCounts) {
const avgSet: number[] = [];
const idxSet: BN[] = [];

blockCounts.reduce((total: BN, value, index): BN => {
blockCounts.reduce((total: BN, value: u32, index: number): BN => {
total = total.add(value);

avgSet.push(total.toNumber() / (index + 1));
avgSet.push(total.muln(100).divn(index + 1).toNumber() / 100);
idxSet.push(value);

return total;
}, new BN(0));

setBlocksChart([idxSet, avgSet]);
}
}, [blockCounts, blocksLabels]);
}, [blockCounts]);

return (
<Columar className={className}>
Expand All @@ -169,7 +173,7 @@ function Validator ({ blockCounts, className, currentIndex, sessionRewards, star
<>
{blocksChart && (
<div className='staking--Chart'>
<h1>{t('blocks per session')}</h1>
<h1>{t('blocks produced')}</h1>
<Chart.Line
colors={COLORS_BLOCKS}
labels={blocksLabels}
Expand All @@ -180,11 +184,11 @@ function Validator ({ blockCounts, className, currentIndex, sessionRewards, star
)}
{rewardsChart && (
<div className='staking--Chart'>
<h1>{t('slashed per session')}</h1>
<h1>{t('rewards & slashes')}</h1>
<Chart.Line
colors={COLORS_BLOCKS}
colors={COLORS_REWARD}
labels={rewardsLabels}
legends={[t('slashed'), t('rewarded')]}
legends={[t('slashed'), t('rewards (est.)'), t('average')]}
values={rewardsChart}
/>
</div>
Expand Down Expand Up @@ -222,13 +226,4 @@ function Validator ({ blockCounts, className, currentIndex, sessionRewards, star
);
}

export default translate(
withCalls<Props>(
['query.imOnline.authoredBlocks', {
isMulti: true,
propName: 'blockCounts',
paramPick: ({ currentIndex, validatorId }: Props): [BN, string][] =>
getIndexRange(currentIndex).map((index): [BN, string] => [index, validatorId])
}]
)(Validator)
);
export default translate(Validator);
Loading