Skip to content

Commit

Permalink
Fix votes breakdown visualization for weighted voting
Browse files Browse the repository at this point in the history
  • Loading branch information
mudrila committed Oct 13, 2023
1 parent 29474ef commit 1ef6926
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 85 deletions.
29 changes: 1 addition & 28 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,11 @@
class WasmChunksFixPlugin {
apply(compiler) {
compiler.hooks.thisCompilation.tap('WasmChunksFixPlugin', compilation => {
compilation.hooks.processAssets.tap({ name: 'WasmChunksFixPlugin' }, assets =>
Object.entries(assets).forEach(([pathname, source]) => {
if (!pathname.match(/\.wasm$/)) return;
compilation.deleteAsset(pathname);

const name = pathname.split('/')[1];
const info = compilation.assetsInfo.get(pathname);
compilation.emitAsset(name, source, info);
})
);
});
}
}

/** @type {import('next').NextConfig} */
module.exports = {
output: undefined,
webpack(config, { isServer, dev }) {
config.experiments = {
asyncWebAssembly: true,
layers: true,
};

webpack(config) {
config.resolve.fallback = {
fs: false,
};

if (!dev && isServer) {
config.output.webassemblyModuleFilename = 'chunks/[id].wasm';
config.plugins.push(new WasmChunksFixPlugin());
}

return config;
},
images: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export default function SnapshotProposalSummary({ proposal }: ISnapshotProposalS
return 'basicSnapshotVotingSystem';
case 'single-choice':
return 'singleSnapshotVotingSystem';
case 'weighted':
return 'weightedSnapshotVotingSystem';
default:
return 'unknownSnapshotVotingSystem';
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { Grid, GridItem, Text } from '@chakra-ui/react';
import { Grid, GridItem, Text, Flex } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import useSnapshotProposal from '../../../hooks/DAO/loaders/snapshot/useSnapshotProposal';
import useDisplayName from '../../../hooks/utils/useDisplayName';
import { useFractal } from '../../../providers/App/AppProvider';
import { ExtendedSnapshotProposal, SnapshotVote } from '../../../types';
import {
ExtendedSnapshotProposal,
SnapshotVote,
SnapshotWeightedVotingChoice,
} from '../../../types';
import StatusBox from '../../ui/badges/StatusBox';

interface ISnapshotProposalVoteItem {
Expand All @@ -12,13 +17,17 @@ interface ISnapshotProposalVoteItem {

export default function SnapshotProposalVoteItem({ proposal, vote }: ISnapshotProposalVoteItem) {
const { t } = useTranslation();
const { getVoteWeight } = useSnapshotProposal(proposal);
const { displayName } = useDisplayName(vote.voter);
const {
readOnly: { user },
} = useFractal();

const isWeighted = proposal.type === 'weighted';

return (
<Grid
templateColumns="repeat(3, 1fr)"
templateColumns={isWeighted ? 'repeat(4, 1fr)' : 'repeat(3, 1fr)'}
width="100%"
>
<GridItem colSpan={1}>
Expand All @@ -27,14 +36,31 @@ export default function SnapshotProposalVoteItem({ proposal, vote }: ISnapshotPr
{user.address === vote.voter && t('isMeSuffix')}
</Text>
</GridItem>
<GridItem colSpan={1}>
<StatusBox>
<Text textStyle="text-sm-mono-semibold">{vote.choice}</Text>
</StatusBox>
<GridItem colSpan={isWeighted ? 2 : 1}>
{isWeighted ? (
<Flex
gap={1}
flexWrap="wrap"
>
{Object.keys(vote.choice as SnapshotWeightedVotingChoice).map((choice: any) => {
return (
<StatusBox key={choice}>
<Text textStyle="text-sm-mono-semibold">
{proposal.choices[(choice as any as number) - 1]}
</Text>
</StatusBox>
);
})}
</Flex>
) : (
<StatusBox>
<Text textStyle="text-sm-mono-semibold">{vote.choice as string}</Text>
</StatusBox>
)}
</GridItem>
<GridItem colSpan={1}>
<Text textStyle="text-base-sans-regular">
{vote.votingWeight} {proposal.strategies[0].params.symbol}
{getVoteWeight(vote)} {proposal.strategies[0].params.symbol}
</Text>
</GridItem>
</Grid>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,26 +43,32 @@ export default function SnapshotProposalVotes({ proposal }: ISnapshotProposalVot
colSpan={4}
rowGap={4}
>
{choices.map(choice => (
<VotesPercentage
key={choice}
label={choice}
percentage={(votesBreakdown[choice]?.total || 0 * 100) / totalVotesCasted}
>
<Text>
{proposal.privacy === 'shutter'
? `? ${strategySymbol}`
: `${votesBreakdown[choice].total} ${strategySymbol}`}
</Text>
</VotesPercentage>
))}
{choices.map(choice => {
const choicePercentageFromTotal =
((votesBreakdown[choice]?.total || 0) * 100) / totalVotesCasted;

return (
<VotesPercentage
key={choice}
label={choice}
percentage={Number(choicePercentageFromTotal.toFixed(2))}
>
<Text>
{proposal.privacy === 'shutter' &&
proposal.state !== FractalProposalState.CLOSED
? `? ${strategySymbol}`
: `${votesBreakdown[choice].total} ${strategySymbol}`}
</Text>
</VotesPercentage>
);
})}
</GridItem>
</Grid>
</ContentBox>
{votes && votes.length !== 0 && (
<ContentBox containerBoxProps={{ bg: BACKGROUND_SEMI_TRANSPARENT }}>
<Text textStyle="text-lg-mono-medium">
{t('votesTitle')} ({totalVotesCasted})
{t('votesTitle')} ({votes.length})
</Text>
<Divider
color="chocolate.700"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
import { useState, useEffect } from 'react';
import useSnapshotProposal from '../../../../hooks/DAO/loaders/snapshot/useSnapshotProposal';
import { ExtendedSnapshotProposal } from '../../../../types';

export default function useTotalVotes({ proposal }: { proposal?: ExtendedSnapshotProposal }) {
const [totalVotesCasted, setTotalVotesCasted] = useState(0);
const { getVoteWeight } = useSnapshotProposal(proposal);

useEffect(() => {
if (proposal) {
let newTotalVotesCasted = 0;
if (proposal.votesBreakdown) {
Object.keys(proposal.votesBreakdown).forEach(voteChoice => {
const voteChoiceBreakdown = proposal.votesBreakdown[voteChoice];
newTotalVotesCasted += voteChoiceBreakdown.total;
});

if (newTotalVotesCasted !== totalVotesCasted) {
setTotalVotesCasted(newTotalVotesCasted);
}
}
proposal.votes.forEach(vote => (newTotalVotesCasted += getVoteWeight(vote)));
setTotalVotesCasted(newTotalVotesCasted);
}
}, [proposal, totalVotesCasted]);
}, [proposal, totalVotesCasted, getVoteWeight]);

return { totalVotesCasted };
}
2 changes: 1 addition & 1 deletion src/components/ui/utils/ProgressBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default function ProgressBar({
height="100%"
position="absolute"
top="0"
left={value > 50 ? `calc(${Math.min(value - 5, 90)}% - 20px)` : value / 2}
left={value > 50 ? `calc(${Math.min(value - 5, 90)}% - 20px)` : `${value / 2}%`}
>
{valueLabel || Math.min(value, 100)}
{unit}
Expand Down
75 changes: 59 additions & 16 deletions src/hooks/DAO/loaders/snapshot/useSnapshotProposal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import { useFractal } from '../../../../providers/App/AppProvider';
import {
ExtendedSnapshotProposal,
FractalProposal,
FractalProposalState,
SnapshotProposal,
SnapshotVote,
SnapshotWeightedVotingChoice,
} from '../../../../types';
import client from './';

Expand All @@ -24,6 +26,13 @@ export default function useSnapshotProposal(proposal: FractalProposal | null | u
() => !!snapshotProposal?.snapshotProposalId,
[snapshotProposal]
);

const getVoteWeight = useCallback(
(vote: SnapshotVote) =>
vote.votingWeight * vote.votingWeightByStrategy.reduce((prev, curr) => prev + curr, 0),
[]
);

const loadProposal = useCallback(async () => {
if (snapshotProposal?.snapshotProposalId) {
const proposalQueryResult = await client
Expand Down Expand Up @@ -99,32 +108,65 @@ export default function useSnapshotProposal(proposal: FractalProposal | null | u
};
} = {};

const getVoteWeight = (vote: SnapshotVote) =>
vote.votingWeight * vote.votingWeightByStrategy.reduce((prev, curr) => prev + curr, 0);
const { choices, type, privacy } = proposalQueryResult;

votesQueryResult.forEach((vote: SnapshotVote) => {
const existingChoiceType = votesBreakdown[vote.choice];
if (existingChoiceType) {
votesBreakdown[vote.choice] = {
total: existingChoiceType.total + getVoteWeight(vote),
votes: [...existingChoiceType.votes, vote],
};
} else {
votesBreakdown[vote.choice] = {
total: getVoteWeight(vote),
votes: [vote],
};
}
Object.keys(choices).forEach(choice => {
votesBreakdown[choice] = {
votes: [],
total: 0,
};
});

const isShielded = privacy === 'shutter';
const isClosed = snapshotProposal.state === FractalProposalState.CLOSED;
if (!(isShielded && !isClosed)) {
votesQueryResult.forEach((vote: SnapshotVote) => {
if (type === 'weighted') {
const voteChoices = vote.choice as SnapshotWeightedVotingChoice;
Object.keys(voteChoices).forEach((choiceIndex: any) => {
// In Snapshot API choices are indexed 1-based. The first choice has index 1.
// https://docs.snapshot.org/tools/api#vote
const voteChoice = choices[choiceIndex - 1];
const existingChoiceType = votesBreakdown[voteChoice];
if (existingChoiceType) {
votesBreakdown[voteChoice] = {
total: existingChoiceType.total + getVoteWeight(vote),
votes: [...existingChoiceType.votes, vote],
};
} else {
votesBreakdown[voteChoice] = {
total: getVoteWeight(vote),
votes: [vote],
};
}
});
} else {
const voteChoice = vote.choice as string;
const existingChoiceType = votesBreakdown[voteChoice];

if (existingChoiceType) {
votesBreakdown[voteChoice] = {
total: existingChoiceType.total + getVoteWeight(vote),
votes: [...existingChoiceType.votes, vote],
};
} else {
votesBreakdown[voteChoice] = {
total: getVoteWeight(vote),
votes: [vote],
};
}
}
});
}

setExtendedSnapshotProposal({
...proposal,
...proposalQueryResult,
votesBreakdown,
votes: votesQueryResult,
} as ExtendedSnapshotProposal);
}
}, [snapshotProposal?.snapshotProposalId, proposal]);
}, [snapshotProposal?.snapshotProposalId, proposal, snapshotProposal?.state, getVoteWeight]);

const loadVotingWeight = useCallback(async () => {
if (snapshotProposal?.snapshotProposalId) {
Expand Down Expand Up @@ -164,6 +206,7 @@ export default function useSnapshotProposal(proposal: FractalProposal | null | u
return {
loadVotingWeight,
loadProposal,
getVoteWeight,
snapshotProposal,
isSnapshotProposal,
extendedSnapshotProposal,
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/en/proposal.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
"votingSystem": "Voting system",
"basicSnapshotVotingSystem": "Basic voting",
"singleSnapshotVotingSystem": "Single choice voting",
"weightedSnapshotVotingSystem": "Weighted voting",
"unknownSnapshotVotingSystem": "Unknown voting system",
"ipfs": "IPFS",
"privacy": "Privacy",
Expand Down
16 changes: 9 additions & 7 deletions src/types/daoProposal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,20 +63,17 @@ interface SnapshotVotingStrategy {
}

interface SnapshotPlugin {}

/**
* @interface ExtendedSnapshotProposal - extension of `SnapshotProposal` to inject voting strategy data, votes, quorum, etc.
* Their data model is quite different comparing to our, so there's not much of point to reuse existing
*/

export interface SnapshotWeightedVotingChoice {
[choice: number]: number;
}
export interface SnapshotVote {
id: string;
voter: string;
votingWeight: number;
votingWeightByStrategy: number[];
votingState: string;
created: number;
choice: string;
choice: string | SnapshotWeightedVotingChoice;
}

export interface SnapshotVoteBreakdown {
Expand All @@ -96,6 +93,11 @@ export type SnapshotProposalType =
| 'ranked-choice'
| 'weighted'
| 'basic';

/**
* @interface ExtendedSnapshotProposal - extension of `SnapshotProposal` to inject voting strategy data, votes, quorum, etc.
* Their data model is quite different comparing to our, so there's not much of point to reuse existing
*/
export interface ExtendedSnapshotProposal extends SnapshotProposal {
snapshot: number; // Number of block
snapshotState: string; // State retrieved from Snapshot
Expand Down

0 comments on commit 1ef6926

Please sign in to comment.