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:
parent
cf2cfad746
commit
a30f2227c4
@ -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';
|
||||||
|
@ -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",
|
||||||
|
@ -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 = () => {
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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%');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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 *
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
18
apps/token/src/routes/staking/shared.ts
Normal file
18
apps/token/src/routes/staking/shared.ts
Normal 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() + '%';
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user