feat(2089): second level validator table mouseovers (#2354)

* feat(1913): validator table column heading mouseovers

* feat(2089): stake needed for promotion tooltip

* feat(2089): normalised voting power tooltip

* feat(2089): total stake tooltip

* feat(2089): total stake tooltip for standby-pending-validators-table

* feat(2089): total penalties tooltip

* feat(2089): tooltip colour tweakage

* feat(2089): unit tests for the shared validator data functions

* feat(2089): removed unused import from tooltip.tsx

* Update apps/token/src/routes/staking/home/validator-tables/standby-pending-validators-table.tsx

Co-authored-by: Dexter Edwards <dexter.edwards93@gmail.com>

* feat(2089): tweaks from PR comments

Co-authored-by: Dexter Edwards <dexter.edwards93@gmail.com>
This commit is contained in:
Sam Keen 2022-12-08 14:57:11 +00:00 committed by GitHub
parent e4afa67cd0
commit 86e4280680
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 517 additions and 106 deletions

View File

@ -577,10 +577,14 @@
"finalOutcomeMayDiffer": "Final outcome may differ",
"votingPower": "Voting power",
"normalisedVotingPower": "Normalised voting power",
"unnormalisedVotingPower": "Unnormalised voting power",
"noValidators": "No validators",
"validator": "Validator",
"stake": "Stake",
"stakeShare": "Stake share",
"stakedByOperator": "Staked by operator",
"stakedByDelegates": "Staked by delegates",
"totalStake": "Total stake",
"pendingStake": "Pending stake",
"totalPenalties": "Total penalties",
"noPenaltyDataFromLastEpoch": "No penalty data from last epoch",
@ -724,6 +728,10 @@
"StakeShareDescription": "The stake a validator represents as a share of total stake across the network.",
"TotalPenaltiesDescription": "Total of penalties taking into account performance (considering proportion of blocks proposed against the number of blocks the validator was expected to propose) and any overstaking.",
"PendingStakeDescription": "The amount of stake that will be added or removed from the validator from the next epoch.",
"StakeNeededForPromotionStandbyDescription": "The amount of stake needed for promotion to consensus, assuming constant performance in line with previous epoch.",
"StakeNeededForPromotionCandidateDescription": "The amount of stake needed for promotion to standby, assuming constant performance in line with previous epoch."
"StakeNeededForPromotionStandbyDescription": "{{prefix}} additional stake needed for promotion to consensus, assuming constant performance in line with previous epoch.",
"StakeNeededForPromotionCandidateDescription": "{{prefix}} additional stake needed for promotion to standby, assuming constant performance in line with previous epoch.",
"Score": "Score",
"performancePenalty": "Performance penalty",
"overstaked": "Overstaked",
"overstakedPenalty": "Overstaked penalty"
}

View File

@ -3,6 +3,8 @@ fragment NodesFragment on Node {
id
name
pubkey
stakedByOperator
stakedByDelegates
stakedTotal
pendingStake
rankingScore {

View File

@ -3,12 +3,12 @@ import { Schema as Types } from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type NodesFragmentFragment = { __typename?: 'Node', avatarUrl?: string | null, id: string, name: string, pubkey: string, stakedTotal: string, pendingStake: string, rankingScore: { __typename?: 'RankingScore', rankingScore: string, stakeScore: string, performanceScore: string, votingPower: string, status: Types.ValidatorStatus } };
export type NodesFragmentFragment = { __typename?: 'Node', avatarUrl?: string | null, id: string, name: string, pubkey: string, stakedByOperator: string, stakedByDelegates: string, stakedTotal: string, pendingStake: string, rankingScore: { __typename?: 'RankingScore', rankingScore: string, stakeScore: string, performanceScore: string, votingPower: string, status: Types.ValidatorStatus } };
export type NodesQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type NodesQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null, end?: any | null, expiry?: any | null } }, nodesConnection: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', avatarUrl?: string | null, id: string, name: string, pubkey: string, stakedTotal: string, pendingStake: string, rankingScore: { __typename?: 'RankingScore', rankingScore: string, stakeScore: string, performanceScore: string, votingPower: string, status: Types.ValidatorStatus } } } | null> | null }, nodeData?: { __typename?: 'NodeData', stakedTotal: string } | null };
export type NodesQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null, end?: any | null, expiry?: any | null } }, nodesConnection: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', avatarUrl?: string | null, id: string, name: string, pubkey: string, stakedByOperator: string, stakedByDelegates: string, stakedTotal: string, pendingStake: string, rankingScore: { __typename?: 'RankingScore', rankingScore: string, stakeScore: string, performanceScore: string, votingPower: string, status: Types.ValidatorStatus } } } | null> | null }, nodeData?: { __typename?: 'NodeData', stakedTotal: string } | null };
export const NodesFragmentFragmentDoc = gql`
fragment NodesFragment on Node {
@ -16,6 +16,8 @@ export const NodesFragmentFragmentDoc = gql`
id
name
pubkey
stakedByOperator
stakedByDelegates
stakedTotal
pendingStake
rankingScore {

View File

@ -21,6 +21,8 @@ const nodeFactory = (
avatarUrl: 'https://upload.wikimedia.org/wikipedia/en/2/25/Marvin-TV-3.jpg',
pubkey: '6abc23391a9f888ab240415bf63d6844b03fc360be822f4a1d2cd832d87b2917',
stakedTotal: '14182454495731682635157',
stakedByOperator: '1000000000000000000000',
stakedByDelegates: '13182454495731682635157',
pendingStake: '0',
rankingScore: {
rankingScore: '0.67845061012234727427532760837568',

View File

@ -4,14 +4,25 @@ import { useNavigate } from 'react-router-dom';
import { AgGridDynamic as AgGrid, Button } from '@vegaprotocol/ui-toolkit';
import { useAppState } from '../../../../contexts/app-state/app-state-context';
import { BigNumber } from '../../../../lib/bignumber';
import { normalisedVotingPower, rawValidatorScore } from '../../shared';
import {
getFormattedPerformanceScore,
getNormalisedVotingPower,
getOverstakedAmount,
getOverstakingPenalty,
getPerformancePenalty,
getRawValidatorScore,
getTotalPenalties,
getUnnormalisedVotingPower,
} from '../../shared';
import {
defaultColDef,
NODE_LIST_GRID_STYLES,
stakedTotalPercentage,
totalPenalties,
TotalPenaltiesRenderer,
TotalStakeRenderer,
ValidatorFields,
ValidatorRenderer,
VotingPowerRenderer,
} from './shared';
import type { AgGridReact } from 'ag-grid-react';
import type { ColDef } from 'ag-grid-community';
@ -66,11 +77,20 @@ export const ConsensusValidatorsTable = ({
id,
name,
avatarUrl,
stakedByDelegates,
stakedByOperator,
stakedTotal,
rankingScore: { stakeScore, votingPower, performanceScore },
pendingStake,
votingPowerRanking,
}) => {
const validatorScore = getRawValidatorScore(previousEpochData, id);
const overstakedAmount = getOverstakedAmount(
validatorScore,
stakedTotal,
totalStake
);
return {
id,
[ValidatorFields.RANKING_INDEX]: votingPowerRanking,
@ -83,10 +103,29 @@ export const ConsensusValidatorsTable = ({
2
),
[ValidatorFields.NORMALISED_VOTING_POWER]:
normalisedVotingPower(votingPower),
getNormalisedVotingPower(votingPower),
[ValidatorFields.UNNORMALISED_VOTING_POWER]:
getUnnormalisedVotingPower(validatorScore),
[ValidatorFields.STAKE_SHARE]: stakedTotalPercentage(stakeScore),
[ValidatorFields.TOTAL_PENALTIES]: totalPenalties(
rawValidatorScore(previousEpochData, id),
[ValidatorFields.STAKED_BY_DELEGATES]: formatNumber(
toBigNum(stakedByDelegates, decimals),
2
),
[ValidatorFields.STAKED_BY_OPERATOR]: formatNumber(
toBigNum(stakedByOperator, decimals),
2
),
[ValidatorFields.PERFORMANCE_SCORE]:
getFormattedPerformanceScore(performanceScore).toString(),
[ValidatorFields.PERFORMANCE_PENALTY]:
getPerformancePenalty(performanceScore),
[ValidatorFields.OVERSTAKED_AMOUNT]: overstakedAmount.toString(),
[ValidatorFields.OVERSTAKING_PENALTY]: getOverstakingPenalty(
overstakedAmount,
totalStake
),
[ValidatorFields.TOTAL_PENALTIES]: getTotalPenalties(
validatorScore,
performanceScore,
stakedTotal,
totalStake
@ -152,12 +191,14 @@ export const ConsensusValidatorsTable = ({
field: ValidatorFields.STAKE,
headerName: t(ValidatorFields.STAKE).toString(),
headerTooltip: t('StakeDescription').toString(),
cellRenderer: TotalStakeRenderer,
width: 120,
},
{
field: ValidatorFields.NORMALISED_VOTING_POWER,
headerName: t(ValidatorFields.NORMALISED_VOTING_POWER).toString(),
headerTooltip: t('NormalisedVotingPowerDescription').toString(),
cellRenderer: VotingPowerRenderer,
width: 200,
sort: 'desc',
},
@ -171,6 +212,7 @@ export const ConsensusValidatorsTable = ({
field: ValidatorFields.TOTAL_PENALTIES,
headerName: t(ValidatorFields.TOTAL_PENALTIES).toString(),
headerTooltip: t('TotalPenaltiesDescription').toString(),
cellRenderer: TotalPenaltiesRenderer,
width: 120,
},
{

View File

@ -1,5 +1,5 @@
import { rawValidatorScore } from '../../shared';
import { stakedTotalPercentage, totalPenalties } from './shared';
import { getRawValidatorScore, getTotalPenalties } from '../../shared';
import { stakedTotalPercentage } from './shared';
const mockPreviousEpochData = {
epoch: {
@ -28,30 +28,30 @@ describe('stakedTotalPercentage', () => {
describe('totalPenalties', () => {
it('should return the correct penalty based on arbitrary values, test 1', () => {
expect(
totalPenalties(
rawValidatorScore(mockPreviousEpochData, '0x123'),
getTotalPenalties(
getRawValidatorScore(mockPreviousEpochData, '0x123'),
'0.1',
'5000',
'100000'
)
).toBe('50%');
).toBe('50.00%');
});
it('should return the correct penalty based on lower performance score than first test', () => {
expect(
totalPenalties(
rawValidatorScore(mockPreviousEpochData, '0x123'),
getTotalPenalties(
getRawValidatorScore(mockPreviousEpochData, '0x123'),
'0.05',
'5000',
'100000'
)
).toBe('75%');
).toBe('75.00%');
});
it('should return the correct penalty based on higher amount of stake than other tests (great penalty due to anti-whaling)', () => {
expect(
totalPenalties(
rawValidatorScore(mockPreviousEpochData, '0x123'),
getTotalPenalties(
getRawValidatorScore(mockPreviousEpochData, '0x123'),
'0.1',
'5000',
'5500'

View File

@ -1,7 +1,15 @@
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { toBigNum } from '@vegaprotocol/react-helpers';
import { Button, TooltipCellComponent } from '@vegaprotocol/ui-toolkit';
import {
formatNumber,
formatNumberPercentage,
toBigNum,
} from '@vegaprotocol/react-helpers';
import {
Button,
Tooltip,
TooltipCellComponent,
} from '@vegaprotocol/ui-toolkit';
import type { NodesFragmentFragment } from '../__generated___/Nodes';
import type { PreviousEpochQuery } from '../../__generated___/PreviousEpoch';
@ -9,11 +17,19 @@ export enum ValidatorFields {
RANKING_INDEX = 'rankingIndex',
VALIDATOR = 'validator',
STAKE = 'stake',
STAKED_BY_DELEGATES = 'stakedByDelegates',
STAKED_BY_OPERATOR = 'stakedByOperator',
PENDING_STAKE = 'pendingStake',
STAKE_SHARE = 'stakeShare',
TOTAL_PENALTIES = 'totalPenalties',
NORMALISED_VOTING_POWER = 'normalisedVotingPower',
UNNORMALISED_VOTING_POWER = 'unnormalisedVotingPower',
STAKE_NEEDED_FOR_PROMOTION = 'stakeNeededForPromotion',
STAKE_NEEDED_FOR_PROMOTION_DESCRIPTION = 'stakeNeededForPromotionDescription',
PERFORMANCE_SCORE = 'performanceScore',
PERFORMANCE_PENALTY = 'performancePenalty',
OVERSTAKED_AMOUNT = 'overstakedAmount',
OVERSTAKING_PENALTY = 'overstakingPenalty',
}
export interface ValidatorsTableProps {
@ -34,27 +50,7 @@ export const NODE_LIST_GRID_STYLES = `
`;
export const stakedTotalPercentage = (stakeScore: string) =>
toBigNum(stakeScore, 0).times(100).dp(2).toString() + '%';
export const totalPenalties = (
rawValidatorScore: string | null | undefined,
performanceScore: string,
stakedTotal: string,
totalStake: string
) => {
const totalPenaltiesCalc =
rawValidatorScore !== null
? 100 *
Math.max(
0,
1 -
(Number(performanceScore) * Number(rawValidatorScore)) /
(Number(stakedTotal) / Number(totalStake || 0))
)
: 0;
return toBigNum(totalPenaltiesCalc, 0).dp(2).toString() + '%';
};
formatNumberPercentage(toBigNum(stakeScore, 0).times(100), 2);
export const defaultColDef = {
sortable: true,
@ -64,6 +60,7 @@ export const defaultColDef = {
cellStyle: { margin: '10px 0' },
tooltipComponent: TooltipCellComponent,
tooltipShowDelay: 0,
tooltipHideDelay: 0,
};
interface ValidatorRendererProps {
@ -94,3 +91,130 @@ export const ValidatorRenderer = ({ data }: ValidatorRendererProps) => {
</div>
);
};
interface StakeNeededForPromotionRendererProps {
data: {
stakeNeededForPromotion: string | undefined;
stakeNeededForPromotionDescription: string;
};
}
export const StakeNeededForPromotionRenderer = ({
data,
}: StakeNeededForPromotionRendererProps) => {
return (
<Tooltip description={data.stakeNeededForPromotionDescription}>
<span>
{data.stakeNeededForPromotion &&
formatNumber(data.stakeNeededForPromotion, 2)}
</span>
</Tooltip>
);
};
interface VotingPowerRendererProps {
data: {
normalisedVotingPower: string | undefined | null;
unnormalisedVotingPower: string | undefined | null;
};
}
export const VotingPowerRenderer = ({ data }: VotingPowerRendererProps) => {
const { t } = useTranslation();
return (
<Tooltip
description={
<>
<div>
{t('unnormalisedVotingPower')}: {data.unnormalisedVotingPower}
</div>
<div>
{t('normalisedVotingPower')}: {data.normalisedVotingPower}
</div>
</>
}
>
<span>{data.normalisedVotingPower}</span>
</Tooltip>
);
};
interface TotalStakeRendererProps {
data: {
stake: string;
stakedByDelegates: string;
stakedByOperator: string;
};
}
export const TotalStakeRenderer = ({ data }: TotalStakeRendererProps) => {
const { t } = useTranslation();
return (
<Tooltip
description={
<>
<div>
{t('stakedByOperator')}: {data.stakedByOperator.toString()}
</div>
<div>
{t('stakedByDelegates')}: {data.stakedByDelegates.toString()}
</div>
<div>
{t('totalStake')}: <span className="font-bold">{data.stake}</span>
</div>
</>
}
>
<span>{data.stake}</span>
</Tooltip>
);
};
interface TotalPenaltiesRendererProps {
data: {
performanceScore: string;
performancePenalty: string;
overstakedAmount: string;
overstakedPenalty: string;
totalPenalties: string;
};
}
export const TotalPenaltiesRenderer = ({
data,
}: TotalPenaltiesRendererProps) => {
const { t } = useTranslation();
return (
<Tooltip
description={
<>
<div>
<span>
{t('performancePenalty')}: {data.performancePenalty}
</span>
<span className="pl-2">
({t('score')} {data.performanceScore})
</span>
</div>
<div>
<span>
{t('overstakedPenalty')}: {data.overstakedPenalty}
</span>
<span className="pl-2">
({t('overstaked')} {data.overstakedAmount})
</span>
</div>
<div>
{t('totalPenalties')}:{' '}
<span className="font-bold">{data.totalPenalties}</span>
</div>
</>
}
>
<span>{data.totalPenalties}</span>
</Tooltip>
);
};

View File

@ -4,14 +4,23 @@ import { useNavigate } from 'react-router-dom';
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
import { useAppState } from '../../../../contexts/app-state/app-state-context';
import { BigNumber } from '../../../../lib/bignumber';
import { rawValidatorScore } from '../../shared';
import {
getFormattedPerformanceScore,
getOverstakedAmount,
getOverstakingPenalty,
getPerformancePenalty,
getRawValidatorScore,
getTotalPenalties,
} from '../../shared';
import {
defaultColDef,
NODE_LIST_GRID_STYLES,
StakeNeededForPromotionRenderer,
stakedTotalPercentage,
totalPenalties,
ValidatorFields,
ValidatorRenderer,
TotalPenaltiesRenderer,
TotalStakeRenderer,
} from './shared';
import type { AgGridReact } from 'ag-grid-react';
import type { ColDef } from 'ag-grid-community';
@ -46,11 +55,20 @@ export const StandbyPendingValidatorsTable = ({
id,
name,
avatarUrl,
stakedByDelegates,
stakedByOperator,
stakedTotal,
rankingScore: { stakeScore, performanceScore },
pendingStake,
}) => {
let individualStakeNeededForPromotion = undefined;
const validatorScore = getRawValidatorScore(previousEpochData, id);
const overstakedAmount = getOverstakedAmount(
validatorScore,
stakedTotal,
totalStake
);
let individualStakeNeededForPromotion,
individualStakeNeededForPromotionDescription;
if (stakeNeededForPromotion) {
const stakedTotalBigNum = new BigNumber(stakedTotal);
@ -61,9 +79,23 @@ export const StandbyPendingValidatorsTable = ({
.dividedBy(performanceScoreBigNum)
.minus(stakedTotalBigNum);
individualStakeNeededForPromotion = calc.isGreaterThan(0)
? calc.toString()
: '0';
if (calc.isGreaterThan(0)) {
individualStakeNeededForPromotion = calc.toString();
individualStakeNeededForPromotionDescription = t(
stakeNeededForPromotionDescription,
{
prefix: formatNumber(calc, 2).toString(),
}
);
} else {
individualStakeNeededForPromotion = '0';
individualStakeNeededForPromotionDescription = t(
stakeNeededForPromotionDescription,
{
prefix: formatNumber(0, 2).toString(),
}
);
}
}
return {
@ -77,10 +109,29 @@ export const StandbyPendingValidatorsTable = ({
2
),
[ValidatorFields.STAKE_NEEDED_FOR_PROMOTION]:
individualStakeNeededForPromotion || t('n/a'),
individualStakeNeededForPromotion || null,
[ValidatorFields.STAKE_NEEDED_FOR_PROMOTION_DESCRIPTION]:
individualStakeNeededForPromotionDescription || t('n/a'),
[ValidatorFields.STAKE_SHARE]: stakedTotalPercentage(stakeScore),
[ValidatorFields.TOTAL_PENALTIES]: totalPenalties(
rawValidatorScore(previousEpochData, id),
[ValidatorFields.STAKED_BY_DELEGATES]: formatNumber(
toBigNum(stakedByDelegates, decimals),
2
),
[ValidatorFields.STAKED_BY_OPERATOR]: formatNumber(
toBigNum(stakedByOperator, decimals),
2
),
[ValidatorFields.PERFORMANCE_SCORE]:
getFormattedPerformanceScore(performanceScore).toString(),
[ValidatorFields.PERFORMANCE_PENALTY]:
getPerformancePenalty(performanceScore),
[ValidatorFields.OVERSTAKED_AMOUNT]: overstakedAmount.toString(),
[ValidatorFields.OVERSTAKING_PENALTY]: getOverstakingPenalty(
overstakedAmount,
totalStake
),
[ValidatorFields.TOTAL_PENALTIES]: getTotalPenalties(
getRawValidatorScore(previousEpochData, id),
performanceScore,
stakedTotal,
totalStake
@ -97,6 +148,7 @@ export const StandbyPendingValidatorsTable = ({
decimals,
previousEpochData,
stakeNeededForPromotion,
stakeNeededForPromotionDescription,
t,
totalStake,
]);
@ -116,12 +168,16 @@ export const StandbyPendingValidatorsTable = ({
field: ValidatorFields.STAKE,
headerName: t(ValidatorFields.STAKE).toString(),
headerTooltip: t('StakeDescription').toString(),
cellRenderer: TotalStakeRenderer,
width: 120,
},
{
field: ValidatorFields.STAKE_NEEDED_FOR_PROMOTION,
headerName: t(ValidatorFields.STAKE_NEEDED_FOR_PROMOTION).toString(),
headerTooltip: stakeNeededForPromotionDescription,
headerTooltip: t(stakeNeededForPromotionDescription, {
prefix: t('The'),
}),
cellRenderer: StakeNeededForPromotionRenderer,
width: 210,
sort: 'asc',
},
@ -135,6 +191,7 @@ export const StandbyPendingValidatorsTable = ({
field: ValidatorFields.TOTAL_PENALTIES,
headerName: t(ValidatorFields.TOTAL_PENALTIES).toString(),
headerTooltip: t('TotalPenaltiesDescription').toString(),
cellRenderer: TotalPenaltiesRenderer,
width: 120,
},
{

View File

@ -123,9 +123,7 @@ export const ValidatorTables = ({
previousEpochData={previousEpochData}
totalStake={totalStake}
stakeNeededForPromotion={stakeNeededForPromotion}
stakeNeededForPromotionDescription={t(
'StakeNeededForPromotionStandbyDescription'
)}
stakeNeededForPromotionDescription="StakeNeededForPromotionStandbyDescription"
/>
</>
)}
@ -154,9 +152,7 @@ export const ValidatorTables = ({
previousEpochData={previousEpochData}
totalStake={totalStake}
stakeNeededForPromotion={stakeNeededForPromotion}
stakeNeededForPromotionDescription={t(
'StakeNeededForPromotionCandidateDescription'
)}
stakeNeededForPromotionDescription="StakeNeededForPromotionCandidateDescription"
/>
</>
)}

View File

@ -10,8 +10,16 @@ import { formatNumber } from '../../../lib/format-number';
import { ExternalLinks, toBigNum } from '@vegaprotocol/react-helpers';
import { useAppState } from '../../../contexts/app-state/app-state-context';
import { Schema } from '@vegaprotocol/types';
import { totalPenalties } from '../home/validator-tables/shared';
import { normalisedVotingPower, rawValidatorScore } from '../shared';
import {
getFormattedPerformanceScore,
getNormalisedVotingPower,
getOverstakedAmount,
getOverstakingPenalty,
getPerformancePenalty,
getRawValidatorScore,
getTotalPenalties,
getUnnormalisedVotingPower,
} from '../shared';
import type { ReactNode } from 'react';
import type { StakingNodeFieldsFragment } from './__generated___/Staking';
import type { PreviousEpochQuery } from '../__generated___/PreviousEpoch';
@ -60,48 +68,26 @@ export const ValidatorTable = ({
const stakedOnNode = toBigNum(node.stakedTotal, decimals);
const location = countryData.find((c) => c.code === node.location)?.name;
const validatorScore = getRawValidatorScore(previousEpochData, node.id);
const performanceScore = new BigNumber(node.rankingScore.performanceScore).dp(
2
const overstakedAmount = getOverstakedAmount(
validatorScore,
stakedTotal,
node.stakedTotal
);
const validatorScore = rawValidatorScore(previousEpochData, node.id);
const overstakedAmount = useMemo(() => {
const amount = validatorScore
? new BigNumber(validatorScore).times(total).minus(stakedOnNode).dp(2)
: new BigNumber(0);
return amount.isNegative() ? new BigNumber(0) : amount;
}, [stakedOnNode, total, validatorScore]);
const stakePercentage =
total.isEqualTo(0) || stakedOnNode.isEqualTo(0)
? '-'
: stakedOnNode.dividedBy(total).times(100).dp(2).toString() + '%';
const performancePenalty =
new BigNumber(1).minus(performanceScore).times(100).toString() + '%';
const totalPenaltiesAmount = totalPenalties(
const totalPenaltiesAmount = getTotalPenalties(
validatorScore,
node.rankingScore.performanceScore,
stakedOnNode.toString(),
total.toString()
);
const overstakingPenalty = useMemo(
() =>
overstakedAmount.dividedBy(stakedOnNode).times(100).dp(2).toString() +
'%',
[overstakedAmount, stakedOnNode]
);
const unnormalisedVotingPower = validatorScore
? new BigNumber(validatorScore).times(100).dp(2).toString() + '%'
: null;
return (
<div className="mb-8" data-testid="validator-table">
<KeyValueTable data-testid="validator-table-profile" title={t('PROFILE')}>
@ -148,7 +134,8 @@ export const ValidatorTable = ({
<KeyValueTableRow>
<span>{t('SERVER LOCATION')}</span>
<ValidatorTableCell>
{location || t('not available')}
{countryData.find((c) => c.code === node.location)?.name ||
t('not available')}
</ValidatorTableCell>
</KeyValueTableRow>
<KeyValueTableRow>
@ -210,15 +197,23 @@ export const ValidatorTable = ({
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('OVERSTAKED PENALTY')}</span>
<span>{overstakingPenalty}</span>
<span>
{getOverstakingPenalty(overstakedAmount, node.stakedTotal)}
</span>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('PERFORMANCE SCORE')}</span>
<span>{performanceScore.toString()}</span>
<span>
{getFormattedPerformanceScore(
node.rankingScore.performanceScore
).toString()}
</span>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('PERFORMANCE PENALITY')}</span>
<span>{performancePenalty}</span>
<span>
{getPerformancePenalty(node.rankingScore.performanceScore)}
</span>
</KeyValueTableRow>
<KeyValueTableRow>
<span>
@ -236,7 +231,7 @@ export const ValidatorTable = ({
>
<KeyValueTableRow>
<span>{t('UNNORMALISED VOTING POWER')}</span>
<span>{unnormalisedVotingPower}</span>
<span>{getUnnormalisedVotingPower(validatorScore)}</span>
</KeyValueTableRow>
<KeyValueTableRow>
<span>
@ -244,7 +239,7 @@ export const ValidatorTable = ({
</span>
<span>
<strong>
{normalisedVotingPower(node.rankingScore.votingPower)}
{getNormalisedVotingPower(node.rankingScore.votingPower)}
</strong>
</span>
</KeyValueTableRow>

View File

@ -0,0 +1,122 @@
import { BigNumber } from '../../lib/bignumber';
import {
getRawValidatorScore,
getNormalisedVotingPower,
getUnnormalisedVotingPower,
getOverstakingPenalty,
getOverstakedAmount,
getFormattedPerformanceScore,
getPerformancePenalty,
getTotalPenalties,
} from './shared';
describe('getRawValidatorScore', () => {
const mockPreviousEpochData = {
epoch: {
id: '123',
validatorsConnection: {
edges: [
{
node: {
id: '0x123',
rewardScore: {
rawValidatorScore: '0.25',
},
},
},
{
node: {
id: '0x234',
rewardScore: {
rawValidatorScore: '0.35',
},
},
},
],
},
},
};
it('should return the rawValidatorScore for the given validator id', () => {
expect(getRawValidatorScore(mockPreviousEpochData, '0x123')).toEqual(
'0.25'
);
expect(getRawValidatorScore(mockPreviousEpochData, '0x234')).toEqual(
'0.35'
);
});
});
describe('getNormalisedVotingPower', () => {
it('should return the normalised voting power', () => {
expect(getNormalisedVotingPower('123')).toEqual('1.23%');
expect(getNormalisedVotingPower('789')).toEqual('7.89%');
});
});
describe('getUnnormalisedVotingPower', () => {
it('should return the unnormalised voting power', () => {
expect(getUnnormalisedVotingPower('0.25')).toEqual('25.00%');
expect(getUnnormalisedVotingPower('0.35')).toEqual('35.00%');
});
it('should return null if the validator score is null', () => {
expect(getUnnormalisedVotingPower(null)).toEqual(null);
});
});
describe('getOverstakingPenalty', () => {
it('should return the overstaking penalty', () => {
expect(
getOverstakingPenalty(new BigNumber(100), Number(1000).toString())
).toEqual('10.00%');
expect(
getOverstakingPenalty(new BigNumber(500), Number(2000).toString())
).toEqual('25.00%');
});
});
describe('getOverstakedAmount', () => {
it('should return the overstaked amount', () => {
expect(
getOverstakedAmount('0.21', Number(100).toString(), Number(20).toString())
).toEqual(new BigNumber(1));
expect(
getOverstakedAmount('0.22', Number(100).toString(), Number(20).toString())
).toEqual(new BigNumber(2));
expect(
getOverstakedAmount('0.30', Number(100).toString(), Number(20).toString())
).toEqual(new BigNumber(10));
});
it('should return 0 if the overstaked amount is negative', () => {
expect(
getOverstakedAmount('0.19', Number(100).toString(), Number(20).toString())
).toEqual(new BigNumber(0));
});
});
describe('getFormattedPerformanceScore', () => {
it('should return the formatted performance score', () => {
expect(getFormattedPerformanceScore('0.25')).toEqual(new BigNumber(0.25));
expect(getFormattedPerformanceScore('0.35')).toEqual(new BigNumber(0.35));
});
});
describe('getPerformancePenalty', () => {
it('should return the performance penalty', () => {
expect(getPerformancePenalty('0.25')).toEqual('75.00%');
expect(getPerformancePenalty('0.5')).toEqual('50.00%');
});
});
describe('getTotalPenalties', () => {
it('should return the total penalties', () => {
expect(getTotalPenalties('0.25', '1', '5000', '10000')).toEqual('50.00%');
expect(getTotalPenalties('0.25', '0.5', '5000', '10000')).toEqual('75.00%');
});
it('should return 0 if the total penalties is negative', () => {
expect(getTotalPenalties('0.25', '0.5', '1000', '10000')).toEqual('0.00%');
});
});

View File

@ -1,7 +1,11 @@
import { removePaginationWrapper, toBigNum } from '@vegaprotocol/react-helpers';
import {
formatNumberPercentage,
removePaginationWrapper,
} from '@vegaprotocol/react-helpers';
import type { PreviousEpochQuery } from './__generated___/PreviousEpoch';
import { BigNumber } from '../../lib/bignumber';
export const rawValidatorScore = (
export const getRawValidatorScore = (
previousEpochData: PreviousEpochQuery | undefined,
id: string
) => {
@ -13,6 +17,69 @@ export const rawValidatorScore = (
: null;
};
export const normalisedVotingPower = (votingPower: string) => {
return toBigNum(votingPower, 0).dividedBy(100).dp(2).toString() + '%';
export const getNormalisedVotingPower = (votingPower: string) =>
formatNumberPercentage(new BigNumber(votingPower).dividedBy(100), 2);
export const getUnnormalisedVotingPower = (
validatorScore: string | null | undefined
) =>
validatorScore
? formatNumberPercentage(new BigNumber(validatorScore).times(100), 2)
: null;
export const getFormattedPerformanceScore = (performanceScore: string) =>
new BigNumber(performanceScore).dp(2);
export const getPerformancePenalty = (performanceScore: string) =>
formatNumberPercentage(
new BigNumber(1)
.minus(getFormattedPerformanceScore(performanceScore))
.times(100),
2
);
export const getOverstakedAmount = (
validatorScore: string | null | undefined,
totalStake: string,
stakedOnNode: string
) => {
const amount = validatorScore
? new BigNumber(validatorScore)
.times(new BigNumber(totalStake))
.minus(new BigNumber(stakedOnNode))
.dp(2)
: new BigNumber(0);
return amount.isNegative() ? new BigNumber(0) : amount;
};
export const getOverstakingPenalty = (
overstakedAmount: BigNumber,
stakedOnNode: string
) =>
formatNumberPercentage(
overstakedAmount.dividedBy(new BigNumber(stakedOnNode)).times(100),
2
);
export const getTotalPenalties = (
rawValidatorScore: string | null | undefined,
performanceScore: string,
stakedOnNode: string,
totalStake: string
) => {
const calc = rawValidatorScore
? new BigNumber(1).minus(
new BigNumber(performanceScore)
.times(new BigNumber(rawValidatorScore))
.dividedBy(
new BigNumber(stakedOnNode).dividedBy(new BigNumber(totalStake))
)
)
: new BigNumber(0);
return formatNumberPercentage(
calc.isPositive() ? calc.times(100) : new BigNumber(0),
2
);
};

View File

@ -5,7 +5,6 @@ import {
Root,
Trigger,
Content,
Arrow,
Portal,
} from '@radix-ui/react-tooltip';
import type { ITooltipParams } from 'ag-grid-community';
@ -41,14 +40,9 @@ export const Tooltip = ({
align={align}
side={side}
alignOffset={8}
className="max-w-sm bg-neutral-200 px-4 py-2 z-20 rounded text-sm break-word"
className="max-w-sm border border-neutral-600 bg-neutral-100 dark:bg-neutral-800 px-4 py-2 z-20 rounded text-sm text-black dark:text-white break-word"
>
<div className="relative z-0">{description}</div>
<Arrow
width={10}
height={5}
className="fill-neutral-200 z-0 translate-x-[1px] translate-y-[-1px]"
/>
</Content>
</Portal>
)}
@ -60,7 +54,7 @@ export const Tooltip = ({
export const TooltipCellComponent = (props: ITooltipParams) => {
return (
<p className="max-w-sm border border-neutral-600 bg-neutral-200 dark:bg-neutral-800 px-4 py-2 z-20 rounded text-sm break-word text-black dark:text-white">
<p className="max-w-sm border border-neutral-600 bg-neutral-100 dark:bg-neutral-800 px-4 py-2 z-20 rounded text-sm break-word text-black dark:text-white">
{props.value}
</p>
);