fix: voting during waiting for node vote (#1748)

* fix: voting during waiting for node vote

allow voting during waiting for node vote, bug fix for vote cancelled, show correct status

* fix: adjust vote information to be more correct

* test: begin adding tests for current proposal status

* chore: clean up render logic, add more tests for current proposal status

* chore: add tests for failed proposals

* test: add final tests for vote information

* test: fix rebase issues
This commit is contained in:
Dexter Edwards 2022-10-17 13:27:59 +01:00 committed by GitHub
parent 5e75e0ee21
commit 1bc41c8a9a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 605 additions and 342 deletions

View File

@ -505,7 +505,7 @@
"voteFailedReason": "Vote closed. Failed due to: ", "voteFailedReason": "Vote closed. Failed due to: ",
"Passed": "Passed", "Passed": "Passed",
"votePassed": "Vote 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", "transactionHashPrompt": "Transaction hash will appear here once the transaction is approved in your Ethereum wallet",
"newWalletVersionAvailable": "A new Vega wallet is available 🎉. ", "newWalletVersionAvailable": "A new Vega wallet is available 🎉. ",
"downloadNewWallet": "Download {{newVersionAvailable}}", "downloadNewWallet": "Download {{newVersionAvailable}}",
@ -564,7 +564,7 @@
"yesPercentage": "Yes percentage", "yesPercentage": "Yes percentage",
"noPercentage": "No percentage", "noPercentage": "No percentage",
"proposalTerms": "Proposal terms", "proposalTerms": "Proposal terms",
"currentlySetTo": "Currently set to ", "currentlySetTo": "Vote currently set to ",
"rankingScore": "Ranking score", "rankingScore": "Ranking score",
"stakeScore": "Stake score", "stakeScore": "Stake score",
"performanceScore": "Performance", "performanceScore": "Performance",
@ -691,5 +691,7 @@
"MoreAssetsInfo": "To see Explorer data on existing assets visit", "MoreAssetsInfo": "To see Explorer data on existing assets visit",
"ProposalNotFound": "Proposal not found", "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.", "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."
} }

View File

@ -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<NetworkParamsQuery> = {
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(
<AppStateProvider>
<MockedProvider mocks={[networkParamsQueryMock]}>
<CurrentProposalStatus proposal={proposal} />
</MockedProvider>
</AppStateProvider>
);
};
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();
});

View File

@ -1,4 +1,4 @@
import React from 'react'; import type { ReactNode } from 'react';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -6,14 +6,39 @@ import { ProposalState } from '@vegaprotocol/types';
import { useVoteInformation } from '../../hooks'; import { useVoteInformation } from '../../hooks';
import type { ProposalFields } from '../../__generated__/ProposalFields'; import type { ProposalFields } from '../../__generated__/ProposalFields';
export const StatusPass = ({ children }: { children: React.ReactNode }) => ( export const StatusPass = ({ children }: { children: ReactNode }) => (
<span className="text-vega-green">{children}</span> <span className="text-vega-green">{children}</span>
); );
export const StatusFail = ({ children }: { children: React.ReactNode }) => ( export const StatusFail = ({ children }: { children: ReactNode }) => (
<span className="text-danger">{children}</span> <span className="text-danger">{children}</span>
); );
const WillPass = ({
willPass,
children,
}: {
willPass: boolean;
children?: ReactNode;
}) => {
const { t } = useTranslation();
if (willPass) {
return (
<>
{children}
<StatusPass>{t('pass')}</StatusPass>
</>
);
} else {
return (
<>
{children}
<StatusFail>{t('fail')}</StatusFail>
</>
);
}
};
export const CurrentProposalStatus = ({ export const CurrentProposalStatus = ({
proposal, proposal,
}: { }: {
@ -36,21 +61,7 @@ export const CurrentProposalStatus = ({
}); });
if (proposal.state === ProposalState.STATE_OPEN) { if (proposal.state === ProposalState.STATE_OPEN) {
if (willPass) { return <WillPass willPass={willPass}>{t('currentlySetTo')}</WillPass>;
return (
<>
{t('currentlySetTo')}
<StatusPass>{t('pass')}</StatusPass>
</>
);
} else {
return (
<>
{t('currentlySetTo')}
<StatusFail>{t('fail')}</StatusFail>
</>
);
}
} }
if ( if (
@ -81,7 +92,11 @@ export const CurrentProposalStatus = ({
return ( return (
<> <>
<span>{t('voteFailedReason')}</span> <span>{t('voteFailedReason')}</span>
<StatusFail>{proposal.state}</StatusFail> <StatusFail>
{proposal.errorDetails ||
proposal.rejectionReason ||
t('unknownReason')}
</StatusFail>
<span>&nbsp;{daysClosedAgo}</span> <span>&nbsp;{daysClosedAgo}</span>
</> </>
); );
@ -106,7 +121,10 @@ export const CurrentProposalStatus = ({
if (proposal.state === ProposalState.STATE_WAITING_FOR_NODE_VOTE) { if (proposal.state === ProposalState.STATE_WAITING_FOR_NODE_VOTE) {
return ( return (
<span>{t('subjectToFurtherActions', { daysAgo: daysClosedAgo })}</span> <WillPass willPass={willPass}>
<span>{t('WaitingForNodeVote')}</span>{' '}
<span>{t('currentlySetTo')}</span>
</WillPass>
); );
} }

View File

@ -31,17 +31,18 @@ import type { Proposals_proposalsConnection_edges_node as ProposalNode } from '.
const renderComponent = ( const renderComponent = (
proposal: ProposalNode, proposal: ProposalNode,
mock = networkParamsQueryMock mock = networkParamsQueryMock
) => ( ) =>
<Router> render(
<MockedProvider mocks={[mock]}> <Router>
<AppStateProvider> <MockedProvider mocks={[mock]}>
<VegaWalletContext.Provider value={mockWalletContext}> <AppStateProvider>
<ProposalsListItemDetails proposal={proposal} /> <VegaWalletContext.Provider value={mockWalletContext}>
</VegaWalletContext.Provider> <ProposalsListItemDetails proposal={proposal} />
</AppStateProvider> </VegaWalletContext.Provider>
</MockedProvider> </AppStateProvider>
</Router> </MockedProvider>
); </Router>
);
beforeAll(() => { beforeAll(() => {
jest.useFakeTimers(); jest.useFakeTimers();
@ -53,15 +54,13 @@ afterAll(() => {
describe('Proposals list item details', () => { describe('Proposals list item details', () => {
it('Renders proposal state: Enacted', () => { it('Renders proposal state: Enacted', () => {
render( renderComponent(
renderComponent( generateProposal({
generateProposal({ state: ProposalState.STATE_ENACTED,
state: ProposalState.STATE_ENACTED, terms: {
terms: { enactmentDatetime: lastWeek.toString(),
enactmentDatetime: lastWeek.toString(), },
}, })
})
)
); );
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Enacted'); expect(screen.getByTestId('proposal-status')).toHaveTextContent('Enacted');
expect(screen.getByTestId('vote-details')).toHaveTextContent( expect(screen.getByTestId('vote-details')).toHaveTextContent(
@ -70,16 +69,14 @@ describe('Proposals list item details', () => {
}); });
it('Renders proposal state: Passed', () => { it('Renders proposal state: Passed', () => {
render( renderComponent(
renderComponent( generateProposal({
generateProposal({ state: ProposalState.STATE_PASSED,
state: ProposalState.STATE_PASSED, terms: {
terms: { closingDatetime: lastWeek.toString(),
closingDatetime: lastWeek.toString(), enactmentDatetime: nextWeek.toString(),
enactmentDatetime: nextWeek.toString(), },
}, })
})
)
); );
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Passed'); expect(screen.getByTestId('proposal-status')).toHaveTextContent('Passed');
expect(screen.getByTestId('vote-details')).toHaveTextContent( expect(screen.getByTestId('vote-details')).toHaveTextContent(
@ -88,15 +85,13 @@ describe('Proposals list item details', () => {
}); });
it('Renders proposal state: Waiting for node vote', () => { it('Renders proposal state: Waiting for node vote', () => {
render( renderComponent(
renderComponent( generateProposal({
generateProposal({ state: ProposalState.STATE_WAITING_FOR_NODE_VOTE,
state: ProposalState.STATE_WAITING_FOR_NODE_VOTE, terms: {
terms: { enactmentDatetime: nextWeek.toString(),
enactmentDatetime: nextWeek.toString(), },
}, })
})
)
); );
expect(screen.getByTestId('proposal-status')).toHaveTextContent( expect(screen.getByTestId('proposal-status')).toHaveTextContent(
'Waiting for node vote' 'Waiting for node vote'
@ -107,15 +102,13 @@ describe('Proposals list item details', () => {
}); });
it('Renders proposal state: Open - 5 minutes left to vote', () => { it('Renders proposal state: Open - 5 minutes left to vote', () => {
render( renderComponent(
renderComponent( generateProposal({
generateProposal({ state: ProposalState.STATE_OPEN,
state: ProposalState.STATE_OPEN, terms: {
terms: { closingDatetime: fiveMinutes.toString(),
closingDatetime: fiveMinutes.toString(), },
}, })
})
)
); );
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open'); expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
expect(screen.getByTestId('vote-details')).toHaveTextContent( 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', () => { it('Renders proposal state: Open - 5 hours left to vote', () => {
render( renderComponent(
renderComponent( generateProposal({
generateProposal({ state: ProposalState.STATE_OPEN,
state: ProposalState.STATE_OPEN, terms: {
terms: { closingDatetime: fiveHours.toString(),
closingDatetime: fiveHours.toString(), },
}, })
})
)
); );
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open'); expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
expect(screen.getByTestId('vote-details')).toHaveTextContent( 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', () => { it('Renders proposal state: Open - 5 days left to vote', () => {
render( renderComponent(
renderComponent( generateProposal({
generateProposal({ state: ProposalState.STATE_OPEN,
state: ProposalState.STATE_OPEN, terms: {
terms: { closingDatetime: fiveDays.toString(),
closingDatetime: fiveDays.toString(), },
}, })
})
)
); );
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open'); expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
expect(screen.getByTestId('vote-details')).toHaveTextContent( expect(screen.getByTestId('vote-details')).toHaveTextContent(
@ -158,36 +147,34 @@ describe('Proposals list item details', () => {
}); });
it('Renders proposal state: Open - user voted for', () => { it('Renders proposal state: Open - user voted for', () => {
render( renderComponent(
renderComponent( generateProposal({
generateProposal({ state: ProposalState.STATE_OPEN,
state: ProposalState.STATE_OPEN, votes: {
votes: { __typename: 'ProposalVotes',
__typename: 'ProposalVotes', yes: {
yes: { votes: [
votes: [ {
{ __typename: 'Vote',
__typename: 'Vote', value: VoteValue.VALUE_YES,
value: VoteValue.VALUE_YES, datetime: lastWeek.toString(),
datetime: lastWeek.toString(), party: {
party: { __typename: 'Party',
__typename: 'Party', id: mockPubkey.publicKey,
id: mockPubkey.publicKey, stakingSummary: {
stakingSummary: { __typename: 'StakingSummary',
__typename: 'StakingSummary', currentStakeAvailable: '1000',
currentStakeAvailable: '1000',
},
}, },
}, },
], },
}, ],
no: generateNoVotes(0),
}, },
terms: { no: generateNoVotes(0),
closingDatetime: nextWeek.toString(), },
}, terms: {
}) closingDatetime: nextWeek.toString(),
) },
})
); );
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open'); expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
expect(screen.getByTestId('vote-details')).toHaveTextContent( expect(screen.getByTestId('vote-details')).toHaveTextContent(
@ -196,36 +183,34 @@ describe('Proposals list item details', () => {
}); });
it('Renders proposal state: Open - user voted against', () => { it('Renders proposal state: Open - user voted against', () => {
render( renderComponent(
renderComponent( generateProposal({
generateProposal({ state: ProposalState.STATE_OPEN,
state: ProposalState.STATE_OPEN, votes: {
votes: { __typename: 'ProposalVotes',
__typename: 'ProposalVotes', no: {
no: { votes: [
votes: [ {
{ __typename: 'Vote',
__typename: 'Vote', value: VoteValue.VALUE_NO,
value: VoteValue.VALUE_NO, datetime: lastWeek.toString(),
datetime: lastWeek.toString(), party: {
party: { __typename: 'Party',
__typename: 'Party', id: mockPubkey.publicKey,
id: mockPubkey.publicKey, stakingSummary: {
stakingSummary: { __typename: 'StakingSummary',
__typename: 'StakingSummary', currentStakeAvailable: '1000',
currentStakeAvailable: '1000',
},
}, },
}, },
], },
}, ],
yes: generateYesVotes(0),
}, },
terms: { yes: generateYesVotes(0),
closingDatetime: nextWeek.toString(), },
}, terms: {
}) closingDatetime: nextWeek.toString(),
) },
})
); );
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open'); expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
expect(screen.getByTestId('vote-details')).toHaveTextContent( expect(screen.getByTestId('vote-details')).toHaveTextContent(
@ -234,19 +219,17 @@ describe('Proposals list item details', () => {
}); });
it('Renders proposal state: Open - participation not reached', () => { it('Renders proposal state: Open - participation not reached', () => {
render( renderComponent(
renderComponent( generateProposal({
generateProposal({ state: ProposalState.STATE_OPEN,
state: ProposalState.STATE_OPEN, terms: {
terms: { enactmentDatetime: nextWeek.toString(),
enactmentDatetime: nextWeek.toString(), },
}, votes: {
votes: { no: generateNoVotes(0),
no: generateNoVotes(0), yes: generateYesVotes(0),
yes: generateYesVotes(0), },
}, })
})
)
); );
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open'); expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
expect(screen.getByTestId('vote-status')).toHaveTextContent( expect(screen.getByTestId('vote-status')).toHaveTextContent(
@ -255,19 +238,17 @@ describe('Proposals list item details', () => {
}); });
it('Renders proposal state: Open - majority not reached', () => { it('Renders proposal state: Open - majority not reached', () => {
render( renderComponent(
renderComponent( generateProposal({
generateProposal({ state: ProposalState.STATE_OPEN,
state: ProposalState.STATE_OPEN, terms: {
terms: { enactmentDatetime: nextWeek.toString(),
enactmentDatetime: nextWeek.toString(), },
}, votes: {
votes: { no: generateNoVotes(1, 1000000000000000000),
no: generateNoVotes(1, 1000000000000000000), yes: generateYesVotes(1, 1000000000000000000),
yes: generateYesVotes(1, 1000000000000000000), },
}, })
})
)
); );
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open'); expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
expect(screen.getByTestId('vote-status')).toHaveTextContent( expect(screen.getByTestId('vote-status')).toHaveTextContent(
@ -276,59 +257,35 @@ describe('Proposals list item details', () => {
}); });
it('Renders proposal state: Open - will pass', () => { it('Renders proposal state: Open - will pass', () => {
render( renderComponent(
renderComponent( generateProposal({
generateProposal({ state: ProposalState.STATE_OPEN,
state: ProposalState.STATE_OPEN, votes: {
votes: { __typename: 'ProposalVotes',
__typename: 'ProposalVotes', yes: generateYesVotes(3000, 1000000000000000000),
yes: generateYesVotes(3000, 1000000000000000000), no: generateNoVotes(0),
no: generateNoVotes(0), },
}, terms: {
terms: { closingDatetime: nextWeek.toString(),
closingDatetime: nextWeek.toString(), },
}, })
})
)
); );
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open'); expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
expect(screen.getByTestId('vote-status')).toHaveTextContent('Set to pass'); 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', () => { it('Renders proposal state: Declined - participation not reached', () => {
render( renderComponent(
renderComponent( generateProposal({
generateProposal({ state: ProposalState.STATE_DECLINED,
state: ProposalState.STATE_DECLINED, terms: {
terms: { enactmentDatetime: lastWeek.toString(),
enactmentDatetime: lastWeek.toString(), },
}, votes: {
votes: { no: generateNoVotes(0),
no: generateNoVotes(0), yes: generateYesVotes(0),
yes: generateYesVotes(0), },
}, })
})
)
); );
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Declined'); expect(screen.getByTestId('proposal-status')).toHaveTextContent('Declined');
expect(screen.getByTestId('vote-status')).toHaveTextContent( expect(screen.getByTestId('vote-status')).toHaveTextContent(
@ -337,19 +294,17 @@ describe('Proposals list item details', () => {
}); });
it('Renders proposal state: Declined - majority not reached', () => { it('Renders proposal state: Declined - majority not reached', () => {
render( renderComponent(
renderComponent( generateProposal({
generateProposal({ state: ProposalState.STATE_DECLINED,
state: ProposalState.STATE_DECLINED, terms: {
terms: { enactmentDatetime: lastWeek.toString(),
enactmentDatetime: lastWeek.toString(), },
}, votes: {
votes: { no: generateNoVotes(1, 1000000000000000000),
no: generateNoVotes(1, 1000000000000000000), yes: generateYesVotes(1, 1000000000000000000),
yes: generateYesVotes(1, 1000000000000000000), },
}, })
})
)
); );
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Declined'); expect(screen.getByTestId('proposal-status')).toHaveTextContent('Declined');
expect(screen.getByTestId('vote-status')).toHaveTextContent( expect(screen.getByTestId('vote-status')).toHaveTextContent(
@ -358,17 +313,15 @@ describe('Proposals list item details', () => {
}); });
it('Renders proposal state: Rejected', () => { it('Renders proposal state: Rejected', () => {
render( renderComponent(
renderComponent( generateProposal({
generateProposal({ state: ProposalState.STATE_REJECTED,
state: ProposalState.STATE_REJECTED, terms: {
terms: { enactmentDatetime: lastWeek.toString(),
enactmentDatetime: lastWeek.toString(), },
}, rejectionReason:
rejectionReason: ProposalRejectionReason.PROPOSAL_ERROR_INVALID_FUTURE_PRODUCT,
ProposalRejectionReason.PROPOSAL_ERROR_INVALID_FUTURE_PRODUCT, })
})
)
); );
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Rejected'); expect(screen.getByTestId('proposal-status')).toHaveTextContent('Rejected');
expect(screen.getByTestId('vote-status')).toHaveTextContent( expect(screen.getByTestId('vote-status')).toHaveTextContent(

View File

@ -133,12 +133,11 @@ export const ProposalsListItemDetails = ({
voteStatus = voteStatus =
(!participationMet && <ParticipationNotReached />) || (!participationMet && <ParticipationNotReached />) ||
(!majorityMet && <MajorityNotReached />) || (!majorityMet && <MajorityNotReached />) ||
(willPass && ( (willPass ? (
<> <>
{t('Set to')} <StatusPass>{t('pass')}</StatusPass> {t('Set to')} <StatusPass>{t('pass')}</StatusPass>
</> </>
)) || ) : (
(!willPass && (
<> <>
{t('Set to')} <StatusFail>{t('fail')}</StatusFail> {t('Set to')} <StatusFail>{t('fail')}</StatusFail>
</> </>

View File

@ -92,17 +92,21 @@ export function useUserVote(
*/ */
async function castVote(value: VoteValue) { async function castVote(value: VoteValue) {
if (!proposalId || !pubKey) return; if (!proposalId || !pubKey) return;
const previousVoteState = voteState;
setVoteState(VoteState.Requested); setVoteState(VoteState.Requested);
try { try {
await sendTx(pubKey, { const res = await sendTx(pubKey, {
voteSubmission: { voteSubmission: {
value: value, value: value,
proposalId, proposalId,
}, },
}); });
setVoteState(VoteState.Pending); if (res === null) {
setVoteState(previousVoteState);
} else {
setVoteState(VoteState.Pending);
}
// Now await vote via poll in parent component // Now await vote via poll in parent component
} catch (err) { } catch (err) {

View File

@ -43,7 +43,7 @@ describe('Vote buttons', () => {
</VegaWalletContext.Provider> </VegaWalletContext.Provider>
</AppStateProvider> </AppStateProvider>
); );
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', () => { it('should provide a connect wallet prompt if no pubkey', () => {

View File

@ -1,6 +1,6 @@
import { gql, useQuery } from '@apollo/client'; import { gql, useQuery } from '@apollo/client';
import { format } from 'date-fns'; import { format } from 'date-fns';
import * as React from 'react'; import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
@ -88,10 +88,18 @@ export const VoteButtons = ({
openVegaWalletDialog: store.openVegaWalletDialog, openVegaWalletDialog: store.openVegaWalletDialog,
})); }));
const [changeVote, setChangeVote] = React.useState(false); 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(() => { const cantVoteUI = React.useMemo(() => {
if (proposalState !== ProposalState.STATE_OPEN) { if (!proposalVotable) {
return t('youDidNotVote'); return t('votingEnded');
} }
if (!pubKey) { if (!pubKey) {
@ -141,13 +149,13 @@ export const VoteButtons = ({
return false; return false;
}, [ }, [
t, proposalVotable,
pubKey, pubKey,
currentStakeAvailable, currentStakeAvailable,
proposalState,
appDispatch,
minVoterBalance, minVoterBalance,
spamProtectionMinTokens, spamProtectionMinTokens,
t,
appDispatch,
openVegaWalletDialog, openVegaWalletDialog,
]); ]);
@ -188,7 +196,7 @@ export const VoteButtons = ({
{voteDatetime ? ( {voteDatetime ? (
<span>{format(voteDatetime, DATE_FORMAT_LONG)}. </span> <span>{format(voteDatetime, DATE_FORMAT_LONG)}. </span>
) : null} ) : null}
{proposalState === ProposalState.STATE_OPEN ? ( {proposalVotable ? (
<ButtonLink <ButtonLink
data-testid="change-vote-button" data-testid="change-vote-button"
onClick={() => { onClick={() => {

View File

@ -3,12 +3,7 @@ import React from 'react';
import { useAppState } from '../../../contexts/app-state/app-state-context'; import { useAppState } from '../../../contexts/app-state/app-state-context';
import { BigNumber } from '../../../lib/bignumber'; import { BigNumber } from '../../../lib/bignumber';
import { addDecimal } from '../../../lib/decimals'; import type { ProposalFields } from '../__generated__/ProposalFields';
import type {
ProposalFields,
ProposalFields_votes_no_votes,
ProposalFields_votes_yes_votes,
} from '../__generated__/ProposalFields';
const useProposalNetworkParams = ({ const useProposalNetworkParams = ({
proposal, proposal,
@ -100,34 +95,12 @@ export const useVoteInformation = ({
); );
const noTokens = React.useMemo(() => { const noTokens = React.useMemo(() => {
if (!proposal.votes.no.votes) { return new BigNumber(proposal.votes.no.totalTokens);
return new BigNumber(0); }, [proposal.votes.no.totalTokens]);
}
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]);
const yesTokens = React.useMemo(() => { const yesTokens = React.useMemo(() => {
if (!proposal.votes.yes.votes) { return new BigNumber(proposal.votes.yes.totalTokens);
return new BigNumber(0); }, [proposal.votes.yes.totalTokens]);
}
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]);
const totalTokensVoted = React.useMemo( const totalTokensVoted = React.useMemo(
() => yesTokens.plus(noTokens), () => yesTokens.plus(noTokens),
@ -153,11 +126,8 @@ export const useVoteInformation = ({
}, [requiredParticipation, totalTokensVoted, totalSupply]); }, [requiredParticipation, totalTokensVoted, totalSupply]);
const majorityMet = React.useMemo(() => { const majorityMet = React.useMemo(() => {
return ( return yesPercentage.isGreaterThanOrEqualTo(requiredMajorityPercentage);
yesPercentage.isGreaterThanOrEqualTo(requiredMajorityPercentage) || }, [yesPercentage, requiredMajorityPercentage]);
noPercentage.isGreaterThanOrEqualTo(requiredMajorityPercentage)
);
}, [yesPercentage, noPercentage, requiredMajorityPercentage]);
const totalTokensPercentage = React.useMemo(() => { const totalTokensPercentage = React.useMemo(() => {
return totalTokensVoted.multipliedBy(100).dividedBy(totalSupply); return totalTokensVoted.multipliedBy(100).dividedBy(totalSupply);
@ -171,7 +141,6 @@ export const useVoteInformation = ({
), ),
[participationMet, requiredMajorityPercentage, yesPercentage] [participationMet, requiredMajorityPercentage, yesPercentage]
); );
return { return {
willPass, willPass,
totalTokensPercentage, totalTokensPercentage,

View File

@ -1,4 +1,5 @@
import { ProposalState, VoteValue } from '@vegaprotocol/types'; import { ProposalState, VoteValue } from '@vegaprotocol/types';
import BigNumber from 'bignumber.js';
import * as faker from 'faker'; import * as faker from 'faker';
import isArray from 'lodash/isArray'; import isArray from 'lodash/isArray';
import mergeWith from 'lodash/mergeWith'; import mergeWith from 'lodash/mergeWith';
@ -7,7 +8,9 @@ import type { DeepPartial } from '../../../lib/type-helpers';
import type { import type {
ProposalFields, ProposalFields,
ProposalFields_votes_no, ProposalFields_votes_no,
ProposalFields_votes_no_votes,
ProposalFields_votes_yes, ProposalFields_votes_yes,
ProposalFields_votes_yes_votes,
} from '../__generated__/ProposalFields'; } from '../__generated__/ProposalFields';
export function generateProposal( export function generateProposal(
@ -76,34 +79,39 @@ export const generateYesVotes = (
numberOfVotes = 5, numberOfVotes = 5,
fixedTokenValue?: number fixedTokenValue?: number
): ProposalFields_votes_yes => { ): 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 { return {
__typename: 'ProposalVoteSide', __typename: 'ProposalVoteSide',
totalNumber: faker.datatype.number({ min: 0, max: 100 }).toString(), totalNumber: votes.length.toString(),
totalTokens: faker.datatype totalTokens: votes
.number({ min: 1, max: 10000000000000000000000 }) .reduce((acc, cur) => {
return acc.plus(cur.party.stakingSummary.currentStakeAvailable);
}, new BigNumber(0))
.toString(), .toString(),
votes: Array.from(Array(numberOfVotes)).map(() => { votes,
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(),
};
}),
}; };
}; };
@ -111,33 +119,37 @@ export const generateNoVotes = (
numberOfVotes = 5, numberOfVotes = 5,
fixedTokenValue?: number fixedTokenValue?: number
): ProposalFields_votes_no => { ): 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 { return {
__typename: 'ProposalVoteSide', __typename: 'ProposalVoteSide',
totalNumber: faker.datatype.number({ min: 0, max: 100 }).toString(), totalNumber: votes.length.toString(),
totalTokens: faker.datatype totalTokens: votes
.number({ min: 1000000000000000000, max: 10000000000000000000000 }) .reduce((acc, cur) => {
return acc.plus(cur.party.stakingSummary.currentStakeAvailable);
}, new BigNumber(0))
.toString(), .toString(),
votes: Array.from(Array(numberOfVotes)).map(() => { votes,
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(),
};
}),
}; };
}; };