Staking Address component breakdown (#2445)
* Staking Address component breakdown * Share Favorite component
This commit is contained in:
parent
bf642b0d0a
commit
3da6f3fbcf
31
packages/page-staking/src/Overview/Address/Favorite.tsx
Normal file
31
packages/page-staking/src/Overview/Address/Favorite.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
// 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 React, { useCallback } from 'react';
|
||||
import { Icon } from '@polkadot/react-components';
|
||||
|
||||
interface Props {
|
||||
address: string;
|
||||
isFavorite: boolean;
|
||||
toggleFavorite: (accountId: string) => void;
|
||||
}
|
||||
|
||||
function Favorite ({ address, isFavorite, toggleFavorite }: Props): React.ReactElement<Props> {
|
||||
const _onFavorite = useCallback(
|
||||
(): void => toggleFavorite(address),
|
||||
[address]
|
||||
);
|
||||
|
||||
return (
|
||||
<td className='favorite'>
|
||||
<Icon
|
||||
className={`${isFavorite && 'isSelected isColorHighlight'}`}
|
||||
name={isFavorite ? 'star' : 'star outline'}
|
||||
onClick={_onFavorite}
|
||||
/>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Favorite);
|
||||
48
packages/page-staking/src/Overview/Address/StakeOther.tsx
Normal file
48
packages/page-staking/src/Overview/Address/StakeOther.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
// 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 { AccountId, Balance } from '@polkadot/types/interfaces';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React from 'react';
|
||||
import { AddressMini, Expander } from '@polkadot/react-components';
|
||||
import { FormatBalance } from '@polkadot/react-query';
|
||||
import { formatNumber } from '@polkadot/util';
|
||||
|
||||
import { useTranslation } from '../../translate';
|
||||
|
||||
interface Props {
|
||||
stakeOther?: BN;
|
||||
nominators: [AccountId, Balance][];
|
||||
}
|
||||
|
||||
function StakeOther ({ nominators, stakeOther }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<td className='top'>
|
||||
{stakeOther?.gtn(0) && (
|
||||
<>
|
||||
<label>{t('other stake')}</label>
|
||||
<Expander summary={
|
||||
<FormatBalance value={stakeOther}>
|
||||
({formatNumber(nominators.length)})
|
||||
</FormatBalance>
|
||||
}>
|
||||
{nominators.map(([who, bonded]): React.ReactNode =>
|
||||
<AddressMini
|
||||
bonded={bonded}
|
||||
key={who.toString()}
|
||||
value={who}
|
||||
withBonded
|
||||
/>
|
||||
)}
|
||||
</Expander>
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(StakeOther);
|
||||
49
packages/page-staking/src/Overview/Address/Status.tsx
Normal file
49
packages/page-staking/src/Overview/Address/Status.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
// 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 React from 'react';
|
||||
import { Badge, Icon } from '@polkadot/react-components';
|
||||
import { formatNumber } from '@polkadot/util';
|
||||
|
||||
import { useTranslation } from '../../translate';
|
||||
|
||||
interface Props {
|
||||
isElected: boolean;
|
||||
onlineCount?: false | number;
|
||||
onlineMessage?: boolean;
|
||||
}
|
||||
|
||||
function Status ({ isElected, onlineCount, onlineMessage }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<td className='together'>
|
||||
{isElected && (
|
||||
<Badge
|
||||
hover={t('Selected for the next session')}
|
||||
info={<Icon name='chevron right' />}
|
||||
isInline
|
||||
isTooltip
|
||||
type='next'
|
||||
/>
|
||||
)}
|
||||
{(!!onlineCount || onlineMessage) && (
|
||||
<Badge
|
||||
hover={t('Active with {{blocks}} blocks authored{{hasMessage}} heartbeat message', {
|
||||
replace: {
|
||||
blocks: formatNumber(onlineCount || 0),
|
||||
hasMessage: onlineMessage ? ' and a' : ', no'
|
||||
}
|
||||
})}
|
||||
info={<Icon name='check' />}
|
||||
isInline
|
||||
isTooltip
|
||||
type='online'
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Status);
|
||||
@ -8,13 +8,16 @@ import { DeriveAccountInfo, DerivedStakingQuery } from '@polkadot/api-derive/typ
|
||||
import BN from 'bn.js';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import ApiPromise from '@polkadot/api/promise';
|
||||
import { AddressMini, AddressSmall, Badge, Expander, Icon } from '@polkadot/react-components';
|
||||
import { AddressSmall, Icon } from '@polkadot/react-components';
|
||||
import { useApi, useCall } from '@polkadot/react-hooks';
|
||||
import { FormatBalance } from '@polkadot/react-query';
|
||||
import keyring from '@polkadot/ui-keyring';
|
||||
import { formatNumber } from '@polkadot/util';
|
||||
|
||||
import { useTranslation } from '../translate';
|
||||
import { useTranslation } from '../../translate';
|
||||
import Favorite from './Favorite';
|
||||
import Status from './Status';
|
||||
import StakeOther from './StakeOther';
|
||||
|
||||
interface Props {
|
||||
address: string;
|
||||
@ -121,10 +124,6 @@ function Address ({ address, className, filterName, hasQueries, isAuthor, isElec
|
||||
);
|
||||
}, [address, filterName, info]);
|
||||
|
||||
const _onFavorite = useCallback(
|
||||
(): void => toggleFavorite(address),
|
||||
[address]
|
||||
);
|
||||
const _onQueryStats = useCallback(
|
||||
(): void => {
|
||||
window.location.hash = `/staking/query/${address}`;
|
||||
@ -134,38 +133,16 @@ function Address ({ address, className, filterName, hasQueries, isAuthor, isElec
|
||||
|
||||
return (
|
||||
<tr className={`${className} ${isAuthor && 'isHighlight'} ${!isVisible && 'staking--hidden'}`}>
|
||||
<td className='favorite'>
|
||||
<Icon
|
||||
className={`${isFavorite && 'isSelected isColorHighlight'}`}
|
||||
name={isFavorite ? 'star' : 'star outline'}
|
||||
onClick={_onFavorite}
|
||||
/>
|
||||
</td>
|
||||
<td className='together'>
|
||||
{isElected && (
|
||||
<Badge
|
||||
hover={t('Selected for the next session')}
|
||||
info={<Icon name='chevron right' />}
|
||||
isInline
|
||||
isTooltip
|
||||
type='next'
|
||||
/>
|
||||
)}
|
||||
{(!!onlineCount || onlineMessage) && (
|
||||
<Badge
|
||||
hover={t('Active with {{blocks}} blocks authored{{hasMessage}} heartbeat message', {
|
||||
replace: {
|
||||
blocks: formatNumber(onlineCount || 0),
|
||||
hasMessage: onlineMessage ? ' and a' : ', no'
|
||||
}
|
||||
})}
|
||||
info={<Icon name='check' />}
|
||||
isInline
|
||||
isTooltip
|
||||
type='online'
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<Favorite
|
||||
address={address}
|
||||
isFavorite={isFavorite}
|
||||
toggleFavorite={toggleFavorite}
|
||||
/>
|
||||
<Status
|
||||
isElected={isElected}
|
||||
onlineCount={onlineCount}
|
||||
onlineMessage={onlineMessage}
|
||||
/>
|
||||
<td>
|
||||
<AddressSmall value={address} />
|
||||
</td>
|
||||
@ -177,27 +154,10 @@ function Address ({ address, className, filterName, hasQueries, isAuthor, isElec
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className='top'>
|
||||
{stakeOther?.gtn(0) && (
|
||||
<>
|
||||
<label>{t('other stake')}</label>
|
||||
<Expander summary={
|
||||
<FormatBalance value={stakeOther}>
|
||||
({formatNumber(nominators.length)})
|
||||
</FormatBalance>
|
||||
}>
|
||||
{nominators.map(([who, bonded]): React.ReactNode =>
|
||||
<AddressMini
|
||||
bonded={bonded}
|
||||
key={who.toString()}
|
||||
value={who}
|
||||
withBonded
|
||||
/>
|
||||
)}
|
||||
</Expander>
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
<StakeOther
|
||||
nominators={nominators}
|
||||
stakeOther={stakeOther}
|
||||
/>
|
||||
<td className='number top'>
|
||||
{commission && (
|
||||
<><label>{t('commission')}</label>{commission}</>
|
||||
@ -214,7 +174,7 @@ function Address ({ address, className, filterName, hasQueries, isAuthor, isElec
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{hasQueries && api.query.imOnline?.authoredBlocks && (
|
||||
{hasQueries && (
|
||||
<Icon
|
||||
name='line graph'
|
||||
onClick={_onQueryStats}
|
||||
75
packages/page-staking/src/Query/ChartPoints.tsx
Normal file
75
packages/page-staking/src/Query/ChartPoints.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
// 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 { DeriveStakerPoints } from '@polkadot/api-derive/types';
|
||||
import { ChartInfo, LineDataEntry, Props } from './types';
|
||||
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Chart, Spinner } from '@polkadot/react-components';
|
||||
import { useApi, useCall } from '@polkadot/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate';
|
||||
|
||||
const COLORS_POINTS = [undefined, '#acacac'];
|
||||
|
||||
function extractPoints (points: DeriveStakerPoints[]): ChartInfo {
|
||||
const labels: string[] = [];
|
||||
const avgSet: LineDataEntry = [];
|
||||
const idxSet: LineDataEntry = [];
|
||||
let avgCount = 0;
|
||||
let total = 0;
|
||||
|
||||
points.forEach(({ era, points }): void => {
|
||||
total += points.toNumber();
|
||||
labels.push(era.toHuman());
|
||||
|
||||
if (points.gtn(0)) {
|
||||
avgCount++;
|
||||
}
|
||||
|
||||
avgSet.push((avgCount ? Math.ceil(total * 100 / avgCount) : 0) / 100);
|
||||
idxSet.push(points);
|
||||
});
|
||||
|
||||
return {
|
||||
chart: [idxSet, avgSet],
|
||||
labels
|
||||
};
|
||||
}
|
||||
|
||||
function ChartPoints ({ validatorId }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [{ chart, labels }, setChart] = useState<ChartInfo>({ chart: [], labels: [] });
|
||||
const stakerPoints = useCall<DeriveStakerPoints[]>(api.derive.staking.stakerPoints as any, [validatorId, true]);
|
||||
const legends = useMemo(() => [
|
||||
t('points'),
|
||||
t('average')
|
||||
], [t]);
|
||||
|
||||
useEffect((): void => {
|
||||
stakerPoints && setChart(
|
||||
extractPoints(stakerPoints)
|
||||
);
|
||||
}, [stakerPoints]);
|
||||
|
||||
return (
|
||||
<div className='staking--Chart'>
|
||||
<h1>{t('era points')}</h1>
|
||||
{chart.length
|
||||
? (
|
||||
<Chart.Line
|
||||
colors={COLORS_POINTS}
|
||||
labels={labels}
|
||||
legends={legends}
|
||||
values={chart}
|
||||
/>
|
||||
)
|
||||
: <Spinner />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(ChartPoints);
|
||||
96
packages/page-staking/src/Query/ChartRewards.tsx
Normal file
96
packages/page-staking/src/Query/ChartRewards.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
// 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 { DeriveEraRewards, DeriveOwnSlashes, DeriveStakerPoints } from '@polkadot/api-derive/types';
|
||||
import { ChartInfo, LineDataEntry, Props } from './types';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Chart, Spinner } from '@polkadot/react-components';
|
||||
import { useApi, useCall } from '@polkadot/react-hooks';
|
||||
import { formatBalance } from '@polkadot/util';
|
||||
|
||||
import { useTranslation } from '../translate';
|
||||
import { balanceToNumber } from './util';
|
||||
|
||||
const COLORS_REWARD = ['#8c2200', '#008c22', '#acacac'];
|
||||
|
||||
function extractRewards (erasRewards: DeriveEraRewards[], ownSlashes: DeriveOwnSlashes[], allPoints: DeriveStakerPoints[], divisor: BN): ChartInfo {
|
||||
const labels: string[] = [];
|
||||
const slashSet: LineDataEntry = [];
|
||||
const rewardSet: LineDataEntry = [];
|
||||
const avgSet: LineDataEntry = [];
|
||||
let avgCount = 0;
|
||||
let total = 0;
|
||||
|
||||
erasRewards.forEach(({ era, eraReward }): void => {
|
||||
const points = allPoints.find((points) => points.era.eq(era));
|
||||
const slashed = ownSlashes.find((slash) => slash.era.eq(era));
|
||||
const reward = points?.eraPoints.gtn(0)
|
||||
? balanceToNumber(points.points.mul(eraReward).div(points.eraPoints), divisor)
|
||||
: 0;
|
||||
const slash = slashed
|
||||
? balanceToNumber(slashed.total, divisor)
|
||||
: 0;
|
||||
|
||||
total += reward;
|
||||
|
||||
if (reward > 0) {
|
||||
avgCount++;
|
||||
}
|
||||
|
||||
labels.push(era.toHuman());
|
||||
rewardSet.push(reward);
|
||||
avgSet.push((avgCount ? Math.ceil(total * 100 / avgCount) : 0) / 100);
|
||||
slashSet.push(slash);
|
||||
});
|
||||
|
||||
return {
|
||||
chart: [slashSet, rewardSet, avgSet],
|
||||
labels
|
||||
};
|
||||
}
|
||||
|
||||
function ChartRewards ({ validatorId }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [{ chart, labels }, setChart] = useState<ChartInfo>({ chart: [], labels: [] });
|
||||
const ownSlashes = useCall<DeriveOwnSlashes[]>(api.derive.staking.ownSlashes as any, [validatorId, true]);
|
||||
const erasRewards = useCall<DeriveEraRewards[]>(api.derive.staking.erasRewards as any, []);
|
||||
const stakerPoints = useCall<DeriveStakerPoints[]>(api.derive.staking.stakerPoints as any, [validatorId, true]);
|
||||
const { currency, divisor } = useMemo((): { currency: string; divisor: BN } => ({
|
||||
currency: formatBalance.getDefaults().unit,
|
||||
divisor: new BN('1'.padEnd(formatBalance.getDefaults().decimals + 1, '0'))
|
||||
}), [formatBalance]);
|
||||
const legends = useMemo(() => [
|
||||
t('{{currency}} slashed', { replace: { currency } }),
|
||||
t('{{currency}} rewards', { replace: { currency } }),
|
||||
t('{{currency}} average', { replace: { currency } })
|
||||
], [currency, t]);
|
||||
|
||||
useEffect((): void => {
|
||||
erasRewards && ownSlashes && stakerPoints && setChart(
|
||||
extractRewards(erasRewards, ownSlashes, stakerPoints, divisor)
|
||||
);
|
||||
}, [erasRewards, stakerPoints]);
|
||||
|
||||
return (
|
||||
<div className='staking--Chart'>
|
||||
<h1>{t('rewards & slashes')}</h1>
|
||||
{chart.length
|
||||
? (
|
||||
<Chart.Line
|
||||
colors={COLORS_REWARD}
|
||||
labels={labels}
|
||||
legends={legends}
|
||||
values={chart}
|
||||
/>
|
||||
)
|
||||
: <Spinner />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(ChartRewards);
|
||||
88
packages/page-staking/src/Query/ChartStake.tsx
Normal file
88
packages/page-staking/src/Query/ChartStake.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
// 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 { DeriveOwnExposure } from '@polkadot/api-derive/types';
|
||||
import { ChartInfo, LineDataEntry, Props } from './types';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Chart, Spinner } from '@polkadot/react-components';
|
||||
import { useApi, useCall } from '@polkadot/react-hooks';
|
||||
import { formatBalance } from '@polkadot/util';
|
||||
|
||||
import { useTranslation } from '../translate';
|
||||
import { balanceToNumber } from './util';
|
||||
|
||||
const COLORS_STAKE = [undefined, '#8c2200', '#acacac'];
|
||||
|
||||
function extractStake (exposures: DeriveOwnExposure[], divisor: BN): ChartInfo {
|
||||
const labels: string[] = [];
|
||||
const cliSet: LineDataEntry = [];
|
||||
const expSet: LineDataEntry = [];
|
||||
const avgSet: LineDataEntry = [];
|
||||
let avgCount = 0;
|
||||
let total = 0;
|
||||
|
||||
exposures.forEach(({ clipped, era, exposure }): void => {
|
||||
const cli = balanceToNumber(clipped.total.unwrap(), divisor);
|
||||
const exp = balanceToNumber(exposure.total.unwrap(), divisor);
|
||||
|
||||
total += cli;
|
||||
|
||||
if (cli > 0) {
|
||||
avgCount++;
|
||||
}
|
||||
|
||||
avgSet.push((avgCount ? Math.ceil(total * 100 / avgCount) : 0) / 100);
|
||||
labels.push(era.toHuman());
|
||||
cliSet.push(cli);
|
||||
expSet.push(exp);
|
||||
});
|
||||
|
||||
return {
|
||||
chart: [cliSet, expSet, avgSet],
|
||||
labels
|
||||
};
|
||||
}
|
||||
|
||||
function ChartStake ({ validatorId }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [{ chart, labels }, setChart] = useState<ChartInfo>({ chart: [], labels: [] });
|
||||
const ownExposure = useCall<DeriveOwnExposure[]>(api.derive.staking.ownExposure as any, [validatorId, true]);
|
||||
const { currency, divisor } = useMemo((): { currency: string; divisor: BN } => ({
|
||||
currency: formatBalance.getDefaults().unit,
|
||||
divisor: new BN('1'.padEnd(formatBalance.getDefaults().decimals + 1, '0'))
|
||||
}), [formatBalance]);
|
||||
const legends = useMemo(() => [
|
||||
t('{{currency}} clipped', { replace: { currency } }),
|
||||
t('{{currency}} total', { replace: { currency } }),
|
||||
t('{{currency}} average', { replace: { currency } })
|
||||
], [currency, t]);
|
||||
|
||||
useEffect((): void => {
|
||||
ownExposure && setChart(
|
||||
extractStake(ownExposure, divisor)
|
||||
);
|
||||
}, [ownExposure]);
|
||||
|
||||
return (
|
||||
<div className='staking--Chart'>
|
||||
<h1>{t('elected stake')}</h1>
|
||||
{chart && !!chart[0]?.length
|
||||
? (
|
||||
<Chart.Line
|
||||
colors={COLORS_STAKE}
|
||||
labels={labels}
|
||||
legends={legends}
|
||||
values={chart}
|
||||
/>
|
||||
)
|
||||
: <Spinner />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(ChartStake);
|
||||
@ -2,212 +2,24 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { DeriveEraRewards, DeriveOwnExposure, DeriveOwnSlashes, DeriveStakerPoints } from '@polkadot/api-derive/types';
|
||||
import { Props } from './types';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Chart, Columar, Column, Spinner } from '@polkadot/react-components';
|
||||
import { useApi, useCall } from '@polkadot/react-hooks';
|
||||
import { formatBalance } from '@polkadot/util';
|
||||
import React from 'react';
|
||||
import { Columar, Column } from '@polkadot/react-components';
|
||||
|
||||
import { useTranslation } from '../translate';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
validatorId: string;
|
||||
}
|
||||
|
||||
type LineDataEntry = (BN | number)[];
|
||||
|
||||
type LineData = LineDataEntry[];
|
||||
|
||||
interface ChartInfo {
|
||||
chart: LineData;
|
||||
labels: string[];
|
||||
}
|
||||
|
||||
const COLORS_REWARD = ['#8c2200', '#008c22', '#acacac'];
|
||||
const COLORS_POINTS = [undefined, '#acacac'];
|
||||
const COLORS_STAKE = [undefined, '#8c2200', '#acacac'];
|
||||
|
||||
function balanceToNumber (amount: BN, divisor: BN): number {
|
||||
return amount.muln(1000).div(divisor).toNumber() / 1000;
|
||||
}
|
||||
|
||||
function extractPoints (points: DeriveStakerPoints[]): ChartInfo {
|
||||
const labels: string[] = [];
|
||||
const avgSet: LineDataEntry = [];
|
||||
const idxSet: LineDataEntry = [];
|
||||
let avgCount = 0;
|
||||
let total = 0;
|
||||
|
||||
points.forEach(({ era, points }): void => {
|
||||
total += points.toNumber();
|
||||
labels.push(era.toHuman());
|
||||
|
||||
if (points.gtn(0)) {
|
||||
avgCount++;
|
||||
}
|
||||
|
||||
avgSet.push((avgCount ? Math.ceil(total * 100 / avgCount) : 0) / 100);
|
||||
idxSet.push(points);
|
||||
});
|
||||
|
||||
return {
|
||||
chart: [idxSet, avgSet],
|
||||
labels
|
||||
};
|
||||
}
|
||||
|
||||
function extractRewards (erasRewards: DeriveEraRewards[], ownSlashes: DeriveOwnSlashes[], allPoints: DeriveStakerPoints[], divisor: BN): ChartInfo {
|
||||
const labels: string[] = [];
|
||||
const slashSet: LineDataEntry = [];
|
||||
const rewardSet: LineDataEntry = [];
|
||||
const avgSet: LineDataEntry = [];
|
||||
let avgCount = 0;
|
||||
let total = 0;
|
||||
|
||||
erasRewards.forEach(({ era, eraReward }): void => {
|
||||
const points = allPoints.find((points) => points.era.eq(era));
|
||||
const slashed = ownSlashes.find((slash) => slash.era.eq(era));
|
||||
const reward = points?.eraPoints.gtn(0)
|
||||
? balanceToNumber(points.points.mul(eraReward).div(points.eraPoints), divisor)
|
||||
: 0;
|
||||
const slash = slashed
|
||||
? balanceToNumber(slashed.total, divisor)
|
||||
: 0;
|
||||
|
||||
total += reward;
|
||||
|
||||
if (reward > 0) {
|
||||
avgCount++;
|
||||
}
|
||||
|
||||
labels.push(era.toHuman());
|
||||
rewardSet.push(reward);
|
||||
avgSet.push((avgCount ? Math.ceil(total * 100 / avgCount) : 0) / 100);
|
||||
slashSet.push(slash);
|
||||
});
|
||||
|
||||
return {
|
||||
chart: [slashSet, rewardSet, avgSet],
|
||||
labels
|
||||
};
|
||||
}
|
||||
|
||||
function extractStake (exposures: DeriveOwnExposure[], divisor: BN): ChartInfo {
|
||||
const labels: string[] = [];
|
||||
const cliSet: LineDataEntry = [];
|
||||
const expSet: LineDataEntry = [];
|
||||
const avgSet: LineDataEntry = [];
|
||||
let avgCount = 0;
|
||||
let total = 0;
|
||||
|
||||
exposures.forEach(({ clipped, era, exposure }): void => {
|
||||
const cli = balanceToNumber(clipped.total.unwrap(), divisor);
|
||||
const exp = balanceToNumber(exposure.total.unwrap(), divisor);
|
||||
|
||||
total += cli;
|
||||
|
||||
if (cli > 0) {
|
||||
avgCount++;
|
||||
}
|
||||
|
||||
avgSet.push((avgCount ? Math.ceil(total * 100 / avgCount) : 0) / 100);
|
||||
labels.push(era.toHuman());
|
||||
cliSet.push(cli);
|
||||
expSet.push(exp);
|
||||
});
|
||||
|
||||
return {
|
||||
chart: [cliSet, expSet, avgSet],
|
||||
labels
|
||||
};
|
||||
}
|
||||
import ChartPoints from './ChartPoints';
|
||||
import ChartRewards from './ChartRewards';
|
||||
import ChartStake from './ChartStake';
|
||||
|
||||
function Validator ({ className, validatorId }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [{ chart: pointChart, labels: pointLabels }, setPointChart] = useState<ChartInfo>({ chart: [], labels: [] });
|
||||
const [{ chart: rewardChart, labels: rewardLabels }, setRewardChart] = useState<ChartInfo>({ chart: [], labels: [] });
|
||||
const [{ chart: stakeChart, labels: stakeLabels }, setStakeCharts] = useState<ChartInfo>({ chart: [], labels: [] });
|
||||
const ownExposure = useCall<DeriveOwnExposure[]>(api.derive.staking.ownExposure as any, [validatorId, true]);
|
||||
const ownSlashes = useCall<DeriveOwnSlashes[]>(api.derive.staking.ownSlashes as any, [validatorId, true]);
|
||||
const erasRewards = useCall<DeriveEraRewards[]>(api.derive.staking.erasRewards as any, []);
|
||||
const stakerPoints = useCall<DeriveStakerPoints[]>(api.derive.staking.stakerPoints as any, [validatorId, true]);
|
||||
const { currency, divisor } = useMemo((): { currency: string; divisor: BN } => ({
|
||||
currency: formatBalance.getDefaults().unit,
|
||||
divisor: new BN('1'.padEnd(formatBalance.getDefaults().decimals + 1, '0'))
|
||||
}), [formatBalance]);
|
||||
const legExpose = useMemo(() => [t('{{currency}} clipped', { replace: { currency } }), t('{{currency}} total', { replace: { currency } }), t('{{currency}} average', { replace: { currency } })], [currency, t]);
|
||||
const legPoints = useMemo(() => [t('points'), t('average')], [t]);
|
||||
const legReward = useMemo(() => [t('{{currency}} slashed', { replace: { currency } }), t('{{currency}} rewards', { replace: { currency } }), t('{{currency}} average', { replace: { currency } })], [currency, t]);
|
||||
|
||||
useEffect((): void => {
|
||||
stakerPoints && setPointChart(
|
||||
extractPoints(stakerPoints)
|
||||
);
|
||||
}, [stakerPoints]);
|
||||
|
||||
useEffect((): void => {
|
||||
erasRewards && ownSlashes && stakerPoints && setRewardChart(
|
||||
extractRewards(erasRewards, ownSlashes, stakerPoints, divisor)
|
||||
);
|
||||
}, [erasRewards, stakerPoints]);
|
||||
|
||||
useEffect((): void => {
|
||||
ownExposure && setStakeCharts(
|
||||
extractStake(ownExposure, divisor)
|
||||
);
|
||||
}, [ownExposure]);
|
||||
|
||||
return (
|
||||
<Columar className={className}>
|
||||
<Column>
|
||||
<div className='staking--Chart'>
|
||||
<h1>{t('era points')}</h1>
|
||||
{pointChart.length
|
||||
? (
|
||||
<Chart.Line
|
||||
colors={COLORS_POINTS}
|
||||
labels={pointLabels}
|
||||
legends={legPoints}
|
||||
values={pointChart}
|
||||
/>
|
||||
)
|
||||
: <Spinner />
|
||||
}
|
||||
</div>
|
||||
<div className='staking--Chart'>
|
||||
<h1>{t('rewards & slashes')}</h1>
|
||||
{rewardChart.length
|
||||
? (
|
||||
<Chart.Line
|
||||
colors={COLORS_REWARD}
|
||||
labels={rewardLabels}
|
||||
legends={legReward}
|
||||
values={rewardChart}
|
||||
/>
|
||||
)
|
||||
: <Spinner />
|
||||
}
|
||||
</div>
|
||||
<ChartPoints validatorId={validatorId} />
|
||||
<ChartRewards validatorId={validatorId} />
|
||||
</Column>
|
||||
<Column>
|
||||
<div className='staking--Chart'>
|
||||
<h1>{t('elected stake')}</h1>
|
||||
{stakeChart && !!stakeChart[0]?.length
|
||||
? (
|
||||
<Chart.Line
|
||||
colors={COLORS_STAKE}
|
||||
labels={stakeLabels}
|
||||
legends={legExpose}
|
||||
values={stakeChart}
|
||||
/>
|
||||
)
|
||||
: <Spinner />
|
||||
}
|
||||
</div>
|
||||
<ChartStake validatorId={validatorId} />
|
||||
</Column>
|
||||
</Columar>
|
||||
);
|
||||
|
||||
19
packages/page-staking/src/Query/types.ts
Normal file
19
packages/page-staking/src/Query/types.ts
Normal file
@ -0,0 +1,19 @@
|
||||
// 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 BN from 'bn.js';
|
||||
|
||||
export interface Props {
|
||||
className?: string;
|
||||
validatorId: string;
|
||||
}
|
||||
|
||||
export type LineDataEntry = (BN | number)[];
|
||||
|
||||
export type LineData = LineDataEntry[];
|
||||
|
||||
export interface ChartInfo {
|
||||
chart: LineData;
|
||||
labels: string[];
|
||||
}
|
||||
9
packages/page-staking/src/Query/util.ts
Normal file
9
packages/page-staking/src/Query/util.ts
Normal file
@ -0,0 +1,9 @@
|
||||
// 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 BN from 'bn.js';
|
||||
|
||||
export function balanceToNumber (amount: BN, divisor: BN): number {
|
||||
return amount.muln(1000).div(divisor).toNumber() / 1000;
|
||||
}
|
||||
@ -10,6 +10,7 @@ import { FormatBalance } from '@polkadot/react-query';
|
||||
import { formatNumber } from '@polkadot/util';
|
||||
|
||||
import { useTranslation } from '../translate';
|
||||
import Favorite from '../Overview/Address/Favorite';
|
||||
|
||||
interface Props {
|
||||
info: ValidatorInfo;
|
||||
@ -19,10 +20,6 @@ interface Props {
|
||||
function Validator ({ info: { accountId, bondOther, bondOwn, bondTotal, commissionPer, isCommission, isFavorite, isNominating, key, numNominators, rankOverall, rewardPayout, validatorPayment }, toggleFavorite }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const _onFavorite = useCallback(
|
||||
(): void => toggleFavorite(key),
|
||||
[key]
|
||||
);
|
||||
const _onQueryStats = useCallback(
|
||||
(): void => {
|
||||
window.location.hash = `/staking/query/${key}`;
|
||||
@ -32,13 +29,11 @@ function Validator ({ info: { accountId, bondOther, bondOwn, bondTotal, commissi
|
||||
|
||||
return (
|
||||
<tr className={`${isNominating && 'isHighlight'}`}>
|
||||
<td className='favorite'>
|
||||
<Icon
|
||||
className={`${isFavorite && 'isSelected isColorHighlight'}`}
|
||||
name={isFavorite ? 'star' : 'star outline'}
|
||||
onClick={_onFavorite}
|
||||
/>
|
||||
</td>
|
||||
<Favorite
|
||||
address={key}
|
||||
isFavorite={isFavorite}
|
||||
toggleFavorite={toggleFavorite}
|
||||
/>
|
||||
<td className='number'>{formatNumber(rankOverall)}</td>
|
||||
<td className='address'>
|
||||
<AddressSmall value={accountId} />
|
||||
|
||||
Loading…
Reference in New Issue
Block a user