feat(2326): style Token validators (#2388)
* feat(2326): styled up validators intro as per designs, including site-wide tweaking to headers for consistency * feat(2326): styled up reduced height epoch progress bar * feat(2326): styled up inline 'reveal all validators' table row. Test failing * feat(2326): failing test fixed * feat(2326): components and styling for validator table * feat(2326): making proposals section consistent with new validators styling * feat(2326): extra consistency changes * feat(2326): linting fixes * feat(2326): a couple of e2e test fixes * feat(2326): more e2e test fixes
This commit is contained in:
parent
e49ad9da6a
commit
071a9ab34b
@ -86,7 +86,7 @@ const classes = {
|
||||
indicatorFailed:
|
||||
'rounded-full transition duration-500 ease-in-out h-12 w-12 py-3 border-2 border-red-600 bg-red-600 text-center text-white font-bold leading-5',
|
||||
textFailed:
|
||||
'absolute top-0 -ml-10 text-center mt-16 w-32 text-xs font-medium uppercase text-red-600',
|
||||
'absolute top-0 -ml-10 text-center mt-16 w-32 text-xs font-medium uppercase text-vega-red',
|
||||
indicatorComplete:
|
||||
'rounded-full transition duration-500 ease-in-out h-12 w-12 py-3 border-2 border-vega-green-dark bg-vega-green-dark text-center text-white leading-5',
|
||||
textComplete:
|
||||
|
@ -1,4 +1,4 @@
|
||||
const proposalDocumentationLink = '[data-testid="external-link"]';
|
||||
const proposalDocumentationLink = '[data-testid="proposal-documentation-link"]';
|
||||
const newProposalButton = '[data-testid="new-proposal-link"]';
|
||||
const newProposalLink = '[data-testid="new-proposal-link"]';
|
||||
const governanceDocsUrl = 'https://vega.xyz/governance';
|
||||
|
@ -25,7 +25,7 @@ context('Staking Page - verify elements on page', function () {
|
||||
});
|
||||
|
||||
it('Should have STAKING ON VEGA header visible', function () {
|
||||
cy.verify_page_header('Staking');
|
||||
cy.verify_page_header('Validators');
|
||||
});
|
||||
|
||||
it('Should have Staking Guide link visible', function () {
|
||||
@ -94,10 +94,6 @@ context('Staking Page - verify elements on page', function () {
|
||||
cy.wrap($pendingStake).should('contain.text', '0.00');
|
||||
});
|
||||
});
|
||||
|
||||
it('Should be able to see button to unhide top validators', function () {
|
||||
cy.get('[data-testid="show-all-validators"]').should('be.visible');
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -1,9 +1,6 @@
|
||||
import { format, formatDistanceStrict } from 'date-fns';
|
||||
import { formatDistanceStrict } from 'date-fns';
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import arrow from '../../images/back.png';
|
||||
import { DATE_FORMAT_DETAILED } from '../../lib/date-formats';
|
||||
import { ProgressBar } from '../progress-bar';
|
||||
|
||||
export interface EpochCountdownProps {
|
||||
@ -58,8 +55,8 @@ export function EpochCountdown({
|
||||
|
||||
return (
|
||||
<div data-testid="epoch-countdown" className="epoch-countdown">
|
||||
<div className="flex items-end">
|
||||
<h3 className="flex-1">
|
||||
<div className="flex items-end mb-3">
|
||||
<h3 className="flex-1 m-0 text-sm">
|
||||
{t('Epoch')} {id}
|
||||
</h3>
|
||||
<p className="text-sm m-0">
|
||||
@ -69,17 +66,6 @@ export function EpochCountdown({
|
||||
</p>
|
||||
</div>
|
||||
<ProgressBar value={progress} />
|
||||
<div className="flex text-sm">
|
||||
<p>{format(startDate, DATE_FORMAT_DETAILED)}</p>
|
||||
<div className="flex-1 text-center">
|
||||
<img
|
||||
className="inline-block w-[5px] rotate-180"
|
||||
alt="arrow"
|
||||
src={arrow}
|
||||
/>
|
||||
</div>
|
||||
<p>{format(endDate, DATE_FORMAT_DETAILED)}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -15,13 +15,37 @@ export const Heading = ({
|
||||
|
||||
return (
|
||||
<header
|
||||
className={classNames('my-0', {
|
||||
className={classNames('mt-10 mb-6', {
|
||||
'mx-auto': centerContent,
|
||||
})}
|
||||
>
|
||||
<h1 className={classNames('font-alpha calt', { 'mb-0': !marginBottom })}>
|
||||
<h1
|
||||
className={classNames('font-alpha calt text-5xl', {
|
||||
'mb-0': !marginBottom,
|
||||
})}
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export const SubHeading = ({
|
||||
title,
|
||||
centerContent = false,
|
||||
marginBottom = true,
|
||||
}: HeadingProps) => {
|
||||
if (!title) return null;
|
||||
|
||||
return (
|
||||
<h2
|
||||
className={classNames('text-2xl font-alpha calt uppercase', {
|
||||
'mx-auto': centerContent,
|
||||
'mb-0': !marginBottom,
|
||||
'mb-4': marginBottom,
|
||||
})}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
);
|
||||
};
|
||||
|
@ -13,10 +13,10 @@ export const ProgressBar = ({ value }: ProgressBarProps) => {
|
||||
aria-valuemin={0}
|
||||
aria-valuenow={percent == null ? undefined : Math.round(percent)}
|
||||
role="progressbar"
|
||||
className="relative border h-[21px]"
|
||||
className="relative h-2 bg-neutral-600 rounded-full overflow-hidden"
|
||||
>
|
||||
<div
|
||||
className="bg-white h-full absolute transition-[width] ease-in-out"
|
||||
className="rounded-full bg-clouds bg-vega-yellow h-full absolute transition-[width] ease-in-out"
|
||||
style={{ width }}
|
||||
/>
|
||||
</div>
|
||||
|
@ -8,7 +8,6 @@
|
||||
"pageTitleLiquidity": "Incentivised Liquidity Programme",
|
||||
"pageTitleRedemptionTranche": "Redeem from Tranche",
|
||||
"pageTitleTranches": "Vesting tranches",
|
||||
"pageTitleStaking": "Staking",
|
||||
"pageTitle404": "Page not found",
|
||||
"pageTitleNotPermitted": "Can not proceed!",
|
||||
"pageTitleDisassociate": "Disassociate $VEGA tokens from a Vega key",
|
||||
@ -17,6 +16,7 @@
|
||||
"pageTitleWithdrawLp": "Withdraw SLP and Rewards",
|
||||
"pageTitleRewards": "Rewards",
|
||||
"pageTitleRejectedProposals": "Rejected proposals",
|
||||
"pageTitleValidators": "Validators",
|
||||
"Vesting": "Vesting",
|
||||
"unstaked": "Unstaked",
|
||||
"of": "of",
|
||||
@ -293,15 +293,7 @@
|
||||
"Governance is coming soon": "Governance is coming soon",
|
||||
"Staking is coming soon": "Staking is coming soon",
|
||||
"VESTING VEGA TOKENS": "in vesting contract",
|
||||
"stakingStep1": "Step 1. Connect to a Vega Wallet",
|
||||
"stakingStep1Text": "You will need a <vegaWalletLink>Vega Wallet</vegaWalletLink> to control stake and receive staking rewards.",
|
||||
"stakingVegaWalletConnected": "Connected to Vega Wallet with public key {{key}}",
|
||||
"stakingStep2": "Step 2. Associate tokens with a Vega Wallet",
|
||||
"stakingAssociateConnectVega": "You need to connect to a Vega Wallet first",
|
||||
"stakingAssociateConnectEth": "You need to connect to an Ethereum wallet first",
|
||||
"stakingHasAssociated": "You have {{tokens}} $VEGA tokens associated.",
|
||||
"stakingAssociateMoreButton": "Associate more $VEGA tokens with wallet",
|
||||
"stakingDisassociateButton": "Disassociate $VEGA tokens from wallet",
|
||||
"stakingIntro": "Earn a share of trading fees and treasury rewards for each full epoch staked.",
|
||||
"stakingConfirm": "Open your wallet app to confirm",
|
||||
"associateButton": "Associate $VEGA tokens with wallet",
|
||||
"nodeQueryFailed": "Could not get data for validator {{node}}",
|
||||
@ -535,13 +527,7 @@
|
||||
"unsupportedChainIdError": "You're connected to an unsupported network",
|
||||
"userRejectionError": "Please authorise this website to access your Ethereum account",
|
||||
"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?",
|
||||
"stakingDescription1": "1. VEGA is an ERC20 token. Associate it with a Vega wallet using the",
|
||||
"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",
|
||||
"stakingDescription4": "4. Move your stake if your validator is penalised",
|
||||
"stakingBridge": "staking bridge",
|
||||
"validatorTableIntro": "View the validator profile pitches and discussion",
|
||||
"onTheForum": "on the forum",
|
||||
"readMoreStaking": "Read more about staking on Vega",
|
||||
"networkDown": "This site is not currently connecting to the network please try again later.",
|
||||
|
@ -1,7 +1,11 @@
|
||||
import { format, isFuture } from 'date-fns';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
KeyValueTable,
|
||||
KeyValueTableRow,
|
||||
RoundedWrapper,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
|
||||
import { CurrentProposalState } from '../current-proposal-state';
|
||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||
@ -17,56 +21,58 @@ export const ProposalChangeTable = ({ proposal }: ProposalChangeTableProps) => {
|
||||
const terms = proposal?.terms;
|
||||
|
||||
return (
|
||||
<KeyValueTable data-testid="proposal-change-table">
|
||||
<KeyValueTableRow>
|
||||
{t('id')}
|
||||
{proposal?.id}
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('state')}
|
||||
<CurrentProposalState proposal={proposal} />
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{isFuture(new Date(terms?.closingDatetime))
|
||||
? t('closesOn')
|
||||
: t('closedOn')}
|
||||
{format(new Date(terms?.closingDatetime), DATE_FORMAT_DETAILED)}
|
||||
</KeyValueTableRow>
|
||||
{terms?.change.__typename !== 'NewFreeform' ? (
|
||||
<RoundedWrapper>
|
||||
<KeyValueTable data-testid="proposal-change-table">
|
||||
<KeyValueTableRow>
|
||||
{isFuture(new Date(terms?.enactmentDatetime || 0))
|
||||
? t('proposedEnactment')
|
||||
: t('enactedOn')}
|
||||
{format(
|
||||
new Date(terms?.enactmentDatetime || 0),
|
||||
DATE_FORMAT_DETAILED
|
||||
)}
|
||||
{t('id')}
|
||||
{proposal?.id}
|
||||
</KeyValueTableRow>
|
||||
) : null}
|
||||
<KeyValueTableRow>
|
||||
{t('proposedBy')}
|
||||
<span style={{ wordBreak: 'break-word' }}>{proposal?.party.id}</span>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('proposedOn')}
|
||||
{format(new Date(proposal?.datetime), DATE_FORMAT_DETAILED)}
|
||||
</KeyValueTableRow>
|
||||
{proposal?.rejectionReason ? (
|
||||
<KeyValueTableRow>
|
||||
{t('rejectionReason')}
|
||||
{proposal.rejectionReason}
|
||||
{t('state')}
|
||||
<CurrentProposalState proposal={proposal} />
|
||||
</KeyValueTableRow>
|
||||
) : null}
|
||||
{proposal?.errorDetails ? (
|
||||
<KeyValueTableRow>
|
||||
{t('errorDetails')}
|
||||
{proposal.errorDetails}
|
||||
{isFuture(new Date(terms?.closingDatetime))
|
||||
? t('closesOn')
|
||||
: t('closedOn')}
|
||||
{format(new Date(terms?.closingDatetime), DATE_FORMAT_DETAILED)}
|
||||
</KeyValueTableRow>
|
||||
) : null}
|
||||
<KeyValueTableRow>
|
||||
{t('type')}
|
||||
{proposal?.terms.change.__typename}
|
||||
</KeyValueTableRow>
|
||||
</KeyValueTable>
|
||||
{terms?.change.__typename !== 'NewFreeform' ? (
|
||||
<KeyValueTableRow>
|
||||
{isFuture(new Date(terms?.enactmentDatetime || 0))
|
||||
? t('proposedEnactment')
|
||||
: t('enactedOn')}
|
||||
{format(
|
||||
new Date(terms?.enactmentDatetime || 0),
|
||||
DATE_FORMAT_DETAILED
|
||||
)}
|
||||
</KeyValueTableRow>
|
||||
) : null}
|
||||
<KeyValueTableRow>
|
||||
{t('proposedBy')}
|
||||
<span style={{ wordBreak: 'break-word' }}>{proposal?.party.id}</span>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('proposedOn')}
|
||||
{format(new Date(proposal?.datetime), DATE_FORMAT_DETAILED)}
|
||||
</KeyValueTableRow>
|
||||
{proposal?.rejectionReason ? (
|
||||
<KeyValueTableRow>
|
||||
{t('rejectionReason')}
|
||||
{proposal.rejectionReason}
|
||||
</KeyValueTableRow>
|
||||
) : null}
|
||||
{proposal?.errorDetails ? (
|
||||
<KeyValueTableRow>
|
||||
{t('errorDetails')}
|
||||
{proposal.errorDetails}
|
||||
</KeyValueTableRow>
|
||||
) : null}
|
||||
<KeyValueTableRow>
|
||||
{t('type')}
|
||||
{proposal?.terms.change.__typename}
|
||||
</KeyValueTableRow>
|
||||
</KeyValueTable>
|
||||
</RoundedWrapper>
|
||||
);
|
||||
};
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Lozenge } from '@vegaprotocol/ui-toolkit';
|
||||
import type { ReactNode } from 'react';
|
||||
import { shorten } from '@vegaprotocol/react-helpers';
|
||||
import { SubHeading } from '../../../../components/heading';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
|
||||
@ -96,12 +97,7 @@ export const ProposalHeader = ({
|
||||
return (
|
||||
<div className="text-sm mb-2">
|
||||
<header data-testid="proposal-title">
|
||||
<h2
|
||||
{...(title && title.length > titleContent.length && { title: title })}
|
||||
className="text-lg mx-0 mt-0 mb-1 font-semibold"
|
||||
>
|
||||
{titleContent || t('Unknown proposal')}
|
||||
</h2>
|
||||
<SubHeading title={titleContent || t('Unknown proposal')} />
|
||||
</header>
|
||||
{description && (
|
||||
<div className="mb-4" data-testid="proposal-description">
|
||||
|
@ -1,13 +1,12 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SubHeading } from '../../../../components/heading';
|
||||
|
||||
export const ProposalNotFound = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<section data-testid="proposal-not-found">
|
||||
<header>
|
||||
<h2 className="text-lg mx-0 mt-0 mb-1 font-semibold">
|
||||
{t('ProposalNotFound')}
|
||||
</h2>
|
||||
<SubHeading title={t('ProposalNotFound')} />
|
||||
</header>
|
||||
<p>{t('ProposalNotFoundDetails')}</p>
|
||||
</section>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
|
||||
import { SubHeading } from '../../../../components/heading';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
import type * as Schema from '@vegaprotocol/types';
|
||||
|
||||
@ -11,7 +12,7 @@ export const ProposalTermsJson = ({
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<section>
|
||||
<h2>{t('proposalTerms')}</h2>
|
||||
<SubHeading title={t('proposalTerms')} />
|
||||
<SyntaxHighlighter data={terms} />
|
||||
</section>
|
||||
);
|
||||
|
@ -3,16 +3,18 @@ import {
|
||||
KeyValueTable,
|
||||
KeyValueTableRow,
|
||||
Thumbs,
|
||||
RoundedWrapper,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
formatNumber,
|
||||
formatNumberPercentage,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { SubHeading } from '../../../../components/heading';
|
||||
import { useVoteInformation } from '../../hooks';
|
||||
import { useAppState } from '../../../../contexts/app-state/app-state-context';
|
||||
import { ProposalType } from '../proposal/proposal';
|
||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import { ProposalType } from '../proposal/proposal';
|
||||
|
||||
interface ProposalVotesTableProps {
|
||||
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
||||
@ -55,104 +57,112 @@ export const ProposalVotesTable = ({
|
||||
: t('byLiquidityVote');
|
||||
|
||||
return (
|
||||
<KeyValueTable
|
||||
title={t('voteBreakdown')}
|
||||
data-testid="proposal-votes-table"
|
||||
numerical={true}
|
||||
headingLevel={4}
|
||||
>
|
||||
<KeyValueTableRow>
|
||||
{t('expectedToPass')}
|
||||
{isUpdateMarket ? (
|
||||
updateMarketWillPass ? (
|
||||
<Thumbs up={true} text={updateMarketVotePassMethod} />
|
||||
) : (
|
||||
<Thumbs up={false} />
|
||||
)
|
||||
) : willPassByTokenVote ? (
|
||||
<Thumbs up={true} />
|
||||
) : (
|
||||
<Thumbs up={false} />
|
||||
)}
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('majorityMet')}
|
||||
{majorityMet ? <Thumbs up={true} /> : <Thumbs up={false} />}
|
||||
</KeyValueTableRow>
|
||||
{isUpdateMarket && (
|
||||
<KeyValueTableRow>
|
||||
{t('majorityLPMet')}
|
||||
{majorityLPMet ? <Thumbs up={true} /> : <Thumbs up={false} />}
|
||||
</KeyValueTableRow>
|
||||
)}
|
||||
<KeyValueTableRow>
|
||||
{t('participationMet')}
|
||||
{participationMet ? <Thumbs up={true} /> : <Thumbs up={false} />}
|
||||
</KeyValueTableRow>
|
||||
{isUpdateMarket && (
|
||||
<KeyValueTableRow>
|
||||
{t('participationLPMet')}
|
||||
{participationLPMet ? <Thumbs up={true} /> : <Thumbs up={false} />}
|
||||
</KeyValueTableRow>
|
||||
)}
|
||||
<KeyValueTableRow>
|
||||
{t('tokenForProposal')}
|
||||
{formatNumber(yesTokens, 2)}
|
||||
</KeyValueTableRow>
|
||||
{isUpdateMarket && (
|
||||
<KeyValueTableRow>
|
||||
{t('tokenLPForProposal')}
|
||||
{formatNumber(yesEquityLikeShareWeight, 2)}
|
||||
</KeyValueTableRow>
|
||||
)}
|
||||
<KeyValueTableRow>
|
||||
{t('totalSupply')}
|
||||
{formatNumber(totalSupply, 2)}
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('tokensAgainstProposal')}
|
||||
{formatNumber(noTokens, 2)}
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('participationRequired')}
|
||||
{formatNumberPercentage(requiredParticipation)}
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('majorityRequired')}
|
||||
{formatNumberPercentage(requiredMajorityPercentage)}
|
||||
</KeyValueTableRow>
|
||||
{!isUpdateMarket && (
|
||||
<>
|
||||
<>
|
||||
<SubHeading title={t('voteBreakdown')} />
|
||||
<RoundedWrapper>
|
||||
<KeyValueTable
|
||||
data-testid="proposal-votes-table"
|
||||
numerical={true}
|
||||
headingLevel={4}
|
||||
>
|
||||
<KeyValueTableRow>
|
||||
{t('numberOfVotingParties')}
|
||||
{formatNumber(totalVotes, 0)}
|
||||
{t('expectedToPass')}
|
||||
{isUpdateMarket ? (
|
||||
updateMarketWillPass ? (
|
||||
<Thumbs up={true} text={updateMarketVotePassMethod} />
|
||||
) : (
|
||||
<Thumbs up={false} />
|
||||
)
|
||||
) : willPassByTokenVote ? (
|
||||
<Thumbs up={true} />
|
||||
) : (
|
||||
<Thumbs up={false} />
|
||||
)}
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('totalTokensVotes')}
|
||||
{formatNumber(totalTokensVoted, 2)}
|
||||
{t('majorityMet')}
|
||||
{majorityMet ? <Thumbs up={true} /> : <Thumbs up={false} />}
|
||||
</KeyValueTableRow>
|
||||
{isUpdateMarket && (
|
||||
<KeyValueTableRow>
|
||||
{t('majorityLPMet')}
|
||||
{majorityLPMet ? <Thumbs up={true} /> : <Thumbs up={false} />}
|
||||
</KeyValueTableRow>
|
||||
)}
|
||||
<KeyValueTableRow>
|
||||
{t('participationMet')}
|
||||
{participationMet ? <Thumbs up={true} /> : <Thumbs up={false} />}
|
||||
</KeyValueTableRow>
|
||||
{isUpdateMarket && (
|
||||
<KeyValueTableRow>
|
||||
{t('participationLPMet')}
|
||||
{participationLPMet ? (
|
||||
<Thumbs up={true} />
|
||||
) : (
|
||||
<Thumbs up={false} />
|
||||
)}
|
||||
</KeyValueTableRow>
|
||||
)}
|
||||
<KeyValueTableRow>
|
||||
{t('tokenForProposal')}
|
||||
{formatNumber(yesTokens, 2)}
|
||||
</KeyValueTableRow>
|
||||
{isUpdateMarket && (
|
||||
<KeyValueTableRow>
|
||||
{t('tokenLPForProposal')}
|
||||
{formatNumber(yesEquityLikeShareWeight, 2)}
|
||||
</KeyValueTableRow>
|
||||
)}
|
||||
<KeyValueTableRow>
|
||||
{t('totalSupply')}
|
||||
{formatNumber(totalSupply, 2)}
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('totalTokenVotedPercentage')}
|
||||
{formatNumberPercentage(totalTokensPercentage, 2)}
|
||||
{t('tokensAgainstProposal')}
|
||||
{formatNumber(noTokens, 2)}
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('numberOfForVotes')}
|
||||
{formatNumber(yesVotes, 0)}
|
||||
{t('participationRequired')}
|
||||
{formatNumberPercentage(requiredParticipation)}
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('numberOfAgainstVotes')}
|
||||
{formatNumber(noVotes, 0)}
|
||||
{t('majorityRequired')}
|
||||
{formatNumberPercentage(requiredMajorityPercentage)}
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('yesPercentage')}
|
||||
{formatNumberPercentage(yesPercentage, 2)}
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('noPercentage')}
|
||||
{formatNumberPercentage(noPercentage, 2)}
|
||||
</KeyValueTableRow>
|
||||
</>
|
||||
)}
|
||||
</KeyValueTable>
|
||||
{!isUpdateMarket && (
|
||||
<>
|
||||
<KeyValueTableRow>
|
||||
{t('numberOfVotingParties')}
|
||||
{formatNumber(totalVotes, 0)}
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('totalTokensVotes')}
|
||||
{formatNumber(totalTokensVoted, 2)}
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('totalTokenVotedPercentage')}
|
||||
{formatNumberPercentage(totalTokensPercentage, 2)}
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('numberOfForVotes')}
|
||||
{formatNumber(yesVotes, 0)}
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('numberOfAgainstVotes')}
|
||||
{formatNumber(noVotes, 0)}
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('yesPercentage')}
|
||||
{formatNumberPercentage(yesPercentage, 2)}
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow noBorder={true}>
|
||||
{t('noPercentage')}
|
||||
{formatNumberPercentage(noPercentage, 2)}
|
||||
</KeyValueTableRow>
|
||||
</>
|
||||
)}
|
||||
</KeyValueTable>
|
||||
</RoundedWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -75,7 +75,7 @@ export const Proposal = ({ proposal }: ProposalProps) => {
|
||||
<AsyncRenderer data={params} loading={loading} error={error}>
|
||||
<section data-testid="proposal">
|
||||
<ProposalHeader proposal={proposal} />
|
||||
<div className="mb-8">
|
||||
<div className="mb-10">
|
||||
<ProposalChangeTable proposal={proposal} />
|
||||
</div>
|
||||
{proposal.terms.change.__typename === 'NewAsset' &&
|
||||
@ -87,7 +87,7 @@ export const Proposal = ({ proposal }: ProposalProps) => {
|
||||
lifetimeLimit={proposal.terms.change.source.lifetimeLimit}
|
||||
/>
|
||||
) : null}
|
||||
<div className="mb-8">
|
||||
<div className="mb-12">
|
||||
<VoteDetails
|
||||
proposal={proposal}
|
||||
proposalType={proposalType}
|
||||
@ -95,7 +95,7 @@ export const Proposal = ({ proposal }: ProposalProps) => {
|
||||
spamProtectionMinTokens={params?.spam_protection_voting_min_tokens}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-8">
|
||||
<div className="mb-10">
|
||||
<ProposalVotesTable proposal={proposal} proposalType={proposalType} />
|
||||
</div>
|
||||
<ProposalTermsJson terms={proposal.terms} />
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { isFuture } from 'date-fns';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Heading } from '../../../../components/heading';
|
||||
import { Heading, SubHeading } from '../../../../components/heading';
|
||||
import { ProposalsListItem } from '../proposals-list-item';
|
||||
import { ProposalsListFilter } from '../proposals-list-filter';
|
||||
import Routes from '../../../routes';
|
||||
@ -47,7 +47,7 @@ export const ProposalsList = ({ proposals }: ProposalsListProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid xs:grid-cols-2 items-center mb-4">
|
||||
<div className="grid xs:grid-cols-2 items-center">
|
||||
<Heading
|
||||
centerContent={false}
|
||||
marginBottom={false}
|
||||
@ -63,25 +63,23 @@ export const ProposalsList = ({ proposals }: ProposalsListProps) => {
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<p className="mb-4">
|
||||
{t(
|
||||
`The Vega network is governed by the community. View active proposals, vote on them or propose changes to the network.`
|
||||
)}{' '}
|
||||
<ExternalLink
|
||||
data-testid="proposal-documentation-link"
|
||||
href={ExternalLinks.GOVERNANCE_PAGE}
|
||||
className="text-white"
|
||||
>
|
||||
{t(`Find out more about Vega governance`)}
|
||||
</ExternalLink>
|
||||
</p>
|
||||
</div>
|
||||
<p className="mb-8">
|
||||
{t(
|
||||
`The Vega network is governed by the community. View active proposals, vote on them or propose changes to the network.`
|
||||
)}{' '}
|
||||
<ExternalLink
|
||||
data-testid="proposal-documentation-link"
|
||||
href={ExternalLinks.GOVERNANCE_PAGE}
|
||||
className="text-white"
|
||||
>
|
||||
{t(`Find out more about Vega governance`)}
|
||||
</ExternalLink>
|
||||
</p>
|
||||
{proposals.length > 0 && (
|
||||
<ProposalsListFilter setFilterString={setFilterString} />
|
||||
)}
|
||||
<section className="-mx-4 p-4 mb-8 bg-neutral-800">
|
||||
<h2 className="text-xl mb-2">{t('openProposals')}</h2>
|
||||
<SubHeading title={t('openProposals')} />
|
||||
{sortedProposals.open.length > 0 ? (
|
||||
<ul data-testid="open-proposals">
|
||||
{sortedProposals.open.filter(filterPredicate).map((proposal) => (
|
||||
@ -95,7 +93,7 @@ export const ProposalsList = ({ proposals }: ProposalsListProps) => {
|
||||
)}
|
||||
</section>
|
||||
<section>
|
||||
<h2 className="text-xl mb-2">{t('closedProposals')}</h2>
|
||||
<SubHeading title={t('closedProposals')} />
|
||||
{sortedProposals.closed.length > 0 ? (
|
||||
<ul data-testid="closed-proposals">
|
||||
{sortedProposals.closed.filter(filterPredicate).map((proposal) => (
|
||||
|
@ -9,6 +9,7 @@ import { useVoteInformation } from '../../hooks';
|
||||
import { useUserVote } from './use-user-vote';
|
||||
import { CurrentProposalStatus } from '../current-proposal-status';
|
||||
import { VoteButtonsContainer } from './vote-buttons';
|
||||
import { SubHeading } from '../../../../components/heading';
|
||||
import { ProposalType } from '../proposal/proposal';
|
||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
@ -53,7 +54,7 @@ export const VoteDetails = ({
|
||||
<>
|
||||
{proposalType === ProposalType.PROPOSAL_UPDATE_MARKET && (
|
||||
<section>
|
||||
<h3 className="text-xl mb-2">{t('liquidityVotes')}</h3>
|
||||
<SubHeading title={t('liquidityVotes')} />
|
||||
<p>
|
||||
<span>
|
||||
<CurrentProposalStatus proposal={proposal} />
|
||||
@ -102,7 +103,7 @@ export const VoteDetails = ({
|
||||
</section>
|
||||
)}
|
||||
<section>
|
||||
<h3 className="text-xl mb-2">{t('tokenVotes')}</h3>
|
||||
<SubHeading title={t('tokenVotes')} />
|
||||
<p>
|
||||
<span>
|
||||
<CurrentProposalStatus proposal={proposal} />
|
||||
@ -176,8 +177,8 @@ export const VoteDetails = ({
|
||||
<p>{t('votingThresholdInfo')}</p>
|
||||
)}
|
||||
{pubKey ? (
|
||||
<>
|
||||
<h3 className="text-xl mb-2">{t('yourVote')}</h3>
|
||||
<section className="mt-10">
|
||||
<SubHeading title={t('yourVote')} />
|
||||
{proposal && (
|
||||
<VoteButtonsContainer
|
||||
voteState={voteState}
|
||||
@ -189,7 +190,7 @@ export const VoteDetails = ({
|
||||
className="flex"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</section>
|
||||
) : (
|
||||
<ConnectToVega />
|
||||
)}
|
||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { Heading } from '../../components/heading';
|
||||
import { Heading, SubHeading } from '../../components/heading';
|
||||
import { ExternalLinks } from '@vegaprotocol/react-helpers';
|
||||
import { useAppState } from '../../contexts/app-state/app-state-context';
|
||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||
@ -32,7 +32,7 @@ const Home = ({ name }: RouteChildProps) => {
|
||||
/>
|
||||
</HomeSection>
|
||||
<HomeSection>
|
||||
<h2>{t('Token Vesting')}</h2>
|
||||
<SubHeading title={t('Token Vesting')} />
|
||||
<p>
|
||||
{t(
|
||||
'The vesting contract holds VEGA tokens until they have become unlocked.'
|
||||
@ -68,7 +68,7 @@ const Home = ({ name }: RouteChildProps) => {
|
||||
</Link>
|
||||
</HomeSection>
|
||||
<HomeSection>
|
||||
<h2 className="uppercase">{t('Use your Vega tokens')}</h2>
|
||||
<SubHeading title={t('Use your Vega tokens')} />
|
||||
<p>
|
||||
{t(
|
||||
'To use your tokens on the Vega network they need to be associated with a Vega wallet/key.'
|
||||
@ -103,7 +103,7 @@ const Home = ({ name }: RouteChildProps) => {
|
||||
<div className="flex gap-12">
|
||||
<div className="flex-1">
|
||||
<HomeSection>
|
||||
<h2>{t('Staking')}</h2>
|
||||
<SubHeading title={t('Staking')} />
|
||||
<p>
|
||||
{t(
|
||||
'VEGA token holders can nominate a validator node and receive staking rewards.'
|
||||
@ -120,7 +120,7 @@ const Home = ({ name }: RouteChildProps) => {
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<HomeSection>
|
||||
<h2>{t('Governance')}</h2>
|
||||
<SubHeading title={t('Governance')} />
|
||||
<p>
|
||||
{t(
|
||||
'VEGA token holders can vote on proposed changes to the network and create proposals.'
|
||||
@ -143,5 +143,5 @@ const Home = ({ name }: RouteChildProps) => {
|
||||
export default Home;
|
||||
|
||||
export const HomeSection = ({ children }: { children: React.ReactNode }) => {
|
||||
return <section className="mb-8">{children}</section>;
|
||||
return <section className="mb-12">{children}</section>;
|
||||
};
|
||||
|
@ -2,7 +2,11 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Callout, Link, Intent, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
KeyValueTable,
|
||||
KeyValueTableRow,
|
||||
RoundedWrapper,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useTranches } from '../../../hooks/use-tranches';
|
||||
import type { BigNumber } from '../../../lib/bignumber';
|
||||
import { formatNumber } from '../../../lib/format-number';
|
||||
@ -43,48 +47,50 @@ export const TokenDetails = ({
|
||||
|
||||
return (
|
||||
<div className="token-details">
|
||||
<KeyValueTable>
|
||||
<KeyValueTableRow>
|
||||
{t('Token address').toUpperCase()}
|
||||
<Link
|
||||
data-testid="token-address"
|
||||
title={t('View on Etherscan (opens in a new tab)')}
|
||||
className="font-mono text-white text-right"
|
||||
href={`${ETHERSCAN_URL}/address/${token.address}`}
|
||||
target="_blank"
|
||||
>
|
||||
{token.address}
|
||||
</Link>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('Vesting contract').toUpperCase()}
|
||||
<Link
|
||||
data-testid="token-contract"
|
||||
title={t('View on Etherscan (opens in a new tab)')}
|
||||
className="font-mono text-white text-right"
|
||||
href={`${ETHERSCAN_URL}/address/${config.token_vesting_contract.address}`}
|
||||
target="_blank"
|
||||
>
|
||||
{config.token_vesting_contract.address}
|
||||
</Link>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('Total supply').toUpperCase()}
|
||||
<span className="font-mono" data-testid="total-supply">
|
||||
{formatNumber(totalSupply, 2)}
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('Circulating supply').toUpperCase()}
|
||||
<TokenDetailsCirculating tranches={tranches} />
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('Staked on Vega validator').toUpperCase()}
|
||||
<span data-testid="staked" className="font-mono">
|
||||
{formatNumber(totalAssociated, 2)}
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
</KeyValueTable>
|
||||
<RoundedWrapper>
|
||||
<KeyValueTable>
|
||||
<KeyValueTableRow>
|
||||
{t('Token address').toUpperCase()}
|
||||
<Link
|
||||
data-testid="token-address"
|
||||
title={t('View on Etherscan (opens in a new tab)')}
|
||||
className="font-mono text-white text-right"
|
||||
href={`${ETHERSCAN_URL}/address/${token.address}`}
|
||||
target="_blank"
|
||||
>
|
||||
{token.address}
|
||||
</Link>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('Vesting contract').toUpperCase()}
|
||||
<Link
|
||||
data-testid="token-contract"
|
||||
title={t('View on Etherscan (opens in a new tab)')}
|
||||
className="font-mono text-white text-right"
|
||||
href={`${ETHERSCAN_URL}/address/${config.token_vesting_contract.address}`}
|
||||
target="_blank"
|
||||
>
|
||||
{config.token_vesting_contract.address}
|
||||
</Link>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('Total supply').toUpperCase()}
|
||||
<span className="font-mono" data-testid="total-supply">
|
||||
{formatNumber(totalSupply, 2)}
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('Circulating supply').toUpperCase()}
|
||||
<TokenDetailsCirculating tranches={tranches} />
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow noBorder={true}>
|
||||
{t('Staked on Vega validator').toUpperCase()}
|
||||
<span data-testid="staked" className="font-mono">
|
||||
{formatNumber(totalAssociated, 2)}
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
</KeyValueTable>
|
||||
</RoundedWrapper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -2,8 +2,12 @@ import { useEthereumConfig } from '@vegaprotocol/web3';
|
||||
import { StakingWalletsContainer } from './components/staking-wallets-container/staking-wallets-container';
|
||||
import { AssociatePage } from './associate-page';
|
||||
import { AssociatePageNoVega } from './associate-page-no-vega';
|
||||
import { Heading } from '../../../components/heading';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const AssociateContainer = () => {
|
||||
const { t } = useTranslation();
|
||||
const { config } = useEthereumConfig();
|
||||
|
||||
if (!config) {
|
||||
@ -11,19 +15,22 @@ export const AssociateContainer = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<StakingWalletsContainer>
|
||||
{({ address, pubKey }) =>
|
||||
pubKey ? (
|
||||
<AssociatePage
|
||||
address={address}
|
||||
vegaKey={pubKey}
|
||||
ethereumConfig={config}
|
||||
/>
|
||||
) : (
|
||||
<AssociatePageNoVega />
|
||||
)
|
||||
}
|
||||
</StakingWalletsContainer>
|
||||
<>
|
||||
<Heading title={t('pageTitleAssociate')} />
|
||||
<StakingWalletsContainer>
|
||||
{({ address, pubKey }) =>
|
||||
pubKey ? (
|
||||
<AssociatePage
|
||||
address={address}
|
||||
vegaKey={pubKey}
|
||||
ethereumConfig={config}
|
||||
/>
|
||||
) : (
|
||||
<AssociatePageNoVega />
|
||||
)
|
||||
}
|
||||
</StakingWalletsContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -2,16 +2,25 @@ import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import { EthConnectPrompt } from '../../../components/eth-connect-prompt';
|
||||
import { DisassociatePage } from './components/disassociate-page';
|
||||
import { Heading } from '../../../components/heading';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const DisassociateContainer = () => {
|
||||
const { t } = useTranslation();
|
||||
const { account } = useWeb3React();
|
||||
const { pubKey } = useVegaWallet();
|
||||
|
||||
if (!account) {
|
||||
return <EthConnectPrompt />;
|
||||
}
|
||||
|
||||
return <DisassociatePage address={account} vegaKey={pubKey ?? ''} />;
|
||||
return (
|
||||
<>
|
||||
<Heading title={t('pageTitleDisassociate')} />
|
||||
{!account ? (
|
||||
<EthConnectPrompt />
|
||||
) : (
|
||||
<DisassociatePage address={account} vegaKey={pubKey ?? ''} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DisassociateContainer;
|
||||
|
@ -45,7 +45,7 @@ export const EpochData = () => {
|
||||
{data?.epoch &&
|
||||
data.epoch.timestamps.start &&
|
||||
data?.epoch.timestamps.expiry && (
|
||||
<div className="mb-8">
|
||||
<div className="mb-10">
|
||||
<EpochCountdown
|
||||
id={data.epoch.id}
|
||||
startDate={new Date(data.epoch.timestamps.start)}
|
||||
|
@ -1,42 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { StakingIntro } from './staking-intro';
|
||||
|
||||
jest.mock('@vegaprotocol/environment', () => ({
|
||||
useEnvironment: () => ({
|
||||
VEGA_DOCS_URL: 'https://docs.vega.xyz',
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('Staking', () => {
|
||||
it('should render the component', () => {
|
||||
render(
|
||||
<Router>
|
||||
<StakingIntro />
|
||||
</Router>
|
||||
);
|
||||
expect(screen.getByTestId('staking-intro')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('callout')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
'1. VEGA is an ERC20 token. Associate it with a Vega wallet using the'
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId('staking-associate-link')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
'2. Use this site and your Vega wallet to nominate a validator. View the validator profile pitches and discussion'
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId('validator-forum-link')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
'3. Earn a share of trading fees and treasury rewards for each full epoch staked'
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('4. Move your stake if your validator is penalised')
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId('staking-guide-link')).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -1,60 +0,0 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Button,
|
||||
Callout,
|
||||
Intent,
|
||||
Link as UTLink,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { createDocsLinks, ExternalLinks } from '@vegaprotocol/react-helpers';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import Routes from '../../../routes/routes';
|
||||
|
||||
export const StakingIntro = () => {
|
||||
const { t } = useTranslation();
|
||||
const { VEGA_DOCS_URL } = useEnvironment();
|
||||
|
||||
return (
|
||||
<section className="mb-8" data-testid="staking-intro">
|
||||
<Callout
|
||||
intent={Intent.Primary}
|
||||
iconName="help"
|
||||
title={t('stakingDescriptionTitle')}
|
||||
>
|
||||
<ol className="mb-4">
|
||||
<li>
|
||||
{t('stakingDescription1')}{' '}
|
||||
<Link
|
||||
to={Routes.ASSOCIATE}
|
||||
className="underline"
|
||||
data-testid="staking-associate-link"
|
||||
>
|
||||
{t('stakingBridge')}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
{t('stakingDescription2a')} {t('stakingDescription2b')}{' '}
|
||||
<UTLink
|
||||
href={ExternalLinks.VALIDATOR_FORUM}
|
||||
target="_blank"
|
||||
data-testid="validator-forum-link"
|
||||
>
|
||||
{t('onTheForum')}
|
||||
</UTLink>
|
||||
</li>
|
||||
<li>{t('stakingDescription3')}</li>
|
||||
<li>{t('stakingDescription4')}</li>
|
||||
</ol>
|
||||
{VEGA_DOCS_URL && (
|
||||
<UTLink
|
||||
href={createDocsLinks(VEGA_DOCS_URL).STAKING_GUIDE}
|
||||
target="_blank"
|
||||
data-testid="staking-guide-link"
|
||||
>
|
||||
<Button>{t('readMoreStaking')}</Button>
|
||||
</UTLink>
|
||||
)}
|
||||
</Callout>
|
||||
</section>
|
||||
);
|
||||
};
|
@ -1,15 +1,33 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StakingIntro } from './staking-intro';
|
||||
import { EpochData } from './epoch-data';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { createDocsLinks } from '@vegaprotocol/react-helpers';
|
||||
import { Heading } from '../../../components/heading';
|
||||
import React from 'react';
|
||||
|
||||
export const Staking = () => {
|
||||
const { t } = useTranslation();
|
||||
const { VEGA_DOCS_URL } = useEnvironment();
|
||||
|
||||
return (
|
||||
<>
|
||||
<StakingIntro />
|
||||
<Heading title={t('pageTitleValidators')} />
|
||||
<section>
|
||||
<h2 className="text-2xl uppercase">{t('Validator nodes')}</h2>
|
||||
<p className="mb-12">
|
||||
{t('stakingIntro')}{' '}
|
||||
{VEGA_DOCS_URL && (
|
||||
<ExternalLink
|
||||
href={createDocsLinks(VEGA_DOCS_URL).STAKING_GUIDE}
|
||||
target="_blank"
|
||||
data-testid="staking-guide-link"
|
||||
className="text-white"
|
||||
>
|
||||
{t('readMoreStaking')}
|
||||
</ExternalLink>
|
||||
)}
|
||||
</p>
|
||||
|
||||
<EpochData data-testid="epoch-data" />
|
||||
</section>
|
||||
</>
|
||||
|
@ -212,10 +212,6 @@ describe('Consensus validators table', () => {
|
||||
|
||||
const grid = screen.getByTestId('consensus-validators-table');
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId('show-all-validators'));
|
||||
});
|
||||
|
||||
expect(
|
||||
grid.querySelector('[role="gridcell"][col-id="validator"]')
|
||||
).toHaveTextContent('T-800 Terminator');
|
||||
|
@ -1,3 +1,4 @@
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import { forwardRef, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
@ -25,22 +26,87 @@ import {
|
||||
VotingPowerRenderer,
|
||||
} from './shared';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import type { ColDef } from 'ag-grid-community';
|
||||
import type { ColDef, RowHeightParams } from 'ag-grid-community';
|
||||
import type { ValidatorsTableProps } from './shared';
|
||||
import { formatNumber, toBigNum } from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
formatNumber,
|
||||
formatNumberPercentage,
|
||||
toBigNum,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
|
||||
interface CanonisedConsensusNodeProps {
|
||||
id: string;
|
||||
[ValidatorFields.RANKING_INDEX]: number;
|
||||
[ValidatorFields.VALIDATOR]: {
|
||||
avatarUrl: string | null | undefined;
|
||||
name: string;
|
||||
};
|
||||
[ValidatorFields.STAKE]: string;
|
||||
[ValidatorFields.STAKE_SHARE]: string;
|
||||
[ValidatorFields.PENDING_STAKE]: string;
|
||||
[ValidatorFields.NORMALISED_VOTING_POWER]: string;
|
||||
[ValidatorFields.UNNORMALISED_VOTING_POWER]: string | null;
|
||||
[ValidatorFields.STAKE_SHARE]: string;
|
||||
[ValidatorFields.STAKED_BY_DELEGATES]: string;
|
||||
[ValidatorFields.STAKED_BY_OPERATOR]: string;
|
||||
[ValidatorFields.PERFORMANCE_SCORE]: string;
|
||||
[ValidatorFields.PERFORMANCE_PENALTY]: string;
|
||||
[ValidatorFields.OVERSTAKED_AMOUNT]: string;
|
||||
[ValidatorFields.OVERSTAKING_PENALTY]: string;
|
||||
[ValidatorFields.TOTAL_PENALTIES]: string;
|
||||
[ValidatorFields.PENDING_STAKE]: string;
|
||||
}
|
||||
|
||||
const getRowHeight = (params: RowHeightParams) => {
|
||||
if (params.data.placeholderForTopThird) {
|
||||
// Note: this value will change if the height of the top third cell renderer changes
|
||||
return 138;
|
||||
}
|
||||
return 52;
|
||||
};
|
||||
|
||||
const TopThirdCellRenderer = (
|
||||
// @ts-ignore no exported type that matches params from AG-grid
|
||||
params,
|
||||
setHideTopThird: Dispatch<SetStateAction<boolean>>
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<a
|
||||
href="/"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setHideTopThird(false);
|
||||
}}
|
||||
className="grid grid-cols-[60px_1fr] w-full h-full py-4 px-0 text-sm text-white text-center overflow-scroll"
|
||||
>
|
||||
<div className="text-xs text-left px-3">
|
||||
{params?.data?.rankingDisplay}
|
||||
</div>
|
||||
<div className="whitespace-normal px-3">
|
||||
<div className="mb-4">
|
||||
<Button
|
||||
data-testid="show-all-validators"
|
||||
rightIcon="arrow-right"
|
||||
className="inline-flex items-center"
|
||||
>
|
||||
{t('Reveal top validators')}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="font-semibold text-white mb-0">
|
||||
{t(
|
||||
'Validators with too great a stake share will have the staking rewards for their delegators penalised.'
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{t(
|
||||
'To avoid penalties and increase decentralisation of the network, delegate to validators below.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export const ConsensusValidatorsTable = ({
|
||||
data,
|
||||
previousEpochData,
|
||||
@ -147,24 +213,86 @@ export const ConsensusValidatorsTable = ({
|
||||
// 1/3 of the validators and we really want people not to stake any more to
|
||||
// that group, because we want to make it require as many difference
|
||||
// validators to collude as possible to halt the network, so we hide them.
|
||||
const removeTopThirdOfStakeScores = canonisedNodes.reduce(
|
||||
const { topThird, remaining } = canonisedNodes.reduce(
|
||||
(acc, node) => {
|
||||
if (acc.cumulativeScore < 3333) {
|
||||
acc.cumulativeScore += Number(
|
||||
if (acc.cumulativeScore < 100 / 3) {
|
||||
acc.cumulativeScore += parseFloat(
|
||||
node[ValidatorFields.NORMALISED_VOTING_POWER]
|
||||
);
|
||||
acc.topThird.push(node);
|
||||
return acc;
|
||||
}
|
||||
acc.remaining.push(node);
|
||||
return acc;
|
||||
},
|
||||
{ remaining: [], cumulativeScore: 0 } as {
|
||||
{ topThird: [], remaining: [], cumulativeScore: 0 } as {
|
||||
topThird: CanonisedConsensusNodeProps[];
|
||||
remaining: CanonisedConsensusNodeProps[];
|
||||
cumulativeScore: number;
|
||||
}
|
||||
);
|
||||
|
||||
return removeTopThirdOfStakeScores.remaining;
|
||||
// We need to combine the top third of validators into a single node, this
|
||||
// way the combined values can be passed to AG grid so that the combined cell's
|
||||
// values are correct for ordering.
|
||||
const combinedTopThird = topThird.reduce((acc, node) => {
|
||||
const {
|
||||
[ValidatorFields.STAKE]: stake,
|
||||
[ValidatorFields.STAKE_SHARE]: stakeShare,
|
||||
[ValidatorFields.PENDING_STAKE]: pendingStake,
|
||||
[ValidatorFields.NORMALISED_VOTING_POWER]: normalisedVotingPower,
|
||||
[ValidatorFields.TOTAL_PENALTIES]: totalPenalties,
|
||||
} = node;
|
||||
|
||||
const {
|
||||
[ValidatorFields.STAKE]: accStake,
|
||||
[ValidatorFields.STAKE_SHARE]: accStakeShare,
|
||||
[ValidatorFields.PENDING_STAKE]: accPendingStake,
|
||||
[ValidatorFields.NORMALISED_VOTING_POWER]: accNormalisedVotingPower,
|
||||
[ValidatorFields.TOTAL_PENALTIES]: accTotalPenalties,
|
||||
} = acc;
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[ValidatorFields.STAKE]: formatNumber(
|
||||
toBigNum(accStake, decimals).plus(toBigNum(stake, decimals)),
|
||||
2
|
||||
),
|
||||
[ValidatorFields.STAKE_SHARE]: formatNumberPercentage(
|
||||
new BigNumber(parseFloat(accStakeShare)).plus(
|
||||
new BigNumber(parseFloat(stakeShare))
|
||||
),
|
||||
2
|
||||
),
|
||||
[ValidatorFields.PENDING_STAKE]: formatNumber(
|
||||
toBigNum(accPendingStake, decimals).plus(
|
||||
toBigNum(pendingStake, decimals)
|
||||
),
|
||||
2
|
||||
),
|
||||
[ValidatorFields.NORMALISED_VOTING_POWER]: formatNumberPercentage(
|
||||
new BigNumber(parseFloat(accNormalisedVotingPower)).plus(
|
||||
new BigNumber(parseFloat(normalisedVotingPower))
|
||||
),
|
||||
2
|
||||
),
|
||||
[ValidatorFields.TOTAL_PENALTIES]: formatNumberPercentage(
|
||||
new BigNumber(parseFloat(accTotalPenalties)).plus(
|
||||
new BigNumber(parseFloat(totalPenalties))
|
||||
),
|
||||
2
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
placeholderForTopThird: true,
|
||||
rankingDisplay: topThird.length === 1 ? '1' : `1 - ${topThird.length}`,
|
||||
...combinedTopThird,
|
||||
},
|
||||
...remaining,
|
||||
];
|
||||
}, [data, decimals, hideTopThird, previousEpochData, totalStake]);
|
||||
|
||||
const ConsensusTable = forwardRef<AgGridReact>((_, gridRef) => {
|
||||
@ -173,7 +301,7 @@ export const ConsensusValidatorsTable = ({
|
||||
{
|
||||
field: ValidatorFields.RANKING_INDEX,
|
||||
headerName: '#',
|
||||
width: 40,
|
||||
width: 60,
|
||||
pinned: 'left',
|
||||
},
|
||||
{
|
||||
@ -227,36 +355,12 @@ export const ConsensusValidatorsTable = ({
|
||||
|
||||
return (
|
||||
<div data-testid="consensus-validators-table">
|
||||
{hideTopThird && (
|
||||
<div className="mb-6 py-4 px-4 md:px-12 bg-neutral-900 text-sm text-center">
|
||||
<div className="mb-4">
|
||||
<Button
|
||||
data-testid="show-all-validators"
|
||||
icon="list"
|
||||
className="inline-flex items-center"
|
||||
onClick={() => setHideTopThird(false)}
|
||||
>
|
||||
{t('Reveal top validators')}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="font-semibold">
|
||||
{t(
|
||||
'Validators with too great a stake share will have the staking rewards for their delegators penalised.'
|
||||
)}
|
||||
</p>
|
||||
<p className="mb-0">
|
||||
{t(
|
||||
'To avoid penalties and increase decentralisation of the network, delegate to validators below.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{nodes.length > 0 && (
|
||||
<AgGrid
|
||||
domLayout="autoHeight"
|
||||
style={{ width: '100%' }}
|
||||
customThemeParams={NODE_LIST_GRID_STYLES}
|
||||
rowHeight={52}
|
||||
getRowHeight={(params: RowHeightParams) => getRowHeight(params)}
|
||||
defaultColDef={defaultColDef}
|
||||
animateRows={true}
|
||||
suppressCellFocus={true}
|
||||
@ -267,6 +371,13 @@ export const ConsensusValidatorsTable = ({
|
||||
onCellClicked={(event) => {
|
||||
navigate(event.data.id);
|
||||
}}
|
||||
isFullWidthRow={(params) =>
|
||||
params.rowNode.data.placeholderForTopThird
|
||||
}
|
||||
// @ts-ignore no exported type that matches params from AG-grid
|
||||
fullWidthCellRenderer={(params) =>
|
||||
TopThirdCellRenderer(params, setHideTopThird)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -57,7 +57,7 @@ export const defaultColDef = {
|
||||
resizable: true,
|
||||
autoHeight: true,
|
||||
comparator: (a: string, b: string) => parseFloat(a) - parseFloat(b),
|
||||
cellStyle: { margin: '10px 0' },
|
||||
cellStyle: { margin: '10px 0', padding: '0 12px' },
|
||||
tooltipComponent: TooltipCellComponent,
|
||||
tooltipShowDelay: 0,
|
||||
tooltipHideDelay: 0,
|
||||
|
@ -3,11 +3,6 @@ import { Trans, useTranslation } from 'react-i18next';
|
||||
import { ConsensusValidatorsTable } from './consensus-validators-table';
|
||||
import { StandbyPendingValidatorsTable } from './standby-pending-validators-table';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type {
|
||||
NodesQuery,
|
||||
NodesFragmentFragment,
|
||||
} from '../__generated___/Nodes';
|
||||
import type { PreviousEpochQuery } from '../../__generated___/PreviousEpoch';
|
||||
import { formatNumber } from '../../../../lib/format-number';
|
||||
import {
|
||||
createDocsLinks,
|
||||
@ -15,8 +10,14 @@ import {
|
||||
toBigNum,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { Link as UTLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { SubHeading } from '../../../../components/heading';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { useAppState } from '../../../../contexts/app-state/app-state-context';
|
||||
import type {
|
||||
NodesQuery,
|
||||
NodesFragmentFragment,
|
||||
} from '../__generated___/Nodes';
|
||||
import type { PreviousEpochQuery } from '../../__generated___/PreviousEpoch';
|
||||
|
||||
export interface ValidatorsTableProps {
|
||||
data: NodesQuery | undefined;
|
||||
@ -99,7 +100,7 @@ export const ValidatorTables = ({
|
||||
<div data-testid="validator-tables">
|
||||
{consensusValidators.length > 0 && (
|
||||
<>
|
||||
<h2>{t('status-tendermint')}</h2>
|
||||
<SubHeading title={t('status-tendermint')} />
|
||||
<ConsensusValidatorsTable
|
||||
data={consensusValidators}
|
||||
previousEpochData={previousEpochData}
|
||||
@ -109,7 +110,7 @@ export const ValidatorTables = ({
|
||||
)}
|
||||
{standbyValidators.length > 0 && (
|
||||
<>
|
||||
<h2>{t('status-ersatz')}</h2>
|
||||
<SubHeading title={t('status-ersatz')} />
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="ersatzDescription"
|
||||
@ -129,7 +130,7 @@ export const ValidatorTables = ({
|
||||
)}
|
||||
{pendingValidators.length > 0 && (
|
||||
<>
|
||||
<h2>{t('status-pending')}</h2>
|
||||
<SubHeading title={t('status-pending')} />
|
||||
<p>
|
||||
{VEGA_DOCS_URL && (
|
||||
<>
|
||||
|
@ -1,32 +1,11 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Outlet, useMatch } from 'react-router-dom';
|
||||
|
||||
import { Heading } from '../../components/heading';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||
import type { RouteChildProps } from '..';
|
||||
|
||||
const StakingRouter = ({ name }: RouteChildProps) => {
|
||||
useDocumentTitle(name);
|
||||
const { t } = useTranslation();
|
||||
const associate = useMatch('/validators/associate');
|
||||
const disassociate = useMatch('/validators/disassociate');
|
||||
|
||||
const title = React.useMemo(() => {
|
||||
if (associate) {
|
||||
return t('pageTitleAssociate');
|
||||
} else if (disassociate) {
|
||||
return t('pageTitleDisassociate');
|
||||
}
|
||||
return t('pageTitleStaking');
|
||||
}, [associate, disassociate, t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading title={title} />
|
||||
<Outlet />
|
||||
</>
|
||||
);
|
||||
return <Outlet />;
|
||||
};
|
||||
|
||||
export default StakingRouter;
|
||||
|
@ -8,6 +8,8 @@ import {
|
||||
removePaginationWrapper,
|
||||
toBigNum,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Icon } from '@vegaprotocol/ui-toolkit';
|
||||
import { EpochCountdown } from '../../../components/epoch-countdown';
|
||||
import { BigNumber } from '../../../lib/bignumber';
|
||||
import { ConnectToVega } from '../../../components/connect-to-vega';
|
||||
@ -16,6 +18,8 @@ import { ValidatorTable } from './validator-table';
|
||||
import { YourStake } from './your-stake';
|
||||
import NodeContainer from './nodes-container';
|
||||
import { useAppState } from '../../../contexts/app-state/app-state-context';
|
||||
import { Heading, SubHeading } from '../../../components/heading';
|
||||
import Routes from '../../routes';
|
||||
import type { StakingQuery } from './__generated___/Staking';
|
||||
import type { PreviousEpochQuery } from '../__generated___/PreviousEpoch';
|
||||
|
||||
@ -100,11 +104,18 @@ export const StakingNode = ({ data, previousEpochData }: StakingNodeProps) => {
|
||||
|
||||
return (
|
||||
<div data-testid="staking-node">
|
||||
<h2 data-test-id="validator-node-title" className="text-2xl break-word">
|
||||
{nodeInfo.name
|
||||
? t('validatorTitle', { nodeName: nodeInfo.name })
|
||||
: t('validatorTitle', { nodeName: t('validatorTitleFallback') })}
|
||||
</h2>
|
||||
<div className="flex items-center gap-1">
|
||||
<Icon name={'chevron-left'} />
|
||||
<Link className="underline" to={Routes.VALIDATORS}>
|
||||
{t('All validators')}
|
||||
</Link>
|
||||
</div>
|
||||
<Heading
|
||||
title={
|
||||
nodeInfo.name ||
|
||||
t('validatorTitle', { nodeName: t('validatorTitleFallback') })
|
||||
}
|
||||
/>
|
||||
<section className="mb-4">
|
||||
<ValidatorTable
|
||||
node={nodeInfo}
|
||||
@ -113,7 +124,7 @@ export const StakingNode = ({ data, previousEpochData }: StakingNodeProps) => {
|
||||
/>
|
||||
</section>
|
||||
{data?.epoch.timestamps.start && data?.epoch.timestamps.expiry && (
|
||||
<section className="mb-4">
|
||||
<section className="mb-10">
|
||||
<EpochCountdown
|
||||
id={data.epoch.id}
|
||||
startDate={new Date(data?.epoch.timestamps.start)}
|
||||
@ -142,7 +153,7 @@ export const StakingNode = ({ data, previousEpochData }: StakingNodeProps) => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<h2>{t('Connect to see your stake')}</h2>
|
||||
<SubHeading title={t('Connect to see your stake')} />
|
||||
<ConnectToVega />
|
||||
</>
|
||||
)}
|
||||
|
@ -3,7 +3,6 @@ import * as Sentry from '@sentry/react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { usePartyDelegationsLazyQuery } from './__generated___/PartyDelegations';
|
||||
import { TokenInput } from '../../../components/token-input';
|
||||
import { useAppState } from '../../../contexts/app-state/app-state-context';
|
||||
@ -16,11 +15,6 @@ import {
|
||||
Radio,
|
||||
RadioGroup,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import type {
|
||||
DelegateSubmissionBody,
|
||||
UndelegateSubmissionBody,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
useNetworkParam,
|
||||
NetworkParams,
|
||||
@ -29,6 +23,12 @@ import {
|
||||
removePaginationWrapper,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { useBalances } from '../../../lib/balances/balances-store';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { SubHeading } from '../../../components/heading';
|
||||
import type {
|
||||
DelegateSubmissionBody,
|
||||
UndelegateSubmissionBody,
|
||||
} from '@vegaprotocol/wallet';
|
||||
|
||||
export enum FormState {
|
||||
Default,
|
||||
@ -186,15 +186,17 @@ export const StakingForm = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>{t('Manage your stake')}</h2>
|
||||
<SubHeading title={t('Manage your stake')} />
|
||||
{formState === FormState.Default &&
|
||||
availableStakeToAdd.isEqualTo(0) &&
|
||||
availableStakeToRemove.isEqualTo(0) && (
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
{lien.isGreaterThan(0) ? (
|
||||
<span className="text-red">{t('stakeNodeWrongVegaKey')}</span>
|
||||
<span className="text-vega-red">
|
||||
{t('stakeNodeWrongVegaKey')}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-red">{t('stakeNodeNone')}</span>
|
||||
<span className="text-vega-orange">{t('stakeNodeNone')}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
@ -4,12 +4,17 @@ 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 { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
KeyValueTable,
|
||||
KeyValueTableRow,
|
||||
RoundedWrapper,
|
||||
} 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 { SubHeading } from '../../../components/heading';
|
||||
import {
|
||||
getFormattedPerformanceScore,
|
||||
getNormalisedVotingPower,
|
||||
@ -89,32 +94,37 @@ export const ValidatorTable = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mb-8" data-testid="validator-table">
|
||||
<KeyValueTable data-testid="validator-table-profile" title={t('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>
|
||||
<span>
|
||||
<strong>{t('STATUS')}</strong>
|
||||
</span>
|
||||
<span data-testid="validator-status">
|
||||
<strong>{t(statusTranslationKey(node.rankingScore.status))}</strong>
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
</KeyValueTable>
|
||||
<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>
|
||||
|
||||
<div className="mb-6 text-sm">
|
||||
{t('stakingDescription2b')}{' '}
|
||||
<div className="mt-[-1.5rem] mb-10">
|
||||
{t('validatorTableIntro')}{' '}
|
||||
<UTLink
|
||||
href={ExternalLinks.VALIDATOR_FORUM}
|
||||
target="_blank"
|
||||
@ -124,126 +134,132 @@ export const ValidatorTable = ({
|
||||
</UTLink>
|
||||
</div>
|
||||
|
||||
<KeyValueTable data-testid="validator-table-address" title={t('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>
|
||||
<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>
|
||||
<SubHeading title={t('ADDRESS')} />
|
||||
<RoundedWrapper>
|
||||
<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>
|
||||
|
||||
<KeyValueTable data-testid="validator-table-stake" title={t('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>
|
||||
<span>{t('STAKE SHARE')}</span>
|
||||
<span data-testid="stake-percentage">{stakePercentage}</span>
|
||||
</KeyValueTableRow>
|
||||
</KeyValueTable>
|
||||
<SubHeading title={t('STAKE')} />
|
||||
<RoundedWrapper>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
{getOverstakingPenalty(overstakedAmount, node.stakedTotal)}
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
<span>{t('PERFORMANCE SCORE')}</span>
|
||||
<span>
|
||||
{getFormattedPerformanceScore(
|
||||
node.rankingScore.performanceScore
|
||||
).toString()}
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
<span>{t('PERFORMANCE PENALITY')}</span>
|
||||
<span>
|
||||
{getPerformancePenalty(node.rankingScore.performanceScore)}
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
<span>
|
||||
<strong>{t('TOTAL PENALTIES')}</strong>
|
||||
</span>
|
||||
<span>
|
||||
<strong>{totalPenaltiesAmount}</strong>
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
</KeyValueTable>
|
||||
<SubHeading title={t('PENALTIES')} />
|
||||
<RoundedWrapper>
|
||||
<KeyValueTable data-testid="validator-table-penalties">
|
||||
<KeyValueTableRow>
|
||||
<span>{t('OVERSTAKED AMOUNT')}</span>
|
||||
<span>{overstakedAmount.toString()}</span>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
<span>{t('OVERSTAKED PENALTY')}</span>
|
||||
<span>
|
||||
{getOverstakingPenalty(overstakedAmount, node.stakedTotal)}
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
<span>{t('PERFORMANCE SCORE')}</span>
|
||||
<span>
|
||||
{getFormattedPerformanceScore(
|
||||
node.rankingScore.performanceScore
|
||||
).toString()}
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
<span>{t('PERFORMANCE PENALITY')}</span>
|
||||
<span>
|
||||
{getPerformancePenalty(node.rankingScore.performanceScore)}
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow noBorder={true}>
|
||||
<span>
|
||||
<strong>{t('TOTAL PENALTIES')}</strong>
|
||||
</span>
|
||||
<span>
|
||||
<strong>{totalPenaltiesAmount}</strong>
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
</KeyValueTable>
|
||||
</RoundedWrapper>
|
||||
|
||||
<KeyValueTable
|
||||
data-testid="validator-table-voting-power"
|
||||
title={t('VOTING POWER')}
|
||||
>
|
||||
<KeyValueTableRow>
|
||||
<span>{t('UNNORMALISED VOTING POWER')}</span>
|
||||
<span>{getUnnormalisedVotingPower(validatorScore)}</span>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
<span>
|
||||
<strong>{t('NORMALISED VOTING POWER')}</strong>
|
||||
</span>
|
||||
<span>
|
||||
<strong>
|
||||
{getNormalisedVotingPower(node.rankingScore.votingPower)}
|
||||
</strong>
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
</KeyValueTable>
|
||||
<SubHeading title={t('VOTING POWER')} />
|
||||
<RoundedWrapper>
|
||||
<KeyValueTable data-testid="validator-table-voting-power">
|
||||
<KeyValueTableRow>
|
||||
<span>{t('UNNORMALISED VOTING POWER')}</span>
|
||||
<span>{getUnnormalisedVotingPower(validatorScore)}</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>
|
||||
);
|
||||
};
|
||||
|
@ -1,7 +1,12 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
KeyValueTable,
|
||||
KeyValueTableRow,
|
||||
RoundedWrapper,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { formatNumber } from '../../../lib/format-number';
|
||||
import { SubHeading } from '../../../components/heading';
|
||||
import type { BigNumber } from '../../../lib/bignumber';
|
||||
|
||||
export interface YourStakeProps {
|
||||
@ -17,21 +22,23 @@ export const YourStake = ({
|
||||
|
||||
return (
|
||||
<div data-testid="your-stake">
|
||||
<h2>{t('Your stake')}</h2>
|
||||
<KeyValueTable>
|
||||
<KeyValueTableRow>
|
||||
{t('Your Stake On Node (This Epoch)')}
|
||||
<span data-testid="stake-this-epoch">
|
||||
{formatNumber(stakeThisEpoch)}
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('Your Stake On Node (Next Epoch)')}
|
||||
<span data-testid="stake-next-epoch">
|
||||
{formatNumber(stakeNextEpoch)}
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
</KeyValueTable>
|
||||
<SubHeading title={t('Your stake')} />
|
||||
<RoundedWrapper>
|
||||
<KeyValueTable>
|
||||
<KeyValueTableRow>
|
||||
{t('Your Stake On Node (This Epoch)')}
|
||||
<span data-testid="stake-this-epoch">
|
||||
{formatNumber(stakeThisEpoch)}
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow noBorder={true}>
|
||||
{t('Your Stake On Node (Next Epoch)')}
|
||||
<span data-testid="stake-next-epoch">
|
||||
{formatNumber(stakeNextEpoch)}
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
</KeyValueTable>
|
||||
</RoundedWrapper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -4,6 +4,7 @@ import { useWeb3React } from '@web3-react/core';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { SubHeading } from '../../components/heading';
|
||||
import { TrancheItem } from '../redemption/tranche-item';
|
||||
import { TrancheLabel } from './tranche-label';
|
||||
import { VestingChart } from './vesting-chart';
|
||||
@ -29,7 +30,7 @@ export const Tranches = () => {
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h2>{t('chartTitle')}</h2>
|
||||
<SubHeading title={t('chartTitle')} />
|
||||
<p>{t('chartAbove')}</p>
|
||||
<VestingChart />
|
||||
<p>{t('chartBelow')}</p>
|
||||
|
@ -40,3 +40,4 @@ export * from './vega-logo';
|
||||
export * from './traffic-light';
|
||||
export * from './toast';
|
||||
export * from './notification';
|
||||
export * from './rounded-wrapper';
|
||||
|
@ -44,9 +44,9 @@ Link.displayName = 'Link';
|
||||
export const ExternalLink = ({ children, className, ...props }: LinkProps) => (
|
||||
<Link
|
||||
className={classNames(className, 'inline-flex items-baseline')}
|
||||
{...props}
|
||||
target="_blank"
|
||||
data-testid="external-link"
|
||||
{...props}
|
||||
>
|
||||
{typeof children === 'string' ? (
|
||||
<>
|
||||
|
1
libs/ui-toolkit/src/components/rounded-wrapper/index.ts
Normal file
1
libs/ui-toolkit/src/components/rounded-wrapper/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './rounded-wrapper';
|
@ -0,0 +1,12 @@
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { RoundedWrapper } from './rounded-wrapper';
|
||||
|
||||
describe('Lozenge', () => {
|
||||
it('should render successfully', () => {
|
||||
const { baseElement } = render(
|
||||
<RoundedWrapper>Rounded wrapper</RoundedWrapper>
|
||||
);
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,34 @@
|
||||
import { RoundedWrapper } from './rounded-wrapper';
|
||||
import type { Story, Meta } from '@storybook/react';
|
||||
import { KeyValueTable, KeyValueTableRow } from '../key-value-table';
|
||||
|
||||
export default {
|
||||
component: RoundedWrapper,
|
||||
title: 'RoundedWrapper',
|
||||
} as Meta;
|
||||
|
||||
const Template: Story = (args) => (
|
||||
<div className="mb-8">
|
||||
<RoundedWrapper {...args} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
children: 'Rounded wrapper for presentation purposes',
|
||||
paddingBottom: true,
|
||||
};
|
||||
|
||||
export const SurroundingKeyValueTable = Template.bind({});
|
||||
SurroundingKeyValueTable.args = {
|
||||
children: (
|
||||
<KeyValueTable>
|
||||
<KeyValueTableRow>
|
||||
Item 1<span>123.45</span>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow noBorder={true}>
|
||||
Item 2<span>543.21</span>
|
||||
</KeyValueTableRow>
|
||||
</KeyValueTable>
|
||||
),
|
||||
};
|
@ -0,0 +1,23 @@
|
||||
import classnames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export interface RoundedWrapperProps {
|
||||
children?: ReactNode;
|
||||
paddingBottom?: boolean;
|
||||
}
|
||||
|
||||
export const RoundedWrapper = ({
|
||||
children,
|
||||
paddingBottom = false,
|
||||
}: RoundedWrapperProps) => (
|
||||
<div
|
||||
className={classnames(
|
||||
'rounded-xl mb-10 pt-4 px-4 border border-neutral-700',
|
||||
{
|
||||
'pb-4': paddingBottom,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
Loading…
Reference in New Issue
Block a user