Skip to content
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
16 changes: 8 additions & 8 deletions src/data/vortexopedia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1379,11 +1379,11 @@ export const vortexopediaTerms: VortexopediaTerm[] = [
name: "Governing threshold",
category: "governance",
short:
"Action quota and uptime requirement per era to remain an active governor counted in quorums.",
"Previous-era action quota used to decide who is counted as an active governor in quorums.",
long: [
"A governor is active if bioauthenticated, node ran 164/168 epochs, and required actions were met in the previous era.",
"A governor is active for quorum purposes when the required governing actions were met in the previous era.",
"Required actions per era include upvoting/downvoting proposals or voting on chamber proposals in Vortex.",
"Meeting the threshold keeps the governor eligible to be counted in quorums for the upcoming era.",
"Current node liveness does not remove an Active Governor status already earned from the previous era.",
],
tags: ["threshold", "quorum", "activity", "governor"],
related: [
Expand All @@ -1393,7 +1393,7 @@ export const vortexopediaTerms: VortexopediaTerm[] = [
"quorum_of_attention",
],
examples: [
"If the action threshold is met and uptime is 164/168 epochs, the governor is counted as active in the next era’s quorum.",
"If the previous-era action threshold is met, the governor is counted as active in the next era’s quorum.",
],
stages: ["global"],
links: [
Expand All @@ -1414,7 +1414,7 @@ export const vortexopediaTerms: VortexopediaTerm[] = [
"You are comfortably above the governing threshold pace for the current era. Status scale: Ahead → Stable → Falling behind → At risk → Losing status.",
long: [
"Ahead means you have already met (or are well on track to exceed) the era’s action threshold early, leaving a buffer for the rest of the era.",
"Staying Ahead typically requires continuing normal participation (pool votes and chamber votes) while maintaining node uptime.",
"Staying Ahead typically requires continuing normal participation through pool votes and chamber votes.",
"This status is based on your completed actions vs required actions for the current governing era, not on proposal outcomes.",
],
tags: ["status", "governance", "threshold", "governor", "activity"],
Expand Down Expand Up @@ -1449,7 +1449,7 @@ export const vortexopediaTerms: VortexopediaTerm[] = [
"You are on pace to meet the governing threshold for the era. Status scale: Ahead → Stable → Falling behind → At risk → Losing status.",
long: [
"Stable means your completed actions are at or near the required threshold pace, and you are not currently trending toward inactivity for the next era.",
"If you stay Stable through the era (and maintain uptime), you remain counted as an active governor for quorum calculations in the next era.",
"If you stay Stable through the era, you remain counted as an active governor for quorum calculations in the next era.",
"This status summarizes action progress for the current era; it can change as time passes and requirements are assessed.",
],
tags: ["status", "governance", "threshold", "governor", "activity"],
Expand Down Expand Up @@ -1519,7 +1519,7 @@ export const vortexopediaTerms: VortexopediaTerm[] = [
"You are unlikely to meet the governing threshold without immediate additional actions. Status scale: Ahead → Stable → Falling behind → At risk → Losing status.",
long: [
"At risk means your current action count is far enough below the era requirement that you may lose active governor status for the next era if you do not act.",
"To improve: complete additional required actions (pool votes and chamber votes) before the era ends and maintain node uptime.",
"To improve: complete additional required actions, such as pool votes and chamber votes, before the era ends.",
"This status summarizes your action deficit; it does not imply slashing or permanent removal—only loss of active quorum eligibility in the next era.",
],
tags: ["status", "governance", "threshold", "governor", "activity"],
Expand Down Expand Up @@ -1555,7 +1555,7 @@ export const vortexopediaTerms: VortexopediaTerm[] = [
long: [
"Losing status indicates a severe shortfall against the era action threshold and/or insufficient remaining time to realistically catch up.",
"If this remains at era close, you may not be counted as an active governor for quorum calculations in the next era.",
"To recover, complete the highest-impact required actions immediately and maintain node uptime; otherwise you transition out of active quorum eligibility.",
"To recover, complete the highest-impact required actions immediately; otherwise you transition out of active quorum eligibility.",
],
tags: ["status", "governance", "threshold", "governor", "activity"],
related: [
Expand Down
21 changes: 17 additions & 4 deletions src/lib/proposalUi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ type ProposalPoolVotingGateInput = {
};

type ProposalOrdinaryVoteGateInput = {
auth?: {
authenticated: boolean;
enabled: boolean;
loading: boolean;
};
closedReason?: string;
submitting: boolean;
viewerIsProposer: boolean;
Expand All @@ -54,18 +59,19 @@ export function getProposalPoolVotingGate({
}: ProposalPoolVotingGateInput): { allowed: boolean; disabledReason: string } {
const allowed =
!viewerIsProposer &&
(!auth.enabled || (auth.authenticated && auth.eligible && !auth.loading));
(!auth.enabled || (auth.authenticated && !auth.loading));
const disabledReason = viewerIsProposer
? "You cannot vote on your own proposal."
: auth.enabled && auth.loading
? "Checking wallet status…"
: auth.enabled && !auth.authenticated
? "Connect your wallet to vote."
: (auth.gateReason ?? "Only active human nodes can vote.");
: "Only chamber Governors can vote. Active Governors are counted for quorum.";
return { allowed, disabledReason };
}

export function getProposalOrdinaryVoteGate({
auth,
closedReason = "Ordinary voting is closed.",
submitting,
viewerIsProposer,
Expand All @@ -74,13 +80,20 @@ export function getProposalOrdinaryVoteGate({
disabled: boolean;
title: string | undefined;
} {
const authBlocked = Boolean(
auth?.enabled && (auth.loading || !auth.authenticated),
);
return {
disabled: submitting || votingClosed || viewerIsProposer,
disabled: submitting || votingClosed || viewerIsProposer || authBlocked,
title: viewerIsProposer
? "You cannot vote on your own proposal."
: votingClosed
? closedReason
: undefined,
: auth?.enabled && auth.loading
? "Checking wallet status…"
: auth?.enabled && !auth.authenticated
? "Connect your wallet to vote."
: undefined,
};
}

Expand Down
1 change: 1 addition & 0 deletions src/pages/proposals/ProposalChamber.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ const ProposalChamber: React.FC = () => {
viewerAddress: auth.address,
});
const ordinaryVoteGate = getProposalOrdinaryVoteGate({
auth,
closedReason:
"Ordinary chamber voting is closed. Only veto actions remain in this window.",
submitting,
Expand Down
2 changes: 2 additions & 0 deletions src/pages/proposals/ProposalPP.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ const ProposalPP: React.FC = () => {
tone="accent"
icon="▲"
label="Upvote"
requiresEligibility={false}
disabled={!votingGate.allowed}
title={votingGate.allowed ? undefined : votingGate.disabledReason}
onClick={() => {
Expand All @@ -142,6 +143,7 @@ const ProposalPP: React.FC = () => {
tone="destructive"
icon="▼"
label="Downvote"
requiresEligibility={false}
disabled={!votingGate.allowed}
title={votingGate.allowed ? undefined : votingGate.disabledReason}
onClick={() => {
Expand Down
3 changes: 3 additions & 0 deletions src/pages/proposals/shared/ProposalOrdinaryVoteActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export function ProposalOrdinaryVoteActions({
<VoteButton
tone="accent"
label="Vote yes"
requiresEligibility={false}
disabled={gate.disabled}
title={gate.title}
onClick={() => onVote("yes", score?.value)}
Expand Down Expand Up @@ -56,13 +57,15 @@ export function ProposalOrdinaryVoteActions({
<VoteButton
tone="destructive"
label="Vote no"
requiresEligibility={false}
disabled={gate.disabled}
title={gate.title}
onClick={() => onVote("no")}
/>
<VoteButton
tone="neutral"
label="Abstain"
requiresEligibility={false}
disabled={gate.disabled}
title={gate.title}
onClick={() => onVote("abstain")}
Expand Down
35 changes: 29 additions & 6 deletions tests/unit/proposal-ui.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ test("getProposalChamberPageDerivation handles referendum and milestone titles",
});
});

test("getProposalPoolVotingGate blocks proposers and wallet gate states", () => {
test("getProposalPoolVotingGate blocks proposers and wallet connection states", () => {
expect(
getProposalPoolVotingGate({
viewerIsProposer: true,
Expand Down Expand Up @@ -245,12 +245,13 @@ test("getProposalPoolVotingGate blocks proposers and wallet gate states", () =>
},
}),
).toEqual({
allowed: false,
disabledReason: "Custom gate reason.",
allowed: true,
disabledReason:
"Only chamber Governors can vote. Active Governors are counted for quorum.",
});
});

test("getProposalPoolVotingGate allows eligible or auth-disabled voters", () => {
test("getProposalPoolVotingGate allows authenticated or auth-disabled voters", () => {
expect(
getProposalPoolVotingGate({
viewerIsProposer: false,
Expand All @@ -263,7 +264,8 @@ test("getProposalPoolVotingGate allows eligible or auth-disabled voters", () =>
}),
).toEqual({
allowed: true,
disabledReason: "Only active human nodes can vote.",
disabledReason:
"Only chamber Governors can vote. Active Governors are counted for quorum.",
});

expect(
Expand All @@ -278,7 +280,8 @@ test("getProposalPoolVotingGate allows eligible or auth-disabled voters", () =>
}),
).toEqual({
allowed: true,
disabledReason: "Only active human nodes can vote.",
disabledReason:
"Only chamber Governors can vote. Active Governors are counted for quorum.",
});
});

Expand Down Expand Up @@ -311,11 +314,31 @@ test("getProposalOrdinaryVoteGate blocks submit, proposer, and closed states", (
disabled: true,
title: "Ordinary chamber voting is closed.",
});

expect(
getProposalOrdinaryVoteGate({
auth: {
enabled: true,
loading: false,
authenticated: false,
},
submitting: false,
viewerIsProposer: false,
}),
).toEqual({
disabled: true,
title: "Connect your wallet to vote.",
});
});

test("getProposalOrdinaryVoteGate allows open non-proposer votes", () => {
expect(
getProposalOrdinaryVoteGate({
auth: {
enabled: true,
loading: false,
authenticated: true,
},
submitting: false,
viewerIsProposer: false,
}),
Expand Down
Loading