From d3cb3896f42aa1ecf24253783766c3b8900b0046 Mon Sep 17 00:00:00 2001 From: Sam Keen Date: Fri, 11 Nov 2022 14:30:03 +0000 Subject: [PATCH] fix(1837): account for proposal vote and enactment deadlines being uncoupled (#2005) * Fix/1837: Remove 2 seconds from proposal vote deadline on submission to ensure the deadline is always slightly below the maximum the API accepts * fix(1837): Adjust for vote deadline and enactment deadline being decoupled in the API * fix(1837): Removed unnecessary dependencies * fix(1837): A couple of extra tests for get-enactment-timestamp * fix(1837): propose-update-asset.tsx tweaked to ensure max enactment button works properly --- apps/token/src/i18n/translations/dev.json | 3 +- ...-form-vote-and-enactment-deadline.spec.tsx | 54 ++++++---- ...posal-form-vote-and-enactment-deadline.tsx | 99 +++++++++++++------ .../propose/freeform/propose-freeform.tsx | 9 +- .../propose-network-parameter.tsx | 30 ++++-- .../propose/new-asset/propose-new-asset.tsx | 37 +++++-- .../propose/new-market/propose-new-market.tsx | 30 ++++-- .../update-asset/propose-update-asset.tsx | 31 ++++-- .../update-market/propose-update-market.tsx | 30 ++++-- .../src/utils/deadline-helpers.spec.ts | 41 +++++++- libs/governance/src/utils/deadline-helpers.ts | 19 +++- .../src/utils/get-closing-timestamp.spec.ts | 29 +++++- .../src/utils/get-closing-timestamp.ts | 24 +++-- .../src/utils/get-enactment-timestamp.spec.ts | 47 +++++++-- .../src/utils/get-enactment-timestamp.ts | 26 ++--- .../utils/get-validation-timestamp.spec.ts | 31 +++++- .../src/utils/get-validation-timestamp.ts | 24 +++-- 17 files changed, 421 insertions(+), 143 deletions(-) diff --git a/apps/token/src/i18n/translations/dev.json b/apps/token/src/i18n/translations/dev.json index c13143e0d..b1f2cf0ae 100644 --- a/apps/token/src/i18n/translations/dev.json +++ b/apps/token/src/i18n/translations/dev.json @@ -675,13 +675,14 @@ "ProposalVoteTitle": "Vote deadline", "ProposalVoteAndEnactmentTitle": "Vote deadline and enactment", "ProposalVoteDeadline": "Time till voting closes", - "ProposalEnactmentDeadline": "Time till enactment (after vote close)", + "ProposalEnactmentDeadline": "Time till enactment (must be equal to or after vote close)", "ProposalValidationDeadline": "Time till ERC-20 asset validation. Maximum value is affected by the vote deadline.", "ThisWillSetVotingDeadlineTo": "This will set the voting deadline to", "ThisWillSetEnactmentDeadlineTo": "This will set the enactment date to", "ThisWillSetValidationDeadlineTo": "This will set the validation deadline to", "Hours": "hours", "ThisWillAdd2MinutesToAllowTimeToConfirmInWallet": "Note: we add 2 minutes of extra time when you choose the minimum value. This gives you time to confirm the proposal in your wallet.", + "ProposalWillFailIfEnactmentIsEarlierThanVotingDeadline": "Proposal will fail if enactment is earlier than the voting deadline", "SelectAMarketToChange": "Select a market to change", "MarketName": "Market name", "MarketCode": "Market code", diff --git a/apps/token/src/routes/governance/components/propose/proposal-form-vote-and-enactment-deadline.spec.tsx b/apps/token/src/routes/governance/components/propose/proposal-form-vote-and-enactment-deadline.spec.tsx index 2d2824f5f..0e04c8c7e 100644 --- a/apps/token/src/routes/governance/components/propose/proposal-form-vote-and-enactment-deadline.spec.tsx +++ b/apps/token/src/routes/governance/components/propose/proposal-form-vote-and-enactment-deadline.spec.tsx @@ -20,7 +20,7 @@ afterEach(() => { const minVoteDeadline = '1h0m0s'; const maxVoteDeadline = '5h0m0s'; const minEnactDeadline = '1h0m0s'; -const maxEnactDeadline = '4h0m0s'; +const maxEnactDeadline = '5h0m0s'; /** * Formats date according to locale. @@ -118,7 +118,7 @@ describe('Proposal form vote, validation and enactment deadline', () => { const maxButton = screen.getByTestId('max-enactment'); const minButton = screen.getByTestId('min-enactment'); fireEvent.click(maxButton); - expect(enactmentDeadlineInput).toHaveValue(4); + expect(enactmentDeadlineInput).toHaveValue(5); fireEvent.click(minButton); expect(enactmentDeadlineInput).toHaveValue(1); }); @@ -154,7 +154,24 @@ describe('Proposal form vote, validation and enactment deadline', () => { '2022-01-01T00:02:00.000Z' ); expect(screen.getByTestId('enactment-date')).toHaveTextContent( - '2022-01-01T02:00:00.000Z' + '2022-01-01T01:02:00.000Z' + ); + // When max values are used, the deadlines should have 2 seconds subtracted + // from them to account any delays + const voteDeadlineMaxButton = screen.getByTestId('max-vote'); + const enactmentDeadlineMaxButton = screen.getByTestId('max-enactment'); + const validationDeadlineMaxButton = screen.getByTestId('max-validation'); + fireEvent.click(voteDeadlineMaxButton); + fireEvent.click(enactmentDeadlineMaxButton); + fireEvent.click(validationDeadlineMaxButton); + expect(screen.getByTestId('voting-date')).toHaveTextContent( + '2022-01-01T04:59:58.000Z' + ); + expect(screen.getByTestId('validation-date')).toHaveTextContent( + '2022-01-01T04:59:58.000Z' + ); + expect(screen.getByTestId('enactment-date')).toHaveTextContent( + '2022-01-01T04:59:58.000Z' ); }); @@ -171,19 +188,7 @@ describe('Proposal form vote, validation and enactment deadline', () => { '2022-01-01T00:02:30.000Z' ); expect(screen.getByTestId('enactment-date')).toHaveTextContent( - '2022-01-01T02:00:30.000Z' - ); - }); - - it('update the vote deadline date and the enactment deadline date when the vote deadline is changed', () => { - renderComponent(); - const voteDeadlineInput = screen.getByTestId('proposal-vote-deadline'); - fireEvent.change(voteDeadlineInput, { target: { value: 2 } }); - expect(screen.getByTestId('voting-date')).toHaveTextContent( - '2022-01-01T02:00:00.000Z' - ); - expect(screen.getByTestId('enactment-date')).toHaveTextContent( - '2022-01-01T03:00:00.000Z' + '2022-01-01T01:02:30.000Z' ); }); @@ -198,7 +203,7 @@ describe('Proposal form vote, validation and enactment deadline', () => { fireEvent.click(voteDeadlineMaxButton); fireEvent.click(validationDeadlineMaxButton); expect(screen.getByTestId('validation-date')).toHaveTextContent( - '2022-01-01T05:00:00.000Z' + '2022-01-01T04:59:58.000Z' ); expect(validationDeadlineInput).toHaveValue(5); fireEvent.click(voteDeadlineMinButton); @@ -207,4 +212,19 @@ describe('Proposal form vote, validation and enactment deadline', () => { ); expect(validationDeadlineInput).toHaveValue(1); }); + + it('displays error text if the vote deadline is set later than the enactment deadline', () => { + renderComponent(); + const voteDeadlineInput = screen.getByTestId('proposal-vote-deadline'); + const enactmentDeadlineInput = screen.getByTestId( + 'proposal-enactment-deadline' + ); + fireEvent.change(voteDeadlineInput, { target: { value: 5 } }); + fireEvent.change(enactmentDeadlineInput, { target: { value: 2 } }); + expect( + screen.getByTestId('enactment-before-voting-deadline') + ).toHaveTextContent( + 'Proposal will fail if enactment is earlier than the voting deadline' + ); + }); }); diff --git a/apps/token/src/routes/governance/components/propose/proposal-form-vote-and-enactment-deadline.tsx b/apps/token/src/routes/governance/components/propose/proposal-form-vote-and-enactment-deadline.tsx index a1da96410..d8f98996a 100644 --- a/apps/token/src/routes/governance/components/propose/proposal-form-vote-and-enactment-deadline.tsx +++ b/apps/token/src/routes/governance/components/propose/proposal-form-vote-and-enactment-deadline.tsx @@ -7,10 +7,12 @@ import { InputError, } from '@vegaprotocol/ui-toolkit'; import { getDateTimeFormat } from '@vegaprotocol/react-helpers'; -import { addHours, addMinutes } from 'date-fns'; +import { addHours } from 'date-fns'; import { + addTwoMinutes, deadlineToSeconds, secondsToRoundedHours, + subtractTwoSeconds, } from '@vegaprotocol/governance'; import { ProposalFormSubheader } from './proposal-form-subheader'; import type { UseFormRegisterReturn } from 'react-hook-form'; @@ -218,6 +220,22 @@ const EnactmentForm = ({ {getDateTimeFormat().format(deadlineDates.enactment)} + {deadlines.enactment === minEnactmentHours && ( + + {t('ThisWillAdd2MinutesToAllowTimeToConfirmInWallet')} + + )} + {deadlines.enactment && deadlines.enactment < deadlines.vote && ( + + {t('ProposalWillFailIfEnactmentIsEarlierThanVotingDeadline')} + + )}

)} @@ -302,16 +320,13 @@ export function ProposalFormVoteAndEnactmentDeadline({ }); const [deadlineDates, setDeadlineDates] = useState({ - vote: - deadlines.vote === minVoteHours - ? addHours(addMinutes(new Date(), 2), deadlines.vote) - : addHours(new Date(), deadlines.vote), + vote: addHours(addTwoMinutes(), deadlines.vote), enactment: deadlines.enactment - ? addHours(new Date(), deadlines.vote + deadlines.enactment) + ? addHours(addTwoMinutes(), deadlines.enactment) : undefined, validation: deadlines.validation === 0 - ? addHours(addMinutes(new Date(), 2), deadlines.validation) + ? addHours(addTwoMinutes(), deadlines.validation) : addHours(new Date(), deadlines.validation), }); @@ -319,21 +334,40 @@ export function ProposalFormVoteAndEnactmentDeadline({ const interval = setInterval(() => { setDeadlineDates((prev) => ({ ...prev, - vote: - deadlines.vote === minVoteHours - ? addHours(addMinutes(new Date(), 2), deadlines.vote) - : addHours(new Date(), deadlines.vote), + vote: addHours( + (deadlines.vote === minVoteHours && addTwoMinutes()) || + (deadlines.vote === maxVoteHours && subtractTwoSeconds()) || + new Date(), + deadlines.vote + ), enactment: deadlines.enactment - ? addHours(new Date(), deadlines.vote + deadlines.enactment) + ? addHours( + (deadlines.enactment === minEnactmentHours && addTwoMinutes()) || + (deadlines.enactment === maxEnactmentHours && + subtractTwoSeconds()) || + new Date(), + deadlines.enactment + ) : undefined, validation: deadlines.validation === 0 - ? addHours(addMinutes(new Date(), 2), deadlines.validation) - : addHours(new Date(), deadlines.validation), + ? addHours(addTwoMinutes(), deadlines.validation) + : addHours( + (deadlines.validation === maxVoteHours && + subtractTwoSeconds()) || + new Date(), + deadlines.validation + ), })); }, 1000); return () => clearInterval(interval); - }, [deadlines, minVoteHours]); + }, [ + deadlines, + maxEnactmentHours, + maxVoteHours, + minEnactmentHours, + minVoteHours, + ]); const updateVoteDeadlineAndDate = (hours: number) => { // Validation, when needed, can only happen within the voting period. Therefore, if the @@ -345,22 +379,24 @@ export function ProposalFormVoteAndEnactmentDeadline({ validation: prev.validation && Math.min(prev.validation, hours), })); - // If the vote deadline is set to minimum, add 2 mins to the date as we do + // If the vote deadlines are set to minimum, add 2 mins to the date as we do // this on submission to allow time to confirm in the wallet. Amending the // vote deadline also changes the enactment date and potentially the validation // date. // The validation deadline date cannot be after the vote deadline date. Therefore, // if the vote deadline is changed, the validation deadline must potentially // be changed to be within the new vote deadline. + // Whilst it's not ideal, currently enactment deadlines are uncoupled from + // vote deadlines in the API. Therefore, the UI currently is too, so updating + // the vote deadline does not update the enactment deadline. setDeadlineDates((prev) => ({ ...prev, - vote: - hours === minVoteHours - ? addHours(addMinutes(new Date(), 2), hours) - : addHours(new Date(), hours), - enactment: deadlines.enactment - ? addHours(new Date(), hours + deadlines.enactment) - : undefined, + vote: addHours( + (hours === minVoteHours && addTwoMinutes()) || + (hours === maxVoteHours && subtractTwoSeconds()) || + new Date(), + hours + ), validation: addHours(new Date(), Math.min(hours, deadlines.validation)), })); }; @@ -373,7 +409,12 @@ export function ProposalFormVoteAndEnactmentDeadline({ setDeadlineDates((prev) => ({ ...prev, - enactment: addHours(deadlineDates.vote, hours), + enactment: addHours( + (hours === minEnactmentHours && addTwoMinutes()) || + (hours === maxEnactmentHours && subtractTwoSeconds()) || + new Date(), + hours + ), })); }; @@ -385,10 +426,12 @@ export function ProposalFormVoteAndEnactmentDeadline({ setDeadlineDates((prev) => ({ ...prev, - validation: - hours === 0 - ? addHours(addMinutes(new Date(), 2), hours) - : addHours(new Date(), hours), + validation: addHours( + (hours === 0 && addTwoMinutes()) || + (hours === maxVoteHours && subtractTwoSeconds()) || + new Date(), + hours + ), })); }; diff --git a/apps/token/src/routes/governance/propose/freeform/propose-freeform.tsx b/apps/token/src/routes/governance/propose/freeform/propose-freeform.tsx index 932cec2b7..1ee29fbd6 100644 --- a/apps/token/src/routes/governance/propose/freeform/propose-freeform.tsx +++ b/apps/token/src/routes/governance/propose/freeform/propose-freeform.tsx @@ -56,6 +56,12 @@ export const ProposeFreeform = () => { params.governance_proposal_freeform_minClose ).toString(); + const isVoteDeadlineAtMaximum = + fields.proposalVoteDeadline === + deadlineToRoundedHours( + params.governance_proposal_freeform_maxClose + ).toString(); + await submit({ rationale: { title: fields.proposalTitle, @@ -65,7 +71,8 @@ export const ProposeFreeform = () => { newFreeform: {}, closingTimestamp: getClosingTimestamp( fields.proposalVoteDeadline, - isVoteDeadlineAtMinimum + isVoteDeadlineAtMinimum, + isVoteDeadlineAtMaximum ), }, }); diff --git a/apps/token/src/routes/governance/propose/network-parameter/propose-network-parameter.tsx b/apps/token/src/routes/governance/propose/network-parameter/propose-network-parameter.tsx index 4a8d0d5cf..04fe118eb 100644 --- a/apps/token/src/routes/governance/propose/network-parameter/propose-network-parameter.tsx +++ b/apps/token/src/routes/governance/propose/network-parameter/propose-network-parameter.tsx @@ -9,7 +9,7 @@ import { getClosingTimestamp, getEnactmentTimestamp, useProposalSubmit, - deadlineToRoundedHours, + doesValueEquateToParam, } from '@vegaprotocol/governance'; import { useEnvironment } from '@vegaprotocol/environment'; import { @@ -103,11 +103,22 @@ export const ProposeNetworkParameter = () => { .split('_') .join('.'); - const isVoteDeadlineAtMinimum = - fields.proposalVoteDeadline === - deadlineToRoundedHours( - params.governance_proposal_updateNetParam_minClose - ).toString(); + const isVoteDeadlineAtMinimum = doesValueEquateToParam( + fields.proposalVoteDeadline, + params.governance_proposal_updateNetParam_minClose + ); + const isVoteDeadlineAtMaximum = doesValueEquateToParam( + fields.proposalVoteDeadline, + params.governance_proposal_updateNetParam_maxClose + ); + const isEnactmentDeadlineAtMinimum = doesValueEquateToParam( + fields.proposalEnactmentDeadline, + params.governance_proposal_updateNetParam_minEnact + ); + const isEnactmentDeadlineAtMaximum = doesValueEquateToParam( + fields.proposalEnactmentDeadline, + params.governance_proposal_updateNetParam_maxEnact + ); await submit({ rationale: { @@ -123,12 +134,13 @@ export const ProposeNetworkParameter = () => { }, closingTimestamp: getClosingTimestamp( fields.proposalVoteDeadline, - isVoteDeadlineAtMinimum + isVoteDeadlineAtMinimum, + isVoteDeadlineAtMaximum ), enactmentTimestamp: getEnactmentTimestamp( - fields.proposalVoteDeadline, fields.proposalEnactmentDeadline, - isVoteDeadlineAtMinimum + isEnactmentDeadlineAtMinimum, + isEnactmentDeadlineAtMaximum ), }, }); diff --git a/apps/token/src/routes/governance/propose/new-asset/propose-new-asset.tsx b/apps/token/src/routes/governance/propose/new-asset/propose-new-asset.tsx index f81098084..176633e0e 100644 --- a/apps/token/src/routes/governance/propose/new-asset/propose-new-asset.tsx +++ b/apps/token/src/routes/governance/propose/new-asset/propose-new-asset.tsx @@ -5,7 +5,7 @@ import { getEnactmentTimestamp, getValidationTimestamp, useProposalSubmit, - deadlineToRoundedHours, + doesValueEquateToParam, } from '@vegaprotocol/governance'; import { useEnvironment } from '@vegaprotocol/environment'; import { @@ -65,11 +65,26 @@ export const ProposeNewAsset = () => { const { finalizedProposal, submit, Dialog } = useProposalSubmit(); const onSubmit = async (fields: NewAssetProposalFormFields) => { - const isVoteDeadlineAtMinimum = - fields.proposalVoteDeadline === - deadlineToRoundedHours( - params.governance_proposal_asset_minClose - ).toString(); + const isVoteDeadlineAtMinimum = doesValueEquateToParam( + fields.proposalVoteDeadline, + params.governance_proposal_asset_minClose + ); + const isVoteDeadlineAtMaximum = doesValueEquateToParam( + fields.proposalVoteDeadline, + params.governance_proposal_asset_maxClose + ); + const isEnactmentDeadlineAtMinimum = doesValueEquateToParam( + fields.proposalEnactmentDeadline, + params.governance_proposal_asset_minEnact + ); + const isEnactmentDeadlineAtMaximum = doesValueEquateToParam( + fields.proposalEnactmentDeadline, + params.governance_proposal_asset_maxEnact + ); + const isValidationDeadlineAtMaximum = doesValueEquateToParam( + fields.proposalValidationDeadline, + params.governance_proposal_asset_maxClose + ); await submit({ rationale: { @@ -82,15 +97,17 @@ export const ProposeNewAsset = () => { }, closingTimestamp: getClosingTimestamp( fields.proposalVoteDeadline, - isVoteDeadlineAtMinimum + isVoteDeadlineAtMinimum, + isVoteDeadlineAtMaximum ), enactmentTimestamp: getEnactmentTimestamp( - fields.proposalVoteDeadline, fields.proposalEnactmentDeadline, - isVoteDeadlineAtMinimum + isEnactmentDeadlineAtMinimum, + isEnactmentDeadlineAtMaximum ), validationTimestamp: getValidationTimestamp( - fields.proposalValidationDeadline + fields.proposalValidationDeadline, + isValidationDeadlineAtMaximum ), }, }); diff --git a/apps/token/src/routes/governance/propose/new-market/propose-new-market.tsx b/apps/token/src/routes/governance/propose/new-market/propose-new-market.tsx index 83aa3a592..33c244649 100644 --- a/apps/token/src/routes/governance/propose/new-market/propose-new-market.tsx +++ b/apps/token/src/routes/governance/propose/new-market/propose-new-market.tsx @@ -4,7 +4,7 @@ import { getClosingTimestamp, getEnactmentTimestamp, useProposalSubmit, - deadlineToRoundedHours, + doesValueEquateToParam, } from '@vegaprotocol/governance'; import { useEnvironment } from '@vegaprotocol/environment'; import { @@ -63,11 +63,22 @@ export const ProposeNewMarket = () => { const { finalizedProposal, submit, Dialog } = useProposalSubmit(); const onSubmit = async (fields: NewMarketProposalFormFields) => { - const isVoteDeadlineAtMinimum = - fields.proposalVoteDeadline === - deadlineToRoundedHours( - params.governance_proposal_market_minClose - ).toString(); + const isVoteDeadlineAtMinimum = doesValueEquateToParam( + fields.proposalVoteDeadline, + params.governance_proposal_market_minClose + ); + const isVoteDeadlineAtMaximum = doesValueEquateToParam( + fields.proposalVoteDeadline, + params.governance_proposal_market_maxClose + ); + const isEnactmentDeadlineAtMinimum = doesValueEquateToParam( + fields.proposalEnactmentDeadline, + params.governance_proposal_market_minEnact + ); + const isEnactmentDeadlineAtMaximum = doesValueEquateToParam( + fields.proposalEnactmentDeadline, + params.governance_proposal_market_maxEnact + ); await submit({ rationale: { @@ -80,12 +91,13 @@ export const ProposeNewMarket = () => { }, closingTimestamp: getClosingTimestamp( fields.proposalVoteDeadline, - isVoteDeadlineAtMinimum + isVoteDeadlineAtMinimum, + isVoteDeadlineAtMaximum ), enactmentTimestamp: getEnactmentTimestamp( - fields.proposalVoteDeadline, fields.proposalEnactmentDeadline, - isVoteDeadlineAtMinimum + isEnactmentDeadlineAtMinimum, + isEnactmentDeadlineAtMaximum ), }, }); diff --git a/apps/token/src/routes/governance/propose/update-asset/propose-update-asset.tsx b/apps/token/src/routes/governance/propose/update-asset/propose-update-asset.tsx index 54275e74c..f944b2b47 100644 --- a/apps/token/src/routes/governance/propose/update-asset/propose-update-asset.tsx +++ b/apps/token/src/routes/governance/propose/update-asset/propose-update-asset.tsx @@ -4,7 +4,7 @@ import { getClosingTimestamp, getEnactmentTimestamp, useProposalSubmit, - deadlineToRoundedHours, + doesValueEquateToParam, } from '@vegaprotocol/governance'; import { useEnvironment } from '@vegaprotocol/environment'; import { @@ -63,11 +63,22 @@ export const ProposeUpdateAsset = () => { const { finalizedProposal, submit, Dialog } = useProposalSubmit(); const onSubmit = async (fields: UpdateAssetProposalFormFields) => { - const isVoteDeadlineAtMinimum = - fields.proposalVoteDeadline === - deadlineToRoundedHours( - params.governance_proposal_updateAsset_minClose - ).toString(); + const isVoteDeadlineAtMinimum = doesValueEquateToParam( + fields.proposalVoteDeadline, + params.governance_proposal_updateAsset_minClose + ); + const isVoteDeadlineAtMaximum = doesValueEquateToParam( + fields.proposalVoteDeadline, + params.governance_proposal_updateAsset_maxClose + ); + const isEnactmentDeadlineAtMinimum = doesValueEquateToParam( + fields.proposalEnactmentDeadline, + params.governance_proposal_updateAsset_minEnact + ); + const isEnactmentDeadlineAtMaximum = doesValueEquateToParam( + fields.proposalEnactmentDeadline, + params.governance_proposal_updateAsset_maxEnact + ); await submit({ rationale: { @@ -80,12 +91,13 @@ export const ProposeUpdateAsset = () => { }, closingTimestamp: getClosingTimestamp( fields.proposalVoteDeadline, - isVoteDeadlineAtMinimum + isVoteDeadlineAtMinimum, + isVoteDeadlineAtMaximum ), enactmentTimestamp: getEnactmentTimestamp( - fields.proposalVoteDeadline, fields.proposalEnactmentDeadline, - isVoteDeadlineAtMinimum + isEnactmentDeadlineAtMinimum, + isEnactmentDeadlineAtMaximum ), }, }); @@ -171,6 +183,7 @@ export const ProposeUpdateAsset = () => { voteErrorMessage={errors?.proposalVoteDeadline?.message} voteMinClose={params.governance_proposal_updateAsset_minClose} voteMaxClose={params.governance_proposal_updateAsset_maxClose} + onEnactMinMax={setValue} enactmentRegister={register('proposalEnactmentDeadline', { required: t('Required'), })} diff --git a/apps/token/src/routes/governance/propose/update-market/propose-update-market.tsx b/apps/token/src/routes/governance/propose/update-market/propose-update-market.tsx index d07ba855b..67131e6b0 100644 --- a/apps/token/src/routes/governance/propose/update-market/propose-update-market.tsx +++ b/apps/token/src/routes/governance/propose/update-market/propose-update-market.tsx @@ -6,7 +6,7 @@ import { getClosingTimestamp, getEnactmentTimestamp, useProposalSubmit, - deadlineToRoundedHours, + doesValueEquateToParam, } from '@vegaprotocol/governance'; import { useEnvironment } from '@vegaprotocol/environment'; import { @@ -123,11 +123,22 @@ export const ProposeUpdateMarket = () => { const { finalizedProposal, submit, Dialog } = useProposalSubmit(); const onSubmit = async (fields: UpdateMarketProposalFormFields) => { - const isVoteDeadlineAtMinimum = - fields.proposalVoteDeadline === - deadlineToRoundedHours( - params.governance_proposal_updateMarket_minClose - ).toString(); + const isVoteDeadlineAtMinimum = doesValueEquateToParam( + fields.proposalVoteDeadline, + params.governance_proposal_updateMarket_minClose + ); + const isVoteDeadlineAtMaximum = doesValueEquateToParam( + fields.proposalVoteDeadline, + params.governance_proposal_updateMarket_maxClose + ); + const isEnactmentDeadlineAtMinimum = doesValueEquateToParam( + fields.proposalEnactmentDeadline, + params.governance_proposal_updateMarket_minEnact + ); + const isEnactmentDeadlineAtMaximum = doesValueEquateToParam( + fields.proposalEnactmentDeadline, + params.governance_proposal_updateMarket_maxEnact + ); await submit({ rationale: { @@ -143,12 +154,13 @@ export const ProposeUpdateMarket = () => { }, closingTimestamp: getClosingTimestamp( fields.proposalVoteDeadline, - isVoteDeadlineAtMinimum + isVoteDeadlineAtMinimum, + isVoteDeadlineAtMaximum ), enactmentTimestamp: getEnactmentTimestamp( - fields.proposalVoteDeadline, fields.proposalEnactmentDeadline, - isVoteDeadlineAtMinimum + isEnactmentDeadlineAtMinimum, + isEnactmentDeadlineAtMaximum ), }, }); diff --git a/libs/governance/src/utils/deadline-helpers.spec.ts b/libs/governance/src/utils/deadline-helpers.spec.ts index eb31151a3..cd4cf4f50 100644 --- a/libs/governance/src/utils/deadline-helpers.spec.ts +++ b/libs/governance/src/utils/deadline-helpers.spec.ts @@ -1,4 +1,9 @@ -import { deadlineToSeconds, secondsToRoundedHours } from './deadline-helpers'; +import { + deadlineToSeconds, + secondsToRoundedHours, + addTwoMinutes, + subtractTwoSeconds, +} from './deadline-helpers'; describe('deadlineToSeconds', () => { it('should throw an error if the deadline does not match the format "XhXmXs"', () => { @@ -39,3 +44,37 @@ describe('secondsToRoundedHours', () => { expect(secondsToRoundedHours(9000)).toEqual(3); }); }); + +describe('addTwoMinutes', () => { + it('should add two minutes to the current time', () => { + const now = new Date(); + const twoMinutesLater = new Date(now.getTime() + 2 * 60 * 1000); + expect(addTwoMinutes(now)).toEqual(twoMinutesLater); + }); + + it('will use the current time if no date is provided', () => { + const now = new Date(); + const twoMinutesLater = new Date(now.getTime() + 2 * 60 * 1000); + expect(addTwoMinutes()).toEqual(twoMinutesLater); + }); + + it('should add two minutes to a given time', () => { + const date = new Date(2020, 0, 1); + const twoMinutesLater = new Date(date.getTime() + 2 * 60 * 1000); + expect(addTwoMinutes(date)).toEqual(twoMinutesLater); + }); +}); + +describe('subtractTwoSeconds', () => { + it('should subtract two seconds to the current time', () => { + const now = new Date(); + const twoSecondsEarlier = new Date(now.getTime() - 2 * 1000); + expect(subtractTwoSeconds(now)).toEqual(twoSecondsEarlier); + }); + + it('should subtract two seconds from a given time', () => { + const date = new Date(2020, 0, 1); + const twoSecondsEarlier = new Date(date.getTime() - 2 * 1000); + expect(subtractTwoSeconds(date)).toEqual(twoSecondsEarlier); + }); +}); diff --git a/libs/governance/src/utils/deadline-helpers.ts b/libs/governance/src/utils/deadline-helpers.ts index 621d224de..62c9dde5a 100644 --- a/libs/governance/src/utils/deadline-helpers.ts +++ b/libs/governance/src/utils/deadline-helpers.ts @@ -1,10 +1,15 @@ import { parse as ISO8601Parse, toSeconds } from 'iso8601-duration'; +import { addMinutes, subSeconds } from 'date-fns'; + +const deadlineRegexChecker = (deadline: string) => { + // check that the deadline string matches the format "XhXmXs" + const regex = /^(\d+h)?(\d+m)?(\d+s)?$/; + return regex.test(deadline); +}; // Converts API deadlines ("XhXmXs") to seconds export const deadlineToSeconds = (deadline: string) => { - // check that the deadline string matches the format "XhXmXs" - const regex = /^(\d+h)?(\d+m)?(\d+s)?$/; - if (!regex.test(deadline)) { + if (!deadlineRegexChecker(deadline)) { throw new Error( `Invalid deadline format, expected format "XhXmXs", got "${deadline}"` ); @@ -20,3 +25,11 @@ export const secondsToRoundedHours = (seconds: number) => { export const deadlineToRoundedHours = (deadline: string) => secondsToRoundedHours(deadlineToSeconds(deadline)); + +export const doesValueEquateToParam = (value: string, param: string) => + value === deadlineToRoundedHours(param).toString(); + +export const addTwoMinutes = (date?: Date) => addMinutes(date || new Date(), 2); + +export const subtractTwoSeconds = (date?: Date) => + subSeconds(date || new Date(), 2); diff --git a/libs/governance/src/utils/get-closing-timestamp.spec.ts b/libs/governance/src/utils/get-closing-timestamp.spec.ts index 1ff61d547..944365422 100644 --- a/libs/governance/src/utils/get-closing-timestamp.spec.ts +++ b/libs/governance/src/utils/get-closing-timestamp.spec.ts @@ -1,5 +1,5 @@ import { getClosingTimestamp } from './get-closing-timestamp'; -import { addHours, addMinutes, getTime } from 'date-fns'; +import { addHours, addMinutes, getTime, subSeconds } from 'date-fns'; beforeEach(() => { jest.useFakeTimers(); @@ -14,6 +14,7 @@ describe('getClosingTimestamp', () => { it('should return the correct timestamp if the proposalVoteDeadline is set to minimum (when 2 mins are added)', () => { const proposalVoteDeadline = '1'; const isMinimumDeadlineSelected = true; + const isMaximumDeadlineSelected = false; const expected = Math.floor( getTime( addHours(addMinutes(new Date(), 2), Number(proposalVoteDeadline)) @@ -21,20 +22,40 @@ describe('getClosingTimestamp', () => { ); const actual = getClosingTimestamp( proposalVoteDeadline, - isMinimumDeadlineSelected + isMinimumDeadlineSelected, + isMaximumDeadlineSelected ); expect(actual).toEqual(expected); }); - it('should return the correct timestamp if the proposalVoteDeadline is not set to minimum (when no extra mins are added)', () => { + it('should return the correct timestamp if the proposalVoteDeadline is not set to minimum or maximum (no extra time added or subtracted)', () => { const proposalVoteDeadline = '2'; const isMinimumDeadlineSelected = false; + const isMaximumDeadlineSelected = false; const expected = Math.floor( getTime(addHours(new Date(), Number(proposalVoteDeadline))) / 1000 ); const actual = getClosingTimestamp( proposalVoteDeadline, - isMinimumDeadlineSelected + isMinimumDeadlineSelected, + isMaximumDeadlineSelected + ); + expect(actual).toEqual(expected); + }); + + it('should return the correct timestamp if the proposalVoteDeadline is set to maximum (when 2 secs are subtracted)', () => { + const proposalVoteDeadline = '3'; + const isMinimumDeadlineSelected = false; + const isMaximumDeadlineSelected = true; + const expected = Math.floor( + getTime( + addHours(subSeconds(new Date(), 2), Number(proposalVoteDeadline)) + ) / 1000 + ); + const actual = getClosingTimestamp( + proposalVoteDeadline, + isMinimumDeadlineSelected, + isMaximumDeadlineSelected ); expect(actual).toEqual(expected); }); diff --git a/libs/governance/src/utils/get-closing-timestamp.ts b/libs/governance/src/utils/get-closing-timestamp.ts index b0af3dc3d..ab5c59d68 100644 --- a/libs/governance/src/utils/get-closing-timestamp.ts +++ b/libs/governance/src/utils/get-closing-timestamp.ts @@ -1,17 +1,25 @@ -import { addHours, addMinutes, getTime } from 'date-fns'; +import { addHours, getTime } from 'date-fns'; +import { addTwoMinutes, subtractTwoSeconds } from './deadline-helpers'; -// If proposaVoteDeadline is at its minimum, then we add -// 2 extra minutes to the closing timestamp to ensure that there's time -// to confirm in the wallet. +// If the vote deadline is at its minimum, then we add 2 extra minutes to the +// closing timestamp to ensure that there's time to confirm in the wallet. + +// If it's at its maximum, remove a couple of seconds to ensure rounding errors +// and communication delays don't cause the deadline to be slightly +// later than the API can accept. export const getClosingTimestamp = ( proposalVoteDeadline: string, - minimumDeadlineSelected: boolean + minimumDeadlineSelected: boolean, + maximumDeadlineSelected: boolean ) => Math.floor( getTime( - minimumDeadlineSelected - ? addHours(addMinutes(new Date(), 2), Number(proposalVoteDeadline)) - : addHours(new Date(), Number(proposalVoteDeadline)) + addHours( + (minimumDeadlineSelected && addTwoMinutes()) || + (maximumDeadlineSelected && subtractTwoSeconds()) || + new Date(), + Number(proposalVoteDeadline) + ) ) / 1000 ); diff --git a/libs/governance/src/utils/get-enactment-timestamp.spec.ts b/libs/governance/src/utils/get-enactment-timestamp.spec.ts index da2da6e3b..a31c6996e 100644 --- a/libs/governance/src/utils/get-enactment-timestamp.spec.ts +++ b/libs/governance/src/utils/get-enactment-timestamp.spec.ts @@ -1,5 +1,5 @@ import { getEnactmentTimestamp } from './get-enactment-timestamp'; -import { addHours, getTime } from 'date-fns'; +import { addHours, addMinutes, getTime, subSeconds } from 'date-fns'; beforeEach(() => { jest.useFakeTimers(); @@ -12,21 +12,48 @@ afterEach(() => { describe('getEnactmentTimestamp', () => { it('should return the correct timestamp', () => { - const proposalVoteDeadline = '2'; const isMinimumVoteDeadlineSelected = false; + const isMaximumVoteDeadlineSelected = false; const enactmentDeadline = '1'; const expected = Math.floor( - getTime( - addHours( - new Date(), - Number(proposalVoteDeadline) + Number(enactmentDeadline) - ) - ) / 1000 + getTime(addHours(new Date(), Number(enactmentDeadline))) / 1000 ); const actual = getEnactmentTimestamp( - proposalVoteDeadline, enactmentDeadline, - isMinimumVoteDeadlineSelected + isMinimumVoteDeadlineSelected, + isMaximumVoteDeadlineSelected + ); + expect(actual).toEqual(expected); + }); + + it('should return the correct timestamp when minimum vote deadline is selected', () => { + const isMinimumVoteDeadlineSelected = true; + const isMaximumVoteDeadlineSelected = false; + const enactmentDeadline = '1'; + const expected = Math.floor( + getTime(addMinutes(addHours(new Date(), Number(enactmentDeadline)), 2)) / + 1000 + ); + const actual = getEnactmentTimestamp( + enactmentDeadline, + isMinimumVoteDeadlineSelected, + isMaximumVoteDeadlineSelected + ); + expect(actual).toEqual(expected); + }); + + it('should return the correct timestamp when maximum vote deadline is selected', () => { + const isMinimumVoteDeadlineSelected = false; + const isMaximumVoteDeadlineSelected = true; + const enactmentDeadline = '1'; + const expected = Math.floor( + getTime(subSeconds(addHours(new Date(), Number(enactmentDeadline)), 2)) / + 1000 + ); + const actual = getEnactmentTimestamp( + enactmentDeadline, + isMinimumVoteDeadlineSelected, + isMaximumVoteDeadlineSelected ); expect(actual).toEqual(expected); }); diff --git a/libs/governance/src/utils/get-enactment-timestamp.ts b/libs/governance/src/utils/get-enactment-timestamp.ts index 22ed4d258..2fe4f6acc 100644 --- a/libs/governance/src/utils/get-enactment-timestamp.ts +++ b/libs/governance/src/utils/get-enactment-timestamp.ts @@ -1,22 +1,24 @@ -import { addHours, fromUnixTime, getTime } from 'date-fns'; -import { getClosingTimestamp } from './get-closing-timestamp'; +import { addHours, getTime } from 'date-fns'; +import { addTwoMinutes, subtractTwoSeconds } from './deadline-helpers'; + +// If the enactment deadline is at its minimum, then we add 2 extra minutes to the +// closing timestamp to ensure that there's time to confirm in the wallet. + +// If it's at its maximum, remove a couple of seconds to ensure rounding errors +// and communication delays don't cause the deadline to be slightly +// later than the API can accept. export const getEnactmentTimestamp = ( - proposalVoteDeadline: string, enactmentDeadline: string, - minimumVoteDeadlineSelected: boolean + minimumDeadlineSelected: boolean, + maximumDeadlineSelected: boolean ) => Math.floor( getTime( addHours( - new Date( - fromUnixTime( - getClosingTimestamp( - proposalVoteDeadline, - minimumVoteDeadlineSelected - ) - ) - ), + (minimumDeadlineSelected && addTwoMinutes()) || + (maximumDeadlineSelected && subtractTwoSeconds()) || + new Date(), Number(enactmentDeadline) ) ) / 1000 diff --git a/libs/governance/src/utils/get-validation-timestamp.spec.ts b/libs/governance/src/utils/get-validation-timestamp.spec.ts index 1c4210e6e..303d26172 100644 --- a/libs/governance/src/utils/get-validation-timestamp.spec.ts +++ b/libs/governance/src/utils/get-validation-timestamp.spec.ts @@ -1,4 +1,4 @@ -import { addHours, addMinutes, getTime } from 'date-fns'; +import { addHours, addMinutes, getTime, subSeconds } from 'date-fns'; import { getValidationTimestamp } from './get-validation-timestamp'; beforeEach(() => { @@ -13,21 +13,44 @@ afterEach(() => { describe('getValidationTimestamp', () => { it('should return the correct timestamp if the proposalValidationDeadline is 0 (when 2 mins are added)', () => { const proposalValidationDeadline = '0'; + const isMaximumDeadlineSelected = false; const expected = Math.floor( getTime( addHours(addMinutes(new Date(), 2), Number(proposalValidationDeadline)) ) / 1000 ); - const actual = getValidationTimestamp(proposalValidationDeadline); + const actual = getValidationTimestamp( + proposalValidationDeadline, + isMaximumDeadlineSelected + ); expect(actual).toEqual(expected); }); - it('should return the correct timestamp if the proposalValidationDeadline is 1 (when no extra mins are added)', () => { + it('should return the correct timestamp if the proposalValidationDeadline is neither maximum nor minimum (when no extra mins are added)', () => { const proposalValidationDeadline = '1'; + const isMaximumDeadlineSelected = false; const expected = Math.floor( getTime(addHours(new Date(), Number(proposalValidationDeadline))) / 1000 ); - const actual = getValidationTimestamp(proposalValidationDeadline); + const actual = getValidationTimestamp( + proposalValidationDeadline, + isMaximumDeadlineSelected + ); + expect(actual).toEqual(expected); + }); + + it('should return the correct timestamp if the proposalValidationDeadline is maximum (when 2 secs are subtracted)', () => { + const proposalValidationDeadline = '2'; + const isMaximumDeadlineSelected = true; + const expected = Math.floor( + getTime( + addHours(subSeconds(new Date(), 2), Number(proposalValidationDeadline)) + ) / 1000 + ); + const actual = getValidationTimestamp( + proposalValidationDeadline, + isMaximumDeadlineSelected + ); expect(actual).toEqual(expected); }); }); diff --git a/libs/governance/src/utils/get-validation-timestamp.ts b/libs/governance/src/utils/get-validation-timestamp.ts index 36a7a7011..945779396 100644 --- a/libs/governance/src/utils/get-validation-timestamp.ts +++ b/libs/governance/src/utils/get-validation-timestamp.ts @@ -1,17 +1,25 @@ -import { addHours, addMinutes, getTime } from 'date-fns'; +import { addHours, getTime } from 'date-fns'; +import { addTwoMinutes, subtractTwoSeconds } from './deadline-helpers'; // If proposalValidationDeadline is at its minimum of 0 hours, then we add // 2 extra minutes to the validation timestamp to ensure that there's time // to confirm in the wallet. -export const getValidationTimestamp = (proposalValidationDeadline: string) => +// If it's at its maximum, remove a couple of seconds to ensure rounding errors +// and communication delays don't cause the proposal deadline to be slightly +// later than the API can accept. + +export const getValidationTimestamp = ( + proposalValidationDeadline: string, + maximumDeadlineSelected: boolean +) => Math.floor( getTime( - proposalValidationDeadline === '0' - ? addHours( - addMinutes(new Date(), 2), - Number(proposalValidationDeadline) - ) - : addHours(new Date(), Number(proposalValidationDeadline)) + addHours( + (proposalValidationDeadline === '0' && addTwoMinutes()) || + (maximumDeadlineSelected && subtractTwoSeconds()) || + new Date(), + Number(proposalValidationDeadline) + ) ) / 1000 );