feat(governance): protocol upgrade proposals list and details (#3363)
This commit is contained in:
parent
c99b7fbb02
commit
ae92ff2580
@ -15,7 +15,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display announcement banner', function () {
|
it.skip('should display announcement banner', function () {
|
||||||
cy.getByTestId('app-announcement')
|
cy.getByTestId('app-announcement')
|
||||||
.should('contain.text', 'TEST ANNOUNCEMENT!')
|
.should('contain.text', 'TEST ANNOUNCEMENT!')
|
||||||
.within(() => {
|
.within(() => {
|
||||||
|
@ -595,6 +595,7 @@
|
|||||||
"noPercentage": "No percentage",
|
"noPercentage": "No percentage",
|
||||||
"proposalTerms": "Proposal terms",
|
"proposalTerms": "Proposal terms",
|
||||||
"currentlySetTo": "Currently expected to ",
|
"currentlySetTo": "Currently expected to ",
|
||||||
|
"currently": "currently",
|
||||||
"finalOutcomeMayDiffer": "Final outcome may differ",
|
"finalOutcomeMayDiffer": "Final outcome may differ",
|
||||||
"votingPower": "Voting power",
|
"votingPower": "Voting power",
|
||||||
"normalisedVotingPower": "Normalised voting power",
|
"normalisedVotingPower": "Normalised voting power",
|
||||||
@ -767,7 +768,7 @@
|
|||||||
"performancePenalty": "Performance penalty",
|
"performancePenalty": "Performance penalty",
|
||||||
"overstaked": "Overstaked",
|
"overstaked": "Overstaked",
|
||||||
"overstakedPenalty": "Overstaked penalty",
|
"overstakedPenalty": "Overstaked penalty",
|
||||||
"homeProposalsIntro": "Decisions on the Vega network are on-chain, with tokenholders creating proposals that other tokenholders vote to approve or reject.",
|
"homeProposalsIntro": "Decisions on the Vega network are on-chain, with tokenholders creating proposals that other tokenholders vote to approve or reject. Network upgrades are proposed and approved by validators.",
|
||||||
"homeProposalsButtonText": "Browse, vote, and propose",
|
"homeProposalsButtonText": "Browse, vote, and propose",
|
||||||
"homeValidatorsIntro": "Vega runs on a delegated proof of stake blockchain, where validators earn fees for validating block transactions. Tokenholders can nominate validators by staking tokens to them.",
|
"homeValidatorsIntro": "Vega runs on a delegated proof of stake blockchain, where validators earn fees for validating block transactions. Tokenholders can nominate validators by staking tokens to them.",
|
||||||
"homeValidatorsButtonText": "Browse, and stake",
|
"homeValidatorsButtonText": "Browse, and stake",
|
||||||
@ -775,5 +776,18 @@
|
|||||||
"homeRewardsButtonText": "See rewards",
|
"homeRewardsButtonText": "See rewards",
|
||||||
"homeVegaTokenIntro": "VEGA Token is a governance asset used to make and vote on proposals, and nominate validators.",
|
"homeVegaTokenIntro": "VEGA Token is a governance asset used to make and vote on proposals, and nominate validators.",
|
||||||
"homeVegaTokenButtonText": "Manage tokens",
|
"homeVegaTokenButtonText": "Manage tokens",
|
||||||
"downloadProposalJson": "Download proposal as JSON"
|
"downloadProposalJson": "Download proposal as JSON",
|
||||||
|
"networkUpgrade": "Network Upgrade",
|
||||||
|
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_APPROVED": "Approved",
|
||||||
|
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING": "Pending",
|
||||||
|
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED": "Rejected",
|
||||||
|
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_UNSPECIFIED": "Unspecified",
|
||||||
|
"vegaRelease{release}": "Vega Release {{release}}",
|
||||||
|
"upgradeBlockHeight": "Upgrade block height",
|
||||||
|
"vegaReleaseTag": "Vega release tag",
|
||||||
|
"approvalStatus": "Approval status",
|
||||||
|
"approvers": "Approvers",
|
||||||
|
"approval (% validator voting power)": "approval (% validator voting power)",
|
||||||
|
"67% voting power required": "67% voting power required",
|
||||||
|
"Token": "Token"
|
||||||
}
|
}
|
||||||
|
@ -10,23 +10,31 @@ import {
|
|||||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||||
import { useRefreshAfterEpoch } from '../../hooks/use-refresh-after-epoch';
|
import { useRefreshAfterEpoch } from '../../hooks/use-refresh-after-epoch';
|
||||||
import { ProposalsListItem } from '../proposals/components/proposals-list-item';
|
import { ProposalsListItem } from '../proposals/components/proposals-list-item';
|
||||||
|
import { ProtocolUpgradeProposalsListItem } from '../proposals/components/protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item';
|
||||||
import Routes from '../routes';
|
import Routes from '../routes';
|
||||||
import { ExternalLinks, removePaginationWrapper } from '@vegaprotocol/utils';
|
import { ExternalLinks, removePaginationWrapper } from '@vegaprotocol/utils';
|
||||||
import { useNodesQuery } from '../staking/home/__generated__/Nodes';
|
import { useNodesQuery } from '../staking/home/__generated__/Nodes';
|
||||||
import { useProposalsQuery } from '../proposals/proposals/__generated__/Proposals';
|
import { useProposalsQuery } from '../proposals/proposals/__generated__/Proposals';
|
||||||
import { getNotRejectedProposals } from '../proposals/proposals/proposals-container';
|
import { useProtocolUpgradesQuery } from '../proposals/protocol-upgrade/__generated__/ProtocolUpgradeProposals';
|
||||||
|
import {
|
||||||
|
getNotRejectedProposals,
|
||||||
|
getNotRejectedProtocolUpgradeProposals,
|
||||||
|
} from '../proposals/proposals/proposals-container';
|
||||||
import { Heading } from '../../components/heading';
|
import { Heading } from '../../components/heading';
|
||||||
import * as Schema from '@vegaprotocol/types';
|
import * as Schema from '@vegaprotocol/types';
|
||||||
import type { RouteChildProps } from '..';
|
import type { RouteChildProps } from '..';
|
||||||
import type { ProposalFieldsFragment } from '../proposals/proposals/__generated__/Proposals';
|
import type { ProposalFieldsFragment } from '../proposals/proposals/__generated__/Proposals';
|
||||||
import type { NodesFragmentFragment } from '../staking/home/__generated__/Nodes';
|
import type { NodesFragmentFragment } from '../staking/home/__generated__/Nodes';
|
||||||
|
import type { ProtocolUpgradeProposalFieldsFragment } from '../proposals/protocol-upgrade/__generated__/ProtocolUpgradeProposals';
|
||||||
|
|
||||||
const nodesToShow = 6;
|
const nodesToShow = 6;
|
||||||
|
|
||||||
const HomeProposals = ({
|
const HomeProposals = ({
|
||||||
proposals,
|
proposals,
|
||||||
|
protocolUpgradeProposals,
|
||||||
}: {
|
}: {
|
||||||
proposals: ProposalFieldsFragment[];
|
proposals: ProposalFieldsFragment[];
|
||||||
|
protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[];
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@ -47,6 +55,10 @@ const HomeProposals = ({
|
|||||||
data-testid="home-proposal-list"
|
data-testid="home-proposal-list"
|
||||||
className="grid md:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2 gap-6"
|
className="grid md:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2 gap-6"
|
||||||
>
|
>
|
||||||
|
{protocolUpgradeProposals.map((proposal, index) => (
|
||||||
|
<ProtocolUpgradeProposalsListItem key={index} proposal={proposal} />
|
||||||
|
))}
|
||||||
|
|
||||||
{proposals.map((proposal) => (
|
{proposals.map((proposal) => (
|
||||||
<ProposalsListItem key={proposal.id} proposal={proposal} />
|
<ProposalsListItem key={proposal.id} proposal={proposal} />
|
||||||
))}
|
))}
|
||||||
@ -107,20 +119,7 @@ const HomeNodes = ({
|
|||||||
|
|
||||||
{trimmedActiveNodes.map(({ id, avatarUrl, name }) => (
|
{trimmedActiveNodes.map(({ id, avatarUrl, name }) => (
|
||||||
<div key={id} data-testid="validators" className="col-span-2">
|
<div key={id} data-testid="validators" className="col-span-2">
|
||||||
<Link to={`${Routes.VALIDATORS}/${id}`}>
|
<ValidatorDetailsLink id={id} avatarUrl={avatarUrl} name={name} />
|
||||||
<RoundedWrapper paddingBottom={true} border={false}>
|
|
||||||
<div className="flex items-center justify-center m-[-1rem] p-4 bg-neutral-900 hover:bg-neutral-800">
|
|
||||||
{avatarUrl && (
|
|
||||||
<img
|
|
||||||
className="h-6 w-6 rounded-full mr-2"
|
|
||||||
src={avatarUrl}
|
|
||||||
alt={`Avatar icon for ${name}`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<span className="text-sm">{name}</span>
|
|
||||||
</div>
|
|
||||||
</RoundedWrapper>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -136,6 +135,35 @@ const HomeNodes = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface ValidatorDetailsLinkProps {
|
||||||
|
id: string;
|
||||||
|
avatarUrl: string | null | undefined;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ValidatorDetailsLink = ({
|
||||||
|
id,
|
||||||
|
avatarUrl,
|
||||||
|
name,
|
||||||
|
}: ValidatorDetailsLinkProps) => {
|
||||||
|
return (
|
||||||
|
<Link to={`${Routes.VALIDATORS}/${id}`}>
|
||||||
|
<RoundedWrapper paddingBottom={true} border={false}>
|
||||||
|
<div className="flex items-center justify-center m-[-1rem] p-3 bg-neutral-900 hover:bg-neutral-800">
|
||||||
|
{avatarUrl && (
|
||||||
|
<img
|
||||||
|
className="h-6 w-6 rounded-full mr-2"
|
||||||
|
src={avatarUrl}
|
||||||
|
alt={`Avatar icon for ${name}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span className="text-sm">{name}</span>
|
||||||
|
</div>
|
||||||
|
</RoundedWrapper>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const GovernanceHome = ({ name }: RouteChildProps) => {
|
const GovernanceHome = ({ name }: RouteChildProps) => {
|
||||||
useDocumentTitle(name);
|
useDocumentTitle(name);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -149,6 +177,16 @@ const GovernanceHome = ({ name }: RouteChildProps) => {
|
|||||||
errorPolicy: 'ignore',
|
errorPolicy: 'ignore',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: protocolUpgradesData,
|
||||||
|
loading: protocolUpgradesLoading,
|
||||||
|
error: protocolUpgradesError,
|
||||||
|
} = useProtocolUpgradesQuery({
|
||||||
|
pollInterval: 5000,
|
||||||
|
fetchPolicy: 'network-only',
|
||||||
|
errorPolicy: 'ignore',
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: validatorsData,
|
data: validatorsData,
|
||||||
error: validatorsError,
|
error: validatorsError,
|
||||||
@ -163,11 +201,38 @@ const GovernanceHome = ({ name }: RouteChildProps) => {
|
|||||||
proposalsData
|
proposalsData
|
||||||
? getNotRejectedProposals<ProposalFieldsFragment>(
|
? getNotRejectedProposals<ProposalFieldsFragment>(
|
||||||
proposalsData.proposalsConnection
|
proposalsData.proposalsConnection
|
||||||
).slice(0, 3)
|
)
|
||||||
: [],
|
: [],
|
||||||
[proposalsData]
|
[proposalsData]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const protocolUpgradeProposals = useMemo(
|
||||||
|
() =>
|
||||||
|
protocolUpgradesData
|
||||||
|
? getNotRejectedProtocolUpgradeProposals<ProtocolUpgradeProposalFieldsFragment>(
|
||||||
|
protocolUpgradesData.protocolUpgradeProposals
|
||||||
|
).filter(
|
||||||
|
(p) =>
|
||||||
|
Number(p.upgradeBlockHeight) >
|
||||||
|
Number(protocolUpgradesData.lastBlockHeight)
|
||||||
|
)
|
||||||
|
: [],
|
||||||
|
[protocolUpgradesData]
|
||||||
|
);
|
||||||
|
|
||||||
|
const totalProposalsDesired = 4;
|
||||||
|
const protocolUpgradeProposalsToShow = protocolUpgradeProposals.slice(
|
||||||
|
0,
|
||||||
|
totalProposalsDesired
|
||||||
|
);
|
||||||
|
const proposalsToShow =
|
||||||
|
protocolUpgradeProposalsToShow.length === totalProposalsDesired
|
||||||
|
? []
|
||||||
|
: proposals.slice(
|
||||||
|
0,
|
||||||
|
totalProposalsDesired - protocolUpgradeProposalsToShow.length
|
||||||
|
);
|
||||||
|
|
||||||
const activeNodes = removePaginationWrapper(
|
const activeNodes = removePaginationWrapper(
|
||||||
validatorsData?.nodesConnection.edges
|
validatorsData?.nodesConnection.edges
|
||||||
);
|
);
|
||||||
@ -182,11 +247,14 @@ const GovernanceHome = ({ name }: RouteChildProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AsyncRenderer
|
<AsyncRenderer
|
||||||
loading={proposalsLoading || validatorsLoading}
|
loading={proposalsLoading || protocolUpgradesLoading || validatorsLoading}
|
||||||
error={proposalsError || validatorsError}
|
error={proposalsError || protocolUpgradesError || validatorsError}
|
||||||
data={proposalsData && validatorsData}
|
data={proposalsData && protocolUpgradesData && validatorsData}
|
||||||
>
|
>
|
||||||
<HomeProposals proposals={proposals} />
|
<HomeProposals
|
||||||
|
proposals={proposalsToShow}
|
||||||
|
protocolUpgradeProposals={protocolUpgradeProposalsToShow}
|
||||||
|
/>
|
||||||
|
|
||||||
<HomeNodes
|
<HomeNodes
|
||||||
activeNodes={activeNodes}
|
activeNodes={activeNodes}
|
||||||
|
@ -63,7 +63,7 @@ const renderComponent = (proposals: ProposalQuery['proposal'][]) => (
|
|||||||
<MockedProvider mocks={[networkParamsQueryMock]}>
|
<MockedProvider mocks={[networkParamsQueryMock]}>
|
||||||
<AppStateProvider>
|
<AppStateProvider>
|
||||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||||
<ProposalsList proposals={proposals} />
|
<ProposalsList proposals={proposals} protocolUpgradeProposals={[]} />
|
||||||
</VegaWalletContext.Provider>
|
</VegaWalletContext.Provider>
|
||||||
</AppStateProvider>
|
</AppStateProvider>
|
||||||
</MockedProvider>
|
</MockedProvider>
|
||||||
|
@ -3,17 +3,21 @@ import { useState } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Heading, SubHeading } from '../../../../components/heading';
|
import { Heading, SubHeading } from '../../../../components/heading';
|
||||||
import { ProposalsListItem } from '../proposals-list-item';
|
import { ProposalsListItem } from '../proposals-list-item';
|
||||||
|
import { ProtocolUpgradeProposalsListItem } from '../protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item';
|
||||||
import { ProposalsListFilter } from '../proposals-list-filter';
|
import { ProposalsListFilter } from '../proposals-list-filter';
|
||||||
import Routes from '../../../routes';
|
import Routes from '../../../routes';
|
||||||
import { Button } from '@vegaprotocol/ui-toolkit';
|
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
|
||||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
|
||||||
import { ExternalLinks } from '@vegaprotocol/utils';
|
import { ExternalLinks } from '@vegaprotocol/utils';
|
||||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||||
|
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||||
|
import type { ProtocolUpgradeProposalFieldsFragment } from '../../protocol-upgrade/__generated__/ProtocolUpgradeProposals';
|
||||||
|
|
||||||
interface ProposalsListProps {
|
interface ProposalsListProps {
|
||||||
proposals: Array<ProposalFieldsFragment | ProposalQuery['proposal']>;
|
proposals: Array<ProposalFieldsFragment | ProposalQuery['proposal']>;
|
||||||
|
protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[];
|
||||||
|
lastBlockHeight?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SortedProposalsProps {
|
interface SortedProposalsProps {
|
||||||
@ -21,7 +25,16 @@ interface SortedProposalsProps {
|
|||||||
closed: Array<ProposalFieldsFragment | ProposalQuery['proposal']>;
|
closed: Array<ProposalFieldsFragment | ProposalQuery['proposal']>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProposalsList = ({ proposals }: ProposalsListProps) => {
|
interface SortedProtocolUpgradeProposalsProps {
|
||||||
|
open: ProtocolUpgradeProposalFieldsFragment[];
|
||||||
|
closed: ProtocolUpgradeProposalFieldsFragment[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProposalsList = ({
|
||||||
|
proposals,
|
||||||
|
protocolUpgradeProposals,
|
||||||
|
lastBlockHeight,
|
||||||
|
}: ProposalsListProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [filterString, setFilterString] = useState('');
|
const [filterString, setFilterString] = useState('');
|
||||||
const sortedProposals = proposals.reduce(
|
const sortedProposals = proposals.reduce(
|
||||||
@ -39,6 +52,21 @@ export const ProposalsList = ({ proposals }: ProposalsListProps) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const sortedProtocolUpgradeProposals = protocolUpgradeProposals.reduce(
|
||||||
|
(acc: SortedProtocolUpgradeProposalsProps, proposal) => {
|
||||||
|
if (Number(proposal?.upgradeBlockHeight) > Number(lastBlockHeight)) {
|
||||||
|
acc.open.push(proposal);
|
||||||
|
} else {
|
||||||
|
acc.closed.push(proposal);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
open: [],
|
||||||
|
closed: [],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const filterPredicate = (
|
const filterPredicate = (
|
||||||
p: ProposalFieldsFragment | ProposalQuery['proposal']
|
p: ProposalFieldsFragment | ProposalQuery['proposal']
|
||||||
) =>
|
) =>
|
||||||
@ -65,7 +93,7 @@ export const ProposalsList = ({ proposals }: ProposalsListProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<p className="mb-8">
|
<p className="mb-8">
|
||||||
{t(
|
{t(
|
||||||
`The Vega network is governed by the community. View active proposals, vote on them or propose changes to the network.`
|
`The Vega network is governed by the community. View active proposals, vote on them or propose changes to the network. Network upgrades are proposed and approved by validators.`
|
||||||
)}{' '}
|
)}{' '}
|
||||||
<ExternalLink
|
<ExternalLink
|
||||||
data-testid="proposal-documentation-link"
|
data-testid="proposal-documentation-link"
|
||||||
@ -80,8 +108,15 @@ export const ProposalsList = ({ proposals }: ProposalsListProps) => {
|
|||||||
)}
|
)}
|
||||||
<section className="-mx-4 p-4 mb-8 bg-neutral-800">
|
<section className="-mx-4 p-4 mb-8 bg-neutral-800">
|
||||||
<SubHeading title={t('openProposals')} />
|
<SubHeading title={t('openProposals')} />
|
||||||
{sortedProposals.open.length > 0 ? (
|
{sortedProposals.open.length > 0 ||
|
||||||
|
sortedProtocolUpgradeProposals.open.length > 0 ? (
|
||||||
<ul data-testid="open-proposals">
|
<ul data-testid="open-proposals">
|
||||||
|
{sortedProtocolUpgradeProposals.open.map((proposal) => (
|
||||||
|
<ProtocolUpgradeProposalsListItem
|
||||||
|
key={proposal.upgradeBlockHeight}
|
||||||
|
proposal={proposal}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
{sortedProposals.open.filter(filterPredicate).map((proposal) => (
|
{sortedProposals.open.filter(filterPredicate).map((proposal) => (
|
||||||
<ProposalsListItem key={proposal?.id} proposal={proposal} />
|
<ProposalsListItem key={proposal?.id} proposal={proposal} />
|
||||||
))}
|
))}
|
||||||
@ -94,8 +129,16 @@ export const ProposalsList = ({ proposals }: ProposalsListProps) => {
|
|||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<SubHeading title={t('closedProposals')} />
|
<SubHeading title={t('closedProposals')} />
|
||||||
{sortedProposals.closed.length > 0 ? (
|
{sortedProposals.closed.length > 0 ||
|
||||||
|
sortedProtocolUpgradeProposals.closed.length > 0 ? (
|
||||||
<ul data-testid="closed-proposals">
|
<ul data-testid="closed-proposals">
|
||||||
|
{sortedProtocolUpgradeProposals.closed.map((proposal) => (
|
||||||
|
<ProtocolUpgradeProposalsListItem
|
||||||
|
key={proposal.upgradeBlockHeight}
|
||||||
|
proposal={proposal}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
{sortedProposals.closed.filter(filterPredicate).map((proposal) => (
|
{sortedProposals.closed.filter(filterPredicate).map((proposal) => (
|
||||||
<ProposalsListItem key={proposal?.id} proposal={proposal} />
|
<ProposalsListItem key={proposal?.id} proposal={proposal} />
|
||||||
))}
|
))}
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
export * from './protocol-update-proposal-detail-approvals';
|
@ -0,0 +1,84 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
RoundedWrapper,
|
||||||
|
KeyValueTable,
|
||||||
|
KeyValueTableRow,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { SubHeading } from '../../../../components/heading';
|
||||||
|
import { ValidatorDetailsLink } from '../../../home';
|
||||||
|
import { getNormalisedVotingPower } from '../../../staking/shared';
|
||||||
|
import type { NodesFragmentFragment } from '../../../staking/home/__generated__/Nodes';
|
||||||
|
|
||||||
|
export interface ProtocolUpdateProposalDetailApprovalsProps {
|
||||||
|
totalConsensusValidators: number;
|
||||||
|
// Consensus validators that have approved the proposal
|
||||||
|
consensusApprovals: NodesFragmentFragment[];
|
||||||
|
consensusApprovalsVotingPowerPercentage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProtocolUpdateProposalDetailApprovals = ({
|
||||||
|
totalConsensusValidators,
|
||||||
|
consensusApprovals,
|
||||||
|
consensusApprovalsVotingPowerPercentage,
|
||||||
|
}: ProtocolUpdateProposalDetailApprovalsProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mb-10">
|
||||||
|
<SubHeading title={t('approvalStatus')} />
|
||||||
|
<RoundedWrapper marginBottomLarge={true} paddingBottom={true}>
|
||||||
|
<KeyValueTable data-testid="protocol-upgrade-approval-status">
|
||||||
|
<KeyValueTableRow noBorder={true}>
|
||||||
|
<span>{`${consensusApprovalsVotingPowerPercentage} ${t(
|
||||||
|
'approval (% validator voting power)'
|
||||||
|
)}`}</span>
|
||||||
|
<span>({t('67% voting power required')})</span>
|
||||||
|
</KeyValueTableRow>
|
||||||
|
</KeyValueTable>
|
||||||
|
</RoundedWrapper>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-10">
|
||||||
|
<SubHeading
|
||||||
|
title={`${t('approvers')} (${
|
||||||
|
consensusApprovals.length
|
||||||
|
}/${totalConsensusValidators} validators)`}
|
||||||
|
/>
|
||||||
|
<RoundedWrapper marginBottomLarge={true} paddingBottom={true}>
|
||||||
|
<KeyValueTable data-testid="protocol-upgrade-approvers">
|
||||||
|
<KeyValueTableRow>
|
||||||
|
<div className="mb-2">{t('validator')}</div>
|
||||||
|
<div className="text-white mb-2">{t('votingPower')}</div>
|
||||||
|
</KeyValueTableRow>
|
||||||
|
|
||||||
|
{consensusApprovals.map((validator, index) => (
|
||||||
|
<KeyValueTableRow
|
||||||
|
noBorder={index === consensusApprovals?.length - 1}
|
||||||
|
key={validator.pubkey}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="-mb-3 mt-1"
|
||||||
|
data-testid={`validator-${validator.id}`}
|
||||||
|
>
|
||||||
|
<ValidatorDetailsLink
|
||||||
|
id={validator.id}
|
||||||
|
avatarUrl={validator.avatarUrl}
|
||||||
|
name={validator.name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
{getNormalisedVotingPower(
|
||||||
|
validator.rankingScore.votingPower,
|
||||||
|
2
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</KeyValueTableRow>
|
||||||
|
))}
|
||||||
|
</KeyValueTable>
|
||||||
|
</RoundedWrapper>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,86 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
import * as Schema from '@vegaprotocol/types';
|
||||||
|
import { ProtocolUpdateProposalDetailApprovals } from './protocol-update-proposal-detail-approvals';
|
||||||
|
import { getNormalisedVotingPower } from '../../../staking/shared';
|
||||||
|
import type { ProtocolUpdateProposalDetailApprovalsProps } from './protocol-update-proposal-detail-approvals';
|
||||||
|
import type { NodesFragmentFragment } from '../../../staking/home/__generated__/Nodes';
|
||||||
|
|
||||||
|
describe('ProtocolUpdateProposalDetailApprovals', () => {
|
||||||
|
const mockValidators: NodesFragmentFragment[] = [
|
||||||
|
{
|
||||||
|
id: 'ccc022b7e63a4d0a6d3a193c3940c88574060e58a184964c994998d86835a1b4',
|
||||||
|
name: 'Marvin',
|
||||||
|
avatarUrl:
|
||||||
|
'https://upload.wikimedia.org/wikipedia/en/2/25/Marvin-TV-3.jpg',
|
||||||
|
pubkey:
|
||||||
|
'6abc23391a9f888ab240415bf63d6844b03fc360be822f4a1d2cd832d87b2917',
|
||||||
|
stakedTotal: '14182454495731682635157',
|
||||||
|
stakedByOperator: '1000000000000000000000',
|
||||||
|
stakedByDelegates: '13182454495731682635157',
|
||||||
|
pendingStake: '0',
|
||||||
|
rankingScore: {
|
||||||
|
rankingScore: '0.67845061012234727427532760837568',
|
||||||
|
stakeScore: '0.3392701644525644',
|
||||||
|
performanceScore: '0.9998677767864936',
|
||||||
|
votingPower: '3500',
|
||||||
|
status: Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
|
||||||
|
__typename: 'RankingScore',
|
||||||
|
},
|
||||||
|
__typename: 'Node',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
totalConsensusValidators: 3,
|
||||||
|
consensusApprovals: mockValidators,
|
||||||
|
consensusApprovalsVotingPowerPercentage: '75%',
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderComponent = (
|
||||||
|
props: Partial<ProtocolUpdateProposalDetailApprovalsProps> = {}
|
||||||
|
) => {
|
||||||
|
const mergedProps = {
|
||||||
|
...defaultProps,
|
||||||
|
...props,
|
||||||
|
};
|
||||||
|
return render(
|
||||||
|
<BrowserRouter>
|
||||||
|
<ProtocolUpdateProposalDetailApprovals {...mergedProps} />
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
renderComponent();
|
||||||
|
expect(
|
||||||
|
screen.getByTestId('protocol-upgrade-approval-status')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByTestId('protocol-upgrade-approvers')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the correct number of consensus approvers', () => {
|
||||||
|
renderComponent();
|
||||||
|
const validators = screen.getAllByTestId(/validator-/i);
|
||||||
|
expect(validators.length).toBe(mockValidators.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the correct approval status with the required percentage', () => {
|
||||||
|
renderComponent();
|
||||||
|
expect(screen.getByText(/75% approval/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/67% voting power required/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the correct voting power for each validator', () => {
|
||||||
|
renderComponent();
|
||||||
|
mockValidators.forEach((validator) => {
|
||||||
|
const normalisedVotingPower = getNormalisedVotingPower(
|
||||||
|
validator.rankingScore.votingPower,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
expect(screen.getByText(normalisedVotingPower)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1 @@
|
|||||||
|
export * from './protocol-upgrade-proposal-detail-header';
|
@ -0,0 +1,18 @@
|
|||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import { ProtocolUpgradeProposalDetailHeader } from './protocol-upgrade-proposal-detail-header';
|
||||||
|
|
||||||
|
describe('ProtocolUpgradeProposalDetailHeader', () => {
|
||||||
|
it('should render successfully', () => {
|
||||||
|
const { baseElement } = render(
|
||||||
|
<ProtocolUpgradeProposalDetailHeader releaseTag="v1.0.0" />
|
||||||
|
);
|
||||||
|
expect(baseElement).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the release tag', () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<ProtocolUpgradeProposalDetailHeader releaseTag="v1.0.0" />
|
||||||
|
);
|
||||||
|
expect(getByText('Vega Release v1.0.0')).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,21 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Intent, Lozenge } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { Heading } from '../../../../components/heading';
|
||||||
|
|
||||||
|
export interface ProtocolUpgradeProposalDetailHeaderProps {
|
||||||
|
releaseTag: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProtocolUpgradeProposalDetailHeader = ({
|
||||||
|
releaseTag,
|
||||||
|
}: ProtocolUpgradeProposalDetailHeaderProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Heading title={t('vegaRelease{release}', { release: releaseTag })} />
|
||||||
|
<div className="mb-10">
|
||||||
|
<Lozenge variant={Intent.Success}>{t('networkUpgrade')}</Lozenge>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1 @@
|
|||||||
|
export * from './protocol-upgrade-proposal-detail-info';
|
@ -0,0 +1,44 @@
|
|||||||
|
// jest tests for the protocol upgrade proposal detail info component
|
||||||
|
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import { ProtocolUpgradeProposalStatus } from '@vegaprotocol/types';
|
||||||
|
import { ProtocolUpgradeProposalDetailInfo } from './protocol-upgrade-proposal-detail-info';
|
||||||
|
|
||||||
|
const renderComponent = () =>
|
||||||
|
render(
|
||||||
|
<ProtocolUpgradeProposalDetailInfo
|
||||||
|
proposal={{
|
||||||
|
vegaReleaseTag: 'v0.1.234',
|
||||||
|
status:
|
||||||
|
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING,
|
||||||
|
upgradeBlockHeight: '12345',
|
||||||
|
approvers: [],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('ProtocolUpgradeProposalDetailInfo', () => {
|
||||||
|
it('should render successfully', () => {
|
||||||
|
const { baseElement } = renderComponent();
|
||||||
|
expect(baseElement).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the upgrade block height', () => {
|
||||||
|
const { getByTestId } = renderComponent();
|
||||||
|
expect(getByTestId('protocol-upgrade-block-height')).toHaveTextContent(
|
||||||
|
'12345'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the state', () => {
|
||||||
|
const { getByTestId } = renderComponent();
|
||||||
|
expect(getByTestId('protocol-upgrade-state')).toHaveTextContent('Pending');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the vega release tag', () => {
|
||||||
|
const { getByTestId } = renderComponent();
|
||||||
|
expect(getByTestId('protocol-upgrade-release-tag')).toHaveTextContent(
|
||||||
|
'v0.1.234'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,58 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
KeyValueTable,
|
||||||
|
KeyValueTableRow,
|
||||||
|
RoundedWrapper,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { SubHeading } from '../../../../components/heading';
|
||||||
|
import type { ProtocolUpgradeProposalFieldsFragment } from '../../protocol-upgrade/__generated__/ProtocolUpgradeProposals';
|
||||||
|
|
||||||
|
export interface ProtocolUpgradeProposalDetailInfoProps {
|
||||||
|
proposal: ProtocolUpgradeProposalFieldsFragment;
|
||||||
|
lastBlockHeight?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProtocolUpgradeProposalDetailInfo = ({
|
||||||
|
proposal,
|
||||||
|
lastBlockHeight,
|
||||||
|
}: ProtocolUpgradeProposalDetailInfoProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-10">
|
||||||
|
<SubHeading title={t('proposal')} />
|
||||||
|
<RoundedWrapper marginBottomLarge={true} paddingBottom={true}>
|
||||||
|
<KeyValueTable data-testid="protocol-upgrade-proposal-details">
|
||||||
|
<KeyValueTableRow>
|
||||||
|
<span className="uppercase">{t('upgradeBlockHeight')}</span>
|
||||||
|
|
||||||
|
<span data-testid="protocol-upgrade-block-height">
|
||||||
|
{proposal.upgradeBlockHeight}{' '}
|
||||||
|
{lastBlockHeight && (
|
||||||
|
<>
|
||||||
|
({t('currently')} {lastBlockHeight})
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</KeyValueTableRow>
|
||||||
|
|
||||||
|
<KeyValueTableRow>
|
||||||
|
<span className="uppercase">{t('state')}</span>
|
||||||
|
|
||||||
|
<span data-testid="protocol-upgrade-state">
|
||||||
|
{t(`${proposal.status}`)}
|
||||||
|
</span>
|
||||||
|
</KeyValueTableRow>
|
||||||
|
|
||||||
|
<KeyValueTableRow noBorder={true}>
|
||||||
|
<span className="uppercase">{t('vegaReleaseTag')}</span>
|
||||||
|
|
||||||
|
<span data-testid="protocol-upgrade-release-tag">
|
||||||
|
{proposal.vegaReleaseTag}
|
||||||
|
</span>
|
||||||
|
</KeyValueTableRow>
|
||||||
|
</KeyValueTable>
|
||||||
|
</RoundedWrapper>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,69 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
import { ProtocolUpgradeProposalsListItem } from './protocol-upgrade-proposals-list-item';
|
||||||
|
import { ProtocolUpgradeProposalStatus } from '@vegaprotocol/types';
|
||||||
|
import type { ProtocolUpgradeProposalFieldsFragment } from '../../protocol-upgrade/__generated__/ProtocolUpgradeProposals';
|
||||||
|
|
||||||
|
const proposal = {
|
||||||
|
status:
|
||||||
|
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING,
|
||||||
|
vegaReleaseTag: 'v1.0.0',
|
||||||
|
upgradeBlockHeight: '12345',
|
||||||
|
} as ProtocolUpgradeProposalFieldsFragment;
|
||||||
|
|
||||||
|
const renderComponent = (proposal: ProtocolUpgradeProposalFieldsFragment) =>
|
||||||
|
render(
|
||||||
|
<BrowserRouter>
|
||||||
|
<ProtocolUpgradeProposalsListItem proposal={proposal} />
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('ProtocolUpgradeProposalsListItem', () => {
|
||||||
|
it('renders the correct status icon for each proposal status', () => {
|
||||||
|
const statuses = [
|
||||||
|
{
|
||||||
|
status:
|
||||||
|
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED,
|
||||||
|
icon: 'protocol-upgrade-proposal-status-icon-rejected',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status:
|
||||||
|
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING,
|
||||||
|
icon: 'protocol-upgrade-proposal-status-icon-pending',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status:
|
||||||
|
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_APPROVED,
|
||||||
|
icon: 'protocol-upgrade-proposal-status-icon-approved',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status:
|
||||||
|
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_UNSPECIFIED,
|
||||||
|
icon: 'protocol-upgrade-proposal-status-icon-unspecified',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
statuses.forEach(({ status, icon }) => {
|
||||||
|
renderComponent({ ...proposal, status });
|
||||||
|
const statusIcon = screen.getByTestId(icon);
|
||||||
|
expect(statusIcon).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the correct Vega release tag', () => {
|
||||||
|
renderComponent(proposal);
|
||||||
|
const releaseTag = screen.getByTestId(
|
||||||
|
'protocol-upgrade-proposal-release-tag'
|
||||||
|
);
|
||||||
|
expect(releaseTag).toHaveTextContent(proposal.vegaReleaseTag);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the correct upgrade block height', () => {
|
||||||
|
renderComponent(proposal);
|
||||||
|
const blockHeight = screen.getByTestId(
|
||||||
|
'protocol-upgrade-proposal-block-height'
|
||||||
|
);
|
||||||
|
expect(blockHeight).toHaveTextContent(proposal.upgradeBlockHeight);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,124 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Icon,
|
||||||
|
Intent,
|
||||||
|
Lozenge,
|
||||||
|
RoundedWrapper,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { stripFullStops } from '@vegaprotocol/utils';
|
||||||
|
import { ProtocolUpgradeProposalStatus } from '@vegaprotocol/types';
|
||||||
|
import { SubHeading } from '../../../../components/heading';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import type { ProtocolUpgradeProposalFieldsFragment } from '../../protocol-upgrade/__generated__/ProtocolUpgradeProposals';
|
||||||
|
import Routes from '../../../routes';
|
||||||
|
|
||||||
|
interface ProtocolProposalsListItemProps {
|
||||||
|
proposal: ProtocolUpgradeProposalFieldsFragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProtocolUpgradeProposalsListItem = ({
|
||||||
|
proposal,
|
||||||
|
}: ProtocolProposalsListItemProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
if (!proposal || !proposal.upgradeBlockHeight) return null;
|
||||||
|
|
||||||
|
let proposalStatusIcon: ReactNode;
|
||||||
|
|
||||||
|
switch (proposal.status) {
|
||||||
|
case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED:
|
||||||
|
proposalStatusIcon = (
|
||||||
|
<div data-testid="protocol-upgrade-proposal-status-icon-rejected">
|
||||||
|
<Icon name={'cross'} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING:
|
||||||
|
proposalStatusIcon = (
|
||||||
|
<div data-testid="protocol-upgrade-proposal-status-icon-pending">
|
||||||
|
<Icon name={'time'} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_APPROVED:
|
||||||
|
proposalStatusIcon = (
|
||||||
|
<div data-testid="protocol-upgrade-proposal-status-icon-approved">
|
||||||
|
<Icon name={'tick'} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_UNSPECIFIED:
|
||||||
|
proposalStatusIcon = (
|
||||||
|
<div data-testid="protocol-upgrade-proposal-status-icon-unspecified">
|
||||||
|
<Icon name={'disable'} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
id={proposal.upgradeBlockHeight}
|
||||||
|
data-testid="protocol-upgrade-proposals-list-item"
|
||||||
|
>
|
||||||
|
<RoundedWrapper paddingBottom={true} heightFull={true}>
|
||||||
|
<div
|
||||||
|
data-testid="protocol-upgrade-proposal-title"
|
||||||
|
className="text-sm mb-2"
|
||||||
|
>
|
||||||
|
<SubHeading title={`Vega release ${proposal.vegaReleaseTag}`} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-sm">
|
||||||
|
<div
|
||||||
|
data-testid="protocol-upgrade-proposal-type"
|
||||||
|
className="flex items-center gap-2 mb-4"
|
||||||
|
>
|
||||||
|
<Lozenge variant={Intent.Success}>{t('networkUpgrade')}</Lozenge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
data-testid="protocol-upgrade-proposal-release-tag"
|
||||||
|
className="mb-2"
|
||||||
|
>
|
||||||
|
<span className="pr-2">{t('vegaReleaseTag')}</span>
|
||||||
|
<Lozenge>{proposal.vegaReleaseTag}</Lozenge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
data-testid="protocol-upgrade-proposal-block-height"
|
||||||
|
className="mb-2"
|
||||||
|
>
|
||||||
|
<span className="pr-2">{t('upgradeBlockHeight')}</span>
|
||||||
|
<Lozenge>{proposal.upgradeBlockHeight}</Lozenge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-[1fr_auto] mt-3 items-start gap-2">
|
||||||
|
<div className="col-start-1 row-start-1 text-white">
|
||||||
|
<div
|
||||||
|
data-testid="protocol-upgrade-proposal-status"
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<span>{t(`${proposal.status}`)}</span>
|
||||||
|
<span>{proposalStatusIcon}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-start-2 row-start-2 justify-self-end">
|
||||||
|
<Link
|
||||||
|
to={`${Routes.PROPOSALS}/protocol-upgrade/${stripFullStops(
|
||||||
|
proposal.vegaReleaseTag
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
|
<Button data-testid="view-proposal-btn" size="sm">
|
||||||
|
{t('View')}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</RoundedWrapper>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
@ -5,13 +5,18 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { SplashLoader } from '../../../components/splash-loader';
|
import { SplashLoader } from '../../../components/splash-loader';
|
||||||
import { ProposalsList } from '../components/proposals-list';
|
import { ProposalsList } from '../components/proposals-list';
|
||||||
import { useProposalsQuery } from './__generated__/Proposals';
|
import { useProposalsQuery } from './__generated__/Proposals';
|
||||||
import type { ProposalFieldsFragment } from './__generated__/Proposals';
|
|
||||||
import type { NodeConnection, NodeEdge } from '@vegaprotocol/utils';
|
|
||||||
import { getNodes } from '@vegaprotocol/utils';
|
import { getNodes } from '@vegaprotocol/utils';
|
||||||
import flow from 'lodash/flow';
|
import flow from 'lodash/flow';
|
||||||
import { ProposalState } from '@vegaprotocol/types';
|
import {
|
||||||
|
ProposalState,
|
||||||
|
ProtocolUpgradeProposalStatus,
|
||||||
|
} from '@vegaprotocol/types';
|
||||||
|
import type { NodeConnection, NodeEdge } from '@vegaprotocol/utils';
|
||||||
|
import type { ProposalFieldsFragment } from './__generated__/Proposals';
|
||||||
|
import type { ProtocolUpgradeProposalFieldsFragment } from '../protocol-upgrade/__generated__/ProtocolUpgradeProposals';
|
||||||
|
|
||||||
import orderBy from 'lodash/orderBy';
|
import orderBy from 'lodash/orderBy';
|
||||||
|
import { useProtocolUpgradesQuery } from '../protocol-upgrade/__generated__/ProtocolUpgradeProposals';
|
||||||
|
|
||||||
const orderByDate = (arr: ProposalFieldsFragment[]) =>
|
const orderByDate = (arr: ProposalFieldsFragment[]) =>
|
||||||
orderBy(
|
orderBy(
|
||||||
@ -20,6 +25,15 @@ const orderByDate = (arr: ProposalFieldsFragment[]) =>
|
|||||||
['desc', 'desc']
|
['desc', 'desc']
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const orderByUpgradeBlockHeight = (
|
||||||
|
arr: ProtocolUpgradeProposalFieldsFragment[]
|
||||||
|
) =>
|
||||||
|
orderBy(
|
||||||
|
arr,
|
||||||
|
[(p) => p?.upgradeBlockHeight, (p) => p.vegaReleaseTag],
|
||||||
|
['desc', 'desc']
|
||||||
|
);
|
||||||
|
|
||||||
export function getNotRejectedProposals<T extends ProposalFieldsFragment>(
|
export function getNotRejectedProposals<T extends ProposalFieldsFragment>(
|
||||||
data?: NodeConnection<NodeEdge<T>> | null
|
data?: NodeConnection<NodeEdge<T>> | null
|
||||||
): T[] {
|
): T[] {
|
||||||
@ -32,6 +46,21 @@ export function getNotRejectedProposals<T extends ProposalFieldsFragment>(
|
|||||||
])(data);
|
])(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getNotRejectedProtocolUpgradeProposals<
|
||||||
|
T extends ProtocolUpgradeProposalFieldsFragment
|
||||||
|
>(data?: NodeConnection<NodeEdge<T>> | null): T[] {
|
||||||
|
return flow([
|
||||||
|
(data) =>
|
||||||
|
getNodes<ProtocolUpgradeProposalFieldsFragment>(data, (p) =>
|
||||||
|
p
|
||||||
|
? p.status !==
|
||||||
|
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED
|
||||||
|
: false
|
||||||
|
),
|
||||||
|
orderByUpgradeBlockHeight,
|
||||||
|
])(data);
|
||||||
|
}
|
||||||
|
|
||||||
export const ProposalsContainer = () => {
|
export const ProposalsContainer = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { data, loading, error } = useProposalsQuery({
|
const { data, loading, error } = useProposalsQuery({
|
||||||
@ -40,6 +69,16 @@ export const ProposalsContainer = () => {
|
|||||||
errorPolicy: 'ignore',
|
errorPolicy: 'ignore',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: protocolUpgradesData,
|
||||||
|
loading: protocolUpgradesLoading,
|
||||||
|
error: protocolUpgradesError,
|
||||||
|
} = useProtocolUpgradesQuery({
|
||||||
|
pollInterval: 5000,
|
||||||
|
fetchPolicy: 'network-only',
|
||||||
|
errorPolicy: 'ignore',
|
||||||
|
});
|
||||||
|
|
||||||
const proposals = useMemo(
|
const proposals = useMemo(
|
||||||
() =>
|
() =>
|
||||||
getNotRejectedProposals<ProposalFieldsFragment>(
|
getNotRejectedProposals<ProposalFieldsFragment>(
|
||||||
@ -48,15 +87,25 @@ export const ProposalsContainer = () => {
|
|||||||
[data]
|
[data]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (error) {
|
const protocolUpgradeProposals = useMemo(
|
||||||
|
() =>
|
||||||
|
protocolUpgradesData
|
||||||
|
? getNotRejectedProtocolUpgradeProposals<ProtocolUpgradeProposalFieldsFragment>(
|
||||||
|
protocolUpgradesData.protocolUpgradeProposals
|
||||||
|
)
|
||||||
|
: [],
|
||||||
|
[protocolUpgradesData]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error || protocolUpgradesError) {
|
||||||
return (
|
return (
|
||||||
<Callout intent={Intent.Danger} title={t('Something went wrong')}>
|
<Callout intent={Intent.Danger} title={t('Something went wrong')}>
|
||||||
<pre>{error.message}</pre>
|
<pre>{error?.message || protocolUpgradesError?.message}</pre>
|
||||||
</Callout>
|
</Callout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading || protocolUpgradesLoading) {
|
||||||
return (
|
return (
|
||||||
<Splash>
|
<Splash>
|
||||||
<SplashLoader />
|
<SplashLoader />
|
||||||
@ -64,5 +113,11 @@ export const ProposalsContainer = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ProposalsList proposals={proposals} />;
|
return (
|
||||||
|
<ProposalsList
|
||||||
|
proposals={proposals}
|
||||||
|
protocolUpgradeProposals={protocolUpgradeProposals}
|
||||||
|
lastBlockHeight={protocolUpgradesData?.lastBlockHeight}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
fragment ProtocolUpgradeProposalFields on ProtocolUpgradeProposal {
|
||||||
|
upgradeBlockHeight
|
||||||
|
vegaReleaseTag
|
||||||
|
approvers
|
||||||
|
status
|
||||||
|
}
|
||||||
|
|
||||||
|
query ProtocolUpgrades {
|
||||||
|
lastBlockHeight
|
||||||
|
protocolUpgradeProposals {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...ProtocolUpgradeProposalFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
59
apps/governance/src/routes/proposals/protocol-upgrade/__generated__/ProtocolUpgradeProposals.ts
generated
Normal file
59
apps/governance/src/routes/proposals/protocol-upgrade/__generated__/ProtocolUpgradeProposals.ts
generated
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import * as Types from '@vegaprotocol/types';
|
||||||
|
|
||||||
|
import { gql } from '@apollo/client';
|
||||||
|
import * as Apollo from '@apollo/client';
|
||||||
|
const defaultOptions = {} as const;
|
||||||
|
export type ProtocolUpgradeProposalFieldsFragment = { __typename?: 'ProtocolUpgradeProposal', upgradeBlockHeight: string, vegaReleaseTag: string, approvers: Array<string>, status: Types.ProtocolUpgradeProposalStatus };
|
||||||
|
|
||||||
|
export type ProtocolUpgradesQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
|
export type ProtocolUpgradesQuery = { __typename?: 'Query', lastBlockHeight: string, protocolUpgradeProposals?: { __typename?: 'ProtocolUpgradeProposalConnection', edges?: Array<{ __typename?: 'ProtocolUpgradeProposalEdge', node: { __typename?: 'ProtocolUpgradeProposal', upgradeBlockHeight: string, vegaReleaseTag: string, approvers: Array<string>, status: Types.ProtocolUpgradeProposalStatus } }> | null } | null };
|
||||||
|
|
||||||
|
export const ProtocolUpgradeProposalFieldsFragmentDoc = gql`
|
||||||
|
fragment ProtocolUpgradeProposalFields on ProtocolUpgradeProposal {
|
||||||
|
upgradeBlockHeight
|
||||||
|
vegaReleaseTag
|
||||||
|
approvers
|
||||||
|
status
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export const ProtocolUpgradesDocument = gql`
|
||||||
|
query ProtocolUpgrades {
|
||||||
|
lastBlockHeight
|
||||||
|
protocolUpgradeProposals {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...ProtocolUpgradeProposalFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${ProtocolUpgradeProposalFieldsFragmentDoc}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useProtocolUpgradesQuery__
|
||||||
|
*
|
||||||
|
* To run a query within a React component, call `useProtocolUpgradesQuery` and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useProtocolUpgradesQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||||
|
* you can use to render your UI.
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { data, loading, error } = useProtocolUpgradesQuery({
|
||||||
|
* variables: {
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useProtocolUpgradesQuery(baseOptions?: Apollo.QueryHookOptions<ProtocolUpgradesQuery, ProtocolUpgradesQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useQuery<ProtocolUpgradesQuery, ProtocolUpgradesQueryVariables>(ProtocolUpgradesDocument, options);
|
||||||
|
}
|
||||||
|
export function useProtocolUpgradesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ProtocolUpgradesQuery, ProtocolUpgradesQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useLazyQuery<ProtocolUpgradesQuery, ProtocolUpgradesQueryVariables>(ProtocolUpgradesDocument, options);
|
||||||
|
}
|
||||||
|
export type ProtocolUpgradesQueryHookResult = ReturnType<typeof useProtocolUpgradesQuery>;
|
||||||
|
export type ProtocolUpgradesLazyQueryHookResult = ReturnType<typeof useProtocolUpgradesLazyQuery>;
|
||||||
|
export type ProtocolUpgradesQueryResult = Apollo.QueryResult<ProtocolUpgradesQuery, ProtocolUpgradesQueryVariables>;
|
@ -0,0 +1 @@
|
|||||||
|
export { ProtocolUpgradeProposalContainer as default } from './protocol-upgrade-proposal-container';
|
@ -0,0 +1,78 @@
|
|||||||
|
import { useEffect, useMemo } from 'react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { removePaginationWrapper, stripFullStops } from '@vegaprotocol/utils';
|
||||||
|
import * as Schema from '@vegaprotocol/types';
|
||||||
|
|
||||||
|
import { ProtocolUpgradeProposal } from './protocol-upgrade-proposal';
|
||||||
|
import { ProposalNotFound } from '../components/proposal-not-found';
|
||||||
|
import { useProtocolUpgradesQuery } from './__generated__/ProtocolUpgradeProposals';
|
||||||
|
import { useNodesQuery } from '../../staking/home/__generated__/Nodes';
|
||||||
|
import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch';
|
||||||
|
|
||||||
|
export const ProtocolUpgradeProposalContainer = () => {
|
||||||
|
const params = useParams<{ proposalReleaseTag: string }>();
|
||||||
|
|
||||||
|
const { data, loading, error, refetch } = useProtocolUpgradesQuery({
|
||||||
|
fetchPolicy: 'network-only',
|
||||||
|
errorPolicy: 'ignore',
|
||||||
|
skip: !params.proposalReleaseTag,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(refetch, 1000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [refetch]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: nodesData,
|
||||||
|
error: nodesError,
|
||||||
|
loading: nodesLoading,
|
||||||
|
refetch: nodesRefetch,
|
||||||
|
} = useNodesQuery();
|
||||||
|
|
||||||
|
useRefreshAfterEpoch(nodesData?.epoch.timestamps.expiry, nodesRefetch);
|
||||||
|
|
||||||
|
const protocolUpgradeProposal = useMemo(() => {
|
||||||
|
if (!data?.protocolUpgradeProposals) return null;
|
||||||
|
|
||||||
|
const proposals = removePaginationWrapper(
|
||||||
|
data.protocolUpgradeProposals.edges
|
||||||
|
);
|
||||||
|
|
||||||
|
return proposals.find(
|
||||||
|
({ vegaReleaseTag }) =>
|
||||||
|
params.proposalReleaseTag &&
|
||||||
|
params.proposalReleaseTag === stripFullStops(vegaReleaseTag)
|
||||||
|
);
|
||||||
|
}, [data, params.proposalReleaseTag]);
|
||||||
|
|
||||||
|
const consensusValidators = useMemo(() => {
|
||||||
|
if (!nodesData?.nodesConnection.edges) return null;
|
||||||
|
|
||||||
|
const nodes = removePaginationWrapper(nodesData.nodesConnection.edges);
|
||||||
|
|
||||||
|
return nodes.filter(
|
||||||
|
({ rankingScore: { status } }) =>
|
||||||
|
status === Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT
|
||||||
|
);
|
||||||
|
}, [nodesData]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AsyncRenderer
|
||||||
|
loading={loading || nodesLoading}
|
||||||
|
error={error || nodesError}
|
||||||
|
data={{ ...data, ...nodesData }}
|
||||||
|
>
|
||||||
|
{protocolUpgradeProposal ? (
|
||||||
|
<ProtocolUpgradeProposal
|
||||||
|
proposal={protocolUpgradeProposal}
|
||||||
|
lastBlockHeight={data?.lastBlockHeight}
|
||||||
|
consensusValidators={consensusValidators}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ProposalNotFound />
|
||||||
|
)}
|
||||||
|
</AsyncRenderer>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,134 @@
|
|||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import * as Schema from '@vegaprotocol/types';
|
||||||
|
import {
|
||||||
|
ProtocolUpgradeProposal,
|
||||||
|
getConsensusApprovals,
|
||||||
|
getConsensusApprovalsVotingPower,
|
||||||
|
getConsensusApprovalsVotingPowerPercentage,
|
||||||
|
} from './protocol-upgrade-proposal';
|
||||||
|
import { ProtocolUpgradeProposalStatus } from '@vegaprotocol/types';
|
||||||
|
import { getNormalisedVotingPower } from '../../staking/shared';
|
||||||
|
import type { NodesFragmentFragment } from '../../staking/home/__generated__/Nodes';
|
||||||
|
import type { ProtocolUpgradeProposalFieldsFragment } from './__generated__/ProtocolUpgradeProposals';
|
||||||
|
|
||||||
|
const mockProposal: ProtocolUpgradeProposalFieldsFragment = {
|
||||||
|
vegaReleaseTag: 'v0.1.234',
|
||||||
|
status:
|
||||||
|
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING,
|
||||||
|
upgradeBlockHeight: '12345',
|
||||||
|
approvers: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockConsensusValidators: NodesFragmentFragment[] = [
|
||||||
|
{
|
||||||
|
id: 'ccc022b7e63a4d0a6d3a193c3940c88574060e58a184964c994998d86835a1b4',
|
||||||
|
name: 'Marvin',
|
||||||
|
avatarUrl: 'https://upload.wikimedia.org/wikipedia/en/2/25/Marvin-TV-3.jpg',
|
||||||
|
pubkey: '6abc23391a9f888ab240415bf63d6844b03fc360be822f4a1d2cd832d87b2917',
|
||||||
|
stakedTotal: '14182454495731682635157',
|
||||||
|
stakedByOperator: '1000000000000000000000',
|
||||||
|
stakedByDelegates: '13182454495731682635157',
|
||||||
|
pendingStake: '0',
|
||||||
|
rankingScore: {
|
||||||
|
rankingScore: '0.67845061012234727427532760837568',
|
||||||
|
stakeScore: '0.3392701644525644',
|
||||||
|
performanceScore: '0.9998677767864936',
|
||||||
|
votingPower: '3500',
|
||||||
|
status: Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
|
||||||
|
__typename: 'RankingScore',
|
||||||
|
},
|
||||||
|
__typename: 'Node',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ccc022b7e63a4d0a6d3a193c3940c88574060e58a184964c994998d86835a1b5',
|
||||||
|
name: 'Barvin',
|
||||||
|
avatarUrl: 'https://upload.wikimedia.org/wikipedia/en/2/25/Barvin-TV-3.jpg',
|
||||||
|
pubkey: '6abc23391a9f888ab240415bf63d6844b03fc360be822f4a1d2cd832d87b291b',
|
||||||
|
stakedTotal: '1418245449573168263',
|
||||||
|
stakedByOperator: '100000000000000000',
|
||||||
|
stakedByDelegates: '1318245449573168263',
|
||||||
|
pendingStake: '0',
|
||||||
|
rankingScore: {
|
||||||
|
rankingScore: '0.5',
|
||||||
|
stakeScore: '0.3',
|
||||||
|
performanceScore: '0.98',
|
||||||
|
votingPower: '2500',
|
||||||
|
status: Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
|
||||||
|
__typename: 'RankingScore',
|
||||||
|
},
|
||||||
|
__typename: 'Node',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const renderComponent = () =>
|
||||||
|
render(
|
||||||
|
<ProtocolUpgradeProposal proposal={mockProposal} consensusValidators={[]} />
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('ProtocolUpgradeProposal', () => {
|
||||||
|
it('should render successfully', () => {
|
||||||
|
const { baseElement } = renderComponent();
|
||||||
|
expect(baseElement).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getConsensusApprovals', () => {
|
||||||
|
it('returns an empty array when there are no matching approvers', () => {
|
||||||
|
const result = getConsensusApprovals(mockConsensusValidators, {
|
||||||
|
...mockProposal,
|
||||||
|
approvers: [],
|
||||||
|
});
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the correct consensus approvals when there are matching approvers', () => {
|
||||||
|
const expectedApprovals = mockConsensusValidators[1];
|
||||||
|
|
||||||
|
const result = getConsensusApprovals(mockConsensusValidators, {
|
||||||
|
...mockProposal,
|
||||||
|
approvers: [
|
||||||
|
'6abc23391a9f888ab240415bf63d6844b03fc360be822f4a1d2cd832d87b291b',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.length).toEqual(1);
|
||||||
|
expect(result[0].id).toEqual(expectedApprovals.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getConsensusApprovalsVotingPower', () => {
|
||||||
|
it('returns 0 when the input array is empty', () => {
|
||||||
|
const result = getConsensusApprovalsVotingPower([]);
|
||||||
|
expect(result).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the correct sum of voting power for a given array of consensus validators', () => {
|
||||||
|
const expectedSum = mockConsensusValidators.reduce(
|
||||||
|
(acc, curr) => acc + Number(curr.rankingScore.votingPower),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = getConsensusApprovalsVotingPower(mockConsensusValidators);
|
||||||
|
expect(result).toBe(expectedSum);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getConsensusApprovalsVotingPowerPercentage', () => {
|
||||||
|
it('returns "0%" when the input value is 0 or less', () => {
|
||||||
|
const resultZero = getConsensusApprovalsVotingPowerPercentage(0);
|
||||||
|
const resultNegative = getConsensusApprovalsVotingPowerPercentage(-10);
|
||||||
|
expect(resultZero).toBe('0%');
|
||||||
|
expect(resultNegative).toBe('0%');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the correct percentage string when the input value is greater than 0', () => {
|
||||||
|
const inputValue = 12345;
|
||||||
|
const expectedPercentage = getNormalisedVotingPower(
|
||||||
|
inputValue.toString(),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = getConsensusApprovalsVotingPowerPercentage(inputValue);
|
||||||
|
expect(result).toBe(expectedPercentage);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,83 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { ProtocolUpgradeProposalDetailHeader } from '../components/protocol-upgrade-proposal-detail-header';
|
||||||
|
import { ProtocolUpdateProposalDetailApprovals } from '../components/protocol-upgrade-proposal-detail-approvals';
|
||||||
|
import { ProtocolUpgradeProposalDetailInfo } from '../components/protocol-upgrade-proposal-detail-info';
|
||||||
|
import { getNormalisedVotingPower } from '../../staking/shared';
|
||||||
|
import type { ProtocolUpgradeProposalFieldsFragment } from './__generated__/ProtocolUpgradeProposals';
|
||||||
|
import type { NodesFragmentFragment } from '../../staking/home/__generated__/Nodes';
|
||||||
|
|
||||||
|
export interface ProtocolUpgradeProposalProps {
|
||||||
|
proposal: ProtocolUpgradeProposalFieldsFragment;
|
||||||
|
consensusValidators: NodesFragmentFragment[] | null;
|
||||||
|
lastBlockHeight?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getConsensusApprovals = (
|
||||||
|
consensusValidators: NodesFragmentFragment[],
|
||||||
|
proposal: ProtocolUpgradeProposalFieldsFragment
|
||||||
|
) =>
|
||||||
|
consensusValidators?.filter(({ pubkey }) =>
|
||||||
|
proposal.approvers?.includes(pubkey)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getConsensusApprovalsVotingPower = (
|
||||||
|
consensusValidators: NodesFragmentFragment[]
|
||||||
|
) =>
|
||||||
|
consensusValidators?.reduce<number>(
|
||||||
|
(acc: number, curr: NodesFragmentFragment) => {
|
||||||
|
return acc + Number(curr.rankingScore.votingPower);
|
||||||
|
},
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getConsensusApprovalsVotingPowerPercentage = (
|
||||||
|
consensusApprovalsVotingPower: number
|
||||||
|
) =>
|
||||||
|
consensusApprovalsVotingPower > 0
|
||||||
|
? getNormalisedVotingPower(consensusApprovalsVotingPower.toString(), 2)
|
||||||
|
: '0%';
|
||||||
|
|
||||||
|
export const ProtocolUpgradeProposal = ({
|
||||||
|
proposal,
|
||||||
|
lastBlockHeight,
|
||||||
|
consensusValidators,
|
||||||
|
}: ProtocolUpgradeProposalProps) => {
|
||||||
|
const consensusApprovals = useMemo(
|
||||||
|
() => getConsensusApprovals(consensusValidators || [], proposal),
|
||||||
|
[consensusValidators, proposal]
|
||||||
|
);
|
||||||
|
|
||||||
|
const consensusApprovalsVotingPower = useMemo(
|
||||||
|
() => getConsensusApprovalsVotingPower(consensusApprovals || []),
|
||||||
|
[consensusApprovals]
|
||||||
|
);
|
||||||
|
|
||||||
|
const consensusApprovalsVotingPowerPercentage = useMemo(
|
||||||
|
() =>
|
||||||
|
getConsensusApprovalsVotingPowerPercentage(consensusApprovalsVotingPower),
|
||||||
|
[consensusApprovalsVotingPower]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section data-testid="protocol-upgrade-proposal">
|
||||||
|
<ProtocolUpgradeProposalDetailHeader
|
||||||
|
releaseTag={proposal.vegaReleaseTag}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProtocolUpgradeProposalDetailInfo
|
||||||
|
proposal={proposal}
|
||||||
|
lastBlockHeight={lastBlockHeight}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{consensusValidators && consensusApprovals && (
|
||||||
|
<ProtocolUpdateProposalDetailApprovals
|
||||||
|
consensusApprovals={consensusApprovals}
|
||||||
|
consensusApprovalsVotingPowerPercentage={
|
||||||
|
consensusApprovalsVotingPowerPercentage
|
||||||
|
}
|
||||||
|
totalConsensusValidators={consensusValidators.length}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
@ -109,6 +109,13 @@ const LazyProposal = React.lazy(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const LazyProtocolUpgradeProposal = React.lazy(
|
||||||
|
() =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "route-governance-protocol-upgrade-proposal", webpackPrefetch: true */ './proposals/protocol-upgrade'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const LazyProposalsList = React.lazy(
|
const LazyProposalsList = React.lazy(
|
||||||
() =>
|
() =>
|
||||||
import(
|
import(
|
||||||
@ -257,6 +264,10 @@ const routerConfig = [
|
|||||||
},
|
},
|
||||||
{ path: 'proposals', element: <LazyProposalsList /> },
|
{ path: 'proposals', element: <LazyProposalsList /> },
|
||||||
{ path: ':proposalId', element: <LazyProposal /> },
|
{ path: ':proposalId', element: <LazyProposal /> },
|
||||||
|
{
|
||||||
|
path: 'protocol-upgrade/:proposalReleaseTag',
|
||||||
|
element: <LazyProtocolUpgradeProposal />,
|
||||||
|
},
|
||||||
{ path: 'rejected', element: <LazyRejectedProposalsList /> },
|
{ path: 'rejected', element: <LazyRejectedProposalsList /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -20,8 +20,8 @@ export const getLastEpochScoreAndPerformance = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getNormalisedVotingPower = (votingPower: string) =>
|
export const getNormalisedVotingPower = (votingPower: string, decimals = 2) =>
|
||||||
formatNumberPercentage(new BigNumber(votingPower).dividedBy(100), 2);
|
formatNumberPercentage(new BigNumber(votingPower).dividedBy(100), decimals);
|
||||||
|
|
||||||
export const getUnnormalisedVotingPower = (
|
export const getUnnormalisedVotingPower = (
|
||||||
validatorScore: string | null | undefined
|
validatorScore: string | null | undefined
|
||||||
|
@ -1,4 +1,10 @@
|
|||||||
import { truncateByChars, ELLIPSIS, shorten, titlefy } from './strings';
|
import {
|
||||||
|
truncateByChars,
|
||||||
|
ELLIPSIS,
|
||||||
|
shorten,
|
||||||
|
titlefy,
|
||||||
|
stripFullStops,
|
||||||
|
} from './strings';
|
||||||
|
|
||||||
describe('truncateByChars', () => {
|
describe('truncateByChars', () => {
|
||||||
it.each([
|
it.each([
|
||||||
@ -49,3 +55,35 @@ describe('titlefy', () => {
|
|||||||
expect(titlefy(words)).toEqual(o);
|
expect(titlefy(words)).toEqual(o);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('stripFullStops', () => {
|
||||||
|
describe('stripFullStops', () => {
|
||||||
|
it('removes full stops from a version string', () => {
|
||||||
|
const input = 'v0.70.0-12075-8fa496c8';
|
||||||
|
const expectedResult = 'v0700-12075-8fa496c8';
|
||||||
|
expect(stripFullStops(input)).toBe(expectedResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not alter a string without full stops', () => {
|
||||||
|
const input = 'v0700-12075-8fa496c8';
|
||||||
|
expect(stripFullStops(input)).toBe(input);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes full stops from a string with consecutive full stops', () => {
|
||||||
|
const input = 'v0..70...0-12075-8fa496c8';
|
||||||
|
const expectedResult = 'v0700-12075-8fa496c8';
|
||||||
|
expect(stripFullStops(input)).toBe(expectedResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes full stops from a string with only full stops', () => {
|
||||||
|
const input = '..........';
|
||||||
|
const expectedResult = '';
|
||||||
|
expect(stripFullStops(input)).toBe(expectedResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not alter an empty string', () => {
|
||||||
|
const input = '';
|
||||||
|
expect(stripFullStops(input)).toBe(input);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -26,3 +26,7 @@ export function titlefy(words: (string | null | undefined)[]) {
|
|||||||
.join(TITLE_SEPARATOR);
|
.join(TITLE_SEPARATOR);
|
||||||
return title;
|
return title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function stripFullStops(input: string) {
|
||||||
|
return input.replace(/\./g, '');
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user