;
}
-export const ProposalsList = ({ proposals }: ProposalsListProps) => {
+interface SortedProtocolUpgradeProposalsProps {
+ open: ProtocolUpgradeProposalFieldsFragment[];
+ closed: ProtocolUpgradeProposalFieldsFragment[];
+}
+
+export const ProposalsList = ({
+ proposals,
+ protocolUpgradeProposals,
+ lastBlockHeight,
+}: ProposalsListProps) => {
const { t } = useTranslation();
const [filterString, setFilterString] = useState('');
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 = (
p: ProposalFieldsFragment | ProposalQuery['proposal']
) =>
@@ -65,7 +93,7 @@ export const ProposalsList = ({ proposals }: ProposalsListProps) => {
{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.`
)}{' '}
{
)}
- {sortedProposals.open.length > 0 ? (
+ {sortedProposals.open.length > 0 ||
+ sortedProtocolUpgradeProposals.open.length > 0 ? (
+ {sortedProtocolUpgradeProposals.open.map((proposal) => (
+
+ ))}
{sortedProposals.open.filter(filterPredicate).map((proposal) => (
))}
@@ -94,8 +129,16 @@ export const ProposalsList = ({ proposals }: ProposalsListProps) => {
- {sortedProposals.closed.length > 0 ? (
+ {sortedProposals.closed.length > 0 ||
+ sortedProtocolUpgradeProposals.closed.length > 0 ? (
+ {sortedProtocolUpgradeProposals.closed.map((proposal) => (
+
+ ))}
+
{sortedProposals.closed.filter(filterPredicate).map((proposal) => (
))}
diff --git a/apps/governance/src/routes/proposals/components/protocol-upgrade-proposal-detail-approvals/index.tsx b/apps/governance/src/routes/proposals/components/protocol-upgrade-proposal-detail-approvals/index.tsx
new file mode 100644
index 000000000..a877bea87
--- /dev/null
+++ b/apps/governance/src/routes/proposals/components/protocol-upgrade-proposal-detail-approvals/index.tsx
@@ -0,0 +1 @@
+export * from './protocol-update-proposal-detail-approvals';
diff --git a/apps/governance/src/routes/proposals/components/protocol-upgrade-proposal-detail-approvals/protocol-update-proposal-detail-approvals.tsx b/apps/governance/src/routes/proposals/components/protocol-upgrade-proposal-detail-approvals/protocol-update-proposal-detail-approvals.tsx
new file mode 100644
index 000000000..9655fd1c2
--- /dev/null
+++ b/apps/governance/src/routes/proposals/components/protocol-upgrade-proposal-detail-approvals/protocol-update-proposal-detail-approvals.tsx
@@ -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 (
+ <>
+
+
+
+
+
+ {`${consensusApprovalsVotingPowerPercentage} ${t(
+ 'approval (% validator voting power)'
+ )}`}
+ ({t('67% voting power required')})
+
+
+
+
+
+
+
+
+
+
+ {t('validator')}
+ {t('votingPower')}
+
+
+ {consensusApprovals.map((validator, index) => (
+
+
+
+
+
+
+ {getNormalisedVotingPower(
+ validator.rankingScore.votingPower,
+ 2
+ )}
+
+
+ ))}
+
+
+
+ >
+ );
+};
diff --git a/apps/governance/src/routes/proposals/components/protocol-upgrade-proposal-detail-approvals/protocol-upgrade-proposal-detail-approvals.spec.tsx b/apps/governance/src/routes/proposals/components/protocol-upgrade-proposal-detail-approvals/protocol-upgrade-proposal-detail-approvals.spec.tsx
new file mode 100644
index 000000000..6092b2927
--- /dev/null
+++ b/apps/governance/src/routes/proposals/components/protocol-upgrade-proposal-detail-approvals/protocol-upgrade-proposal-detail-approvals.spec.tsx
@@ -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 = {}
+ ) => {
+ const mergedProps = {
+ ...defaultProps,
+ ...props,
+ };
+ return render(
+
+
+
+ );
+ };
+
+ 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();
+ });
+ });
+});
diff --git a/apps/governance/src/routes/proposals/components/protocol-upgrade-proposal-detail-header/index.tsx b/apps/governance/src/routes/proposals/components/protocol-upgrade-proposal-detail-header/index.tsx
new file mode 100644
index 000000000..8ad369de8
--- /dev/null
+++ b/apps/governance/src/routes/proposals/components/protocol-upgrade-proposal-detail-header/index.tsx
@@ -0,0 +1 @@
+export * from './protocol-upgrade-proposal-detail-header';
diff --git a/apps/governance/src/routes/proposals/components/protocol-upgrade-proposal-detail-header/protocol-upgrade-proposal-detail-header.spec.tsx b/apps/governance/src/routes/proposals/components/protocol-upgrade-proposal-detail-header/protocol-upgrade-proposal-detail-header.spec.tsx
new file mode 100644
index 000000000..2d7f8e978
--- /dev/null
+++ b/apps/governance/src/routes/proposals/components/protocol-upgrade-proposal-detail-header/protocol-upgrade-proposal-detail-header.spec.tsx
@@ -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(
+
+ );
+ expect(baseElement).toBeTruthy();
+ });
+
+ it('should render the release tag', () => {
+ const { getByText } = render(
+
+ );
+ expect(getByText('Vega Release v1.0.0')).toBeTruthy();
+ });
+});
diff --git a/apps/governance/src/routes/proposals/components/protocol-upgrade-proposal-detail-header/protocol-upgrade-proposal-detail-header.tsx b/apps/governance/src/routes/proposals/components/protocol-upgrade-proposal-detail-header/protocol-upgrade-proposal-detail-header.tsx
new file mode 100644
index 000000000..c788bd751
--- /dev/null
+++ b/apps/governance/src/routes/proposals/components/protocol-upgrade-proposal-detail-header/protocol-upgrade-proposal-detail-header.tsx
@@ -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 (
+ <>
+
+
+ {t('networkUpgrade')}
+
+ >
+ );
+};
diff --git a/apps/governance/src/routes/proposals/components/protocol-upgrade-proposal-detail-info/index.tsx b/apps/governance/src/routes/proposals/components/protocol-upgrade-proposal-detail-info/index.tsx
new file mode 100644
index 000000000..ea34734b2
--- /dev/null
+++ b/apps/governance/src/routes/proposals/components/protocol-upgrade-proposal-detail-info/index.tsx
@@ -0,0 +1 @@
+export * from './protocol-upgrade-proposal-detail-info';
diff --git a/apps/governance/src/routes/proposals/components/protocol-upgrade-proposal-detail-info/protocol-upgrade-proposal-detail-info.spec.tsx b/apps/governance/src/routes/proposals/components/protocol-upgrade-proposal-detail-info/protocol-upgrade-proposal-detail-info.spec.tsx
new file mode 100644
index 000000000..2ec6b2f65
--- /dev/null
+++ b/apps/governance/src/routes/proposals/components/protocol-upgrade-proposal-detail-info/protocol-upgrade-proposal-detail-info.spec.tsx
@@ -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(
+
+ );
+
+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'
+ );
+ });
+});
diff --git a/apps/governance/src/routes/proposals/components/protocol-upgrade-proposal-detail-info/protocol-upgrade-proposal-detail-info.tsx b/apps/governance/src/routes/proposals/components/protocol-upgrade-proposal-detail-info/protocol-upgrade-proposal-detail-info.tsx
new file mode 100644
index 000000000..3aace79fd
--- /dev/null
+++ b/apps/governance/src/routes/proposals/components/protocol-upgrade-proposal-detail-info/protocol-upgrade-proposal-detail-info.tsx
@@ -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 (
+
+
+
+
+
+ {t('upgradeBlockHeight')}
+
+
+ {proposal.upgradeBlockHeight}{' '}
+ {lastBlockHeight && (
+ <>
+ ({t('currently')} {lastBlockHeight})
+ >
+ )}
+
+
+
+
+ {t('state')}
+
+
+ {t(`${proposal.status}`)}
+
+
+
+
+ {t('vegaReleaseTag')}
+
+
+ {proposal.vegaReleaseTag}
+
+
+
+
+
+ );
+};
diff --git a/apps/governance/src/routes/proposals/components/protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item.spec.tsx b/apps/governance/src/routes/proposals/components/protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item.spec.tsx
new file mode 100644
index 000000000..65b78c073
--- /dev/null
+++ b/apps/governance/src/routes/proposals/components/protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item.spec.tsx
@@ -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(
+
+
+
+ );
+
+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);
+ });
+});
diff --git a/apps/governance/src/routes/proposals/components/protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item.tsx b/apps/governance/src/routes/proposals/components/protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item.tsx
new file mode 100644
index 000000000..c4be5acda
--- /dev/null
+++ b/apps/governance/src/routes/proposals/components/protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item.tsx
@@ -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 = (
+
+
+
+ );
+ break;
+ case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING:
+ proposalStatusIcon = (
+
+
+
+ );
+ break;
+ case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_APPROVED:
+ proposalStatusIcon = (
+
+
+
+ );
+ break;
+ case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_UNSPECIFIED:
+ proposalStatusIcon = (
+
+
+
+ );
+ break;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ {t('networkUpgrade')}
+
+
+
+ {t('vegaReleaseTag')}
+ {proposal.vegaReleaseTag}
+
+
+
+ {t('upgradeBlockHeight')}
+ {proposal.upgradeBlockHeight}
+
+
+
+
+
+ {t(`${proposal.status}`)}
+ {proposalStatusIcon}
+
+
+
+
+
+
+ {t('View')}
+
+
+
+
+
+
+
+ );
+};
diff --git a/apps/governance/src/routes/proposals/proposals/proposals-container.tsx b/apps/governance/src/routes/proposals/proposals/proposals-container.tsx
index 5bc93df4e..4a92bd5d9 100644
--- a/apps/governance/src/routes/proposals/proposals/proposals-container.tsx
+++ b/apps/governance/src/routes/proposals/proposals/proposals-container.tsx
@@ -5,13 +5,18 @@ import { useTranslation } from 'react-i18next';
import { SplashLoader } from '../../../components/splash-loader';
import { ProposalsList } from '../components/proposals-list';
import { useProposalsQuery } from './__generated__/Proposals';
-import type { ProposalFieldsFragment } from './__generated__/Proposals';
-import type { NodeConnection, NodeEdge } from '@vegaprotocol/utils';
import { getNodes } from '@vegaprotocol/utils';
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 { useProtocolUpgradesQuery } from '../protocol-upgrade/__generated__/ProtocolUpgradeProposals';
const orderByDate = (arr: ProposalFieldsFragment[]) =>
orderBy(
@@ -20,6 +25,15 @@ const orderByDate = (arr: ProposalFieldsFragment[]) =>
['desc', 'desc']
);
+const orderByUpgradeBlockHeight = (
+ arr: ProtocolUpgradeProposalFieldsFragment[]
+) =>
+ orderBy(
+ arr,
+ [(p) => p?.upgradeBlockHeight, (p) => p.vegaReleaseTag],
+ ['desc', 'desc']
+ );
+
export function getNotRejectedProposals(
data?: NodeConnection> | null
): T[] {
@@ -32,6 +46,21 @@ export function getNotRejectedProposals(
])(data);
}
+export function getNotRejectedProtocolUpgradeProposals<
+ T extends ProtocolUpgradeProposalFieldsFragment
+>(data?: NodeConnection> | null): T[] {
+ return flow([
+ (data) =>
+ getNodes(data, (p) =>
+ p
+ ? p.status !==
+ ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED
+ : false
+ ),
+ orderByUpgradeBlockHeight,
+ ])(data);
+}
+
export const ProposalsContainer = () => {
const { t } = useTranslation();
const { data, loading, error } = useProposalsQuery({
@@ -40,6 +69,16 @@ export const ProposalsContainer = () => {
errorPolicy: 'ignore',
});
+ const {
+ data: protocolUpgradesData,
+ loading: protocolUpgradesLoading,
+ error: protocolUpgradesError,
+ } = useProtocolUpgradesQuery({
+ pollInterval: 5000,
+ fetchPolicy: 'network-only',
+ errorPolicy: 'ignore',
+ });
+
const proposals = useMemo(
() =>
getNotRejectedProposals(
@@ -48,15 +87,25 @@ export const ProposalsContainer = () => {
[data]
);
- if (error) {
+ const protocolUpgradeProposals = useMemo(
+ () =>
+ protocolUpgradesData
+ ? getNotRejectedProtocolUpgradeProposals(
+ protocolUpgradesData.protocolUpgradeProposals
+ )
+ : [],
+ [protocolUpgradesData]
+ );
+
+ if (error || protocolUpgradesError) {
return (
- {error.message}
+ {error?.message || protocolUpgradesError?.message}
);
}
- if (loading) {
+ if (loading || protocolUpgradesLoading) {
return (
@@ -64,5 +113,11 @@ export const ProposalsContainer = () => {
);
}
- return ;
+ return (
+
+ );
};
diff --git a/apps/governance/src/routes/proposals/protocol-upgrade/ProtocolUpgradeProposals.graphql b/apps/governance/src/routes/proposals/protocol-upgrade/ProtocolUpgradeProposals.graphql
new file mode 100644
index 000000000..35959a69f
--- /dev/null
+++ b/apps/governance/src/routes/proposals/protocol-upgrade/ProtocolUpgradeProposals.graphql
@@ -0,0 +1,17 @@
+fragment ProtocolUpgradeProposalFields on ProtocolUpgradeProposal {
+ upgradeBlockHeight
+ vegaReleaseTag
+ approvers
+ status
+}
+
+query ProtocolUpgrades {
+ lastBlockHeight
+ protocolUpgradeProposals {
+ edges {
+ node {
+ ...ProtocolUpgradeProposalFields
+ }
+ }
+ }
+}
diff --git a/apps/governance/src/routes/proposals/protocol-upgrade/__generated__/ProtocolUpgradeProposals.ts b/apps/governance/src/routes/proposals/protocol-upgrade/__generated__/ProtocolUpgradeProposals.ts
new file mode 100644
index 000000000..ef5f9928f
--- /dev/null
+++ b/apps/governance/src/routes/proposals/protocol-upgrade/__generated__/ProtocolUpgradeProposals.ts
@@ -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, 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, 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) {
+ const options = {...defaultOptions, ...baseOptions}
+ return Apollo.useQuery(ProtocolUpgradesDocument, options);
+ }
+export function useProtocolUpgradesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) {
+ const options = {...defaultOptions, ...baseOptions}
+ return Apollo.useLazyQuery(ProtocolUpgradesDocument, options);
+ }
+export type ProtocolUpgradesQueryHookResult = ReturnType;
+export type ProtocolUpgradesLazyQueryHookResult = ReturnType;
+export type ProtocolUpgradesQueryResult = Apollo.QueryResult;
\ No newline at end of file
diff --git a/apps/governance/src/routes/proposals/protocol-upgrade/index.tsx b/apps/governance/src/routes/proposals/protocol-upgrade/index.tsx
new file mode 100644
index 000000000..75513a25b
--- /dev/null
+++ b/apps/governance/src/routes/proposals/protocol-upgrade/index.tsx
@@ -0,0 +1 @@
+export { ProtocolUpgradeProposalContainer as default } from './protocol-upgrade-proposal-container';
diff --git a/apps/governance/src/routes/proposals/protocol-upgrade/protocol-upgrade-proposal-container.tsx b/apps/governance/src/routes/proposals/protocol-upgrade/protocol-upgrade-proposal-container.tsx
new file mode 100644
index 000000000..72b54e039
--- /dev/null
+++ b/apps/governance/src/routes/proposals/protocol-upgrade/protocol-upgrade-proposal-container.tsx
@@ -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 (
+
+ {protocolUpgradeProposal ? (
+
+ ) : (
+
+ )}
+
+ );
+};
diff --git a/apps/governance/src/routes/proposals/protocol-upgrade/protocol-upgrade-proposal.spec.tsx b/apps/governance/src/routes/proposals/protocol-upgrade/protocol-upgrade-proposal.spec.tsx
new file mode 100644
index 000000000..8e59e8b0d
--- /dev/null
+++ b/apps/governance/src/routes/proposals/protocol-upgrade/protocol-upgrade-proposal.spec.tsx
@@ -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(
+
+ );
+
+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);
+ });
+});
diff --git a/apps/governance/src/routes/proposals/protocol-upgrade/protocol-upgrade-proposal.tsx b/apps/governance/src/routes/proposals/protocol-upgrade/protocol-upgrade-proposal.tsx
new file mode 100644
index 000000000..6610a17ab
--- /dev/null
+++ b/apps/governance/src/routes/proposals/protocol-upgrade/protocol-upgrade-proposal.tsx
@@ -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(
+ (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 (
+
+
+
+
+
+ {consensusValidators && consensusApprovals && (
+
+ )}
+
+ );
+};
diff --git a/apps/governance/src/routes/router-config.tsx b/apps/governance/src/routes/router-config.tsx
index 1d457c761..bbea78836 100644
--- a/apps/governance/src/routes/router-config.tsx
+++ b/apps/governance/src/routes/router-config.tsx
@@ -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(
() =>
import(
@@ -257,6 +264,10 @@ const routerConfig = [
},
{ path: 'proposals', element: },
{ path: ':proposalId', element: },
+ {
+ path: 'protocol-upgrade/:proposalReleaseTag',
+ element: ,
+ },
{ path: 'rejected', element: },
],
},
diff --git a/apps/governance/src/routes/staking/shared.ts b/apps/governance/src/routes/staking/shared.ts
index 165c4b30d..f869dce1a 100644
--- a/apps/governance/src/routes/staking/shared.ts
+++ b/apps/governance/src/routes/staking/shared.ts
@@ -20,8 +20,8 @@ export const getLastEpochScoreAndPerformance = (
};
};
-export const getNormalisedVotingPower = (votingPower: string) =>
- formatNumberPercentage(new BigNumber(votingPower).dividedBy(100), 2);
+export const getNormalisedVotingPower = (votingPower: string, decimals = 2) =>
+ formatNumberPercentage(new BigNumber(votingPower).dividedBy(100), decimals);
export const getUnnormalisedVotingPower = (
validatorScore: string | null | undefined
diff --git a/libs/utils/src/lib/format/strings.spec.ts b/libs/utils/src/lib/format/strings.spec.ts
index f91633654..d864b1310 100644
--- a/libs/utils/src/lib/format/strings.spec.ts
+++ b/libs/utils/src/lib/format/strings.spec.ts
@@ -1,4 +1,10 @@
-import { truncateByChars, ELLIPSIS, shorten, titlefy } from './strings';
+import {
+ truncateByChars,
+ ELLIPSIS,
+ shorten,
+ titlefy,
+ stripFullStops,
+} from './strings';
describe('truncateByChars', () => {
it.each([
@@ -49,3 +55,35 @@ describe('titlefy', () => {
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);
+ });
+ });
+});
diff --git a/libs/utils/src/lib/format/strings.ts b/libs/utils/src/lib/format/strings.ts
index 7e7c1f030..4f455e9ee 100644
--- a/libs/utils/src/lib/format/strings.ts
+++ b/libs/utils/src/lib/format/strings.ts
@@ -26,3 +26,7 @@ export function titlefy(words: (string | null | undefined)[]) {
.join(TITLE_SEPARATOR);
return title;
}
+
+export function stripFullStops(input: string) {
+ return input.replace(/\./g, '');
+}