Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Snapshot proposal enhancement #1291

Merged
merged 3 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions public/images/shutter-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
106 changes: 91 additions & 15 deletions src/components/Proposals/ProposalActions/CastVote.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Button, Tooltip } from '@chakra-ui/react';
import { CloseX, Check } from '@decent-org/fractal-ui';
import { Button, Tooltip, Box, Text, Image, Flex } from '@chakra-ui/react';
import { CloseX, Check, SingleCheckOutline } from '@decent-org/fractal-ui';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import useSnapshotProposal from '../../../hooks/DAO/loaders/snapshot/useSnapshotProposal';
Expand All @@ -12,22 +12,32 @@ import {
AzoriusVoteChoice,
ExtendedSnapshotProposal,
} from '../../../types';
import WeightedInput from '../../ui/forms/WeightedInput';
import { useVoteContext } from '../ProposalVotes/context/VoteContext';

function Vote({
proposal,
extendedSnapshotProposal,
onCastSnapshotVote,
}: {
proposal: FractalProposal;
extendedSnapshotProposal?: ExtendedSnapshotProposal;
onCastSnapshotVote?: () => Promise<void>;
}) {
const [pending, setPending] = useState<boolean>(false);
const { t } = useTranslation(['common', 'proposal']);
const { t } = useTranslation(['common', 'proposal', 'transaction']);
const { isLoaded: isCurrentBlockLoaded, currentBlockNumber } = useCurrentBlockNumber();

const azoriusProposal = proposal as AzoriusProposal;

const { castVote, castSnapshotVote } = useCastVote({
const {
castVote,
castSnapshotVote,
handleChangeSnapshotWeightedChoice,
handleSelectSnapshotChoice,
selectedChoice,
snapshotWeightedChoice,
} = useCastVote({
proposal,
setPending,
extendedSnapshotProposal,
Expand All @@ -44,7 +54,7 @@ function Vote({
// If user is lucky enough - he could create a proposal and proceed to vote on it
// even before the block, in which proposal was created, was mined.
// This gives a weird behavior when casting vote fails due to requirement under LinearERC20Voting contract that current block number
// Shouldn't be equal to proposal's start block number. Which is dictated by the need to have voting tokens delegation being "finalized" to prevent proposal hijacking.
// shouldn't be equal to proposal's start block number. Which is dictated by the need to have voting tokens delegation being "finalized" to prevent proposal hijacking.
const proposalStartBlockNotFinalized = Boolean(
!isSnapshotProposal &&
isCurrentBlockLoaded &&
Expand All @@ -60,19 +70,85 @@ function Vote({
hasVotedLoading;

if (isSnapshotProposal && extendedSnapshotProposal) {
const isWeighted = extendedSnapshotProposal.type === 'weighted';
const weightedTotalValue = snapshotWeightedChoice.reduce((prev, curr) => prev + curr, 0);
const voteDisabled =
(!isWeighted && typeof selectedChoice === 'undefined') ||
(isWeighted && weightedTotalValue === 0);
return (
<>
{extendedSnapshotProposal.choices.map((choice, i) => (
<Button
key={choice}
width="full"
isDisabled={disabled}
onClick={() => castSnapshotVote(i + 1)}
marginTop={5}
{isWeighted && snapshotWeightedChoice.length > 0
? extendedSnapshotProposal.choices.map((choice, i) => (
<WeightedInput
key={choice}
label={choice}
totalValue={weightedTotalValue}
value={snapshotWeightedChoice[i]}
onChange={newValue => handleChangeSnapshotWeightedChoice(i, newValue)}
/>
))
: extendedSnapshotProposal.choices.map((choice, i) => (
<Button
key={choice}
variant="secondary"
width="full"
onClick={() => handleSelectSnapshotChoice(i)}
marginTop={5}
>
{selectedChoice === i && <Check boxSize="1.5rem" />}
{choice}
</Button>
))}
<Button
width="full"
isDisabled={voteDisabled}
onClick={() => castSnapshotVote(onCastSnapshotVote)}
marginTop={5}
>
{t('vote')}
</Button>
{hasVoted && (
<Box
mt={4}
color="grayscale.500"
fontWeight="600"
>
{choice}
</Button>
))}
<Flex>
<SingleCheckOutline
boxSize="1.5rem"
mr={2}
/>
<Text>{t('successCastVote', { ns: 'transaction' })}</Text>
</Flex>
<Text>{t('snapshotRecastVoteHelper', { ns: 'transaction' })}</Text>
</Box>
)}
<Box
mt={4}
color="grayscale.700"
>
<Text>{t('poweredBy')}</Text>
<Flex>
<Flex mr={1}>
<Image
src="/images/snapshot-icon.svg"
alt="Snapshot icon"
mr={1}
/>
<Text>{t('snapshot')}</Text>
</Flex>
{extendedSnapshotProposal.privacy === 'shutter' && (
<Flex>
<Image
src="/images/shutter-icon.svg"
alt="Shutter icon"
mr={1}
/>
<Text>{t('shutter')}</Text>
</Flex>
)}
</Flex>
</Box>
</>
);
}
Expand Down
21 changes: 15 additions & 6 deletions src/components/Proposals/ProposalActions/ProposalAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,23 @@ import { useVoteContext } from '../ProposalVotes/context/VoteContext';
import CastVote from './CastVote';
import { Execute } from './Execute';

// TODO: Refactor extendedSnapshotProposal and onCastSnapshotVote to the context
export function ProposalActions({
proposal,
extendedSnapshotProposal,
onCastSnapshotVote,
}: {
proposal: FractalProposal;
extendedSnapshotProposal?: ExtendedSnapshotProposal;
onCastSnapshotVote?: () => Promise<void>;
}) {
switch (proposal.state) {
case FractalProposalState.ACTIVE:
return (
<CastVote
proposal={proposal}
extendedSnapshotProposal={extendedSnapshotProposal}
onCastSnapshotVote={onCastSnapshotVote}
/>
);
case FractalProposalState.EXECUTABLE:
Expand All @@ -45,10 +49,12 @@ export function ProposalAction({
proposal,
expandedView,
extendedSnapshotProposal,
onCastSnapshotVote,
}: {
proposal: FractalProposal;
expandedView?: boolean;
extendedSnapshotProposal?: ExtendedSnapshotProposal;
onCastSnapshotVote?: () => Promise<void>;
}) {
const {
node: { daoAddress },
Expand All @@ -65,11 +71,12 @@ export function ProposalAction({
);

const showActionButton =
user.votingWeight.gt(0) &&
(isActiveProposal ||
proposal.state === FractalProposalState.EXECUTABLE ||
proposal.state === FractalProposalState.TIMELOCKABLE ||
proposal.state === FractalProposalState.TIMELOCKED);
(isSnapshotProposal && canVote) ||
(user.votingWeight.gt(0) &&
(isActiveProposal ||
proposal.state === FractalProposalState.EXECUTABLE ||
proposal.state === FractalProposalState.TIMELOCKABLE ||
proposal.state === FractalProposalState.TIMELOCKED));

const handleClick = () => {
if (isSnapshotProposal) {
Expand Down Expand Up @@ -125,7 +132,8 @@ export function ProposalAction({
}

if (expandedView) {
if (user.votingWeight.eq(0) || (isActiveProposal && !canVote)) return null;
if (!isSnapshotProposal && (user.votingWeight.eq(0) || (isActiveProposal && !canVote)))
return null;

return (
<ContentBox containerBoxProps={{ bg: BACKGROUND_SEMI_TRANSPARENT }}>
Expand All @@ -140,6 +148,7 @@ export function ProposalAction({
<ProposalActions
proposal={proposal}
extendedSnapshotProposal={extendedSnapshotProposal}
onCastSnapshotVote={onCastSnapshotVote}
/>
</ContentBox>
);
Expand Down
29 changes: 20 additions & 9 deletions src/components/Proposals/ProposalVotes/context/VoteContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
AzoriusProposal,
MultisigProposal,
GovernanceType,
ExtendedSnapshotProposal,
} from '../../../../types';

interface IVoteContext {
Expand Down Expand Up @@ -36,8 +37,10 @@ export const useVoteContext = () => {
export function VoteContextProvider({
proposal,
children,
extendedSnapshotProposal,
}: {
proposal: FractalProposal;
extendedSnapshotProposal?: ExtendedSnapshotProposal;
children: ReactNode;
}) {
const [canVote, setCanVote] = useState(false);
Expand All @@ -61,8 +64,10 @@ export function VoteContextProvider({
const getHasVoted = useCallback(() => {
setHasVotedLoading(true);
if (isSnapshotProposal) {
// Snapshot proposals not tracking votes
setHasVoted(false);
setHasVoted(
!!extendedSnapshotProposal &&
!!extendedSnapshotProposal.votes.find(vote => vote.voter === user.address)
);
} else if (dao?.isAzorius) {
const azoriusProposal = proposal as AzoriusProposal;
if (azoriusProposal?.votes) {
Expand All @@ -75,18 +80,17 @@ export function VoteContextProvider({
);
}
setHasVotedLoading(false);
}, [dao, isSnapshotProposal, proposal, user.address]);
}, [dao, isSnapshotProposal, proposal, user.address, extendedSnapshotProposal]);

const getCanVote = useCallback(
async (refetchUserTokens?: boolean) => {
setCanVoteLoading(true);
let newCanVote = false;
if (isSnapshotProposal) {
const votingWeightData = await loadVotingWeight();
newCanVote = votingWeightData.votingWeight > 1;
}
if (user.address) {
if (type === GovernanceType.AZORIUS_ERC20) {
if (isSnapshotProposal) {
const votingWeightData = await loadVotingWeight();
newCanVote = votingWeightData.votingWeight >= 1;
} else if (type === GovernanceType.AZORIUS_ERC20) {
newCanVote = user.votingWeight.gt(0) && !hasVoted;
} else if (type === GovernanceType.AZORIUS_ERC721) {
if (refetchUserTokens) {
Expand Down Expand Up @@ -131,7 +135,14 @@ export function VoteContextProvider({

return (
<VoteContext.Provider
value={{ canVote, canVoteLoading, hasVoted, hasVotedLoading, getHasVoted, getCanVote }}
value={{
canVote,
canVoteLoading,
hasVoted,
hasVotedLoading,
getHasVoted,
getCanVote,
}}
>
{children}
</VoteContext.Provider>
Expand Down
Loading