From 4434c787bec020a3669dd87770184206e7d00552 Mon Sep 17 00:00:00 2001 From: david Date: Mon, 8 Jun 2026 17:47:19 +0200 Subject: [PATCH] feat(buyer): show live wallet balance on the Wallet tab Adds GET /v1/wallet/balance (sums the buyer wallet UTxOs via queryUtxosByAddress from Ogmios) and renders it on the Wallet page as AP3X with a UTxO count and a refresh button. Co-Authored-By: Claude Opus 4.8 (1M context) --- buyer/src/server.ts | 31 +++++++++++++++ buyer/src/ui/pages/Wallet.tsx | 75 ++++++++++++++++++++++++++++++++++- 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/buyer/src/server.ts b/buyer/src/server.ts index 065bfc9..95fe3c9 100644 --- a/buyer/src/server.ts +++ b/buyer/src/server.ts @@ -379,6 +379,37 @@ export function createApp(deps: AppDeps): Express { } }); + // ── GET /v1/wallet/balance — live buyer-wallet balance from Ogmios. + // Sums lovelace across all UTxOs at the buyer's address. The Wallet page + // renders this as AP3X (lovelace / 1e6). + app.get("/v1/wallet/balance", async (_req: Request, res: Response) => { + if (!resolved) { + return jsonError( + res, + 503, + "service_unavailable", + "buyer-app booted without chain deps; /v1/wallet/balance disabled", + ); + } + try { + const utxos = await resolved.chain.queryUtxosByAddress(resolved.walletKey.address); + let lovelace = 0n; + for (const u of utxos) lovelace += u.lovelace; + return res.status(200).json({ + address: resolved.walletKey.address, + lovelace: lovelace.toString(), + utxo_count: utxos.length, + }); + } catch (err) { + return jsonError( + res, + 502, + "chain_unreachable", + err instanceof Error ? err.message : String(err), + ); + } + }); + // ── /v1/indexer/* — generic GET passthrough to the internal indexer. // The SPA's SDK is configured with `indexerUrl = ${origin}/v1/indexer`, // so calls like `/suppliers`, `/escrows?buyer=…`, `/capabilities` land diff --git a/buyer/src/ui/pages/Wallet.tsx b/buyer/src/ui/pages/Wallet.tsx index 7ff7248..54769b2 100644 --- a/buyer/src/ui/pages/Wallet.tsx +++ b/buyer/src/ui/pages/Wallet.tsx @@ -1,16 +1,89 @@ /** - * buyer/src/ui/pages/Wallet.tsx — wallet address + balance view. + * buyer/src/ui/pages/Wallet.tsx — wallet address + live balance view. + * + * Balance is fetched from /v1/wallet/balance, which sums the buyer wallet's + * UTxOs live from Ogmios, and rendered as AP3X (lovelace / 1e6). */ +import { useEffect, useState } from "react"; import { useMarketplace } from "../state/MarketplaceContext.js"; +function formatAp3x(lovelace: string): string { + return (Number(lovelace) / 1e6).toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 6, + }); +} + export default function Wallet() { const marketplace = useMarketplace(); const walletKey = marketplace.getWalletKey(); + + const [lovelace, setLovelace] = useState(null); + const [utxoCount, setUtxoCount] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(true); + const [reloadKey, setReloadKey] = useState(0); + + useEffect(() => { + let cancelled = false; + setLoading(true); + setError(null); + fetch("/v1/wallet/balance") + .then(async (r) => { + if (!r.ok) { + const j = (await r.json().catch(() => ({}))) as { message?: string; error?: string }; + throw new Error(j.message ?? j.error ?? `${r.status} ${r.statusText}`); + } + return r.json() as Promise<{ lovelace: string; utxo_count: number }>; + }) + .then((j) => { + if (!cancelled) { + setLovelace(j.lovelace); + setUtxoCount(j.utxo_count); + } + }) + .catch((e: Error) => { + if (!cancelled) setError(e.message); + }) + .finally(() => { + if (!cancelled) setLoading(false); + }); + return () => { + cancelled = true; + }; + }, [reloadKey]); + return (

Wallet

+
+
Balance
+
+ {loading ? ( + Loading… + ) : error ? ( + {error} + ) : ( + <> + {formatAp3x(lovelace ?? "0")} AP3X + {utxoCount !== null && ( + ({utxoCount} UTxO{utxoCount === 1 ? "" : "s"}) + )} + + )} + +
+
Address
{walletKey.address}