feat(governance): tooltips for the validator details page (#2960)
This commit is contained in:
parent
abf69786ba
commit
0033f3c5f5
@ -201,12 +201,21 @@ context('Staking Page - verify elements on page', function () {
|
||||
cy.get(stakeShare)
|
||||
.invoke('text')
|
||||
.then(($stakePercentage) => {
|
||||
if ($stakePercentage != '-') {
|
||||
cy.wrap($stakePercentage).should(
|
||||
'match',
|
||||
/\b(?<!\.)(?!0+(?:\.0+)?%)(?:\d|[1-9]\d|100)(?:(?<!100)\.\d+)?%/
|
||||
);
|
||||
}
|
||||
// The pattern must start at a word boundary (\b).
|
||||
// The pattern cannot be immediately preceded by a dot ((?<!\.)).
|
||||
// The pattern can be one of the following:
|
||||
// A percentage value of zero (0%), or
|
||||
// A non-zero percentage value that can be:
|
||||
// A single digit (\d) between 0 and 9, or
|
||||
// A two-digit number between 0 and 99 (\d{1,2}), or
|
||||
// The number 100.
|
||||
// The pattern can optionally include a decimal point and one or more digits after the decimal point ((?:(?<!100)\.\d+)?). However, if the number is 100, it cannot have a decimal point.
|
||||
// The pattern must end with a percentage sign (%).
|
||||
|
||||
cy.wrap($stakePercentage).should(
|
||||
'match',
|
||||
/\b(?<!\.)(?:0+(?:\.0+)?%|(?:\d|\d{1,2}|100)(?:(?<!100)\.\d+)?)%/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -744,13 +744,22 @@
|
||||
"Proposals": "Proposals",
|
||||
"Validators": "Validators",
|
||||
"Redeem": "Redeem",
|
||||
"validatorFormIntro": "To learn more about validators and how scores are calculated,",
|
||||
"readMoreValidatorForm": "read about staking on Vega",
|
||||
"StakeDescription": "The total amount $VEGA staked to this validator including self-stake and all delegation.",
|
||||
"NormalisedVotingPowerDescription": "Voting power is the relative weighting given to the validator in tendermint consensus. It is calculated based on the stake share of the validator minus any penalties due to overstaking or performance.",
|
||||
"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": "{{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.",
|
||||
"StakedByOperatorDescription": "The stake provided as self-stake by the node operator, must be at least the minimum stake as defined by network parameter",
|
||||
"AboutThisValidatorDescription": "External URL provided by the validator linking to information about themselves",
|
||||
"ValidatorStatusDescription": "Consensus, Standby or Pending (Candidate), depending on how much stake the validator has attracted",
|
||||
"StakedByDelegatesDescription": "The stake delegated to the node by other users",
|
||||
"OverstakedPenaltyDescription": "A penalty applied for having more stake than the optimal stake for the network. Designed to avoid concentration of voting power with a small number of validators",
|
||||
"PerformancePenaltyDescription": "Performance score is a measure of how often a validator proposed blocks in the last epoch relative to how many they should be expected to propose based on their voting power. Performance penalty is applied for having a performance score of less than 1",
|
||||
"UnnormalisedVotingPowerDescription": "The voting power of the validator based on their final validator score after all penalties have been applied",
|
||||
"NormalisedVotingPowerDescription": "The voting power of the validator, adjusted to ensure all validator scores sum to 1, used for distribution of rewards",
|
||||
"Score": "Score",
|
||||
"performancePenalty": "Performance penalty",
|
||||
"overstaked": "Overstaked",
|
||||
|
145
apps/token/src/routes/staking/node/validator-table.spec.tsx
Normal file
145
apps/token/src/routes/staking/node/validator-table.spec.tsx
Normal file
@ -0,0 +1,145 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { AppStateProvider } from '../../../contexts/app-state/app-state-provider';
|
||||
import { ValidatorTable } from './validator-table';
|
||||
import { ValidatorStatus } from '@vegaprotocol/types';
|
||||
import countryData from '../../../components/country-selector/country-data';
|
||||
import { formatNumber, toBigNum } from '@vegaprotocol/react-helpers';
|
||||
|
||||
const mockNode = {
|
||||
id: 'bb1822715aa86ce0e205aa4c78e9b71cdeaec94596ce72d366f0d50589eb1bf5',
|
||||
name: 'Marvin',
|
||||
pubkey: 'f45085a3bbce4e8197910448a0f22e7d5e79f8234336053bc11b177b0d70e785',
|
||||
infoUrl: 'https://en.wikipedia.org/wiki/Marvin_the_Paranoid_Android',
|
||||
location: 'AQ',
|
||||
ethereumAddress: '0x6ae2ff81b4a00f2edbed1fe3551ee0e3d81aa4f4',
|
||||
stakedByOperator: '3000000000000000000000',
|
||||
stakedByDelegates: '1280000000000000000',
|
||||
stakedTotal: '3001280000000000000000',
|
||||
pendingStake: '0',
|
||||
epochData: null,
|
||||
rankingScore: {
|
||||
rankingScore: '0.3999466965166174',
|
||||
stakeScore: '0.1999733482583087',
|
||||
performanceScore: '1',
|
||||
votingPower: '2000',
|
||||
status: ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
|
||||
},
|
||||
};
|
||||
|
||||
const mockStakedTotal = '15008.4';
|
||||
const decimals = 18;
|
||||
|
||||
const renderComponent = () =>
|
||||
render(
|
||||
<AppStateProvider>
|
||||
<ValidatorTable node={mockNode} stakedTotal={mockStakedTotal} />
|
||||
</AppStateProvider>
|
||||
);
|
||||
|
||||
describe('ValidatorTable', () => {
|
||||
it('should render successfully', () => {
|
||||
const { baseElement } = renderComponent();
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render a link to the staking guide', () => {
|
||||
const { getByTestId } = renderComponent();
|
||||
expect(getByTestId('validator-table-staking-guide-link')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render the correct node id', () => {
|
||||
const { getByTestId } = renderComponent();
|
||||
expect(getByTestId('validator-id')).toHaveTextContent(mockNode.id);
|
||||
});
|
||||
|
||||
it('should render the validator description url', () => {
|
||||
const { getByTestId } = renderComponent();
|
||||
expect(getByTestId('validator-description-url')).toHaveAttribute(
|
||||
'href',
|
||||
mockNode.infoUrl
|
||||
);
|
||||
});
|
||||
|
||||
it('should render the correct validator status', () => {
|
||||
const { getByTestId } = renderComponent();
|
||||
expect(getByTestId('validator-status')).toHaveTextContent('Consensus');
|
||||
});
|
||||
|
||||
it('should render a link to the validator forum', () => {
|
||||
const { getByTestId } = renderComponent();
|
||||
expect(getByTestId('validator-forum-link')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render the pubkey', () => {
|
||||
const { getByTestId } = renderComponent();
|
||||
expect(getByTestId('validator-public-key')).toHaveTextContent(
|
||||
mockNode.pubkey
|
||||
);
|
||||
});
|
||||
|
||||
it('should render the server location', () => {
|
||||
const location = countryData.find((c) => c.code === mockNode.location);
|
||||
|
||||
const { getByTestId } = renderComponent();
|
||||
expect(getByTestId('validator-server-location')).toHaveTextContent(
|
||||
// @ts-ignore - location is not null
|
||||
location.name
|
||||
);
|
||||
});
|
||||
|
||||
it('should render the ethereum address', () => {
|
||||
const { getByTestId } = renderComponent();
|
||||
expect(getByTestId('validator-eth-address')).toHaveTextContent(
|
||||
mockNode.ethereumAddress
|
||||
);
|
||||
});
|
||||
|
||||
it('should render the staked by operator', () => {
|
||||
const stakedByOperator = formatNumber(
|
||||
toBigNum(mockNode.stakedByOperator, decimals)
|
||||
);
|
||||
|
||||
const { getByTestId } = renderComponent();
|
||||
expect(getByTestId('staked-by-operator')).toHaveTextContent(
|
||||
stakedByOperator
|
||||
);
|
||||
});
|
||||
|
||||
it('should render the staked by delegates', () => {
|
||||
const stakedByDelegates = formatNumber(
|
||||
toBigNum(mockNode.stakedByDelegates, decimals)
|
||||
);
|
||||
|
||||
const { getByTestId } = renderComponent();
|
||||
expect(getByTestId('staked-by-delegates')).toHaveTextContent(
|
||||
stakedByDelegates
|
||||
);
|
||||
});
|
||||
|
||||
it('should render the total stake', () => {
|
||||
const stakedTotal = formatNumber(toBigNum(mockNode.stakedTotal, decimals));
|
||||
|
||||
const { getByTestId } = renderComponent();
|
||||
expect(getByTestId('total-stake')).toHaveTextContent(stakedTotal);
|
||||
});
|
||||
|
||||
it('should render the pending stake', () => {
|
||||
const pendingStake = formatNumber(
|
||||
toBigNum(mockNode.pendingStake, decimals)
|
||||
);
|
||||
|
||||
const { getByTestId } = renderComponent();
|
||||
expect(getByTestId('pending-stake')).toHaveTextContent(pendingStake);
|
||||
});
|
||||
|
||||
it('should render the values calculated by the helper functions', () => {
|
||||
// these functions are all tested in shared.spec.ts
|
||||
const { getByTestId } = renderComponent();
|
||||
expect(getByTestId('stake-percentage')).toBeInTheDocument();
|
||||
expect(getByTestId('overstaking-penalty')).toBeInTheDocument();
|
||||
expect(getByTestId('performance-penalty')).toBeInTheDocument();
|
||||
expect(getByTestId('total-penalties')).toBeInTheDocument();
|
||||
expect(getByTestId('unnormalised-voting-power')).toBeInTheDocument();
|
||||
expect(getByTestId('normalised-voting-power')).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -1,19 +1,25 @@
|
||||
import { useMemo } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import countryData from '../../../components/country-selector/country-data';
|
||||
import { Link as UTLink, Link } from '@vegaprotocol/ui-toolkit';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import {
|
||||
createDocsLinks,
|
||||
ExternalLinks,
|
||||
toBigNum,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import {
|
||||
Link as UTLink,
|
||||
Link,
|
||||
Tooltip,
|
||||
KeyValueTable,
|
||||
KeyValueTableRow,
|
||||
RoundedWrapper,
|
||||
ExternalLink,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { BigNumber } from '../../../lib/bignumber';
|
||||
import { formatNumber } from '../../../lib/format-number';
|
||||
import { ExternalLinks, toBigNum } from '@vegaprotocol/react-helpers';
|
||||
import { useAppState } from '../../../contexts/app-state/app-state-context';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import countryData from '../../../components/country-selector/country-data';
|
||||
import { SubHeading } from '../../../components/heading';
|
||||
import {
|
||||
getLastEpochScoreAndPerformance,
|
||||
@ -23,6 +29,7 @@ import {
|
||||
getPerformancePenalty,
|
||||
getTotalPenalties,
|
||||
getUnnormalisedVotingPower,
|
||||
getStakePercentage,
|
||||
} from '../shared';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { StakingNodeFieldsFragment } from './__generated__/Staking';
|
||||
@ -62,7 +69,7 @@ export const ValidatorTable = ({
|
||||
stakedTotal,
|
||||
previousEpochData,
|
||||
}: ValidatorTableProps) => {
|
||||
const { ETHERSCAN_URL } = useEnvironment();
|
||||
const { ETHERSCAN_URL, VEGA_DOCS_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
appState: { decimals },
|
||||
@ -81,10 +88,7 @@ export const ValidatorTable = ({
|
||||
node.stakedTotal
|
||||
);
|
||||
|
||||
const stakePercentage =
|
||||
total.isEqualTo(0) || stakedOnNode.isEqualTo(0)
|
||||
? '-'
|
||||
: stakedOnNode.dividedBy(total).times(100).dp(2).toString() + '%';
|
||||
const stakePercentage = getStakePercentage(total, stakedOnNode);
|
||||
|
||||
const totalPenaltiesAmount = getTotalPenalties(
|
||||
rawValidatorScore,
|
||||
@ -94,158 +98,207 @@ export const ValidatorTable = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="my-12" data-testid="validator-table">
|
||||
<SubHeading title={t('profile')} />
|
||||
<RoundedWrapper>
|
||||
<KeyValueTable data-testid="validator-table-profile">
|
||||
<KeyValueTableRow>
|
||||
<span>{t('id')}</span>
|
||||
<ValidatorTableCell dataTestId="validator-id">
|
||||
{node.id}
|
||||
</ValidatorTableCell>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
<span>{t('ABOUT THIS VALIDATOR')}</span>
|
||||
<span>
|
||||
<a href={node.infoUrl}>{node.infoUrl}</a>
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow noBorder={true}>
|
||||
<span>
|
||||
<strong>{t('STATUS')}</strong>
|
||||
</span>
|
||||
<span data-testid="validator-status">
|
||||
<strong>
|
||||
{t(statusTranslationKey(node.rankingScore.status))}
|
||||
</strong>
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
</KeyValueTable>
|
||||
</RoundedWrapper>
|
||||
<>
|
||||
<p className="mb-12">
|
||||
{t('validatorFormIntro')}{' '}
|
||||
{VEGA_DOCS_URL && (
|
||||
<ExternalLink
|
||||
href={createDocsLinks(VEGA_DOCS_URL).STAKING_GUIDE}
|
||||
target="_blank"
|
||||
data-testid="validator-table-staking-guide-link"
|
||||
className="text-white"
|
||||
>
|
||||
{t('readMoreValidatorForm')}
|
||||
</ExternalLink>
|
||||
)}
|
||||
</p>
|
||||
|
||||
<div className="mb-10">
|
||||
{t('validatorTableIntro')}{' '}
|
||||
<UTLink
|
||||
href={ExternalLinks.VALIDATOR_FORUM}
|
||||
target="_blank"
|
||||
data-testid="validator-forum-link"
|
||||
>
|
||||
{t('onTheForum')}
|
||||
</UTLink>
|
||||
<div className="my-12" data-testid="validator-table">
|
||||
<SubHeading title={t('profile')} />
|
||||
<RoundedWrapper>
|
||||
<KeyValueTable data-testid="validator-table-profile">
|
||||
<KeyValueTableRow>
|
||||
<span>{t('id')}</span>
|
||||
<ValidatorTableCell dataTestId="validator-id">
|
||||
{node.id}
|
||||
</ValidatorTableCell>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
<span>{t('ABOUT THIS VALIDATOR')}</span>
|
||||
|
||||
<Tooltip description={t('AboutThisValidatorDescription')}>
|
||||
<a data-testid="validator-description-url" href={node.infoUrl}>
|
||||
{node.infoUrl}
|
||||
</a>
|
||||
</Tooltip>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow noBorder={true}>
|
||||
<span>
|
||||
<strong>{t('STATUS')}</strong>
|
||||
</span>
|
||||
|
||||
<Tooltip description={t('ValidatorStatusDescription')}>
|
||||
<span data-testid="validator-status">
|
||||
<strong>
|
||||
{t(statusTranslationKey(node.rankingScore.status))}
|
||||
</strong>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</KeyValueTableRow>
|
||||
</KeyValueTable>
|
||||
</RoundedWrapper>
|
||||
|
||||
<div className="mb-10">
|
||||
{t('validatorTableIntro')}{' '}
|
||||
<UTLink
|
||||
href={ExternalLinks.VALIDATOR_FORUM}
|
||||
target="_blank"
|
||||
data-testid="validator-forum-link"
|
||||
>
|
||||
{t('onTheForum')}
|
||||
</UTLink>
|
||||
</div>
|
||||
|
||||
<SubHeading title={t('ADDRESS')} />
|
||||
<RoundedWrapper marginBottomLarge={true}>
|
||||
<KeyValueTable data-testid="validator-table-address">
|
||||
<KeyValueTableRow>
|
||||
<span>{t('VEGA ADDRESS / PUBLIC KEY')}</span>
|
||||
<ValidatorTableCell dataTestId="validator-public-key">
|
||||
{node.pubkey}
|
||||
</ValidatorTableCell>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
<span>{t('SERVER LOCATION')}</span>
|
||||
<ValidatorTableCell dataTestId="validator-server-location">
|
||||
{countryData.find((c) => c.code === node.location)?.name ||
|
||||
t('not available')}
|
||||
</ValidatorTableCell>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow noBorder={true}>
|
||||
<span>{t('ETHEREUM ADDRESS')}</span>
|
||||
<span data-testid="validator-eth-address">
|
||||
<Link
|
||||
title={t('View on Etherscan (opens in a new tab)')}
|
||||
href={`${ETHERSCAN_URL}/address/${node.ethereumAddress}`}
|
||||
target="_blank"
|
||||
>
|
||||
{node.ethereumAddress}
|
||||
</Link>
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
</KeyValueTable>
|
||||
</RoundedWrapper>
|
||||
|
||||
<SubHeading title={t('STAKE')} />
|
||||
<RoundedWrapper marginBottomLarge={true}>
|
||||
<KeyValueTable data-testid="validator-table-stake">
|
||||
<KeyValueTableRow>
|
||||
<span>{t('STAKED BY OPERATOR')}</span>
|
||||
|
||||
<Tooltip description={t('StakedByOperatorDescription')}>
|
||||
<span data-testid="staked-by-operator">
|
||||
{formatNumber(toBigNum(node.stakedByOperator, decimals))}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
<span>{t('STAKED BY DELEGATES')}</span>
|
||||
|
||||
<Tooltip description={t('StakedByDelegatesDescription')}>
|
||||
<span data-testid="staked-by-delegates">
|
||||
{formatNumber(toBigNum(node.stakedByDelegates, decimals))}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
<span>
|
||||
<strong>{t('TOTAL STAKE')}</strong>
|
||||
</span>
|
||||
|
||||
<span data-testid="total-stake">
|
||||
<strong>
|
||||
{formatNumber(toBigNum(node.stakedTotal, decimals))}
|
||||
</strong>
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
<span>{t('PENDING STAKE')}</span>
|
||||
|
||||
<Tooltip description={t('PendingStakeDescription')}>
|
||||
<span data-testid="pending-stake">
|
||||
{formatNumber(toBigNum(node.pendingStake, decimals))}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow noBorder={true}>
|
||||
<span>{t('STAKE SHARE')}</span>
|
||||
|
||||
<Tooltip description={t('StakeShareDescription')}>
|
||||
<span data-testid="stake-percentage">{stakePercentage}</span>
|
||||
</Tooltip>
|
||||
</KeyValueTableRow>
|
||||
</KeyValueTable>
|
||||
</RoundedWrapper>
|
||||
|
||||
<SubHeading title={t('PENALTIES')} />
|
||||
<RoundedWrapper marginBottomLarge={true}>
|
||||
<KeyValueTable data-testid="validator-table-penalties">
|
||||
<KeyValueTableRow>
|
||||
<span>{t('OVERSTAKED PENALTY')}</span>
|
||||
|
||||
<Tooltip description={t('OverstakedPenaltyDescription')}>
|
||||
<span data-testid="overstaking-penalty">
|
||||
{getOverstakingPenalty(overstakedAmount, node.stakedTotal)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
<span>{t('PERFORMANCE PENALTY')}</span>
|
||||
|
||||
<Tooltip description={t('PerformancePenaltyDescription')}>
|
||||
<span data-testid="performance-penalty">
|
||||
{getPerformancePenalty(performanceScore)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow noBorder={true}>
|
||||
<span>
|
||||
<strong>{t('TOTAL PENALTIES')}</strong>
|
||||
</span>
|
||||
<span data-testid="total-penalties">
|
||||
<strong>{totalPenaltiesAmount}</strong>
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
</KeyValueTable>
|
||||
</RoundedWrapper>
|
||||
|
||||
<SubHeading title={t('VOTING POWER')} />
|
||||
<RoundedWrapper marginBottomLarge={true}>
|
||||
<KeyValueTable data-testid="validator-table-voting-power">
|
||||
<KeyValueTableRow>
|
||||
<span>{t('UNNORMALISED VOTING POWER')}</span>
|
||||
|
||||
<Tooltip description={t('UnnormalisedVotingPowerDescription')}>
|
||||
<span data-testid="unnormalised-voting-power">
|
||||
{getUnnormalisedVotingPower(rawValidatorScore)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow noBorder={true}>
|
||||
<span>
|
||||
<strong>{t('NORMALISED VOTING POWER')}</strong>
|
||||
</span>
|
||||
|
||||
<Tooltip description={t('NormalisedVotingPowerDescription')}>
|
||||
<strong data-testid="normalised-voting-power">
|
||||
{getNormalisedVotingPower(node.rankingScore.votingPower)}
|
||||
</strong>
|
||||
</Tooltip>
|
||||
</KeyValueTableRow>
|
||||
</KeyValueTable>
|
||||
</RoundedWrapper>
|
||||
</div>
|
||||
|
||||
<SubHeading title={t('ADDRESS')} />
|
||||
<RoundedWrapper marginBottomLarge={true}>
|
||||
<KeyValueTable data-testid="validator-table-address">
|
||||
<KeyValueTableRow>
|
||||
<span>{t('VEGA ADDRESS / PUBLIC KEY')}</span>
|
||||
<ValidatorTableCell dataTestId="validator-public-key">
|
||||
{node.pubkey}
|
||||
</ValidatorTableCell>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
<span>{t('SERVER LOCATION')}</span>
|
||||
<ValidatorTableCell>
|
||||
{countryData.find((c) => c.code === node.location)?.name ||
|
||||
t('not available')}
|
||||
</ValidatorTableCell>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow noBorder={true}>
|
||||
<span>{t('ETHEREUM ADDRESS')}</span>
|
||||
<span>
|
||||
<Link
|
||||
title={t('View on Etherscan (opens in a new tab)')}
|
||||
href={`${ETHERSCAN_URL}/address/${node.ethereumAddress}`}
|
||||
target="_blank"
|
||||
>
|
||||
{node.ethereumAddress}
|
||||
</Link>
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
</KeyValueTable>
|
||||
</RoundedWrapper>
|
||||
|
||||
<SubHeading title={t('STAKE')} />
|
||||
<RoundedWrapper marginBottomLarge={true}>
|
||||
<KeyValueTable data-testid="validator-table-stake">
|
||||
<KeyValueTableRow>
|
||||
<span>{t('STAKED BY OPERATOR')}</span>
|
||||
<span data-testid="staked-by-operator">
|
||||
{formatNumber(toBigNum(node.stakedByOperator, decimals))}
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
<span>{t('STAKED BY DELEGATES')}</span>
|
||||
<span data-testid="staked-by-delegates">
|
||||
{formatNumber(toBigNum(node.stakedByDelegates, decimals))}
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
<span>
|
||||
<strong>{t('TOTAL STAKE')}</strong>
|
||||
</span>
|
||||
<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 noBorder={true}>
|
||||
<span>{t('STAKE SHARE')}</span>
|
||||
<span data-testid="stake-percentage">{stakePercentage}</span>
|
||||
</KeyValueTableRow>
|
||||
</KeyValueTable>
|
||||
</RoundedWrapper>
|
||||
|
||||
<SubHeading title={t('PENALTIES')} />
|
||||
<RoundedWrapper marginBottomLarge={true}>
|
||||
<KeyValueTable data-testid="validator-table-penalties">
|
||||
<KeyValueTableRow>
|
||||
<span>{t('OVERSTAKED PENALTY')}</span>
|
||||
<span>
|
||||
{getOverstakingPenalty(overstakedAmount, node.stakedTotal)}
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
<span>{t('PERFORMANCE PENALTY')}</span>
|
||||
<span>{getPerformancePenalty(performanceScore)}</span>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow noBorder={true}>
|
||||
<span>
|
||||
<strong>{t('TOTAL PENALTIES')}</strong>
|
||||
</span>
|
||||
<span>
|
||||
<strong>{totalPenaltiesAmount}</strong>
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
</KeyValueTable>
|
||||
</RoundedWrapper>
|
||||
|
||||
<SubHeading title={t('VOTING POWER')} />
|
||||
<RoundedWrapper marginBottomLarge={true}>
|
||||
<KeyValueTable data-testid="validator-table-voting-power">
|
||||
<KeyValueTableRow>
|
||||
<span>{t('UNNORMALISED VOTING POWER')}</span>
|
||||
<span>{getUnnormalisedVotingPower(rawValidatorScore)}</span>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow noBorder={true}>
|
||||
<span>
|
||||
<strong>{t('NORMALISED VOTING POWER')}</strong>
|
||||
</span>
|
||||
<span>
|
||||
<strong>
|
||||
{getNormalisedVotingPower(node.rankingScore.votingPower)}
|
||||
</strong>
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
</KeyValueTable>
|
||||
</RoundedWrapper>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
getFormattedPerformanceScore,
|
||||
getPerformancePenalty,
|
||||
getTotalPenalties,
|
||||
getStakePercentage,
|
||||
} from './shared';
|
||||
|
||||
describe('getLastEpochScoreAndPerformance', () => {
|
||||
@ -139,3 +140,23 @@ describe('getTotalPenalties', () => {
|
||||
expect(getTotalPenalties('0.25', '0.5', '1000', '10000')).toEqual('0.00%');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStakePercentage', () => {
|
||||
it('should return the stake percentage', () => {
|
||||
expect(
|
||||
getStakePercentage(new BigNumber('1000'), new BigNumber('100'))
|
||||
).toEqual('10%');
|
||||
expect(
|
||||
getStakePercentage(new BigNumber('1000'), new BigNumber('500'))
|
||||
).toEqual('50%');
|
||||
expect(
|
||||
getStakePercentage(new BigNumber('1000'), new BigNumber('257.5'))
|
||||
).toEqual('25.75%');
|
||||
});
|
||||
|
||||
it('should return "0%" if the total stake is 0', () => {
|
||||
expect(getStakePercentage(new BigNumber('0'), new BigNumber('0'))).toEqual(
|
||||
'0%'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -95,3 +95,8 @@ export const getTotalPenalties = (
|
||||
2
|
||||
);
|
||||
};
|
||||
|
||||
export const getStakePercentage = (total: BigNumber, stakedOnNode: BigNumber) =>
|
||||
total.isEqualTo(0) || stakedOnNode.isEqualTo(0)
|
||||
? '0%'
|
||||
: stakedOnNode.dividedBy(total).times(100).dp(2).toString() + '%';
|
||||
|
Loading…
Reference in New Issue
Block a user