From c8d55c9db6913a2a1d39beb8e52f6684bc4e266c Mon Sep 17 00:00:00 2001 From: selankon Date: Mon, 12 Feb 2024 06:24:09 -0500 Subject: [PATCH] Gasless fix proposal dates (#1199) * Ensures that the onchain endate is not undefined, during the calculation of the CreateMajorityVotingProposalParams * Adds offset to the end date to avoid onchain proposal finish before the offchain proposal * Add offset to ensure offchain is started when proposal starts --------- Signed-off-by: emmdim --- src/context/createGaslessProposal.tsx | 19 +++-- src/context/createProposal.tsx | 115 ++++++++++++++++---------- src/utils/types.ts | 12 +++ 3 files changed, 93 insertions(+), 53 deletions(-) diff --git a/src/context/createGaslessProposal.tsx b/src/context/createGaslessProposal.tsx index b8af202b8..09b785ee6 100644 --- a/src/context/createGaslessProposal.tsx +++ b/src/context/createGaslessProposal.tsx @@ -1,5 +1,4 @@ import { - CreateMajorityVotingProposalParams, Erc20TokenDetails, Erc20WrapperTokenDetails, VoteValues, @@ -24,8 +23,9 @@ import { StepsMap, StepStatus, useFunctionStepper, -} from 'hooks/useFunctionStepper'; -import {useCensus3Client, useCensus3CreateToken} from 'hooks/useCensus3'; +} from '../hooks/useFunctionStepper'; +import {useCensus3Client, useCensus3CreateToken} from '../hooks/useCensus3'; +import {GaslessProposalCreationParams} from '../utils/types'; export enum GaslessProposalStepId { REGISTER_VOCDONI_ACCOUNT = 'REGISTER_VOCDONI_ACCOUNT', @@ -57,7 +57,7 @@ export type UseCreateElectionProps = Omit< interface IProposalToElectionProps { metadata: ProposalMetadata; - data: CreateMajorityVotingProposalParams; + data: GaslessProposalCreationParams; census: Census; } @@ -70,8 +70,8 @@ const proposalToElection = ({ title: metadata.title, description: metadata.description, question: metadata.summary, - startDate: data.startDate, - endDate: data.endDate!, + startDate: data.gaslessStartDate, + endDate: data.gaslessEndDate, meta: data, // Store all DAO metadata to retrieve it easily census: census, }; @@ -221,10 +221,11 @@ const useCreateGaslessProposal = ({ const createProposal = useCallback( async ( metadata: ProposalMetadata, - data: CreateMajorityVotingProposalParams, + data: GaslessProposalCreationParams, handleOnchainProposal: ( electionId?: string, - vochainCensus?: TokenCensus + vochainCensus?: TokenCensus, + gaslessParams?: GaslessProposalCreationParams ) => Promise ) => { if (globalState === StepStatus.ERROR) { @@ -261,7 +262,7 @@ const useCreateGaslessProposal = ({ // 3. Register the proposal onchain await doStep( GaslessProposalStepId.CREATE_ONCHAIN_PROPOSAL, - async () => await handleOnchainProposal(electionId, census) + async () => await handleOnchainProposal(electionId, census, data) ); // 4. All ready diff --git a/src/context/createProposal.tsx b/src/context/createProposal.tsx index 23e561f2b..0873ffc74 100644 --- a/src/context/createProposal.tsx +++ b/src/context/createProposal.tsx @@ -91,7 +91,12 @@ import {proposalStorage} from 'utils/localStorage/proposalStorage'; import {Proposal} from 'utils/paths'; import {getNonEmptyActions} from 'utils/proposals'; import {isNativeToken} from 'utils/tokens'; -import {ProposalFormData, ProposalId, ProposalResource} from 'utils/types'; +import { + GaslessProposalCreationParams, + ProposalFormData, + ProposalId, + ProposalResource, +} from 'utils/types'; import GaslessProposalModal from '../containers/transactionModals/gaslessProposalModal'; import {StepStatus} from '../hooks/useFunctionStepper'; import {useCreateGaslessProposal} from './createGaslessProposal'; @@ -105,13 +110,6 @@ type Props = { children: ReactNode; }; -// This omitted Gasless params are added after Vocdoni election created -// This type is used to store information needed before creating the proposal in the vochain -type PartialGaslessParams = Omit< - CreateGasslessProposalParams, - 'vochainProposalId' | 'censusURI' | 'censusRoot' | 'totalVotingPower' ->; - const CreateProposalWrapper: React.FC = ({ showTxModal, setShowTxModal, @@ -157,7 +155,7 @@ const CreateProposalWrapper: React.FC = ({ const [proposalId, setProposalId] = useState(); const [proposalCreationData, setProposalCreationData] = useState< - CreateMajorityVotingProposalParams | PartialGaslessParams + CreateMajorityVotingProposalParams | GaslessProposalCreationParams >(); const [creationProcessState, setCreationProcessState] = useState(TransactionState.WAITING); @@ -554,14 +552,26 @@ const CreateProposalWrapper: React.FC = ({ endDateTime = new Date(endDateTimeMill); - // In case the endDate is close to being minimum durable, (and we starting immediately) + // In case the endDate is close to being minimum durable, (and starting immediately) // to avoid passing late-date possibly, we just rely on SDK to set proper Date if ( + // If is Gasless, undefined is not allowed on vocdoni SDK election creation, and end date need to be specified + // to be synced with the offchain proposal + !gasless && endDateTime.valueOf() <= minEndDateTimeMills && startSwitch === 'now' ) { - /* Pass enddate as undefined to SDK to auto-calculate min endDate */ + /* Pass end date as undefined to SDK to auto-calculate min endDate */ endDateTime = undefined; + } else if ( + // In order to have a concordance between onchain and offchain endates, we add an offset to the end date to avoid + // transaction fail due the end date is before the min end date + gasless && + endDateTime.valueOf() <= minEndDateTimeMills && + startSwitch === 'now' + ) { + const endDateOffset = 5; // Minutes + endDateTime.setMinutes(endDateTime.getMinutes() + endDateOffset); } } else { // In case exact time specified by user @@ -621,7 +631,22 @@ const CreateProposalWrapper: React.FC = ({ ]); const getOffChainProposalParams = useCallback( - (params: CreateMajorityVotingProposalParams): PartialGaslessParams => { + ( + params: CreateMajorityVotingProposalParams + ): GaslessProposalCreationParams => { + // The offchain offset is used to ensure that the offchain proposal is enough long to don't overlap the onchain proposal + // limits. As both chains don't use the same clock, and we are calculating the times using blocks, we ensure that + // the times will be properly set to let the voters vote between the onchain proposal limits. + const offchainOffsets = 1; // Minutes + const gaslessEndDate = new Date(params.endDate!); + gaslessEndDate.setMinutes(params.endDate!.getMinutes() + offchainOffsets); + let gaslessStartDate; + if (params.startDate) { + gaslessStartDate = new Date(params.startDate); + gaslessStartDate.setMinutes( + params.startDate.getMinutes() - offchainOffsets + ); + } return { ...params, // If the value is undefined will take the expiration time defined at DAO creation level. @@ -629,8 +654,12 @@ const CreateProposalWrapper: React.FC = ({ // We could define a different expiration date for this proposal but is not designed // to do this at ux level. (kon) tallyEndDate: undefined, - startDate: params.startDate, + // We ensure that the onchain endate is not undefined, during the calculation of the CreateMajorityVotingProposalParams endDate: params.endDate!, + // Add offset to the end date to avoid onchain proposal finish before the offchain proposal + gaslessEndDate, + // Add offset to ensure offchain is started when proposal starts + gaslessStartDate, }; }, [] @@ -644,15 +673,16 @@ const CreateProposalWrapper: React.FC = ({ } if (!proposalCreationData) return; - return gasless - ? (pluginClient as GaslessVotingClient).estimation.createProposal( - proposalCreationData as CreateGasslessProposalParams - ) - : ( - pluginClient as TokenVotingClient | MultisigClient - ).estimation.createProposal( - proposalCreationData as CreateMajorityVotingProposalParams - ); + if (gasless) { + return (pluginClient as GaslessVotingClient).estimation.createProposal( + proposalCreationData as CreateGasslessProposalParams + ); + } + return ( + pluginClient as TokenVotingClient | MultisigClient + ).estimation.createProposal( + proposalCreationData as CreateMajorityVotingProposalParams + ); }, [gasless, pluginClient, proposalCreationData]); const { @@ -825,7 +855,11 @@ const CreateProposalWrapper: React.FC = ({ }, [pluginAddress, pluginType, queryClient]); const handlePublishProposal = useCallback( - async (vochainProposalId?: string, vochainCensus?: TokenCensus) => { + async ( + vochainProposalId?: string, + vochainCensus?: TokenCensus, + gaslessParams?: CreateMajorityVotingProposalParams + ) => { if (!pluginClient) { return new Error('ERC20 SDK client is not initialized correctly'); } @@ -846,17 +880,14 @@ const CreateProposalWrapper: React.FC = ({ }); let proposalIterator: AsyncGenerator; - if (gasless && vochainProposalId && vochainCensus) { + if (gasless && vochainProposalId && vochainCensus && gaslessParams) { // This is the last step of a gasless proposal creation // If some of the previous steps failed, and the user press the try again button, the end date is the same as when // the user opened the modal. So I get fresh calculated params, to check if the start date is on 6 minutes (for // example), the end date will be updated from now to 6 minutes more. - const updatedParams = getOffChainProposalParams( - (await getProposalCreationParams()).params - ); const params: CreateGasslessProposalParams = { - ...(updatedParams as PartialGaslessParams), + ...getOffChainProposalParams(gaslessParams), censusRoot: vochainCensus.censusId!, censusURI: vochainCensus.censusURI!, totalVotingPower: vochainCensus.weight!, @@ -953,7 +984,6 @@ const CreateProposalWrapper: React.FC = ({ gasless, isOnWrongNetwork, getOffChainProposalParams, - getProposalCreationParams, handleCloseModal, open, network, @@ -970,25 +1000,19 @@ const CreateProposalWrapper: React.FC = ({ } const {params, metadata} = await getProposalCreationParams(); - if (!params.endDate) { - const startDate = params.startDate || new Date(); - params.endDate = new Date( - startDate.valueOf() + - daysToMills(minDays || 0) + - hoursToMills(minHours || 0) + - minutesToMills(minMinutes || 0) - ); - } - await createProposal(metadata, params, handlePublishProposal); + + await createProposal( + metadata, + getOffChainProposalParams(params), + handlePublishProposal + ); }, [ pluginClient, daoToken, getProposalCreationParams, createProposal, + getOffChainProposalParams, handlePublishProposal, - minDays, - minHours, - minMinutes, ]); /************************************************* @@ -999,8 +1023,11 @@ const CreateProposalWrapper: React.FC = ({ async function setProposalData() { if (showTxModal && creationProcessState === TransactionState.WAITING) { if (gasless) { - const {params: gaslessParams} = await getProposalCreationParams(); - setProposalCreationData(getOffChainProposalParams(gaslessParams)); + setProposalCreationData( + getOffChainProposalParams( + (await getProposalCreationParams()).params + ) + ); } else { const {params} = await getProposalCreationParams(); setProposalCreationData(params); diff --git a/src/utils/types.ts b/src/utils/types.ts index 93c82ec47..1fdbb9ed4 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -16,6 +16,7 @@ import { VersionTag, } from '@aragon/sdk-client-common'; import { + CreateGasslessProposalParams, GaslessPluginVotingSettings, GaslessVotingProposal, GaslessVotingProposalListItem, @@ -193,6 +194,17 @@ export type DetailedProposal = | MultisigProposal | TokenVotingProposal | GaslessVotingProposal; + +// This omitted Gasless params are added after Vocdoni election created +// This type is used to store information needed before creating the proposal in the vochain +export type GaslessProposalCreationParams = Omit< + CreateGasslessProposalParams, + 'vochainProposalId' | 'censusURI' | 'censusRoot' | 'totalVotingPower' +> & { + gaslessStartDate: Date | undefined; + gaslessEndDate: Date; +}; + export type ProposalListItem = | TokenVotingProposalListItem | MultisigProposalListItem