diff --git a/apps/token/src/i18n/translations/dev.json b/apps/token/src/i18n/translations/dev.json
index bba3a170d..3af1c0d58 100644
--- a/apps/token/src/i18n/translations/dev.json
+++ b/apps/token/src/i18n/translations/dev.json
@@ -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"
}
diff --git a/apps/token/src/routes/staking/home/Nodes.graphql b/apps/token/src/routes/staking/home/Nodes.graphql
index d387cc0a9..46b80bf9a 100644
--- a/apps/token/src/routes/staking/home/Nodes.graphql
+++ b/apps/token/src/routes/staking/home/Nodes.graphql
@@ -3,6 +3,8 @@ fragment NodesFragment on Node {
id
name
pubkey
+ stakedByOperator
+ stakedByDelegates
stakedTotal
pendingStake
rankingScore {
diff --git a/apps/token/src/routes/staking/home/__generated___/Nodes.ts b/apps/token/src/routes/staking/home/__generated___/Nodes.ts
index 9e26f7fdb..537544df7 100644
--- a/apps/token/src/routes/staking/home/__generated___/Nodes.ts
+++ b/apps/token/src/routes/staking/home/__generated___/Nodes.ts
@@ -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 {
diff --git a/apps/token/src/routes/staking/home/validator-tables/consensus-validators-table.spec.tsx b/apps/token/src/routes/staking/home/validator-tables/consensus-validators-table.spec.tsx
index c255054af..bbb30043c 100644
--- a/apps/token/src/routes/staking/home/validator-tables/consensus-validators-table.spec.tsx
+++ b/apps/token/src/routes/staking/home/validator-tables/consensus-validators-table.spec.tsx
@@ -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',
diff --git a/apps/token/src/routes/staking/home/validator-tables/consensus-validators-table.tsx b/apps/token/src/routes/staking/home/validator-tables/consensus-validators-table.tsx
index 65b4581e2..8b6afd8dd 100644
--- a/apps/token/src/routes/staking/home/validator-tables/consensus-validators-table.tsx
+++ b/apps/token/src/routes/staking/home/validator-tables/consensus-validators-table.tsx
@@ -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,
},
{
diff --git a/apps/token/src/routes/staking/home/validator-tables/shared.spec.tsx b/apps/token/src/routes/staking/home/validator-tables/shared.spec.tsx
index d4685a391..8913beff9 100644
--- a/apps/token/src/routes/staking/home/validator-tables/shared.spec.tsx
+++ b/apps/token/src/routes/staking/home/validator-tables/shared.spec.tsx
@@ -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'
diff --git a/apps/token/src/routes/staking/home/validator-tables/shared.tsx b/apps/token/src/routes/staking/home/validator-tables/shared.tsx
index 5cbf1a8cb..7fc3298ca 100644
--- a/apps/token/src/routes/staking/home/validator-tables/shared.tsx
+++ b/apps/token/src/routes/staking/home/validator-tables/shared.tsx
@@ -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) => {
);
};
+
+interface StakeNeededForPromotionRendererProps {
+ data: {
+ stakeNeededForPromotion: string | undefined;
+ stakeNeededForPromotionDescription: string;
+ };
+}
+
+export const StakeNeededForPromotionRenderer = ({
+ data,
+}: StakeNeededForPromotionRendererProps) => {
+ return (
+
+
+ {data.stakeNeededForPromotion &&
+ formatNumber(data.stakeNeededForPromotion, 2)}
+
+
+ );
+};
+
+interface VotingPowerRendererProps {
+ data: {
+ normalisedVotingPower: string | undefined | null;
+ unnormalisedVotingPower: string | undefined | null;
+ };
+}
+
+export const VotingPowerRenderer = ({ data }: VotingPowerRendererProps) => {
+ const { t } = useTranslation();
+
+ return (
+
+
+ {t('unnormalisedVotingPower')}: {data.unnormalisedVotingPower}
+
+
+ {t('normalisedVotingPower')}: {data.normalisedVotingPower}
+
+ >
+ }
+ >
+ {data.normalisedVotingPower}
+
+ );
+};
+
+interface TotalStakeRendererProps {
+ data: {
+ stake: string;
+ stakedByDelegates: string;
+ stakedByOperator: string;
+ };
+}
+
+export const TotalStakeRenderer = ({ data }: TotalStakeRendererProps) => {
+ const { t } = useTranslation();
+
+ return (
+
+
+ {t('stakedByOperator')}: {data.stakedByOperator.toString()}
+
+
+ {t('stakedByDelegates')}: {data.stakedByDelegates.toString()}
+
+
+ {t('totalStake')}: {data.stake}
+
+ >
+ }
+ >
+ {data.stake}
+
+ );
+};
+
+interface TotalPenaltiesRendererProps {
+ data: {
+ performanceScore: string;
+ performancePenalty: string;
+ overstakedAmount: string;
+ overstakedPenalty: string;
+ totalPenalties: string;
+ };
+}
+
+export const TotalPenaltiesRenderer = ({
+ data,
+}: TotalPenaltiesRendererProps) => {
+ const { t } = useTranslation();
+
+ return (
+
+
+
+ {t('performancePenalty')}: {data.performancePenalty}
+
+
+ ({t('score')} {data.performanceScore})
+
+
+
+
+ {t('overstakedPenalty')}: {data.overstakedPenalty}
+
+
+ ({t('overstaked')} {data.overstakedAmount})
+
+
+
+ {t('totalPenalties')}:{' '}
+ {data.totalPenalties}
+
+ >
+ }
+ >
+ {data.totalPenalties}
+
+ );
+};
diff --git a/apps/token/src/routes/staking/home/validator-tables/standby-pending-validators-table.tsx b/apps/token/src/routes/staking/home/validator-tables/standby-pending-validators-table.tsx
index 16bea58a1..3ce9254db 100644
--- a/apps/token/src/routes/staking/home/validator-tables/standby-pending-validators-table.tsx
+++ b/apps/token/src/routes/staking/home/validator-tables/standby-pending-validators-table.tsx
@@ -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,
},
{
diff --git a/apps/token/src/routes/staking/home/validator-tables/validator-tables.tsx b/apps/token/src/routes/staking/home/validator-tables/validator-tables.tsx
index 98424b8c5..9b2c21ce3 100644
--- a/apps/token/src/routes/staking/home/validator-tables/validator-tables.tsx
+++ b/apps/token/src/routes/staking/home/validator-tables/validator-tables.tsx
@@ -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"
/>
>
)}
diff --git a/apps/token/src/routes/staking/node/validator-table.tsx b/apps/token/src/routes/staking/node/validator-table.tsx
index 2072b7a9d..22f95f7f3 100644
--- a/apps/token/src/routes/staking/node/validator-table.tsx
+++ b/apps/token/src/routes/staking/node/validator-table.tsx
@@ -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 (
@@ -148,7 +134,8 @@ export const ValidatorTable = ({
{t('SERVER LOCATION')}
- {location || t('not available')}
+ {countryData.find((c) => c.code === node.location)?.name ||
+ t('not available')}
@@ -210,15 +197,23 @@ export const ValidatorTable = ({
{t('OVERSTAKED PENALTY')}
- {overstakingPenalty}
+
+ {getOverstakingPenalty(overstakedAmount, node.stakedTotal)}
+
{t('PERFORMANCE SCORE')}
- {performanceScore.toString()}
+
+ {getFormattedPerformanceScore(
+ node.rankingScore.performanceScore
+ ).toString()}
+
{t('PERFORMANCE PENALITY')}
- {performancePenalty}
+
+ {getPerformancePenalty(node.rankingScore.performanceScore)}
+
@@ -236,7 +231,7 @@ export const ValidatorTable = ({
>
{t('UNNORMALISED VOTING POWER')}
- {unnormalisedVotingPower}
+ {getUnnormalisedVotingPower(validatorScore)}
@@ -244,7 +239,7 @@ export const ValidatorTable = ({
- {normalisedVotingPower(node.rankingScore.votingPower)}
+ {getNormalisedVotingPower(node.rankingScore.votingPower)}
diff --git a/apps/token/src/routes/staking/shared.spec.ts b/apps/token/src/routes/staking/shared.spec.ts
new file mode 100644
index 000000000..7e62fde9e
--- /dev/null
+++ b/apps/token/src/routes/staking/shared.spec.ts
@@ -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%');
+ });
+});
diff --git a/apps/token/src/routes/staking/shared.ts b/apps/token/src/routes/staking/shared.ts
index 3794ce064..86f53f3f6 100644
--- a/apps/token/src/routes/staking/shared.ts
+++ b/apps/token/src/routes/staking/shared.ts
@@ -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
+ );
};
diff --git a/libs/ui-toolkit/src/components/tooltip/tooltip.tsx b/libs/ui-toolkit/src/components/tooltip/tooltip.tsx
index 2206be27b..990429210 100644
--- a/libs/ui-toolkit/src/components/tooltip/tooltip.tsx
+++ b/libs/ui-toolkit/src/components/tooltip/tooltip.tsx
@@ -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"
>
{description}
-
)}
@@ -60,7 +54,7 @@ export const Tooltip = ({
export const TooltipCellComponent = (props: ITooltipParams) => {
return (
-
+
{props.value}
);