diff --git a/apps/token/src/i18n/translations/dev.json b/apps/token/src/i18n/translations/dev.json index 08afd712f..9c0008f44 100644 --- a/apps/token/src/i18n/translations/dev.json +++ b/apps/token/src/i18n/translations/dev.json @@ -505,7 +505,7 @@ "voteFailedReason": "Vote closed. Failed due to: ", "Passed": "Passed", "votePassed": "Vote passed.", - "subjectToFurtherActions": "Vote passed {{daysAgo}} subject to further actions.", + "WaitingForNodeVote": "Waiting for nodes to validate asset. ", "transactionHashPrompt": "Transaction hash will appear here once the transaction is approved in your Ethereum wallet", "newWalletVersionAvailable": "A new Vega wallet is available 🎉. ", "downloadNewWallet": "Download {{newVersionAvailable}}", @@ -564,7 +564,7 @@ "yesPercentage": "Yes percentage", "noPercentage": "No percentage", "proposalTerms": "Proposal terms", - "currentlySetTo": "Currently set to ", + "currentlySetTo": "Vote currently set to ", "rankingScore": "Ranking score", "stakeScore": "Stake score", "performanceScore": "Performance", @@ -691,5 +691,7 @@ "MoreAssetsInfo": "To see Explorer data on existing assets visit", "ProposalNotFound": "Proposal not found", "ProposalNotFoundDetails": "The proposal you are looking for is not here, it may have been enacted before the last chain restore. You could check the Vega forums/discord instead for information about it.", - "FreeformProposal": "Freeform proposal" + "FreeformProposal": "Freeform proposal", + "unknownReason": "unknown reason", + "votingEnded": "Voting has ended." } diff --git a/apps/token/src/routes/governance/components/current-proposal-status/current-proposal-status.spec.tsx b/apps/token/src/routes/governance/components/current-proposal-status/current-proposal-status.spec.tsx new file mode 100644 index 000000000..7b51585ad --- /dev/null +++ b/apps/token/src/routes/governance/components/current-proposal-status/current-proposal-status.spec.tsx @@ -0,0 +1,298 @@ +import type { MockedResponse } from '@apollo/client/testing'; +import { MockedProvider } from '@apollo/client/testing'; +import { render, screen } from '@testing-library/react'; +import { NETWORK_PARAMETERS_QUERY } from '@vegaprotocol/react-helpers'; +import { ProposalRejectionReason, ProposalState } from '@vegaprotocol/types'; +import type { NetworkParamsQuery } from '@vegaprotocol/web3'; +import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider'; +import { generateProposal } from '../../test-helpers/generate-proposals'; +import type { ProposalFields } from '../../__generated__/ProposalFields'; +import { CurrentProposalStatus } from './current-proposal-status'; + +const networkParamsQueryMock: MockedResponse = { + request: { + query: NETWORK_PARAMETERS_QUERY, + }, + result: { + data: { + networkParameters: [ + { + __typename: 'NetworkParameter', + key: 'governance.proposal.updateNetParam.requiredMajority', + value: '0.00000001', + }, + { + __typename: 'NetworkParameter', + key: 'governance.proposal.updateNetParam.requiredParticipation', + value: '0.000000001', + }, + ], + }, + }, +}; + +const renderComponent = ({ proposal }: { proposal: ProposalFields }) => { + render( + + + + + + ); +}; + +beforeEach(() => { + jest.useFakeTimers(); + jest.setSystemTime(60 * 60 * 1000); +}); + +afterEach(() => { + jest.useRealTimers(); +}); + +it('Proposal open - renders will fail state if the proposal will fail', async () => { + const proposal = generateProposal(); + const failedProposal: ProposalFields = { + ...proposal, + votes: { + __typename: 'ProposalVotes', + yes: { + __typename: 'ProposalVoteSide', + totalNumber: '0', + totalTokens: '0', + votes: null, + }, + no: { + __typename: 'ProposalVoteSide', + totalNumber: '0', + totalTokens: '0', + votes: null, + }, + }, + }; + renderComponent({ proposal: failedProposal }); + expect(await screen.findByText('Vote currently set to')).toBeInTheDocument(); + expect(await screen.findByText('fail')).toBeInTheDocument(); +}); + +it('Proposal open - renders will pass state if the proposal will pass', async () => { + const proposal = generateProposal(); + + renderComponent({ proposal }); + expect(await screen.findByText('Vote currently set to')).toBeInTheDocument(); + expect(await screen.findByText('pass')).toBeInTheDocument(); +}); + +it('Proposal enacted - renders vote passed and time since enactment', async () => { + const proposal = generateProposal(); + + renderComponent({ + proposal: { + ...proposal, + state: ProposalState.STATE_ENACTED, + terms: { + ...proposal.terms, + enactmentDatetime: new Date(0).toISOString(), + }, + }, + }); + expect(await screen.findByText('Vote passed.')).toBeInTheDocument(); + expect(await screen.findByText('about 1 hour ago')).toBeInTheDocument(); +}); + +it('Proposal passed - renders vote passed and time since vote closed', async () => { + const proposal = generateProposal(); + + renderComponent({ + proposal: { + ...proposal, + state: ProposalState.STATE_PASSED, + terms: { + ...proposal.terms, + closingDatetime: new Date(0).toISOString(), + }, + }, + }); + expect(await screen.findByText('Vote passed.')).toBeInTheDocument(); + expect(await screen.findByText('about 1 hour ago')).toBeInTheDocument(); +}); + +it('Proposal waiting for node vote - will pass - renders if the vote will pass and status', async () => { + const proposal = generateProposal(); + const failedProposal: ProposalFields = { + ...proposal, + state: ProposalState.STATE_WAITING_FOR_NODE_VOTE, + votes: { + __typename: 'ProposalVotes', + yes: { + __typename: 'ProposalVoteSide', + totalNumber: '0', + totalTokens: '0', + votes: null, + }, + no: { + __typename: 'ProposalVoteSide', + totalNumber: '0', + totalTokens: '0', + votes: null, + }, + }, + }; + renderComponent({ proposal: failedProposal }); + expect( + await screen.findByText('Waiting for nodes to validate asset.') + ).toBeInTheDocument(); + expect(await screen.findByText('Vote currently set to')).toBeInTheDocument(); + expect(await screen.findByText('fail')).toBeInTheDocument(); +}); + +it('Proposal waiting for node vote - will fail - renders if the vote will pass and status', async () => { + const proposal = generateProposal(); + + renderComponent({ + proposal: { + ...proposal, + state: ProposalState.STATE_WAITING_FOR_NODE_VOTE, + }, + }); + expect( + await screen.findByText('Waiting for nodes to validate asset.') + ).toBeInTheDocument(); + expect(await screen.findByText('Vote currently set to')).toBeInTheDocument(); + expect(await screen.findByText('pass')).toBeInTheDocument(); +}); + +it('Proposal failed - renders vote failed reason and vote closed ago', async () => { + const proposal = generateProposal(); + + renderComponent({ + proposal: { + ...proposal, + state: ProposalState.STATE_FAILED, + errorDetails: 'foo', + terms: { + ...proposal.terms, + closingDatetime: new Date(0).toISOString(), + }, + }, + }); + expect( + await screen.findByText('Vote closed. Failed due to:') + ).toBeInTheDocument(); + expect(await screen.findByText('foo')).toBeInTheDocument(); + expect(await screen.findByText('about 1 hour ago')).toBeInTheDocument(); +}); + +it('Proposal failed - renders rejection reason there are no error details', async () => { + const proposal = generateProposal(); + + renderComponent({ + proposal: { + ...proposal, + state: ProposalState.STATE_FAILED, + rejectionReason: + ProposalRejectionReason.PROPOSAL_ERROR_CLOSE_TIME_TOO_LATE, + terms: { + ...proposal.terms, + closingDatetime: new Date(0).toISOString(), + }, + }, + }); + expect( + await screen.findByText('Vote closed. Failed due to:') + ).toBeInTheDocument(); + expect( + await screen.findByText('PROPOSAL_ERROR_CLOSE_TIME_TOO_LATE') + ).toBeInTheDocument(); + expect(await screen.findByText('about 1 hour ago')).toBeInTheDocument(); +}); + +it('Proposal failed - renders unknown reason if there are no error details or rejection reason', async () => { + const proposal = generateProposal(); + + renderComponent({ + proposal: { + ...proposal, + state: ProposalState.STATE_FAILED, + terms: { + ...proposal.terms, + closingDatetime: new Date(0).toISOString(), + }, + }, + }); + expect( + await screen.findByText('Vote closed. Failed due to:') + ).toBeInTheDocument(); + expect(await screen.findByText('unknown reason')).toBeInTheDocument(); + expect(await screen.findByText('about 1 hour ago')).toBeInTheDocument(); +}); + +it('Proposal failed - renders participation not met if participation is not met', async () => { + const proposal = generateProposal(); + + renderComponent({ + proposal: { + ...proposal, + state: ProposalState.STATE_FAILED, + terms: { + ...proposal.terms, + closingDatetime: new Date(0).toISOString(), + }, + votes: { + __typename: 'ProposalVotes', + yes: { + __typename: 'ProposalVoteSide', + totalNumber: '0', + totalTokens: '0', + votes: null, + }, + no: { + __typename: 'ProposalVoteSide', + totalNumber: '0', + totalTokens: '0', + votes: null, + }, + }, + }, + }); + expect( + await screen.findByText('Vote closed. Failed due to:') + ).toBeInTheDocument(); + expect(await screen.findByText('Participation not met')).toBeInTheDocument(); + expect(await screen.findByText('about 1 hour ago')).toBeInTheDocument(); +}); + +it('Proposal failed - renders majority not met if majority is not met', async () => { + const proposal = generateProposal(); + + renderComponent({ + proposal: { + ...proposal, + state: ProposalState.STATE_FAILED, + terms: { + ...proposal.terms, + closingDatetime: new Date(0).toISOString(), + }, + votes: { + __typename: 'ProposalVotes', + yes: { + __typename: 'ProposalVoteSide', + totalNumber: '0', + totalTokens: '0', + votes: null, + }, + no: { + __typename: 'ProposalVoteSide', + totalNumber: '1', + totalTokens: '25242474195500835440000', + votes: null, + }, + }, + }, + }); + expect( + await screen.findByText('Vote closed. Failed due to:') + ).toBeInTheDocument(); + expect(await screen.findByText('Majority not met')).toBeInTheDocument(); + expect(await screen.findByText('about 1 hour ago')).toBeInTheDocument(); +}); diff --git a/apps/token/src/routes/governance/components/current-proposal-status/current-proposal-status.tsx b/apps/token/src/routes/governance/components/current-proposal-status/current-proposal-status.tsx index eac728a25..84fd2ee5d 100644 --- a/apps/token/src/routes/governance/components/current-proposal-status/current-proposal-status.tsx +++ b/apps/token/src/routes/governance/components/current-proposal-status/current-proposal-status.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import type { ReactNode } from 'react'; import { formatDistanceToNow } from 'date-fns'; import { useTranslation } from 'react-i18next'; @@ -6,14 +6,39 @@ import { ProposalState } from '@vegaprotocol/types'; import { useVoteInformation } from '../../hooks'; import type { ProposalFields } from '../../__generated__/ProposalFields'; -export const StatusPass = ({ children }: { children: React.ReactNode }) => ( +export const StatusPass = ({ children }: { children: ReactNode }) => ( {children} ); -export const StatusFail = ({ children }: { children: React.ReactNode }) => ( +export const StatusFail = ({ children }: { children: ReactNode }) => ( {children} ); +const WillPass = ({ + willPass, + children, +}: { + willPass: boolean; + children?: ReactNode; +}) => { + const { t } = useTranslation(); + if (willPass) { + return ( + <> + {children} + {t('pass')} + + ); + } else { + return ( + <> + {children} + {t('fail')} + + ); + } +}; + export const CurrentProposalStatus = ({ proposal, }: { @@ -36,21 +61,7 @@ export const CurrentProposalStatus = ({ }); if (proposal.state === ProposalState.STATE_OPEN) { - if (willPass) { - return ( - <> - {t('currentlySetTo')} - {t('pass')} - - ); - } else { - return ( - <> - {t('currentlySetTo')} - {t('fail')} - - ); - } + return {t('currentlySetTo')}; } if ( @@ -81,7 +92,11 @@ export const CurrentProposalStatus = ({ return ( <> {t('voteFailedReason')} - {proposal.state} + + {proposal.errorDetails || + proposal.rejectionReason || + t('unknownReason')} +  {daysClosedAgo} ); @@ -106,7 +121,10 @@ export const CurrentProposalStatus = ({ if (proposal.state === ProposalState.STATE_WAITING_FOR_NODE_VOTE) { return ( - {t('subjectToFurtherActions', { daysAgo: daysClosedAgo })} + + {t('WaitingForNodeVote')}{' '} + {t('currentlySetTo')} + ); } diff --git a/apps/token/src/routes/governance/components/proposals-list-item/proposals-list-item-details.spec.tsx b/apps/token/src/routes/governance/components/proposals-list-item/proposals-list-item-details.spec.tsx index 1aed2d962..df7dabdb8 100644 --- a/apps/token/src/routes/governance/components/proposals-list-item/proposals-list-item-details.spec.tsx +++ b/apps/token/src/routes/governance/components/proposals-list-item/proposals-list-item-details.spec.tsx @@ -31,17 +31,18 @@ import type { Proposals_proposalsConnection_edges_node as ProposalNode } from '. const renderComponent = ( proposal: ProposalNode, mock = networkParamsQueryMock -) => ( - - - - - - - - - -); +) => + render( + + + + + + + + + + ); beforeAll(() => { jest.useFakeTimers(); @@ -53,15 +54,13 @@ afterAll(() => { describe('Proposals list item details', () => { it('Renders proposal state: Enacted', () => { - render( - renderComponent( - generateProposal({ - state: ProposalState.STATE_ENACTED, - terms: { - enactmentDatetime: lastWeek.toString(), - }, - }) - ) + renderComponent( + generateProposal({ + state: ProposalState.STATE_ENACTED, + terms: { + enactmentDatetime: lastWeek.toString(), + }, + }) ); expect(screen.getByTestId('proposal-status')).toHaveTextContent('Enacted'); expect(screen.getByTestId('vote-details')).toHaveTextContent( @@ -70,16 +69,14 @@ describe('Proposals list item details', () => { }); it('Renders proposal state: Passed', () => { - render( - renderComponent( - generateProposal({ - state: ProposalState.STATE_PASSED, - terms: { - closingDatetime: lastWeek.toString(), - enactmentDatetime: nextWeek.toString(), - }, - }) - ) + renderComponent( + generateProposal({ + state: ProposalState.STATE_PASSED, + terms: { + closingDatetime: lastWeek.toString(), + enactmentDatetime: nextWeek.toString(), + }, + }) ); expect(screen.getByTestId('proposal-status')).toHaveTextContent('Passed'); expect(screen.getByTestId('vote-details')).toHaveTextContent( @@ -88,15 +85,13 @@ describe('Proposals list item details', () => { }); it('Renders proposal state: Waiting for node vote', () => { - render( - renderComponent( - generateProposal({ - state: ProposalState.STATE_WAITING_FOR_NODE_VOTE, - terms: { - enactmentDatetime: nextWeek.toString(), - }, - }) - ) + renderComponent( + generateProposal({ + state: ProposalState.STATE_WAITING_FOR_NODE_VOTE, + terms: { + enactmentDatetime: nextWeek.toString(), + }, + }) ); expect(screen.getByTestId('proposal-status')).toHaveTextContent( 'Waiting for node vote' @@ -107,15 +102,13 @@ describe('Proposals list item details', () => { }); it('Renders proposal state: Open - 5 minutes left to vote', () => { - render( - renderComponent( - generateProposal({ - state: ProposalState.STATE_OPEN, - terms: { - closingDatetime: fiveMinutes.toString(), - }, - }) - ) + renderComponent( + generateProposal({ + state: ProposalState.STATE_OPEN, + terms: { + closingDatetime: fiveMinutes.toString(), + }, + }) ); expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open'); expect(screen.getByTestId('vote-details')).toHaveTextContent( @@ -124,15 +117,13 @@ describe('Proposals list item details', () => { }); it('Renders proposal state: Open - 5 hours left to vote', () => { - render( - renderComponent( - generateProposal({ - state: ProposalState.STATE_OPEN, - terms: { - closingDatetime: fiveHours.toString(), - }, - }) - ) + renderComponent( + generateProposal({ + state: ProposalState.STATE_OPEN, + terms: { + closingDatetime: fiveHours.toString(), + }, + }) ); expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open'); expect(screen.getByTestId('vote-details')).toHaveTextContent( @@ -141,15 +132,13 @@ describe('Proposals list item details', () => { }); it('Renders proposal state: Open - 5 days left to vote', () => { - render( - renderComponent( - generateProposal({ - state: ProposalState.STATE_OPEN, - terms: { - closingDatetime: fiveDays.toString(), - }, - }) - ) + renderComponent( + generateProposal({ + state: ProposalState.STATE_OPEN, + terms: { + closingDatetime: fiveDays.toString(), + }, + }) ); expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open'); expect(screen.getByTestId('vote-details')).toHaveTextContent( @@ -158,36 +147,34 @@ describe('Proposals list item details', () => { }); it('Renders proposal state: Open - user voted for', () => { - render( - renderComponent( - generateProposal({ - state: ProposalState.STATE_OPEN, - votes: { - __typename: 'ProposalVotes', - yes: { - votes: [ - { - __typename: 'Vote', - value: VoteValue.VALUE_YES, - datetime: lastWeek.toString(), - party: { - __typename: 'Party', - id: mockPubkey.publicKey, - stakingSummary: { - __typename: 'StakingSummary', - currentStakeAvailable: '1000', - }, + renderComponent( + generateProposal({ + state: ProposalState.STATE_OPEN, + votes: { + __typename: 'ProposalVotes', + yes: { + votes: [ + { + __typename: 'Vote', + value: VoteValue.VALUE_YES, + datetime: lastWeek.toString(), + party: { + __typename: 'Party', + id: mockPubkey.publicKey, + stakingSummary: { + __typename: 'StakingSummary', + currentStakeAvailable: '1000', }, }, - ], - }, - no: generateNoVotes(0), + }, + ], }, - terms: { - closingDatetime: nextWeek.toString(), - }, - }) - ) + no: generateNoVotes(0), + }, + terms: { + closingDatetime: nextWeek.toString(), + }, + }) ); expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open'); expect(screen.getByTestId('vote-details')).toHaveTextContent( @@ -196,36 +183,34 @@ describe('Proposals list item details', () => { }); it('Renders proposal state: Open - user voted against', () => { - render( - renderComponent( - generateProposal({ - state: ProposalState.STATE_OPEN, - votes: { - __typename: 'ProposalVotes', - no: { - votes: [ - { - __typename: 'Vote', - value: VoteValue.VALUE_NO, - datetime: lastWeek.toString(), - party: { - __typename: 'Party', - id: mockPubkey.publicKey, - stakingSummary: { - __typename: 'StakingSummary', - currentStakeAvailable: '1000', - }, + renderComponent( + generateProposal({ + state: ProposalState.STATE_OPEN, + votes: { + __typename: 'ProposalVotes', + no: { + votes: [ + { + __typename: 'Vote', + value: VoteValue.VALUE_NO, + datetime: lastWeek.toString(), + party: { + __typename: 'Party', + id: mockPubkey.publicKey, + stakingSummary: { + __typename: 'StakingSummary', + currentStakeAvailable: '1000', }, }, - ], - }, - yes: generateYesVotes(0), + }, + ], }, - terms: { - closingDatetime: nextWeek.toString(), - }, - }) - ) + yes: generateYesVotes(0), + }, + terms: { + closingDatetime: nextWeek.toString(), + }, + }) ); expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open'); expect(screen.getByTestId('vote-details')).toHaveTextContent( @@ -234,19 +219,17 @@ describe('Proposals list item details', () => { }); it('Renders proposal state: Open - participation not reached', () => { - render( - renderComponent( - generateProposal({ - state: ProposalState.STATE_OPEN, - terms: { - enactmentDatetime: nextWeek.toString(), - }, - votes: { - no: generateNoVotes(0), - yes: generateYesVotes(0), - }, - }) - ) + renderComponent( + generateProposal({ + state: ProposalState.STATE_OPEN, + terms: { + enactmentDatetime: nextWeek.toString(), + }, + votes: { + no: generateNoVotes(0), + yes: generateYesVotes(0), + }, + }) ); expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open'); expect(screen.getByTestId('vote-status')).toHaveTextContent( @@ -255,19 +238,17 @@ describe('Proposals list item details', () => { }); it('Renders proposal state: Open - majority not reached', () => { - render( - renderComponent( - generateProposal({ - state: ProposalState.STATE_OPEN, - terms: { - enactmentDatetime: nextWeek.toString(), - }, - votes: { - no: generateNoVotes(1, 1000000000000000000), - yes: generateYesVotes(1, 1000000000000000000), - }, - }) - ) + renderComponent( + generateProposal({ + state: ProposalState.STATE_OPEN, + terms: { + enactmentDatetime: nextWeek.toString(), + }, + votes: { + no: generateNoVotes(1, 1000000000000000000), + yes: generateYesVotes(1, 1000000000000000000), + }, + }) ); expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open'); expect(screen.getByTestId('vote-status')).toHaveTextContent( @@ -276,59 +257,35 @@ describe('Proposals list item details', () => { }); it('Renders proposal state: Open - will pass', () => { - render( - renderComponent( - generateProposal({ - state: ProposalState.STATE_OPEN, - votes: { - __typename: 'ProposalVotes', - yes: generateYesVotes(3000, 1000000000000000000), - no: generateNoVotes(0), - }, - terms: { - closingDatetime: nextWeek.toString(), - }, - }) - ) + renderComponent( + generateProposal({ + state: ProposalState.STATE_OPEN, + votes: { + __typename: 'ProposalVotes', + yes: generateYesVotes(3000, 1000000000000000000), + no: generateNoVotes(0), + }, + terms: { + closingDatetime: nextWeek.toString(), + }, + }) ); expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open'); expect(screen.getByTestId('vote-status')).toHaveTextContent('Set to pass'); }); - it('Renders proposal state: Open - will fail', () => { - render( - renderComponent( - generateProposal({ - state: ProposalState.STATE_OPEN, - votes: { - __typename: 'ProposalVotes', - yes: generateYesVotes(0), - no: generateNoVotes(3000, 1000000000000000000), - }, - terms: { - closingDatetime: nextWeek.toString(), - }, - }) - ) - ); - expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open'); - expect(screen.getByTestId('vote-status')).toHaveTextContent('Set to fail'); - }); - it('Renders proposal state: Declined - participation not reached', () => { - render( - renderComponent( - generateProposal({ - state: ProposalState.STATE_DECLINED, - terms: { - enactmentDatetime: lastWeek.toString(), - }, - votes: { - no: generateNoVotes(0), - yes: generateYesVotes(0), - }, - }) - ) + renderComponent( + generateProposal({ + state: ProposalState.STATE_DECLINED, + terms: { + enactmentDatetime: lastWeek.toString(), + }, + votes: { + no: generateNoVotes(0), + yes: generateYesVotes(0), + }, + }) ); expect(screen.getByTestId('proposal-status')).toHaveTextContent('Declined'); expect(screen.getByTestId('vote-status')).toHaveTextContent( @@ -337,19 +294,17 @@ describe('Proposals list item details', () => { }); it('Renders proposal state: Declined - majority not reached', () => { - render( - renderComponent( - generateProposal({ - state: ProposalState.STATE_DECLINED, - terms: { - enactmentDatetime: lastWeek.toString(), - }, - votes: { - no: generateNoVotes(1, 1000000000000000000), - yes: generateYesVotes(1, 1000000000000000000), - }, - }) - ) + renderComponent( + generateProposal({ + state: ProposalState.STATE_DECLINED, + terms: { + enactmentDatetime: lastWeek.toString(), + }, + votes: { + no: generateNoVotes(1, 1000000000000000000), + yes: generateYesVotes(1, 1000000000000000000), + }, + }) ); expect(screen.getByTestId('proposal-status')).toHaveTextContent('Declined'); expect(screen.getByTestId('vote-status')).toHaveTextContent( @@ -358,17 +313,15 @@ describe('Proposals list item details', () => { }); it('Renders proposal state: Rejected', () => { - render( - renderComponent( - generateProposal({ - state: ProposalState.STATE_REJECTED, - terms: { - enactmentDatetime: lastWeek.toString(), - }, - rejectionReason: - ProposalRejectionReason.PROPOSAL_ERROR_INVALID_FUTURE_PRODUCT, - }) - ) + renderComponent( + generateProposal({ + state: ProposalState.STATE_REJECTED, + terms: { + enactmentDatetime: lastWeek.toString(), + }, + rejectionReason: + ProposalRejectionReason.PROPOSAL_ERROR_INVALID_FUTURE_PRODUCT, + }) ); expect(screen.getByTestId('proposal-status')).toHaveTextContent('Rejected'); expect(screen.getByTestId('vote-status')).toHaveTextContent( diff --git a/apps/token/src/routes/governance/components/proposals-list-item/proposals-list-item-details.tsx b/apps/token/src/routes/governance/components/proposals-list-item/proposals-list-item-details.tsx index 7fb3a04f5..ef85bff72 100644 --- a/apps/token/src/routes/governance/components/proposals-list-item/proposals-list-item-details.tsx +++ b/apps/token/src/routes/governance/components/proposals-list-item/proposals-list-item-details.tsx @@ -133,12 +133,11 @@ export const ProposalsListItemDetails = ({ voteStatus = (!participationMet && ) || (!majorityMet && ) || - (willPass && ( + (willPass ? ( <> {t('Set to')} {t('pass')} - )) || - (!willPass && ( + ) : ( <> {t('Set to')} {t('fail')} diff --git a/apps/token/src/routes/governance/components/vote-details/use-user-vote.tsx b/apps/token/src/routes/governance/components/vote-details/use-user-vote.tsx index 0c3968ac7..ee34def99 100644 --- a/apps/token/src/routes/governance/components/vote-details/use-user-vote.tsx +++ b/apps/token/src/routes/governance/components/vote-details/use-user-vote.tsx @@ -92,17 +92,21 @@ export function useUserVote( */ async function castVote(value: VoteValue) { if (!proposalId || !pubKey) return; - + const previousVoteState = voteState; setVoteState(VoteState.Requested); try { - await sendTx(pubKey, { + const res = await sendTx(pubKey, { voteSubmission: { value: value, proposalId, }, }); - setVoteState(VoteState.Pending); + if (res === null) { + setVoteState(previousVoteState); + } else { + setVoteState(VoteState.Pending); + } // Now await vote via poll in parent component } catch (err) { diff --git a/apps/token/src/routes/governance/components/vote-details/vote-buttons.spec.tsx b/apps/token/src/routes/governance/components/vote-details/vote-buttons.spec.tsx index e8d35e746..80baa58d5 100644 --- a/apps/token/src/routes/governance/components/vote-details/vote-buttons.spec.tsx +++ b/apps/token/src/routes/governance/components/vote-details/vote-buttons.spec.tsx @@ -43,7 +43,7 @@ describe('Vote buttons', () => { ); - expect(screen.getByText('Voting has ended. You did not vote')).toBeTruthy(); + expect(screen.getByText('Voting has ended.')).toBeTruthy(); }); it('should provide a connect wallet prompt if no pubkey', () => { diff --git a/apps/token/src/routes/governance/components/vote-details/vote-buttons.tsx b/apps/token/src/routes/governance/components/vote-details/vote-buttons.tsx index cd4cd7562..dbff7c45a 100644 --- a/apps/token/src/routes/governance/components/vote-details/vote-buttons.tsx +++ b/apps/token/src/routes/governance/components/vote-details/vote-buttons.tsx @@ -1,6 +1,6 @@ import { gql, useQuery } from '@apollo/client'; import { format } from 'date-fns'; -import * as React from 'react'; +import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { @@ -88,10 +88,18 @@ export const VoteButtons = ({ openVegaWalletDialog: store.openVegaWalletDialog, })); const [changeVote, setChangeVote] = React.useState(false); + const proposalVotable = useMemo( + () => + [ + ProposalState.STATE_OPEN, + ProposalState.STATE_WAITING_FOR_NODE_VOTE, + ].includes(proposalState), + [proposalState] + ); const cantVoteUI = React.useMemo(() => { - if (proposalState !== ProposalState.STATE_OPEN) { - return t('youDidNotVote'); + if (!proposalVotable) { + return t('votingEnded'); } if (!pubKey) { @@ -141,13 +149,13 @@ export const VoteButtons = ({ return false; }, [ - t, + proposalVotable, pubKey, currentStakeAvailable, - proposalState, - appDispatch, minVoterBalance, spamProtectionMinTokens, + t, + appDispatch, openVegaWalletDialog, ]); @@ -188,7 +196,7 @@ export const VoteButtons = ({ {voteDatetime ? ( {format(voteDatetime, DATE_FORMAT_LONG)}. ) : null} - {proposalState === ProposalState.STATE_OPEN ? ( + {proposalVotable ? ( { diff --git a/apps/token/src/routes/governance/hooks/use-vote-information.ts b/apps/token/src/routes/governance/hooks/use-vote-information.ts index e028c14b0..aac1e12c1 100644 --- a/apps/token/src/routes/governance/hooks/use-vote-information.ts +++ b/apps/token/src/routes/governance/hooks/use-vote-information.ts @@ -3,12 +3,7 @@ import React from 'react'; import { useAppState } from '../../../contexts/app-state/app-state-context'; import { BigNumber } from '../../../lib/bignumber'; -import { addDecimal } from '../../../lib/decimals'; -import type { - ProposalFields, - ProposalFields_votes_no_votes, - ProposalFields_votes_yes_votes, -} from '../__generated__/ProposalFields'; +import type { ProposalFields } from '../__generated__/ProposalFields'; const useProposalNetworkParams = ({ proposal, @@ -100,34 +95,12 @@ export const useVoteInformation = ({ ); const noTokens = React.useMemo(() => { - if (!proposal.votes.no.votes) { - return new BigNumber(0); - } - const totalNoVotes = proposal.votes.no.votes.reduce( - (prevValue: BigNumber, newValue: ProposalFields_votes_no_votes) => { - return new BigNumber( - newValue.party.stakingSummary.currentStakeAvailable - ).plus(prevValue); - }, - new BigNumber(0) - ); - return new BigNumber(addDecimal(totalNoVotes, 18)); - }, [proposal.votes.no.votes]); + return new BigNumber(proposal.votes.no.totalTokens); + }, [proposal.votes.no.totalTokens]); const yesTokens = React.useMemo(() => { - if (!proposal.votes.yes.votes) { - return new BigNumber(0); - } - const totalYesVotes = proposal.votes.yes.votes.reduce( - (prevValue: BigNumber, newValue: ProposalFields_votes_yes_votes) => { - return new BigNumber( - newValue.party.stakingSummary.currentStakeAvailable - ).plus(prevValue); - }, - new BigNumber(0) - ); - return new BigNumber(addDecimal(totalYesVotes, 18)); - }, [proposal.votes.yes.votes]); + return new BigNumber(proposal.votes.yes.totalTokens); + }, [proposal.votes.yes.totalTokens]); const totalTokensVoted = React.useMemo( () => yesTokens.plus(noTokens), @@ -153,11 +126,8 @@ export const useVoteInformation = ({ }, [requiredParticipation, totalTokensVoted, totalSupply]); const majorityMet = React.useMemo(() => { - return ( - yesPercentage.isGreaterThanOrEqualTo(requiredMajorityPercentage) || - noPercentage.isGreaterThanOrEqualTo(requiredMajorityPercentage) - ); - }, [yesPercentage, noPercentage, requiredMajorityPercentage]); + return yesPercentage.isGreaterThanOrEqualTo(requiredMajorityPercentage); + }, [yesPercentage, requiredMajorityPercentage]); const totalTokensPercentage = React.useMemo(() => { return totalTokensVoted.multipliedBy(100).dividedBy(totalSupply); @@ -171,7 +141,6 @@ export const useVoteInformation = ({ ), [participationMet, requiredMajorityPercentage, yesPercentage] ); - return { willPass, totalTokensPercentage, diff --git a/apps/token/src/routes/governance/test-helpers/generate-proposals.ts b/apps/token/src/routes/governance/test-helpers/generate-proposals.ts index eb2a47542..8fe72fa64 100644 --- a/apps/token/src/routes/governance/test-helpers/generate-proposals.ts +++ b/apps/token/src/routes/governance/test-helpers/generate-proposals.ts @@ -1,4 +1,5 @@ import { ProposalState, VoteValue } from '@vegaprotocol/types'; +import BigNumber from 'bignumber.js'; import * as faker from 'faker'; import isArray from 'lodash/isArray'; import mergeWith from 'lodash/mergeWith'; @@ -7,7 +8,9 @@ import type { DeepPartial } from '../../../lib/type-helpers'; import type { ProposalFields, ProposalFields_votes_no, + ProposalFields_votes_no_votes, ProposalFields_votes_yes, + ProposalFields_votes_yes_votes, } from '../__generated__/ProposalFields'; export function generateProposal( @@ -76,34 +79,39 @@ export const generateYesVotes = ( numberOfVotes = 5, fixedTokenValue?: number ): ProposalFields_votes_yes => { + const votes = Array.from(Array(numberOfVotes)).map(() => { + const vote: ProposalFields_votes_yes_votes = { + __typename: 'Vote', + value: VoteValue.VALUE_YES, + party: { + __typename: 'Party', + id: faker.datatype.uuid(), + stakingSummary: { + __typename: 'StakingSummary', + currentStakeAvailable: fixedTokenValue + ? fixedTokenValue.toString() + : faker.datatype + .number({ + min: 1000000000000000000, + max: 10000000000000000000000, + }) + .toString(), + }, + }, + datetime: faker.date.past().toISOString(), + }; + + return vote; + }); return { __typename: 'ProposalVoteSide', - totalNumber: faker.datatype.number({ min: 0, max: 100 }).toString(), - totalTokens: faker.datatype - .number({ min: 1, max: 10000000000000000000000 }) + totalNumber: votes.length.toString(), + totalTokens: votes + .reduce((acc, cur) => { + return acc.plus(cur.party.stakingSummary.currentStakeAvailable); + }, new BigNumber(0)) .toString(), - votes: Array.from(Array(numberOfVotes)).map(() => { - return { - __typename: 'Vote', - value: VoteValue.VALUE_YES, - party: { - id: faker.datatype.uuid(), - __typename: 'Party', - stakingSummary: { - __typename: 'StakingSummary', - currentStakeAvailable: fixedTokenValue - ? fixedTokenValue.toString() - : faker.datatype - .number({ - min: 1000000000000000000, - max: 10000000000000000000000, - }) - .toString(), - }, - }, - datetime: faker.date.past().toISOString(), - }; - }), + votes, }; }; @@ -111,33 +119,37 @@ export const generateNoVotes = ( numberOfVotes = 5, fixedTokenValue?: number ): ProposalFields_votes_no => { + const votes = Array.from(Array(numberOfVotes)).map(() => { + const vote: ProposalFields_votes_no_votes = { + __typename: 'Vote', + value: VoteValue.VALUE_NO, + party: { + id: faker.datatype.uuid(), + __typename: 'Party', + stakingSummary: { + __typename: 'StakingSummary', + currentStakeAvailable: fixedTokenValue + ? fixedTokenValue.toString() + : faker.datatype + .number({ + min: 1000000000000000000, + max: 10000000000000000000000, + }) + .toString(), + }, + }, + datetime: faker.date.past().toISOString(), + }; + return vote; + }); return { __typename: 'ProposalVoteSide', - totalNumber: faker.datatype.number({ min: 0, max: 100 }).toString(), - totalTokens: faker.datatype - .number({ min: 1000000000000000000, max: 10000000000000000000000 }) + totalNumber: votes.length.toString(), + totalTokens: votes + .reduce((acc, cur) => { + return acc.plus(cur.party.stakingSummary.currentStakeAvailable); + }, new BigNumber(0)) .toString(), - votes: Array.from(Array(numberOfVotes)).map(() => { - return { - __typename: 'Vote', - value: VoteValue.VALUE_NO, - party: { - id: faker.datatype.uuid(), - __typename: 'Party', - stakingSummary: { - __typename: 'StakingSummary', - currentStakeAvailable: fixedTokenValue - ? fixedTokenValue.toString() - : faker.datatype - .number({ - min: 1000000000000000000, - max: 10000000000000000000000, - }) - .toString(), - }, - }, - datetime: faker.date.past().toISOString(), - }; - }), + votes, }; };