diff --git a/apps/governance-e2e/src/integration/view/home.cy.ts b/apps/governance-e2e/src/integration/view/home.cy.ts index 2890c7f23..817a99489 100644 --- a/apps/governance-e2e/src/integration/view/home.cy.ts +++ b/apps/governance-e2e/src/integration/view/home.cy.ts @@ -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') .should('contain.text', 'TEST ANNOUNCEMENT!') .within(() => { diff --git a/apps/governance/src/i18n/translations/dev.json b/apps/governance/src/i18n/translations/dev.json index 952d6886b..f2bdcc66c 100644 --- a/apps/governance/src/i18n/translations/dev.json +++ b/apps/governance/src/i18n/translations/dev.json @@ -595,6 +595,7 @@ "noPercentage": "No percentage", "proposalTerms": "Proposal terms", "currentlySetTo": "Currently expected to ", + "currently": "currently", "finalOutcomeMayDiffer": "Final outcome may differ", "votingPower": "Voting power", "normalisedVotingPower": "Normalised voting power", @@ -767,7 +768,7 @@ "performancePenalty": "Performance penalty", "overstaked": "Overstaked", "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", "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", @@ -775,5 +776,18 @@ "homeRewardsButtonText": "See rewards", "homeVegaTokenIntro": "VEGA Token is a governance asset used to make and vote on proposals, and nominate validators.", "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" } diff --git a/apps/governance/src/routes/home/index.tsx b/apps/governance/src/routes/home/index.tsx index 6f3b2acb1..3f5751ccf 100644 --- a/apps/governance/src/routes/home/index.tsx +++ b/apps/governance/src/routes/home/index.tsx @@ -10,23 +10,31 @@ import { import { useDocumentTitle } from '../../hooks/use-document-title'; import { useRefreshAfterEpoch } from '../../hooks/use-refresh-after-epoch'; 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 { ExternalLinks, removePaginationWrapper } from '@vegaprotocol/utils'; import { useNodesQuery } from '../staking/home/__generated__/Nodes'; 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 * as Schema from '@vegaprotocol/types'; import type { RouteChildProps } from '..'; import type { ProposalFieldsFragment } from '../proposals/proposals/__generated__/Proposals'; import type { NodesFragmentFragment } from '../staking/home/__generated__/Nodes'; +import type { ProtocolUpgradeProposalFieldsFragment } from '../proposals/protocol-upgrade/__generated__/ProtocolUpgradeProposals'; const nodesToShow = 6; const HomeProposals = ({ proposals, + protocolUpgradeProposals, }: { proposals: ProposalFieldsFragment[]; + protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[]; }) => { const { t } = useTranslation(); @@ -47,6 +55,10 @@ const HomeProposals = ({ data-testid="home-proposal-list" className="grid md:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2 gap-6" > + {protocolUpgradeProposals.map((proposal, index) => ( + + ))} + {proposals.map((proposal) => ( ))} @@ -107,20 +119,7 @@ const HomeNodes = ({ {trimmedActiveNodes.map(({ id, avatarUrl, name }) => (
- - -
- {avatarUrl && ( - {`Avatar - )} - {name} -
-
- +
))} @@ -136,6 +135,35 @@ const HomeNodes = ({ ); }; +interface ValidatorDetailsLinkProps { + id: string; + avatarUrl: string | null | undefined; + name: string; +} + +export const ValidatorDetailsLink = ({ + id, + avatarUrl, + name, +}: ValidatorDetailsLinkProps) => { + return ( + + +
+ {avatarUrl && ( + {`Avatar + )} + {name} +
+
+ + ); +}; + const GovernanceHome = ({ name }: RouteChildProps) => { useDocumentTitle(name); const { t } = useTranslation(); @@ -149,6 +177,16 @@ const GovernanceHome = ({ name }: RouteChildProps) => { errorPolicy: 'ignore', }); + const { + data: protocolUpgradesData, + loading: protocolUpgradesLoading, + error: protocolUpgradesError, + } = useProtocolUpgradesQuery({ + pollInterval: 5000, + fetchPolicy: 'network-only', + errorPolicy: 'ignore', + }); + const { data: validatorsData, error: validatorsError, @@ -163,11 +201,38 @@ const GovernanceHome = ({ name }: RouteChildProps) => { proposalsData ? getNotRejectedProposals( proposalsData.proposalsConnection - ).slice(0, 3) + ) : [], [proposalsData] ); + const protocolUpgradeProposals = useMemo( + () => + protocolUpgradesData + ? getNotRejectedProtocolUpgradeProposals( + 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( validatorsData?.nodesConnection.edges ); @@ -182,11 +247,14 @@ const GovernanceHome = ({ name }: RouteChildProps) => { return ( - + ( - + diff --git a/apps/governance/src/routes/proposals/components/proposals-list/proposals-list.tsx b/apps/governance/src/routes/proposals/components/proposals-list/proposals-list.tsx index ab09aef1f..f31921db5 100644 --- a/apps/governance/src/routes/proposals/components/proposals-list/proposals-list.tsx +++ b/apps/governance/src/routes/proposals/components/proposals-list/proposals-list.tsx @@ -3,17 +3,21 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Heading, SubHeading } from '../../../../components/heading'; 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 Routes from '../../../routes'; import { Button } from '@vegaprotocol/ui-toolkit'; 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 { 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 { proposals: Array; + protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[]; + lastBlockHeight?: string; } interface SortedProposalsProps { @@ -21,7 +25,16 @@ interface SortedProposalsProps { closed: Array; } -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} +
    +
    + +
    + + + +
    +
    +
    +
    +
  • + ); +}; 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, ''); +}