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:
parent
e4afa67cd0
commit
86e4280680
@ -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"
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ fragment NodesFragment on Node {
|
||||
id
|
||||
name
|
||||
pubkey
|
||||
stakedByOperator
|
||||
stakedByDelegates
|
||||
stakedTotal
|
||||
pendingStake
|
||||
rankingScore {
|
||||
|
@ -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 {
|
||||
|
@ -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',
|
||||
|
@ -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,
|
||||
},
|
||||
{
|
||||
|
@ -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'
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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,
|
||||
},
|
||||
{
|
||||
|
@ -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"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
@ -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>
|
||||
|
122
apps/token/src/routes/staking/shared.spec.ts
Normal file
122
apps/token/src/routes/staking/shared.spec.ts
Normal 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%');
|
||||
});
|
||||
});
|
@ -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
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user