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:
Sam Keen 2022-12-14 13:25:03 +00:00 committed by GitHub
parent e49ad9da6a
commit 071a9ab34b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 776 additions and 640 deletions

View File

@ -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:

View File

@ -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';

View File

@ -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');
});
}
);

View File

@ -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>
);
}

View File

@ -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>
);
};

View File

@ -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>

View File

@ -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.",

View File

@ -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>
);
};

View File

@ -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">

View File

@ -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>

View File

@ -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>
);

View File

@ -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>
</>
);
};

View File

@ -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} />

View File

@ -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) => (

View File

@ -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 />
)}

View File

@ -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>;
};

View File

@ -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>
);
};

View File

@ -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>
</>
);
};

View File

@ -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;

View File

@ -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)}

View File

@ -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();
});
});

View File

@ -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>
);
};

View File

@ -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>
</>

View File

@ -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');

View File

@ -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>

View File

@ -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,

View File

@ -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 && (
<>

View File

@ -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;

View File

@ -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 />
</>
)}

View File

@ -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>
)}

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>

View File

@ -40,3 +40,4 @@ export * from './vega-logo';
export * from './traffic-light';
export * from './toast';
export * from './notification';
export * from './rounded-wrapper';

View File

@ -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' ? (
<>

View File

@ -0,0 +1 @@
export * from './rounded-wrapper';

View File

@ -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();
});
});

View File

@ -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>
),
};

View File

@ -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>
);