Skip to content
Open
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 70 additions & 8 deletions src/components/transactions/GovVote/GovVoteActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { ChainId } from '@aave/contract-helpers';
import { GelatoRelay } from '@gelatonetwork/relay-sdk';
import { Trans } from '@lingui/macro';
import { useQueryClient } from '@tanstack/react-query';
import { AbiCoder, keccak256, RLP } from 'ethers/lib/utils';
import { AbiCoder, keccak256, parseUnits, RLP } from 'ethers/lib/utils';
import { useState } from 'react';
import { MOCK_SIGNED_HASH } from 'src/helpers/useTransactionHandler';
import { useGovernanceTokensAndPowers } from 'src/hooks/governance/useGovernanceTokensAndPowers';
import { useModalContext } from 'src/hooks/useModal';
import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
import { VoteProposalData } from 'src/modules/governance/types';
import { ProposalDetailDisplay, VoteProposalData } from 'src/modules/governance/types';
import { useRootStore } from 'src/store/root';
import { governanceV3Config } from 'src/ui-config/governanceConfig';
import { getProvider } from 'src/utils/marketsAndNetworksConfig';
Expand Down Expand Up @@ -194,6 +194,7 @@ export const GovVoteActions = ({
setApprovalTxState,
approvalTxState,
setTxError,
args,
} = useModalContext();
const user = useRootStore((store) => store.account);

Expand Down Expand Up @@ -266,17 +267,46 @@ export const GovVoteActions = ({
loading: false,
success: true,
});
queryClient.invalidateQueries({ queryKey: ['governance_proposal', proposalId, user] });
queryClient.invalidateQueries({
queryKey: ['governance-detail-cache', proposalId, user],
});

const power = parseFloat(args?.power || '0');
const votingPowerWei = args?.power ? parseUnits(args.power, 18).toString() : '1';
const updater = (old: ProposalDetailDisplay | null | undefined) => {
if (!old?.voteProposalData) return old;
const forVotes = old.voteInfo.forVotes + (support ? power : 0);
const againstVotes = old.voteInfo.againstVotes + (support ? 0 : power);
const total = forVotes + againstVotes;
const currentDifferential = forVotes - againstVotes;
return {
...old,
voteInfo: {
...old.voteInfo,
forVotes,
againstVotes,
forPercent: total > 0 ? forVotes / total : 0,
againstPercent: total > 0 ? againstVotes / total : 0,
currentDifferential,
quorumReached: forVotes >= old.voteInfo.quorum,
differentialReached: currentDifferential >= old.voteInfo.requiredDifferential,
},
voteProposalData: {
...old.voteProposalData,
votedInfo: { support, votingPower: votingPowerWei },
},
};
};
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The updater function is duplicated in two places (lines 273-296 and lines 338-361). Consider extracting this logic into a reusable function to improve maintainability and reduce the risk of inconsistent updates if one location is changed but not the other.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The optimistic update does not handle the case where the user has already voted. If a user has previously voted and somehow votes again (e.g., due to a race condition or frontend state issue), the vote counts will be incorrect because the old vote is not subtracted before adding the new vote. Consider checking if old.voteProposalData.votedInfo exists and subtracting the previous vote before adding the new one, or skip the optimistic update entirely if the user has already voted.

Copilot uses AI. Check for mistakes.
queryClient.setQueryData(['governance-detail-cache', proposalId, user], updater);
queryClient.setQueryData(['governance-detail-graph', proposalId, user], updater);
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the queries 'governance-detail-cache' or 'governance-detail-graph' don't have cached data yet when setQueryData is called, the updater will receive undefined and return early. This is correct defensive behavior, but consider adding a fallback to invalidateQueries if the optimistic update fails (i.e., if old is null/undefined) to ensure the UI eventually reflects the correct state after the backend updates.

Suggested change
queryClient.setQueryData(['governance-detail-cache', proposalId, user], updater);
queryClient.setQueryData(['governance-detail-graph', proposalId, user], updater);
const governanceDetailCacheKey = ['governance-detail-cache', proposalId, user] as const;
const governanceDetailGraphKey = ['governance-detail-graph', proposalId, user] as const;
const updatedCacheDetail = queryClient.setQueryData(governanceDetailCacheKey, updater);
const updatedGraphDetail = queryClient.setQueryData(governanceDetailGraphKey, updater);
if (updatedCacheDetail == null) {
queryClient.invalidateQueries({ queryKey: governanceDetailCacheKey });
}
if (updatedGraphDetail == null) {
queryClient.invalidateQueries({ queryKey: governanceDetailGraphKey });
}

Copilot uses AI. Check for mistakes.

queryClient.invalidateQueries({ queryKey: ['proposalVotes', proposalId] });
queryClient.invalidateQueries({
queryKey: ['governance-voters-cache-for', proposalId],
});
queryClient.invalidateQueries({
queryKey: ['governance-voters-cache-against', proposalId],
});
queryClient.invalidateQueries({
queryKey: ['governance-voters-graph', proposalId],
});
return;
} else {
setTimeout(checkForStatus, 5000);
Expand All @@ -301,15 +331,47 @@ export const GovVoteActions = ({
success: true,
});

queryClient.invalidateQueries({ queryKey: ['governance_proposal', proposalId, user] });
queryClient.invalidateQueries({ queryKey: ['governance-detail-cache', proposalId, user] });
// Optimistically update votedInfo and vote counts so the UI reflects
// the vote immediately without waiting for the cache service to index
const power = parseFloat(args?.power || '0');
const votingPowerWei = args?.power ? parseUnits(args.power, 18).toString() : '1';
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When args.power is undefined or null, the power calculation defaults to 0, but votingPowerWei defaults to '1'. This inconsistency means the vote counts will be updated with 0 but the votedInfo will show a voting power of 1 wei. These should be consistent - either both should be '0' or the function should validate that args.power is present before proceeding with the optimistic update.

Suggested change
const votingPowerWei = args?.power ? parseUnits(args.power, 18).toString() : '1';
const votingPowerWei = parseUnits(args?.power || '0', 18).toString();

Copilot uses AI. Check for mistakes.
const updater = (old: ProposalDetailDisplay | null | undefined) => {
if (!old?.voteProposalData) return old;
const forVotes = old.voteInfo.forVotes + (support ? power : 0);
const againstVotes = old.voteInfo.againstVotes + (support ? 0 : power);
const total = forVotes + againstVotes;
const currentDifferential = forVotes - againstVotes;
return {
...old,
voteInfo: {
...old.voteInfo,
forVotes,
againstVotes,
forPercent: total > 0 ? forVotes / total : 0,
againstPercent: total > 0 ? againstVotes / total : 0,
currentDifferential,
quorumReached: forVotes >= old.voteInfo.quorum,
differentialReached: currentDifferential >= old.voteInfo.requiredDifferential,
},
voteProposalData: {
...old.voteProposalData,
votedInfo: { support, votingPower: votingPowerWei },
},
};
};
queryClient.setQueryData(['governance-detail-cache', proposalId, user], updater);
queryClient.setQueryData(['governance-detail-graph', proposalId, user], updater);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Revalidate detail queries after optimistic vote write

These setQueryData writes update the proposal detail cache optimistically but never trigger a follow-up refetch for governance-detail-*, so the optimistic snapshot can become the long-lived source of truth. In this repo, queries default to refetchOnWindowFocus: false (pages/_app.page.tsx) and useGovernanceProposalDetail also disables refetchOnMount/refetchOnReconnect, so if other users voted after this page was loaded (or any power mismatch occurs), the displayed totals/quorum/differential can stay incorrect for the session; keep the optimistic update, but add a delayed or conditional invalidation/refetch for the detail keys.

Useful? React with 👍 / 👎.


queryClient.invalidateQueries({ queryKey: ['proposalVotes', proposalId] });
queryClient.invalidateQueries({
queryKey: ['governance-voters-cache-for', proposalId],
});
queryClient.invalidateQueries({
queryKey: ['governance-voters-cache-against', proposalId],
});
queryClient.invalidateQueries({
queryKey: ['governance-voters-graph', proposalId],
});
}
} catch (err) {
setMainTxState({
Expand Down
Loading