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; loader?: (manifest: DeploymentManifest) => Promise; now?: () => Date; }>; const manifestQueryKey = ["deployment-manifest"] as const; const defaultNow = () => new Date(); async function fetchManifest(): Promise { 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; } async function fetchSnapshot(manifest: DeploymentManifest): Promise { 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(undefined); const manifestQuery = useQuery({ queryKey: manifestQueryKey, queryFn: loadManifest, retry: false, retryOnMount: false, staleTime: Number.POSITIVE_INFINITY, refetchOnMount: false, refetchOnReconnect: false, refetchOnWindowFocus: false, }); 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, retryOnMount: false, refetchOnMount: false, refetchOnReconnect: false, refetchOnWindowFocus: false, }); const mismatch = snapshotQuery.error !== null && isChainMismatch(snapshotQuery.error); const latestSnapshot = useRef(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" }; }