From daf2a0de42327d14332a1d3300a2151b73a51703 Mon Sep 17 00:00:00 2001 From: thebeyondr <19380973+thebeyondr@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:16:20 -0500 Subject: [PATCH 1/4] fix(treasury): clarify voting ineligibility for new delegators - Show Ineligible status instead of disabled buttons when user had no stake at snapshot - Hide null address delegate from UI - Display snapshot date in ineligibility message - Link to LIP-89 for stake snapshot explanation - Restructure widget into Results and Your vote sections - Use thinner, read-only styled vote bars Fixes #301 --- components/TreasuryVotingWidget/index.tsx | 709 ++++++++++++---------- 1 file changed, 401 insertions(+), 308 deletions(-) diff --git a/components/TreasuryVotingWidget/index.tsx b/components/TreasuryVotingWidget/index.tsx index 879af615..71424114 100644 --- a/components/TreasuryVotingWidget/index.tsx +++ b/components/TreasuryVotingWidget/index.tsx @@ -4,7 +4,8 @@ import { ProposalExtended } from "@lib/api/treasury"; import { ProposalVotingPower } from "@lib/api/types/get-treasury-proposal"; import dayjs from "@lib/dayjs"; import { abbreviateNumber, fromWei, shortenAddress } from "@lib/utils"; -import { Box, Button, Flex, Heading, Text } from "@livepeer/design-system"; +import { Box, Button, Flex, Link, Text } from "@livepeer/design-system"; +import { InfoCircledIcon } from "@modulz/radix-icons"; import { useAccountAddress } from "hooks"; import numbro from "numbro"; import { useState } from "react"; @@ -19,328 +20,420 @@ type Props = { const formatPercent = (percent: number) => numbro(percent).format({ output: "percent", - mantissa: 4, + mantissa: 2, }); const formatLPT = (lpt: string | undefined) => abbreviateNumber(fromWei(lpt ?? "0"), 4); +const zeroAddress = "0x0000000000000000000000000000000000000000"; + +const SectionLabel = ({ children }: { children: React.ReactNode }) => ( + + {children} + +); + const TreasuryVotingWidget = ({ proposal, vote, ...props }: Props) => { const accountAddress = useAccountAddress(); const [reason, setReason] = useState(""); + const hasVotingPower = + !!vote && !!vote.self && parseFloat(vote.self.votes) > 0; + const canVoteNow = + proposal.state === "Active" && vote?.self?.hasVoted === false; + const isIneligible = canVoteNow && !hasVotingPower; + const hasDelegate = + !!vote?.delegate && + vote.delegate.address.toLowerCase() !== zeroAddress; + + // Determine user vote status + const getUserVoteStatus = () => { + if (!vote?.self) return null; + if (isIneligible) return "Ineligible"; + if (vote.self.hasVoted) return "Voted"; + return "Not voted"; + }; + + const userVoteStatus = getUserVoteStatus(); + return ( - - - - - Do you support{" "} - {proposal.attributes?.lip - ? `LIP-${proposal.attributes?.lip}` - : "this proposal"} - ? - + + + {/* ========== RESULTS SECTION ========== */} + + Results - - - - - - Against - - - {formatPercent(proposal.votes.percent.against)} - - - - - - For - - - {formatPercent(proposal.votes.percent.for)} + {/* Vote bars - read-only styled */} + + {/* Against bar */} + + + Against + + + + - - - - - Abstain + + + {formatPercent(proposal.votes.percent.against)} + + + + {/* For bar */} + + + For + + + + - - {formatPercent(proposal.votes.percent.abstain)} + + + {formatPercent(proposal.votes.percent.for)} + + + + {/* Abstain bar */} + + + Abstain + + + + - - - - {abbreviateNumber(proposal.votes.total.voters, 4)} LPT voted ·{" "} - {proposal.state !== "Pending" && proposal.state !== "Active" - ? "Final Results" - : dayjs.duration(proposal.votes.voteEndTime.diff()).humanize() + - " left"} - - + + + {formatPercent(proposal.votes.percent.abstain)} + + + - {accountAddress ? ( - <> - - {vote && !vote?.delegate ? null : ( - - - Your Delegate{" "} - {vote?.delegate && - `(${shortenAddress(vote?.delegate.address)})`} - {vote?.delegate && - ` ${ - vote.delegate.hasVoted - ? "already voted" - : "hasn't voted yet" - }`} - - - {vote?.delegate ? formatLPT(vote.delegate.votes) : "N/A"} - - - )} - - - You ( - {accountAddress.replace(accountAddress.slice(5, 39), "…")}) - {vote?.self && - ` ${ - vote.self.hasVoted - ? "already voted" - : "haven't voted yet" - }`} - - - {vote?.self ? formatLPT(vote.self.votes) : "N/A"} - - - {!vote?.self.hasVoted && proposal.state === "Active" && ( - - - My Voting Power - - - {formatLPT(vote?.self.votes)} LPT - - - )} - + {/* Summary line */} + + {abbreviateNumber(proposal.votes.total.voters, 4)} LPT voted ·{" "} + {proposal.state !== "Pending" && proposal.state !== "Active" + ? "Final Results" + : dayjs.duration(proposal.votes.voteEndTime.diff()).humanize() + + " left"} + + - {proposal?.state === "Active" && - vote?.self.hasVoted === false && ( - - 0)} - variant="red" - size="4" - choiceId={0} - proposalId={proposal?.id} - reason={reason} - > - Against - - 0)} - variant="primary" - choiceId={1} - size="4" - proposalId={proposal?.id} - reason={reason} - > - For - - 0)} - variant="gray" - size="4" - choiceId={2} - proposalId={proposal?.id} - reason={reason} - > - Abstain - + {/* ========== YOUR VOTE SECTION ========== */} + {accountAddress ? ( + + Your vote - 0)} - /> - - )} + {/* Delegate vote status */} + {hasDelegate && ( + + + Delegate vote ({shortenAddress(vote.delegate!.address)}) + + + {vote.delegate!.hasVoted ? "Voted" : "Not voted"} + + + )} - {["Succeeded", "Queued"].includes(proposal?.state) && ( - - - - )} - - ) : ( + {/* User vote status */} + + Status + + {userVoteStatus} + + + + {/* Voting power */} + + Voting power + + {vote?.self ? formatLPT(vote.self.votes) : "0"} LPT + + + + {/* ========== ACTION AREA ========== */} + + {/* Eligible: show vote buttons + reason */} + {canVoteNow && hasVotingPower && ( + + + Against + + + For + + + Abstain + + + + + + + )} + + {/* Ineligible: show info banner + links, no buttons, no reason */} + {isIneligible && ( + + + + + + Ineligible to vote + + + You had 0 LPT staked on{" "} + {proposal.votes.voteStartTime.format("MMM D, YYYY")} when + this proposal was created. + + + + Learn about stake snapshots + + + + + + )} + + {/* Queue/Execute buttons for passed proposals */} + {["Succeeded", "Queued"].includes(proposal?.state) && ( + + + + )} + + ) : ( + /* No wallet connected */ + + Your vote Connect your wallet to vote. - - )} - + + + )} ); From 1b14184614976d457fb3b9c9bcfdec989f79e827 Mon Sep 17 00:00:00 2001 From: thebeyondr <19380973+thebeyondr@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:19:47 -0500 Subject: [PATCH 2/4] refactor(TreasuryVotingWidget): improve code formatting and readability --- components/TreasuryVotingWidget/index.tsx | 783 +++++++++++----------- 1 file changed, 397 insertions(+), 386 deletions(-) diff --git a/components/TreasuryVotingWidget/index.tsx b/components/TreasuryVotingWidget/index.tsx index 71424114..156642e2 100644 --- a/components/TreasuryVotingWidget/index.tsx +++ b/components/TreasuryVotingWidget/index.tsx @@ -20,7 +20,7 @@ type Props = { const formatPercent = (percent: number) => numbro(percent).format({ output: "percent", - mantissa: 2, + mantissa: 2, }); const formatLPT = (lpt: string | undefined) => @@ -29,18 +29,18 @@ const formatLPT = (lpt: string | undefined) => const zeroAddress = "0x0000000000000000000000000000000000000000"; const SectionLabel = ({ children }: { children: React.ReactNode }) => ( - - {children} - + + {children} + ); const TreasuryVotingWidget = ({ proposal, vote, ...props }: Props) => { @@ -48,392 +48,406 @@ const TreasuryVotingWidget = ({ proposal, vote, ...props }: Props) => { const [reason, setReason] = useState(""); - const hasVotingPower = - !!vote && !!vote.self && parseFloat(vote.self.votes) > 0; - const canVoteNow = - proposal.state === "Active" && vote?.self?.hasVoted === false; - const isIneligible = canVoteNow && !hasVotingPower; - const hasDelegate = - !!vote?.delegate && - vote.delegate.address.toLowerCase() !== zeroAddress; + const hasVotingPower = + !!vote && !!vote.self && parseFloat(vote.self.votes) > 0; + const canVoteNow = + proposal.state === "Active" && vote?.self?.hasVoted === false; + const isIneligible = canVoteNow && !hasVotingPower; + const hasDelegate = + !!vote?.delegate && vote.delegate.address.toLowerCase() !== zeroAddress; - // Determine user vote status - const getUserVoteStatus = () => { - if (!vote?.self) return null; - if (isIneligible) return "Ineligible"; - if (vote.self.hasVoted) return "Voted"; - return "Not voted"; - }; + // Determine user vote status + const getUserVoteStatus = () => { + if (!vote?.self) return null; + if (isIneligible) return "Ineligible"; + if (vote.self.hasVoted) return "Voted"; + return "Not voted"; + }; - const userVoteStatus = getUserVoteStatus(); + const userVoteStatus = getUserVoteStatus(); return ( - - - {/* ========== RESULTS SECTION ========== */} - - Results + + + {/* ========== RESULTS SECTION ========== */} + + Results - {/* Vote bars - read-only styled */} - - {/* Against bar */} - - - Against - - - - + {/* Vote bars - read-only styled */} + + {/* Against bar */} + + + Against + + + + - - - {formatPercent(proposal.votes.percent.against)} - - + + + {formatPercent(proposal.votes.percent.against)} + + - {/* For bar */} - - - For - - - - + {/* For bar */} + + + For + + + + - - - {formatPercent(proposal.votes.percent.for)} - - + + + {formatPercent(proposal.votes.percent.for)} + + - {/* Abstain bar */} - - - Abstain - - - - + {/* Abstain bar */} + + + Abstain + + + + - - - {formatPercent(proposal.votes.percent.abstain)} - - - + + + {formatPercent(proposal.votes.percent.abstain)} + + + - {/* Summary line */} - - {abbreviateNumber(proposal.votes.total.voters, 4)} LPT voted ·{" "} - {proposal.state !== "Pending" && proposal.state !== "Active" - ? "Final Results" - : dayjs.duration(proposal.votes.voteEndTime.diff()).humanize() + - " left"} - - + {/* Summary line */} + + {abbreviateNumber(proposal.votes.total.voters, 4)} LPT voted ·{" "} + {proposal.state !== "Pending" && proposal.state !== "Active" + ? "Final Results" + : dayjs.duration(proposal.votes.voteEndTime.diff()).humanize() + + " left"} + + - {/* ========== YOUR VOTE SECTION ========== */} - {accountAddress ? ( - - Your vote + {/* ========== YOUR VOTE SECTION ========== */} + {accountAddress ? ( + + Your vote - {/* Delegate vote status */} - {hasDelegate && ( - - - Delegate vote ({shortenAddress(vote.delegate!.address)}) - - - {vote.delegate!.hasVoted ? "Voted" : "Not voted"} - - - )} + {/* Delegate vote status */} + {hasDelegate && ( + + + Delegate vote ({shortenAddress(vote.delegate!.address)}) + + + {vote.delegate!.hasVoted ? "Voted" : "Not voted"} + + + )} - {/* User vote status */} - - Status - - {userVoteStatus} - - + {/* User vote status */} + + Status + + {userVoteStatus} + + - {/* Voting power */} - - Voting power - - {vote?.self ? formatLPT(vote.self.votes) : "0"} LPT - - + {/* Voting power */} + + Voting power + + {vote?.self ? formatLPT(vote.self.votes) : "0"} LPT + + - {/* ========== ACTION AREA ========== */} + {/* ========== ACTION AREA ========== */} - {/* Eligible: show vote buttons + reason */} - {canVoteNow && hasVotingPower && ( - - - Against - - - For - - - Abstain - + {/* Eligible: show vote buttons + reason */} + {canVoteNow && hasVotingPower && ( + + + Against + + + For + + + Abstain + - - - - - )} + + + + + )} - {/* Ineligible: show info banner + links, no buttons, no reason */} - {isIneligible && ( - - - - - - Ineligible to vote - - - You had 0 LPT staked on{" "} - {proposal.votes.voteStartTime.format("MMM D, YYYY")} when - this proposal was created. - - - - Learn about stake snapshots - - - - - - )} + {/* Ineligible: show info banner + links, no buttons, no reason */} + {isIneligible && ( + + + + + + Ineligible to vote + + + You had 0 LPT staked on{" "} + {proposal.votes.voteStartTime.format("MMM D, YYYY")} when + this proposal was created. + + + + Learn about stake snapshots + + + + + + )} - {/* Queue/Execute buttons for passed proposals */} - {["Succeeded", "Queued"].includes(proposal?.state) && ( - - - - )} - - ) : ( - /* No wallet connected */ - - Your vote + {/* Queue/Execute buttons for passed proposals */} + {["Succeeded", "Queued"].includes(proposal?.state) && ( + + + + )} + + ) : ( + /* No wallet connected */ + + Your vote - + Connect your wallet to vote. - - - )} + + + )} ); From 13711cc9e065743b68efa12f9dc5866adbb8a40f Mon Sep 17 00:00:00 2001 From: ECWireless Date: Sun, 14 Dec 2025 15:42:23 -0700 Subject: [PATCH 3/4] fix: broken lock file --- pnpm-lock.yaml | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ed8a1625..97f96d23 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8015,8 +8015,8 @@ packages: numbro@2.5.0: resolution: {integrity: sha512-xDcctDimhzko/e+y+Q2/8i3qNC9Svw1QgOkSkQoO0kIPI473tR9QRbo2KP88Ty9p8WbPy+3OpTaAIzehtuHq+A==} - nwsapi@2.2.22: - resolution: {integrity: sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==} + nwsapi@2.2.23: + resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} obj-multiplex@1.0.0: resolution: {integrity: sha512-0GNJAOsHoBHeNTvl5Vt6IWnpUEcc3uSRxzBri7EDyIcMgYvnY2JL2qdeV5zTMjWQX5OHcD5amcW2HFfDh0gjIA==} @@ -19797,7 +19797,7 @@ snapshots: http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.22 + nwsapi: 2.2.23 parse5: 7.3.0 rrweb-cssom: 0.8.0 saxes: 6.0.0 @@ -20929,7 +20929,7 @@ snapshots: dependencies: bignumber.js: 9.3.1 - nwsapi@2.2.22: {} + nwsapi@2.2.23: {} obj-multiplex@1.0.0: dependencies: @@ -22515,14 +22515,6 @@ snapshots: tmpl@1.0.5: {} - tldts-core@6.1.86: {} - - tldts@6.1.86: - dependencies: - tldts-core: 6.1.86 - - tmpl@1.0.5: {} - to-buffer@1.2.2: dependencies: isarray: 2.0.5 From a53342e5bce58a1c1d715c6ca37bea2b859c4af6 Mon Sep 17 00:00:00 2001 From: thebeyondr <19380973+thebeyondr@users.noreply.github.com> Date: Mon, 15 Dec 2025 14:05:38 -0500 Subject: [PATCH 4/4] refactor(TreasuryVotingWidget): enhance user vote status handling and improve accessibility - Introduced useMemo for calculating user vote status to optimize performance. - Removed hardcoded zero address definition and imported it from 'viem'. - Added 'rel="noopener noreferrer"' to external link for improved security. --- components/TreasuryVotingWidget/index.tsx | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/components/TreasuryVotingWidget/index.tsx b/components/TreasuryVotingWidget/index.tsx index 156642e2..167c1194 100644 --- a/components/TreasuryVotingWidget/index.tsx +++ b/components/TreasuryVotingWidget/index.tsx @@ -8,7 +8,8 @@ import { Box, Button, Flex, Link, Text } from "@livepeer/design-system"; import { InfoCircledIcon } from "@modulz/radix-icons"; import { useAccountAddress } from "hooks"; import numbro from "numbro"; -import { useState } from "react"; +import { useMemo, useState } from "react"; +import { zeroAddress } from "viem"; import VoteButton from "../VoteButton"; @@ -26,8 +27,6 @@ const formatPercent = (percent: number) => const formatLPT = (lpt: string | undefined) => abbreviateNumber(fromWei(lpt ?? "0"), 4); -const zeroAddress = "0x0000000000000000000000000000000000000000"; - const SectionLabel = ({ children }: { children: React.ReactNode }) => ( { const hasDelegate = !!vote?.delegate && vote.delegate.address.toLowerCase() !== zeroAddress; - // Determine user vote status - const getUserVoteStatus = () => { + const userVoteStatus = useMemo(() => { if (!vote?.self) return null; if (isIneligible) return "Ineligible"; if (vote.self.hasVoted) return "Voted"; return "Not voted"; - }; - - const userVoteStatus = getUserVoteStatus(); + }, [vote?.self, isIneligible]); return ( @@ -407,6 +403,7 @@ const TreasuryVotingWidget = ({ proposal, vote, ...props }: Props) => {