Skip to content

Commit

Permalink
Add voter breakdown (#89)
Browse files Browse the repository at this point in the history
* Use check icon for votes

* Make dependent on canVote for title

* Make dependent on canVote for title

* Add voter breakdown

* Fix

* Fix save issue

* Cleanup
  • Loading branch information
ChewingGlass authored Oct 12, 2023
1 parent d1acc2a commit 4948afb
Show file tree
Hide file tree
Showing 4 changed files with 225 additions and 15 deletions.
169 changes: 169 additions & 0 deletions components/VoteBreakdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { PublicKey } from "@solana/web3.js";
import { useVotes } from "../hooks/useVotes";
import {
useProposal,
useProposalConfig,
} from "@helium/modular-governance-hooks";
import { useMemo, useState } from "react";
import { useRegistrar } from "@helium/voter-stake-registry-hooks";
import { useMint } from "@helium/helium-react-hooks";
import { humanReadable } from "../utils/formatting";
import BN from "bn.js";
import Loading from "./Loading";

export function VoteBreakdown({ proposalKey }: { proposalKey: PublicKey }) {
const { markers, loading: loadingMarkers } = useVotes(proposalKey);
const { info: proposal, loading: loadingProp } = useProposal(proposalKey);
const { info: proposalConfig, loading: loadingConf } = useProposalConfig(
proposal?.proposalConfig
);
const { info: registrar, loading: loadingReg } = useRegistrar(
proposalConfig?.voteController
);
const decimals = useMint(registrar?.votingMints[0].mint)?.info?.decimals;
const totalVotes = useMemo(
() =>
(proposal?.choices || []).reduce((acc, { weight }) => {
return acc.add(weight);
}, new BN(0)),
[proposal?.choices]
);
const loading = loadingMarkers || loadingProp || loadingConf || loadingReg;
const [displayCount, setDisplayCount] = useState(20);

const groupedSortedMarkers = useMemo(() => {
const grouped = Object.values(
(markers || []).reduce((acc, marker) => {
const key = marker.voter.toBase58() + marker.choices.join(",");
if (!acc[key]) {
acc[key] = {
voter: marker.voter,
choices: [],
totalWeight: new BN(0),
};
}
acc[key].choices = marker.choices;
acc[key].totalWeight = acc[key].totalWeight.add(marker.weight);
return acc;
}, {} as Record<string, { voter: PublicKey; choices: number[]; totalWeight: BN }>)
);

const sortedMarkers = grouped.sort((a, b) =>
b.totalWeight.sub(a.totalWeight).toNumber()
);

return sortedMarkers;
}, [markers]);

const csvData = useMemo(() => {
const rows = [];
rows.push(["Owner", "Choices", "Vote Weight", "Percentage"]);

groupedSortedMarkers.forEach((marker) => {
const owner = marker.voter.toBase58();
const choices = marker.choices
.map((c) => proposal.choices[c].name)
.join(", ");
const voteWeight = humanReadable(marker.totalWeight, decimals);
const percentage = (
marker.totalWeight.mul(new BN(100000)).div(totalVotes).toNumber() / 1000
).toFixed(2);

rows.push([owner, choices, voteWeight, percentage]);
});

const csvContent = rows.map((row) => row.join(",")).join("\n");
return csvContent;
}, [groupedSortedMarkers]);
const displayedMarkers = useMemo(
() => groupedSortedMarkers.slice(0, displayCount),
[groupedSortedMarkers]
);

return (
<div className="flex flex-col">
<table className="table-auto text-white">
<thead>
<tr>
<th className="px-4 py-2">Owner</th>
<th className="px-4 py-2">Choices</th>
<th className="px-4 py-2">Vote Weight</th>
<th className="px-4 py-2">Percentage</th>
</tr>
</thead>
<tbody>
{loading && <Loading />}
{(displayedMarkers || []).map((marker, index) => (
<tr
key={marker.voter.toBase58()}
className={index % 2 === 0 ? "bg-hv-gray-500" : "bg-hv-gray-600"}
>
<td className="px-4 py-2">
<a
className="text-hv-green-500"
target="_blank"
href={`https://explorer.solana.com/address/${marker.voter.toBase58()}`}
>
{ellipsisMiddle(marker.voter.toBase58())}
</a>
</td>
<td className="px-4 py-2">
{marker.choices.map((c) => proposal.choices[c].name).join(", ")}
</td>
<td className="px-4 py-2">
{humanReadable(marker.totalWeight, decimals)}
</td>
<td className="px-4 py-2">
{/* Add two decimals precision */}
{(
marker.totalWeight
.mul(new BN(100000))
.div(totalVotes)
.toNumber() / 1000
).toFixed(2)}
</td>
</tr>
))}
</tbody>
</table>
<div className="flex flex-col justify-center w-full">
{displayCount < groupedSortedMarkers.length && (
<button
className="px-6 py-3 hover:bg-hv-gray-500 transition-all duration-200 rounded-lg text-lg text-hv-green-500 whitespace-nowrap outline-none border border-solid border-transparent focus:border-hv-green-500 block text-center"
onClick={() => setDisplayCount((c) => c + 20)}
>
Load More
</button>
)}
<button
className="px-6 py-3 hover:bg-hv-gray-500 transition-all duration-200 rounded-lg text-lg text-hv-green-500 whitespace-nowrap outline-none border border-solid border-transparent focus:border-hv-green-500 block text-center"
onClick={() => {
const blob = new Blob([csvData], {
type: "text/csv;charset=utf-8;",
});
const link = document.createElement("a");
if (link.download !== undefined) {
const url = URL.createObjectURL(blob);
link.setAttribute("href", url);
link.setAttribute("download", "vote_breakdown.csv");
link.style.visibility = "hidden";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}}
>
Download CSV
</button>
</div>
</div>
);
}

function ellipsisMiddle(wallet: string): string {
const length = wallet.length;
const start = wallet.slice(0, 5);
const end = wallet.slice(length - 5, length);
const middle = "...";
return start + middle + end;
}
16 changes: 2 additions & 14 deletions components/VoteResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,12 @@ const VoteResults: React.FC<{
completed: boolean;
decimals?: number;
}> = ({ decimals, outcomes, completed }) => {

// const winner = outcomesResults[0];

return (
<div className="pt-0">
<div className="w-full flex flex-col lg:flex-row justify-between mb-5 sm:mb-10">
<p className="text-xl sm:text-3xl text-white font-semibold tracking-tighter">
{completed ? "Final Results" : "Preliminary Results"}
</p>
{/* {completed && (
<p className="text-lg sm:text-3xl text-hv-gray-400 font-semibold tracking-tighter pt-4 lg:pt-0">
<span className="text-white">{winner.value}</span> wins with{" "}
{winner.hntPercent.toLocaleString(undefined, {
maximumFractionDigits: 2,
})}
% of Vote
</p>
)} */}
</div>
<div className="flex flex-col space-y-10">
{outcomes.map((r, i) => {
Expand All @@ -48,7 +36,7 @@ const VoteResults: React.FC<{
"bg-hv-blue-500": bg === "blue",
"bg-hv-purple-500": bg === "purple",
"bg-hv-orange-500": bg === "orange",
"bg-hv-turquoise-500": bg === "turquoise",
"bg-hv-turquoise-500": bg === "turquoise",
})}
/>
<div
Expand All @@ -57,7 +45,7 @@ const VoteResults: React.FC<{
"bg-hv-blue-500": bg === "blue",
"bg-hv-purple-500": bg === "purple",
"bg-hv-orange-500": bg === "orange",
"bg-hv-turquoise-500": bg === "turquoise",
"bg-hv-turquoise-500": bg === "turquoise",
})}
style={{
width: `${r.percent}%`,
Expand Down
35 changes: 35 additions & 0 deletions hooks/useVotes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Program } from "@coral-xyz/anchor";
import { useAnchorProvider } from "@helium/helium-react-hooks";
import { VoterStakeRegistry } from "@helium/idls/lib/types/voter_stake_registry";
import { init } from "@helium/voter-stake-registry-sdk";
import { PublicKey } from "@solana/web3.js";
import { useAsync } from "react-async-hook";

export function useVotes(proposal: PublicKey) {
const provider = useAnchorProvider();
const { result: sdk, loading: loadingSdk } = useAsync(
async (provider) => init(provider),
[provider]
);
// @ts-ignore
const { result: markers, loading } = useAsync(
async (sdk: Program<VoterStakeRegistry> | undefined) => {
if (sdk) {
return (await sdk.account.voteMarkerV0.all([
{
memcmp: {
offset: 8 + (2 * 32),
bytes: proposal.toBase58()
}
},
])).map(i => i.account);
}
},
[sdk]
);

return {
loading: loading || loadingSdk,
markers
}
}
20 changes: 19 additions & 1 deletion pages/[proposalKey].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import classNames from "classnames";
import { format } from "date-fns";
import Link from "next/link";
import { useRouter } from "next/router";
import { useMemo } from "react";
import { useMemo, useState } from "react";
import { useAsync } from "react-async-hook";
import ReactMarkdown from "react-markdown";
import ContentSection from "../components/ContentSection";
Expand All @@ -24,6 +24,9 @@ import VoteOptionsSection from "../components/VoteOptionsSection";
import VoteResults from "../components/VoteResults";
import { useNetwork } from "../hooks/useNetwork";
import { humanReadable } from "../utils/formatting";
import { AiFillCaretDown, AiFillCaretUp } from "react-icons/ai";
import { VoteBreakdown } from "../components/VoteBreakdown";
import Button, { LinkButton, SecondaryButton } from "../components/Button";

const VoteDetailsPage = ({ name: initName, content }: { name: string, content: string }) => {
const router = useRouter();
Expand All @@ -45,6 +48,7 @@ const VoteDetailsPage = ({ name: initName, content }: { name: string, content: s
);
const { info: registrar } = useRegistrar(proposalConfig?.voteController);
const decimals = useMint(registrar?.votingMints[0].mint)?.info?.decimals;
const [showBreakdown, setshowBreakdown] = useState(false);

const endTs =
resolution &&
Expand Down Expand Up @@ -228,6 +232,20 @@ const VoteDetailsPage = ({ name: initName, content }: { name: string, content: s
</div>
</div>
)}
<ContentSection className="mt-10 sm:mt-14">
<p className="mb-4 text-xl sm:text-3xl text-white font-semibold tracking-tighter">
Voter Breakdown
</p>
{!showBreakdown && (
<button
className="px-6 py-3 hover:bg-hv-gray-500 transition-all duration-200 rounded-lg text-lg text-hv-green-500 whitespace-nowrap outline-none border border-solid border-transparent focus:border-hv-green-500 mx-auto mt-4 block text-center"
onClick={() => setshowBreakdown(!showBreakdown)}
>
Load Breakdown
</button>
)}
{showBreakdown && <VoteBreakdown proposalKey={proposalK} />}
</ContentSection>

<ContentSection className="mt-10 sm:mt-14">
<div className="bg-hv-gray-775 rounded-xl px-4 sm:px-7 py-4 sm:py-7 flex flex-row justify-between align-center w-full">
Expand Down

0 comments on commit 4948afb

Please sign in to comment.