feat: add read-only bank operations console
This commit is contained in:
@@ -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" };
|
||||
}
|
||||
Reference in New Issue
Block a user