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: ",
"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."
}

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 { 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 }) => (
<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>
);
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 = ({
proposal,
}: {
@ -36,21 +61,7 @@ export const CurrentProposalStatus = ({
});
if (proposal.state === ProposalState.STATE_OPEN) {
if (willPass) {
return (
<>
{t('currentlySetTo')}
<StatusPass>{t('pass')}</StatusPass>
</>
);
} else {
return (
<>
{t('currentlySetTo')}
<StatusFail>{t('fail')}</StatusFail>
</>
);
}
return <WillPass willPass={willPass}>{t('currentlySetTo')}</WillPass>;
}
if (
@ -81,7 +92,11 @@ export const CurrentProposalStatus = ({
return (
<>
<span>{t('voteFailedReason')}</span>
<StatusFail>{proposal.state}</StatusFail>
<StatusFail>
{proposal.errorDetails ||
proposal.rejectionReason ||
t('unknownReason')}
</StatusFail>
<span>&nbsp;{daysClosedAgo}</span>
</>
);
@ -106,7 +121,10 @@ export const CurrentProposalStatus = ({
if (proposal.state === ProposalState.STATE_WAITING_FOR_NODE_VOTE) {
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 = (
proposal: ProposalNode,
mock = networkParamsQueryMock
) => (
<Router>
<MockedProvider mocks={[mock]}>
<AppStateProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<ProposalsListItemDetails proposal={proposal} />
</VegaWalletContext.Provider>
</AppStateProvider>
</MockedProvider>
</Router>
);
) =>
render(
<Router>
<MockedProvider mocks={[mock]}>
<AppStateProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<ProposalsListItemDetails proposal={proposal} />
</VegaWalletContext.Provider>
</AppStateProvider>
</MockedProvider>
</Router>
);
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(

View File

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

View File

@ -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) {

View File

@ -43,7 +43,7 @@ describe('Vote buttons', () => {
</VegaWalletContext.Provider>
</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', () => {

View File

@ -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 ? (
<span>{format(voteDatetime, DATE_FORMAT_LONG)}. </span>
) : null}
{proposalState === ProposalState.STATE_OPEN ? (
{proposalVotable ? (
<ButtonLink
data-testid="change-vote-button"
onClick={() => {

View File

@ -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,

View File

@ -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,
};
};