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) => {