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) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
// ── /v1/indexer/* — generic GET passthrough to the internal indexer.
|
||||||
// The SPA's SDK is configured with `indexerUrl = ${origin}/v1/indexer`,
|
// The SPA's SDK is configured with `indexerUrl = ${origin}/v1/indexer`,
|
||||||
// so calls like `/suppliers`, `/escrows?buyer=…`, `/capabilities` land
|
// so calls like `/suppliers`, `/escrows?buyer=…`, `/capabilities` land
|
||||||
|
|||||||
@@ -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";
|
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() {
|
export default function Wallet() {
|
||||||
const marketplace = useMarketplace();
|
const marketplace = useMarketplace();
|
||||||
const walletKey = marketplace.getWalletKey();
|
const walletKey = marketplace.getWalletKey();
|
||||||
|
|
||||||
|
const [lovelace, setLovelace] = useState<string | null>(null);
|
||||||
|
const [utxoCount, setUtxoCount] = useState<number | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(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 (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h1 className="text-2xl font-semibold">Wallet</h1>
|
<h1 className="text-2xl font-semibold">Wallet</h1>
|
||||||
<dl className="space-y-2">
|
<dl className="space-y-2">
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm text-gray-500">Balance</dt>
|
||||||
|
<dd className="flex items-baseline gap-3" data-testid="wallet-balance">
|
||||||
|
{loading ? (
|
||||||
|
<span className="text-gray-400">Loading…</span>
|
||||||
|
) : error ? (
|
||||||
|
<span className="text-sm text-red-600">{error}</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="text-2xl font-semibold">{formatAp3x(lovelace ?? "0")} AP3X</span>
|
||||||
|
{utxoCount !== null && (
|
||||||
|
<span className="text-xs text-gray-400">({utxoCount} UTxO{utxoCount === 1 ? "" : "s"})</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setReloadKey((k) => k + 1)}
|
||||||
|
disabled={loading}
|
||||||
|
className="ml-2 rounded border border-gray-300 px-2 py-0.5 text-xs text-gray-600 hover:bg-gray-50 disabled:opacity-50"
|
||||||
|
data-testid="wallet-balance-refresh"
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt className="text-sm text-gray-500">Address</dt>
|
<dt className="text-sm text-gray-500">Address</dt>
|
||||||
<dd className="font-mono text-sm break-all">{walletKey.address}</dd>
|
<dd className="font-mono text-sm break-all">{walletKey.address}</dd>
|
||||||
|
|||||||
Reference in New Issue
Block a user