Skip to content

Commit

Permalink
Fixes for upvoting
Browse files Browse the repository at this point in the history
  • Loading branch information
jmrossy committed Mar 8, 2024
1 parent 7321b25 commit 4688544
Show file tree
Hide file tree
Showing 11 changed files with 108 additions and 18 deletions.
2 changes: 1 addition & 1 deletion src/app/governance/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ function ProposalChainData({ propData }: { propData: MergedProposalData }) {
{stage === ProposalStage.Queued && <ProposalUpvoteButton proposalId={id} />}
{stage === ProposalStage.Referendum && <ProposalVoteButtons proposalId={id} />}
{expiryTimestamp && (
<div>{`Voting ends in ${getHumanReadableDuration(expiryTimestamp)}`}</div>
<div>{`Expires in ${getHumanReadableDuration(expiryTimestamp - Date.now())}`}</div>
)}
{stage >= ProposalStage.Referendum && <ProposalVoteChart propData={propData} />}
{stage === ProposalStage.Referendum && <ProposalQuorumChart propData={propData} />}
Expand Down
1 change: 1 addition & 0 deletions src/config/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const MAX_NUM_GROUPS_VOTED_FOR = 10;
// Governance
export const PROPOSAL_V1_MAX_ID = 110; // Proposals before this use old vote events
export const QUEUED_STAGE_EXPIRY_TIME = 2_419_200_000; // 4 weeks
export const DEQUEUE_FREQUENCY = 86_400_000; // 1 day
export const APPROVAL_STAGE_EXPIRY_TIME = 86_400_000; // 1 day
export const EXECUTION_STAGE_EXPIRY_TIME = 259_200_000; // 3 days

Expand Down
21 changes: 14 additions & 7 deletions src/features/governance/UpvoteForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { Form, Formik, FormikErrors } from 'formik';
import { FormSubmitButton } from 'src/components/buttons/FormSubmitButton';
import { ProposalFormDetails } from 'src/features/governance/components/ProposalFormDetails';
import { useProposalQueue } from 'src/features/governance/hooks/useProposalQueue';
import { useGovernanceVotingPower } from 'src/features/governance/hooks/useVotingStatus';
import {
useGovernanceVotingPower,
useIsGovernanceUpVoting,
} from 'src/features/governance/hooks/useVotingStatus';
import { UpvoteFormValues, UpvoteRecord } from 'src/features/governance/types';
import { getUpvoteTxPlan } from 'src/features/governance/votePlan';
import { OnConfirmedFn } from 'src/features/transactions/types';
Expand All @@ -24,13 +27,14 @@ export function UpvoteForm({
}) {
const { address } = useAccount();
const { queue } = useProposalQueue();
const { votingPower } = useGovernanceVotingPower();
const { isUpvoting } = useIsGovernanceUpVoting(address);
const { votingPower } = useGovernanceVotingPower(address);

const { getNextTx, onTxSuccess } = useTransactionPlan<UpvoteFormValues>({
createTxPlan: (v) => getUpvoteTxPlan(v, queue || [], votingPower || 0n),
onPlanSuccess: (v, r) =>
onConfirmed({
message: `Upvote successful`,
message: 'Upvote successful',
receipt: r,
properties: [{ label: 'Proposal', value: `#${v.proposalId}` }],
}),
Expand All @@ -40,8 +44,8 @@ export function UpvoteForm({
const onSubmit = (values: UpvoteFormValues) => writeContract(getNextTx(values));

const validate = (values: UpvoteFormValues) => {
if (!address || !queue || !isNullish(votingPower)) return { amount: 'Form data not ready' };
return validateForm(values, queue);
if (!address || !queue || isNullish(votingPower)) return { amount: 'Form data not ready' };
return validateForm(values, queue, isUpvoting);
};

return (
Expand All @@ -58,7 +62,7 @@ export function UpvoteForm({
{({ values }) => (
<Form className="mt-4 flex flex-1 flex-col justify-between">
<div className="space-y-3">
<ProposalFormDetails proposalId={values.proposalId} />
<ProposalFormDetails proposalId={values.proposalId} votingPower={votingPower} />
</div>
<FormSubmitButton isLoading={isLoading} loadingText={'Upvoting'}>
Upvote
Expand All @@ -72,14 +76,17 @@ export function UpvoteForm({
function validateForm(
values: UpvoteFormValues,
queue: UpvoteRecord[],
isUpvoting: boolean,
): FormikErrors<UpvoteFormValues> {
const { proposalId } = values;

if (!queue.find((p) => p.proposalId === proposalId)) {
return { proposalId: 'Proposal ID not eligible' };
}

// TODO enforce that account has not already upvoted
if (isUpvoting) {
return { proposalId: 'Account already upvoting' };
}

return {};
}
2 changes: 1 addition & 1 deletion src/features/governance/components/ProposalCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function ProposalCard({ propData }: { propData: MergedProposalData }) {
const endTimeValue = timestampExecuted
? `Executed ${getHumanReadableTimeString(timestampExecuted)}`
: expiryTimestamp
? `Expires ${getHumanReadableDuration(expiryTimestamp)}`
? `Expires ${getHumanReadableDuration(expiryTimestamp - Date.now())}`
: undefined;

const sum = bigIntSum(Object.values(votes || {})) || 1n;
Expand Down
12 changes: 11 additions & 1 deletion src/features/governance/components/ProposalFormDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { formatNumberString } from 'src/components/numbers/Amount';
import { useGovernanceProposal } from 'src/features/governance/hooks/useGovernanceProposals';
import { trimToLength } from 'src/utils/strings';

export function ProposalFormDetails({ proposalId }: { proposalId: number }) {
export function ProposalFormDetails({
proposalId,
votingPower,
}: {
proposalId: number;
votingPower?: bigint;
}) {
const propData = useGovernanceProposal(proposalId);
const { proposal, metadata } = propData || {};
const proposedTimeValue = proposal?.timestamp
Expand All @@ -13,6 +20,9 @@ export function ProposalFormDetails({ proposalId }: { proposalId: number }) {
<>
<h3>{`Proposal ID: #${proposalId} ${cgpId}`}</h3>
{metadata && <p className="text-sm">{trimToLength(metadata.title, 35)}</p>}
{votingPower && (
<p className="text-sm">{`Voting power: ${formatNumberString(votingPower, 2, true)}`}</p>
)}
{proposedTimeValue && (
<div className="text-sm text-taupe-600">{`Proposed ${proposedTimeValue}`}</div>
)}
Expand Down
10 changes: 10 additions & 0 deletions src/features/governance/components/ProposalVoteButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import clsx from 'clsx';
import { SolidButton } from 'src/components/buttons/SolidButton';
import { HelpIcon } from 'src/components/icons/HelpIcon';
import { formatNumberString } from 'src/components/numbers/Amount';
import { useQueueHasReadyProposals } from 'src/features/governance/hooks/useProposalQueue';
import {
useGovernanceVoteRecord,
useGovernanceVotingPower,
Expand All @@ -12,6 +13,8 @@ import { useTransactionModal } from 'src/features/transactions/TransactionModal'
import { useAccount } from 'wagmi';

export function ProposalUpvoteButton({ proposalId }: { proposalId?: number }) {
const { hasReadyProposals } = useQueueHasReadyProposals();

const showTxModal = useTransactionModal(TransactionFlowType.Upvote, { proposalId });

return (
Expand All @@ -23,7 +26,14 @@ export function ProposalUpvoteButton({ proposalId }: { proposalId?: number }) {
<SolidButton
className="btn-neutral w-full"
onClick={() => showTxModal()}
disabled={hasReadyProposals}
>{`➕ Upvote`}</SolidButton>
{hasReadyProposals && (
<p className="max-w-[20rem] text-xs text-gray-600">
Upvoting is disabled while there are queued proposals ready for approval. Please check
again later.
</p>
)}
</>
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/features/governance/components/ProposalVoteChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function ProposalVoteChart({ propData }: { propData: MergedProposalData }
acc[v] = {
label: '',
value: fromWei(votes?.[v] || 0n),
percentage: percent(votes?.[v] || 0n, totalVotes),
percentage: percent(votes?.[v] || 0n, totalVotes || 1n),
color: VoteToColor[v],
};
return acc;
Expand Down
3 changes: 2 additions & 1 deletion src/features/governance/hooks/useGovernanceProposals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ async function fetchGovernanceProposals(publicClient: PublicClient): Promise<Pro
}

const [queuedIds, queuedUpvotes] = queued.result;
const dequeuedIds = dequeued.result;
// Filter out queues with id of 0, not sure why they are included
const dequeuedIds = dequeued.result.filter((id: bigint) => !!id);
const allIdsAndUpvotes = [
...queuedIds.map((id, i) => ({ id, upvotes: queuedUpvotes[i] })),
...dequeuedIds.map((id) => ({ id, upvotes: 0n })),
Expand Down
23 changes: 21 additions & 2 deletions src/features/governance/hooks/useProposalQueue.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { governanceABI } from '@celo/abis';
import { useToastError } from 'src/components/notifications/useToastError';
import { DEQUEUE_FREQUENCY } from 'src/config/consts';
import { Addresses } from 'src/config/contracts';
import { UpvoteRecord } from 'src/features/governance/types';
import { useGovernanceProposals } from 'src/features/governance/hooks/useGovernanceProposals';
import { ProposalStage, UpvoteRecord } from 'src/features/governance/types';
import { useReadContract } from 'wagmi';

// Returns the upvote records for queued governance proposals
Expand All @@ -21,7 +23,7 @@ export function useProposalQueue() {
if (data) {
const ids = data[0];
const upvotes = data[1];
queue = ids.map((id, i) => ({ proposalId: Number(id), upvotes: upvotes[i] || 0n }));
queue = ids.map((id, i) => ({ proposalId: Number(id), upvotes: BigInt(upvotes[i] || 0n) }));
}

return {
Expand Down Expand Up @@ -52,3 +54,20 @@ export function useProposalDequeue() {
isLoading,
};
}

// Checks if the queue has proposals that are ready to be dequeued
// This is important because currently, upvote txs don't work in this scenario
export function useQueueHasReadyProposals() {
const { proposals, isLoading } = useGovernanceProposals();

const readyQueuedProposals = proposals?.filter(
(p) =>
p.proposal?.stage === ProposalStage.Queued &&
Date.now() - p.proposal?.timestamp > DEQUEUE_FREQUENCY,
);

return {
isLoading,
hasReadyProposals: !!readyQueuedProposals?.length,
};
}
33 changes: 33 additions & 0 deletions src/features/governance/hooks/useVotingStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,39 @@ export function useIsGovernanceVoting(address?: Address) {
};
}

export function useIsGovernanceUpVoting(address?: Address) {
const { data, isError, isLoading, error } = useReadContract({
address: Addresses.Governance,
abi: governanceABI,
functionName: 'getUpvoteRecord',
args: [address || ZERO_ADDRESS],
query: {
enabled: !!address,
staleTime: 1 * 60 * 1000, // 1 minute
},
});

const { isUpvoting, upvoteRecord } = useMemo(() => {
if (!data || !data[0]) return { isUpvoting: false, upvoteRecord: undefined };
return {
isUpvoting: true,
upvoteRecord: {
proposalId: Number(data[0]),
upvotes: data[1],
},
};
}, [data]);

useToastError(error, 'Error fetching upvoting status');

return {
isUpvoting,
upvoteRecord,
isError,
isLoading,
};
}

export function useGovernanceVoteRecord(address?: Address, proposalId?: number) {
const { dequeue } = useProposalDequeue();

Expand Down
17 changes: 13 additions & 4 deletions src/features/governance/votePlan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
} from 'src/features/governance/types';
import { TxPlan } from 'src/features/transactions/types';
import { logger } from 'src/utils/logger';
import { deepCopy } from 'src/utils/objects';

export function getVoteTxPlan(values: VoteFormValues, dequeued: number[]): TxPlan {
const { proposalId, vote } = values;
Expand Down Expand Up @@ -50,15 +49,25 @@ export function getUpvoteTxPlan(
}

// Based on https://github.com/celo-org/developer-tooling/blob/ae51ca8851e6684d372f976dd8610ddf502a266b/packages/sdk/contractkit/src/wrappers/Governance.ts#L765
// TODO this fails when there are queued proposals that are ready to be de-queued
// See dequeueProposalsIfReady in governance.sol
function lesserAndGreaterAfterUpvote(
proposalId: number,
queue: UpvoteRecord[],
votingPower: bigint,
): { lesserID: number; greaterID: number } {
const proposalIndex = queue.findIndex((p) => p.proposalId === proposalId);
const newQueue = deepCopy(queue);
newQueue[proposalIndex].upvotes += votingPower;
newQueue.sort((a, b) => (a.upvotes > b.upvotes ? 1 : -1));
const newQueue = [...queue];
newQueue[proposalIndex] = {
proposalId,
upvotes: queue[proposalIndex].upvotes + votingPower,
};
// Sort in ascending order by upvotes
newQueue.sort((a, b) => {
if (a === b) return 0;
if (a.upvotes > b.upvotes) return 1;
else return -1;
});
const newIndex = newQueue.findIndex((p) => p.proposalId === proposalId);
return {
lesserID: newIndex === 0 ? 0 : newQueue[newIndex - 1].proposalId,
Expand Down

0 comments on commit 4688544

Please sign in to comment.