feat: add read-only bank operations console

This commit is contained in:
golem
2026-08-21 03:35:45 -06:00
parent 9ce8843279
commit 10507e769d
18 changed files with 967 additions and 1 deletions
+118
View File
@@ -0,0 +1,118 @@
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useCallback, useEffect, useMemo, useRef } from "react";
import type { PublicClient } from "viem";
import { useWatchBlockNumber } from "wagmi";
import { createManifestPublicClient } from "../config/chains";
import { parseDeploymentManifest } from "../config/manifest";
import { loadDashboardSnapshot, ViemBankReader } from "../data/bankClient";
import type { DashboardSnapshot, DeploymentManifest } from "../types/dashboard";
export type DashboardState =
| Readonly<{ status: "loading" }>
| Readonly<{ status: "ready"; manifest: DeploymentManifest; snapshot: DashboardSnapshot }>
| Readonly<{ status: "stale"; manifest: DeploymentManifest; snapshot: DashboardSnapshot; failedAt: Date; error: string }>
| Readonly<{ status: "disconnected"; manifest: DeploymentManifest; snapshot?: undefined; failedAt: Date; error: string }>
| Readonly<{ status: "invalid-manifest"; error: string }>
| Readonly<{ status: "chain-mismatch"; manifest: DeploymentManifest; error: string }>;
export type BankDashboardOptions = Readonly<{
loadManifest?: () => Promise<unknown>;
loader?: (manifest: DeploymentManifest) => Promise<DashboardSnapshot>;
now?: () => Date;
}>;
const manifestQueryKey = ["deployment-manifest"] as const;
const defaultNow = () => new Date();
async function fetchManifest(): Promise<unknown> {
const response = await fetch("/deployment.json", { headers: { Accept: "application/json" } });
if (!response.ok) throw new Error(`deployment manifest request failed with HTTP ${response.status}`);
return response.json() as Promise<unknown>;
}
async function fetchSnapshot(manifest: DeploymentManifest): Promise<DashboardSnapshot> {
const client = createManifestPublicClient(manifest);
return loadDashboardSnapshot(new ViemBankReader(client as PublicClient), manifest);
}
function message(error: unknown): string {
return error instanceof Error ? error.message : "Unknown dashboard error";
}
function isChainMismatch(error: unknown): boolean {
return /endpoint chain ID \d+ does not match manifest chain ID \d+/i.test(message(error));
}
export function useBankDashboard(options: BankDashboardOptions = {}): DashboardState {
const loadManifest = options.loadManifest ?? fetchManifest;
const loader = options.loader ?? fetchSnapshot;
const now = options.now ?? defaultNow;
const queryClient = useQueryClient();
const lastWatchedBlock = useRef<bigint | undefined>(undefined);
const manifestQuery = useQuery({
queryKey: manifestQueryKey,
queryFn: loadManifest,
retry: false,
staleTime: Number.POSITIVE_INFINITY,
});
const parsedManifest = useMemo(() => {
if (manifestQuery.data === undefined) return undefined;
try {
return { manifest: parseDeploymentManifest(manifestQuery.data) } as const;
} catch (error) {
return { error: message(error) } as const;
}
}, [manifestQuery.data]);
const manifest = parsedManifest && "manifest" in parsedManifest ? parsedManifest.manifest : undefined;
const snapshotQueryKey = useMemo(
() => ["bank-dashboard", manifest?.chainId, manifest?.proxy] as const,
[manifest?.chainId, manifest?.proxy],
);
const snapshotQuery = useQuery({
queryKey: snapshotQueryKey,
queryFn: () => {
if (!manifest) throw new Error("deployment manifest is unavailable");
return loader(manifest);
},
enabled: manifest !== undefined,
retry: false,
});
const mismatch = snapshotQuery.error !== null && isChainMismatch(snapshotQuery.error);
const latestSnapshot = useRef<DashboardSnapshot | undefined>(undefined);
useEffect(() => {
latestSnapshot.current = snapshotQuery.data;
}, [snapshotQuery.data]);
const reconcileBlock = useCallback((blockNumber: bigint) => {
if (!manifest || mismatch || latestSnapshot.current?.blockNumber === blockNumber || lastWatchedBlock.current === blockNumber) return;
lastWatchedBlock.current = blockNumber;
void queryClient.invalidateQueries({ queryKey: snapshotQueryKey, exact: true });
}, [manifest, mismatch, queryClient, snapshotQueryKey]);
useWatchBlockNumber({
chainId: manifest?.chainId,
enabled: manifest !== undefined && !mismatch,
onBlockNumber: reconcileBlock,
});
const failedAt = useMemo(
() => snapshotQuery.error === null ? undefined : now(),
[now, snapshotQuery.error],
);
if (manifestQuery.isPending) return { status: "loading" };
if (manifestQuery.error !== null) return { status: "invalid-manifest", error: message(manifestQuery.error) };
if (parsedManifest && "error" in parsedManifest) return { status: "invalid-manifest", error: parsedManifest.error ?? "Deployment manifest is invalid" };
if (!manifest) return { status: "loading" };
if (mismatch) return { status: "chain-mismatch", manifest, error: message(snapshotQuery.error) };
if (snapshotQuery.error !== null) {
const error = message(snapshotQuery.error);
if (snapshotQuery.data) return { status: "stale", manifest, snapshot: snapshotQuery.data, failedAt: failedAt ?? now(), error };
return { status: "disconnected", manifest, failedAt: failedAt ?? now(), error };
}
if (snapshotQuery.data) return { status: "ready", manifest, snapshot: snapshotQuery.data };
return { status: "loading" };
}