feat(2025): validator table broken into sub-tables (#2290)

* feat(2025): validator table broken into sub-tables and showing all the new data

* feat(2025): fixed test broken due to a refactor

* feat(2025): removed unused redundancy in shared.tsx

* feat(2025): tweak to types import location in consensus-validators-table.spec.tsx

* feat(2025): removed tests for deleted validator table elements
This commit is contained in:
Sam Keen 2022-12-01 11:39:15 +00:00 committed by GitHub
parent cf2cfad746
commit a30f2227c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 316 additions and 167 deletions

View File

@ -12,8 +12,6 @@ const stakedByDelegates = '[data-testid="staked-by-delegates"]';
const stakeShare = '[data-testid="stake-percentage"]'; const stakeShare = '[data-testid="stake-percentage"]';
const epochCountDown = '[data-testid="epoch-countdown"]'; const epochCountDown = '[data-testid="epoch-countdown"]';
const stakeNumberRegex = /^\d*\.?\d*$/; const stakeNumberRegex = /^\d*\.?\d*$/;
const ownStake = '[data-testid="own-stake"]';
const nominatedStake = '[data-testid="nominated-stake"]';
context('Staking Page - verify elements on page', function () { context('Staking Page - verify elements on page', function () {
before('navigate to staking page', function () { before('navigate to staking page', function () {
@ -173,16 +171,6 @@ context('Staking Page - verify elements on page', function () {
}); });
}); });
// 2001-STKE-013
it('Should be able to see own stake this epoch', function () {
cy.get(ownStake).invoke('text').should('match', stakeNumberRegex);
});
// 2001-STKE-014
it('Should be able to see nominated stake this epoch', function () {
cy.get(nominatedStake).invoke('text').should('match', stakeNumberRegex);
});
// 2001-STKE-011 2002-SINC-001 2002-SINC-002 // 2001-STKE-011 2002-SINC-001 2002-SINC-002
it('Should be able to see epoch information', function () { it('Should be able to see epoch information', function () {
const epochTitle = 'h3'; const epochTitle = 'h3';

View File

@ -537,7 +537,8 @@
"unknownEthereumConnectionError": "An unknown error occurred. Check the console in your browser's web developer tools for more details", "unknownEthereumConnectionError": "An unknown error occurred. Check the console in your browser's web developer tools for more details",
"stakingDescriptionTitle": "How does staking on Vega work?", "stakingDescriptionTitle": "How does staking on Vega work?",
"stakingDescription1": "1. VEGA is an ERC20 token. Associate it with a Vega wallet using the", "stakingDescription1": "1. VEGA is an ERC20 token. Associate it with a Vega wallet using the",
"stakingDescription2": "2. Use this site and your Vega wallet to nominate a validator. View the validator profile pitches and discussion", "stakingDescription2a": "2. Use this site and your Vega wallet to nominate a validator.",
"stakingDescription2b": "View the validator profile pitches and discussion",
"stakingDescription3": "3. Earn a share of trading fees and treasury rewards for each full epoch staked", "stakingDescription3": "3. Earn a share of trading fees and treasury rewards for each full epoch staked",
"stakingDescription4": "4. Move your stake if your validator is penalised", "stakingDescription4": "4. Move your stake if your validator is penalised",
"stakingBridge": "staking bridge", "stakingBridge": "staking bridge",

View File

@ -2,7 +2,7 @@ import { useEffect } from 'react';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { EpochCountdown } from '../../../components/epoch-countdown'; import { EpochCountdown } from '../../../components/epoch-countdown';
import { useNodesQuery } from './__generated___/Nodes'; import { useNodesQuery } from './__generated___/Nodes';
import { usePreviousEpochQuery } from './__generated___/PreviousEpoch'; import { usePreviousEpochQuery } from '../__generated___/PreviousEpoch';
import { ValidatorTables } from './validator-tables'; import { ValidatorTables } from './validator-tables';
export const EpochData = () => { export const EpochData = () => {

View File

@ -32,7 +32,7 @@ export const StakingIntro = () => {
</Link> </Link>
</li> </li>
<li> <li>
{t('stakingDescription2')}{' '} {t('stakingDescription2a')} {t('stakingDescription2b')}{' '}
<UTLink <UTLink
href={ExternalLinks.VALIDATOR_FORUM} href={ExternalLinks.VALIDATOR_FORUM}
target="_blank" target="_blank"

View File

@ -4,13 +4,13 @@ import { ConsensusValidatorsTable } from './consensus-validators-table';
import { MockedProvider } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import { NodesDocument } from '../__generated___/Nodes'; import { NodesDocument } from '../__generated___/Nodes';
import { PreviousEpochDocument } from '../__generated___/PreviousEpoch'; import { PreviousEpochDocument } from '../../__generated___/PreviousEpoch';
import { Schema } from '@vegaprotocol/types'; import { Schema } from '@vegaprotocol/types';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider'; import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import type { MockedResponse } from '@apollo/client/testing'; import type { MockedResponse } from '@apollo/client/testing';
import type { PartialDeep } from 'type-fest'; import type { PartialDeep } from 'type-fest';
import type { NodesFragmentFragment } from '../__generated___/Nodes'; import type { NodesFragmentFragment } from '../__generated___/Nodes';
import type { PreviousEpochQuery } from '../__generated___/PreviousEpoch'; import type { PreviousEpochQuery } from '../../__generated___/PreviousEpoch';
const nodeFactory = ( const nodeFactory = (
overrides?: PartialDeep<NodesFragmentFragment> overrides?: PartialDeep<NodesFragmentFragment>

View File

@ -3,6 +3,8 @@ import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { AgGridDynamic as AgGrid, Button } from '@vegaprotocol/ui-toolkit'; import { AgGridDynamic as AgGrid, Button } from '@vegaprotocol/ui-toolkit';
import { useAppState } from '../../../../contexts/app-state/app-state-context'; import { useAppState } from '../../../../contexts/app-state/app-state-context';
import { BigNumber } from '../../../../lib/bignumber';
import { normalisedVotingPower, rawValidatorScore } from '../../shared';
import { import {
defaultColDef, defaultColDef,
NODE_LIST_GRID_STYLES, NODE_LIST_GRID_STYLES,
@ -47,8 +49,8 @@ export const ConsensusValidatorsTable = ({
const canonisedNodes = data const canonisedNodes = data
.sort((a, b) => { .sort((a, b) => {
const aVotingPower = toBigNum(a.rankingScore.votingPower, 0); const aVotingPower = new BigNumber(a.rankingScore.votingPower);
const bVotingPower = toBigNum(b.rankingScore.votingPower, 0); const bVotingPower = new BigNumber(b.rankingScore.votingPower);
return bVotingPower.minus(aVotingPower).toNumber(); return bVotingPower.minus(aVotingPower).toNumber();
}) })
.map((node, index) => { .map((node, index) => {
@ -69,9 +71,6 @@ export const ConsensusValidatorsTable = ({
pendingStake, pendingStake,
votingPowerRanking, votingPowerRanking,
}) => { }) => {
const normalisedVotingPower =
toBigNum(votingPower, 0).dividedBy(100).dp(2).toString() + '%';
return { return {
id, id,
[ValidatorFields.RANKING_INDEX]: votingPowerRanking, [ValidatorFields.RANKING_INDEX]: votingPowerRanking,
@ -83,11 +82,11 @@ export const ConsensusValidatorsTable = ({
toBigNum(stakedTotal, decimals), toBigNum(stakedTotal, decimals),
2 2
), ),
[ValidatorFields.NORMALISED_VOTING_POWER]: normalisedVotingPower, [ValidatorFields.NORMALISED_VOTING_POWER]:
normalisedVotingPower(votingPower),
[ValidatorFields.STAKE_SHARE]: stakedTotalPercentage(stakeScore), [ValidatorFields.STAKE_SHARE]: stakedTotalPercentage(stakeScore),
[ValidatorFields.TOTAL_PENALTIES]: totalPenalties( [ValidatorFields.TOTAL_PENALTIES]: totalPenalties(
previousEpochData, rawValidatorScore(previousEpochData, id),
id,
performanceScore, performanceScore,
stakedTotal, stakedTotal,
totalStake totalStake

View File

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

View File

@ -1,9 +1,9 @@
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { removePaginationWrapper, toBigNum } from '@vegaprotocol/react-helpers'; import { useTranslation } from 'react-i18next';
import { toBigNum } from '@vegaprotocol/react-helpers';
import { Button } from '@vegaprotocol/ui-toolkit'; import { Button } from '@vegaprotocol/ui-toolkit';
import type { NodesFragmentFragment } from '../__generated___/Nodes'; import type { NodesFragmentFragment } from '../__generated___/Nodes';
import type { PreviousEpochQuery } from '../__generated___/PreviousEpoch'; import type { PreviousEpochQuery } from '../../__generated___/PreviousEpoch';
import { useTranslation } from 'react-i18next';
export enum ValidatorFields { export enum ValidatorFields {
RANKING_INDEX = 'rankingIndex', RANKING_INDEX = 'rankingIndex',
@ -37,19 +37,11 @@ export const stakedTotalPercentage = (stakeScore: string) =>
toBigNum(stakeScore, 0).times(100).dp(2).toString() + '%'; toBigNum(stakeScore, 0).times(100).dp(2).toString() + '%';
export const totalPenalties = ( export const totalPenalties = (
previousEpochData: PreviousEpochQuery | undefined, rawValidatorScore: string | null | undefined,
id: string,
performanceScore: string, performanceScore: string,
stakedTotal: string, stakedTotal: string,
totalStake: string totalStake: string
) => { ) => {
const rawValidatorScore = previousEpochData
? removePaginationWrapper(
previousEpochData.epoch?.validatorsConnection?.edges
).find((validator) => validator?.id === id)?.rewardScore
?.rawValidatorScore
: null;
const totalPenaltiesCalc = const totalPenaltiesCalc =
rawValidatorScore !== null rawValidatorScore !== null
? 100 * ? 100 *

View File

@ -3,6 +3,8 @@ import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit'; import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
import { useAppState } from '../../../../contexts/app-state/app-state-context'; import { useAppState } from '../../../../contexts/app-state/app-state-context';
import { BigNumber } from '../../../../lib/bignumber';
import { rawValidatorScore } from '../../shared';
import { import {
defaultColDef, defaultColDef,
NODE_LIST_GRID_STYLES, NODE_LIST_GRID_STYLES,
@ -49,9 +51,9 @@ export const StandbyPendingValidatorsTable = ({
let individualStakeNeededForPromotion = undefined; let individualStakeNeededForPromotion = undefined;
if (stakeNeededForPromotion) { if (stakeNeededForPromotion) {
const stakedTotalBigNum = toBigNum(stakedTotal, 0); const stakedTotalBigNum = new BigNumber(stakedTotal);
const stakeNeededBigNum = toBigNum(stakeNeededForPromotion, 0); const stakeNeededBigNum = new BigNumber(stakeNeededForPromotion);
const performanceScoreBigNum = toBigNum(performanceScore, 0); const performanceScoreBigNum = new BigNumber(performanceScore);
const calc = stakeNeededBigNum const calc = stakeNeededBigNum
.dividedBy(performanceScoreBigNum) .dividedBy(performanceScoreBigNum)
@ -76,8 +78,7 @@ export const StandbyPendingValidatorsTable = ({
individualStakeNeededForPromotion || t('n/a'), individualStakeNeededForPromotion || t('n/a'),
[ValidatorFields.STAKE_SHARE]: stakedTotalPercentage(stakeScore), [ValidatorFields.STAKE_SHARE]: stakedTotalPercentage(stakeScore),
[ValidatorFields.TOTAL_PENALTIES]: totalPenalties( [ValidatorFields.TOTAL_PENALTIES]: totalPenalties(
previousEpochData, rawValidatorScore(previousEpochData, id),
id,
performanceScore, performanceScore,
stakedTotal, stakedTotal,
totalStake totalStake

View File

@ -7,7 +7,7 @@ import type {
NodesQuery, NodesQuery,
NodesFragmentFragment, NodesFragmentFragment,
} from '../__generated___/Nodes'; } from '../__generated___/Nodes';
import type { PreviousEpochQuery } from '../__generated___/PreviousEpoch'; import type { PreviousEpochQuery } from '../../__generated___/PreviousEpoch';
import { formatNumber } from '../../../../lib/format-number'; import { formatNumber } from '../../../../lib/format-number';
import { import {
createDocsLinks, createDocsLinks,

View File

@ -2,6 +2,12 @@ import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useVegaWallet } from '@vegaprotocol/wallet';
import {
addDecimal,
removePaginationWrapper,
toBigNum,
} from '@vegaprotocol/react-helpers';
import { EpochCountdown } from '../../../components/epoch-countdown'; import { EpochCountdown } from '../../../components/epoch-countdown';
import { BigNumber } from '../../../lib/bignumber'; import { BigNumber } from '../../../lib/bignumber';
import { ConnectToVega } from '../../../components/connect-to-vega'; import { ConnectToVega } from '../../../components/connect-to-vega';
@ -9,41 +15,39 @@ import { StakingForm } from './staking-form';
import { ValidatorTable } from './validator-table'; import { ValidatorTable } from './validator-table';
import { YourStake } from './your-stake'; import { YourStake } from './your-stake';
import NodeContainer from './nodes-container'; import NodeContainer from './nodes-container';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useAppState } from '../../../contexts/app-state/app-state-context'; import { useAppState } from '../../../contexts/app-state/app-state-context';
import {
addDecimal,
removePaginationWrapper,
toBigNum,
} from '@vegaprotocol/react-helpers';
import type { StakingQuery } from './__generated___/Staking'; import type { StakingQuery } from './__generated___/Staking';
import type { PreviousEpochQuery } from '../__generated___/PreviousEpoch';
interface StakingNodeProps { interface StakingNodeProps {
data?: StakingQuery; data?: StakingQuery;
previousEpochData?: PreviousEpochQuery;
} }
export const StakingNode = ({ data }: StakingNodeProps) => { export const StakingNode = ({ data, previousEpochData }: StakingNodeProps) => {
const { pubKey: vegaKey } = useVegaWallet(); const { pubKey: vegaKey } = useVegaWallet();
const { const {
appState: { decimals }, appState: { decimals },
} = useAppState(); } = useAppState();
const { node } = useParams<{ node: string }>(); const { node } = useParams<{ node: string }>();
const { t } = useTranslation(); const { t } = useTranslation();
const nodeInfo = React.useMemo(
() => const { nodeInfo, currentEpoch, delegations } = React.useMemo(
removePaginationWrapper(data?.nodesConnection?.edges).find( () => ({
nodeInfo: removePaginationWrapper(data?.nodesConnection?.edges).find(
({ id }) => id === node ({ id }) => id === node
), ),
[node, data] currentEpoch: data?.epoch.id,
); delegations: removePaginationWrapper(
data?.party?.delegationsConnection?.edges
const currentEpoch = React.useMemo(() => { ),
return data?.epoch.id; }),
}, [data?.epoch.id]); [
data?.epoch.id,
const delegations = React.useMemo( data?.nodesConnection?.edges,
() => removePaginationWrapper(data?.party?.delegationsConnection?.edges), data?.party?.delegationsConnection?.edges,
[data?.party?.delegationsConnection?.edges] node,
]
); );
const stakeThisEpoch = React.useMemo(() => { const stakeThisEpoch = React.useMemo(() => {
@ -105,7 +109,7 @@ export const StakingNode = ({ data }: StakingNodeProps) => {
<ValidatorTable <ValidatorTable
node={nodeInfo} node={nodeInfo}
stakedTotal={addDecimal(data?.nodeData?.stakedTotal || '0', decimals)} stakedTotal={addDecimal(data?.nodeData?.stakedTotal || '0', decimals)}
stakeThisEpoch={stakeThisEpoch} previousEpochData={previousEpochData}
/> />
</section> </section>
{data?.epoch.timestamps.start && data?.epoch.timestamps.expiry && ( {data?.epoch.timestamps.start && data?.epoch.timestamps.expiry && (
@ -148,6 +152,10 @@ export const StakingNode = ({ data }: StakingNodeProps) => {
export const Node = () => { export const Node = () => {
return ( return (
<NodeContainer>{({ data }) => <StakingNode data={data} />}</NodeContainer> <NodeContainer>
{({ data, previousEpochData }) => (
<StakingNode data={data} previousEpochData={previousEpochData} />
)}
</NodeContainer>
); );
}; };

View File

@ -5,7 +5,9 @@ import { useTranslation } from 'react-i18next';
import { useStakingQuery } from './__generated___/Staking'; import { useStakingQuery } from './__generated___/Staking';
import { SplashLoader } from '../../../components/splash-loader'; import { SplashLoader } from '../../../components/splash-loader';
import { usePreviousEpochQuery } from '../__generated___/PreviousEpoch';
import type { StakingQuery } from './__generated___/Staking'; import type { StakingQuery } from './__generated___/Staking';
import type { PreviousEpochQuery } from '../__generated___/PreviousEpoch';
// TODO should only request a single node. When migrating from deprecated APIs we should address this. // TODO should only request a single node. When migrating from deprecated APIs we should address this.
@ -14,14 +16,28 @@ const RPC_ERROR = 'rpc error: code = NotFound desc = NotFound error';
export const NodeContainer = ({ export const NodeContainer = ({
children, children,
}: { }: {
children: ({ data }: { data?: StakingQuery }) => React.ReactElement; children: ({
data,
previousEpochData,
}: {
data?: StakingQuery;
previousEpochData?: PreviousEpochQuery;
}) => React.ReactElement;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { pubKey } = useVegaWallet(); const { pubKey } = useVegaWallet();
const { data, loading, error, refetch } = useStakingQuery({ const { data, loading, error, refetch } = useStakingQuery({
variables: { partyId: pubKey || '' }, variables: { partyId: pubKey || '' },
}); });
const { data: previousEpochData } = usePreviousEpochQuery({
variables: {
epochId: (Number(data?.epoch.id) - 1).toString(),
},
skip: !data?.epoch.id,
});
// @todo - this epoch querying needs to be hoisted as the validator tables rely
// on it too
React.useEffect(() => { React.useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
if (!data?.epoch.timestamps.expiry) return; if (!data?.epoch.timestamps.expiry) return;
@ -59,7 +75,7 @@ export const NodeContainer = ({
); );
} }
return children({ data }); return children({ data, previousEpochData });
}; };
export default NodeContainer; export default NodeContainer;

View File

@ -1,15 +1,20 @@
import React from 'react'; import { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Link } from '@vegaprotocol/ui-toolkit'; import countryData from '../../../components/country-selector/country-data';
import { Link as UTLink, Link } from '@vegaprotocol/ui-toolkit';
import { useEnvironment } from '@vegaprotocol/environment'; import { useEnvironment } from '@vegaprotocol/environment';
import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit'; import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit';
import { BigNumber } from '../../../lib/bignumber'; import { BigNumber } from '../../../lib/bignumber';
import { formatNumber } from '../../../lib/format-number'; import { formatNumber } from '../../../lib/format-number';
import type { StakingNodeFieldsFragment } from './__generated___/Staking'; import { ExternalLinks, toBigNum } from '@vegaprotocol/react-helpers';
import { toBigNum } from '@vegaprotocol/react-helpers';
import { useAppState } from '../../../contexts/app-state/app-state-context'; import { useAppState } from '../../../contexts/app-state/app-state-context';
import { Schema } from '@vegaprotocol/types'; import { Schema } from '@vegaprotocol/types';
import { totalPenalties } from '../home/validator-tables/shared';
import { normalisedVotingPower, rawValidatorScore } from '../shared';
import type { ReactNode } from 'react';
import type { StakingNodeFieldsFragment } from './__generated___/Staking';
import type { PreviousEpochQuery } from '../__generated___/PreviousEpoch';
const statuses = { const statuses = {
[Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_ERSATZ]: 'status-ersatz', [Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_ERSATZ]: 'status-ersatz',
@ -25,10 +30,10 @@ const ValidatorTableCell = ({
children, children,
dataTestId, dataTestId,
}: { }: {
children: React.ReactNode; children: ReactNode;
dataTestId?: string; dataTestId?: string;
}) => ( }) => (
<span datatest-id={dataTestId} className="break-words"> <span data-testid={dataTestId} className="break-words">
{children} {children}
</span> </span>
); );
@ -36,109 +41,214 @@ const ValidatorTableCell = ({
export interface ValidatorTableProps { export interface ValidatorTableProps {
node: StakingNodeFieldsFragment; node: StakingNodeFieldsFragment;
stakedTotal: string; stakedTotal: string;
stakeThisEpoch: BigNumber; stakedTotalAllNodes?: string;
previousEpochData?: PreviousEpochQuery;
} }
export const ValidatorTable = ({ export const ValidatorTable = ({
node, node,
stakedTotal, stakedTotal,
stakeThisEpoch, previousEpochData,
}: ValidatorTableProps) => { }: ValidatorTableProps) => {
const { ETHERSCAN_URL } = useEnvironment(); const { ETHERSCAN_URL } = useEnvironment();
const { t } = useTranslation(); const { t } = useTranslation();
const { const {
appState: { decimals }, appState: { decimals },
} = useAppState(); } = useAppState();
const stakePercentage = React.useMemo(() => {
const total = new BigNumber(stakedTotal); const total = useMemo(() => new BigNumber(stakedTotal), [stakedTotal]);
const stakedOnNode = toBigNum(node.stakedTotal, decimals);
const stakedTotalPercentage = const stakedOnNode = toBigNum(node.stakedTotal, decimals);
total.isEqualTo(0) || stakedOnNode.isEqualTo(0)
? '-' const location = countryData.find((c) => c.code === node.location)?.name;
: stakedOnNode.dividedBy(total).times(100).dp(2).toString() + '%';
return stakedTotalPercentage; const performanceScore = new BigNumber(node.rankingScore.performanceScore).dp(
}, [decimals, node.stakedTotal, stakedTotal]); 2
);
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(
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 ( return (
<KeyValueTable data-testid="validator-table"> <div className="mb-8" data-testid="validator-table">
<KeyValueTableRow> <KeyValueTable data-testid="validator-table-profile" title={t('PROFILE')}>
<span>{t('id')}:</span> <KeyValueTableRow>
<ValidatorTableCell dataTestId="validator-id"> <span>{t('id')}</span>
{node.id} <ValidatorTableCell dataTestId="validator-id">
</ValidatorTableCell> {node.id}
</KeyValueTableRow> </ValidatorTableCell>
<KeyValueTableRow> </KeyValueTableRow>
<span>{t('VEGA ADDRESS / PUBLIC KEY')}</span> <KeyValueTableRow>
<ValidatorTableCell dataTestId="validator-public-key"> <span>{t('ABOUT THIS VALIDATOR')}</span>
{node.pubkey} <span>
</ValidatorTableCell> <a href={node.infoUrl}>{node.infoUrl}</a>
</KeyValueTableRow> </span>
<KeyValueTableRow> </KeyValueTableRow>
<span>{t('ABOUT THIS VALIDATOR')}</span> <KeyValueTableRow>
<span> <span>
<a href={node.infoUrl}>{node.infoUrl}</a> <strong>{t('STATUS')}</strong>
</span> </span>
</KeyValueTableRow> <span data-testid="validator-status">
<KeyValueTableRow> <strong>{t(statusTranslationKey(node.rankingScore.status))}</strong>
<span>{t('STATUS')}</span> </span>
<span data-testid="validator-status"> </KeyValueTableRow>
{t(statusTranslationKey(node.rankingScore.status))} </KeyValueTable>
</span>
</KeyValueTableRow> <div className="mb-6 text-sm">
<KeyValueTableRow> {t('stakingDescription2b')}{' '}
<span>{t('IP ADDRESS')}</span> <UTLink
<span>{node.location}</span> href={ExternalLinks.VALIDATOR_FORUM}
</KeyValueTableRow> target="_blank"
<KeyValueTableRow> data-testid="validator-forum-link"
<span>{t('ETHEREUM ADDRESS')}</span> >
<span> {t('onTheForum')}
<Link </UTLink>
title={t('View on Etherscan (opens in a new tab)')} </div>
href={`${ETHERSCAN_URL}/address/${node.ethereumAddress}`}
target="_blank" <KeyValueTable data-testid="validator-table-address" title={t('ADDRESS')}>
> <KeyValueTableRow>
{node.ethereumAddress} <span>{t('VEGA ADDRESS / PUBLIC KEY')}</span>
</Link> <ValidatorTableCell dataTestId="validator-public-key">
</span> {node.pubkey}
</KeyValueTableRow> </ValidatorTableCell>
<KeyValueTableRow> </KeyValueTableRow>
<span>{t('TOTAL STAKE')}</span> <KeyValueTableRow>
<span data-testid="total-stake"> <span>{t('SERVER LOCATION')}</span>
{formatNumber(toBigNum(node.stakedTotal, decimals))} <ValidatorTableCell>
</span> {location || t('not available')}
</KeyValueTableRow> </ValidatorTableCell>
<KeyValueTableRow> </KeyValueTableRow>
<span>{t('PENDING STAKE')}</span> <KeyValueTableRow>
<span data-testid="pending-stake"> <span>{t('ETHEREUM ADDRESS')}</span>
{formatNumber(toBigNum(node.pendingStake, decimals))} <span>
</span> <Link
</KeyValueTableRow> title={t('View on Etherscan (opens in a new tab)')}
<KeyValueTableRow> href={`${ETHERSCAN_URL}/address/${node.ethereumAddress}`}
<span>{t('STAKED BY OPERATOR')}</span> target="_blank"
<span data-testid="staked-by-operator"> >
{formatNumber(toBigNum(node.stakedByOperator, decimals))} {node.ethereumAddress}
</span> </Link>
</KeyValueTableRow> </span>
<KeyValueTableRow> </KeyValueTableRow>
<span>{t('STAKED BY DELEGATES')}</span> </KeyValueTable>
<span data-testid="staked-by-delegates">
{formatNumber(toBigNum(node.stakedByDelegates, 18))} <KeyValueTable data-testid="validator-table-stake" title={t('STAKE')}>
</span> <KeyValueTableRow>
</KeyValueTableRow> <span>{t('STAKED BY OPERATOR')}</span>
<KeyValueTableRow> <span data-testid="staked-by-operator">
<span>{t('STAKE SHARE')}</span> {formatNumber(toBigNum(node.stakedByOperator, decimals))}
<span data-testid="stake-percentage">{stakePercentage}</span> </span>
</KeyValueTableRow> </KeyValueTableRow>
<KeyValueTableRow> <KeyValueTableRow>
<span>{t('OWN STAKE (THIS EPOCH)')}</span> <span>{t('STAKED BY DELEGATES')}</span>
<span data-testid="own-stake">{formatNumber(stakeThisEpoch)}</span> <span data-testid="staked-by-delegates">
</KeyValueTableRow> {formatNumber(toBigNum(node.stakedByDelegates, decimals))}
<KeyValueTableRow> </span>
<span>{t('NOMINATED (THIS EPOCH)')}</span> </KeyValueTableRow>
<span data-testid="nominated-stake"> <KeyValueTableRow>
{formatNumber(toBigNum(node.stakedByDelegates, decimals))} <span>
</span> <strong>{t('TOTAL STAKE')}</strong>
</KeyValueTableRow> </span>
</KeyValueTable> <span data-testid="total-stake">
<strong>
{formatNumber(toBigNum(node.stakedTotal, decimals))}
</strong>
</span>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('PENDING STAKE')}</span>
<span data-testid="pending-stake">
{formatNumber(toBigNum(node.pendingStake, decimals))}
</span>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('STAKE SHARE')}</span>
<span data-testid="stake-percentage">{stakePercentage}</span>
</KeyValueTableRow>
</KeyValueTable>
<KeyValueTable
data-testid="validator-table-penalties"
title={t('PENALTIES')}
>
<KeyValueTableRow>
<span>{t('OVERSTAKED AMOUNT')}</span>
<span>{overstakedAmount.toString()}</span>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('OVERSTAKED PENALTY')}</span>
<span>{overstakingPenalty}</span>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('PERFORMANCE SCORE')}</span>
<span>{performanceScore.toString()}</span>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('PERFORMANCE PENALITY')}</span>
<span>{performancePenalty}</span>
</KeyValueTableRow>
<KeyValueTableRow>
<span>
<strong>{t('TOTAL PENALTIES')}</strong>
</span>
<span>
<strong>{totalPenaltiesAmount}</strong>
</span>
</KeyValueTableRow>
</KeyValueTable>
<KeyValueTable
data-testid="validator-table-voting-power"
title={t('VOTING POWER')}
>
<KeyValueTableRow>
<span>{t('UNNORMALISED VOTING POWER')}</span>
<span>{unnormalisedVotingPower}</span>
</KeyValueTableRow>
<KeyValueTableRow>
<span>
<strong>{t('NORMALISED VOTING POWER')}</strong>
</span>
<span>
<strong>
{normalisedVotingPower(node.rankingScore.votingPower)}
</strong>
</span>
</KeyValueTableRow>
</KeyValueTable>
</div>
); );
}; };

View File

@ -0,0 +1,18 @@
import { removePaginationWrapper, toBigNum } from '@vegaprotocol/react-helpers';
import type { PreviousEpochQuery } from './__generated___/PreviousEpoch';
export const rawValidatorScore = (
previousEpochData: PreviousEpochQuery | undefined,
id: string
) => {
return previousEpochData
? removePaginationWrapper(
previousEpochData.epoch?.validatorsConnection?.edges
).find((validator) => validator?.id === id)?.rewardScore
?.rawValidatorScore
: null;
};
export const normalisedVotingPower = (votingPower: string) => {
return toBigNum(votingPower, 0).dividedBy(100).dp(2).toString() + '%';
};