From a3914f47fab77881f1b3048e582531b12f3868aa Mon Sep 17 00:00:00 2001 From: FlowmemoryAI <283694809+FlowmemoryAI@users.noreply.github.com> Date: Mon, 18 May 2026 14:18:46 -0500 Subject: [PATCH] Add public Uniswap V4 hooks page --- apps/dashboard/src/App.tsx | 2 + apps/dashboard/src/components/AppShell.tsx | 5 +- apps/dashboard/src/styles.css | 647 ++++++++++++++++++ apps/dashboard/src/test/dashboardData.test.ts | 14 + apps/dashboard/src/views/UniswapHooksView.tsx | 303 ++++++++ 5 files changed, 970 insertions(+), 1 deletion(-) create mode 100644 apps/dashboard/src/views/UniswapHooksView.tsx diff --git a/apps/dashboard/src/App.tsx b/apps/dashboard/src/App.tsx index 4116c42c..9a5e5358 100644 --- a/apps/dashboard/src/App.tsx +++ b/apps/dashboard/src/App.tsx @@ -18,6 +18,7 @@ import { OpsView } from "./views/OpsView"; import { OverviewView } from "./views/OverviewView"; import { RawJsonInspectorView } from "./views/RawJsonInspectorView"; import { RootfieldsView } from "./views/RootfieldsView"; +import { UniswapHooksView } from "./views/UniswapHooksView"; import { VerifierReportsView } from "./views/VerifierReportsView"; import { WalletView } from "./views/WalletView"; import { WorkbenchView } from "./views/WorkbenchView"; @@ -124,6 +125,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/apps/dashboard/src/components/AppShell.tsx b/apps/dashboard/src/components/AppShell.tsx index 7e18af83..3f2abb24 100644 --- a/apps/dashboard/src/components/AppShell.tsx +++ b/apps/dashboard/src/components/AppShell.tsx @@ -10,6 +10,7 @@ import { ClipboardCheck, ArrowRightLeft, Compass, + GitBranch, RadioReceiver, LayoutDashboard, Monitor, @@ -36,6 +37,7 @@ const NAV_ITEMS = [ { to: "/wallet", label: "Wallet", icon: Wallet }, { to: "/tester", label: "Tester launch", icon: UserPlus }, { to: "/bridge", label: "Bridge pilot", icon: ArrowRightLeft }, + { to: "/hooks", label: "V4 hooks", icon: GitBranch }, { to: "/explorer", label: "Explorer", icon: Compass }, { to: "/ops", label: "Ops", icon: ShieldAlert }, { to: "/overview", label: "Overview", icon: LayoutDashboard }, @@ -55,7 +57,8 @@ export function AppShell({ data, canaryData, workbench, children }: AppShellProp const location = useLocation(); const isBridgeRoute = location.pathname.startsWith("/bridge"); const isWalletRoute = location.pathname.startsWith("/wallet"); - if (isBridgeRoute || isWalletRoute) { + const isHooksRoute = location.pathname.startsWith("/hooks"); + if (isBridgeRoute || isWalletRoute || isHooksRoute) { return <>{children}; } diff --git a/apps/dashboard/src/styles.css b/apps/dashboard/src/styles.css index 96f2781a..dce60d83 100644 --- a/apps/dashboard/src/styles.css +++ b/apps/dashboard/src/styles.css @@ -5617,3 +5617,650 @@ code { gap: 12px; } } + +/* Public Uniswap V4 hooks route */ +.hooks-public-page { + min-height: 100dvh; + overflow-x: hidden; + background: #f6f5ee; + color: #101815; +} + +.hooks-public-page a { + color: inherit; +} + +.hooks-public-nav, +.hooks-public-main { + width: min(100%, 1390px); + margin: 0 auto; +} + +.hooks-public-nav { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + min-height: 74px; + padding: 14px 28px; + border-bottom: 1px solid rgba(35, 49, 43, 0.14); +} + +.hooks-brand { + display: inline-flex; + align-items: center; + gap: 12px; + min-width: 0; + text-decoration: none; +} + +.hooks-brand-mark { + display: grid; + place-items: center; + width: 42px; + height: 42px; + flex: 0 0 auto; + color: #f5fff9; + border-radius: 8px; + background: #184c3f; + box-shadow: inset 0 -10px 0 rgba(0, 0, 0, 0.12); +} + +.hooks-brand strong, +.hooks-brand small { + display: block; +} + +.hooks-brand strong { + font-size: 1rem; +} + +.hooks-brand small { + margin-top: 2px; + color: #58635d; + font-size: 0.78rem; +} + +.hooks-public-nav nav { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 8px; +} + +.hooks-public-nav nav a { + min-height: 34px; + padding: 8px 10px; + border-radius: 8px; + color: #31423b; + font-size: 0.88rem; + font-weight: 650; + text-decoration: none; +} + +.hooks-public-nav nav a:hover { + background: rgba(24, 76, 63, 0.08); +} + +.hooks-public-main { + display: grid; + gap: 22px; + padding: 30px 28px 48px; +} + +.hooks-public-page .eyebrow { + color: #59645e; + letter-spacing: 0; +} + +.hooks-hero { + display: grid; + grid-template-columns: minmax(0, 1.08fr) minmax(430px, 0.92fr); + gap: 28px; + align-items: stretch; + min-height: 520px; +} + +.hooks-hero-copy { + display: flex; + min-width: 0; + flex-direction: column; + justify-content: center; + padding: 30px 0 34px; +} + +.hooks-hero-copy h1 { + max-width: 760px; + margin: 14px 0 18px; + font-size: 4.35rem; + font-weight: 760; + line-height: 0.98; + letter-spacing: 0; + text-wrap: balance; +} + +.hooks-hero-copy p { + max-width: 690px; + margin: 0; + color: #40504a; + font-size: 1.17rem; + line-height: 1.55; +} + +.hooks-action-row { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 30px; +} + +.hooks-action-row a { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + min-height: 44px; + padding: 0 14px; + border: 1px solid #c7d2cb; + border-radius: 8px; + background: #fffef9; + color: #123a31; + font-weight: 720; + text-decoration: none; +} + +.hooks-action-row a:first-child { + border-color: #184c3f; + background: #184c3f; + color: #f6fff9; +} + +.hooks-signal-board, +.hooks-panel, +.hooks-metric-grid article { + border: 1px solid rgba(35, 49, 43, 0.14); + border-radius: 8px; + background: rgba(255, 254, 249, 0.88); + box-shadow: 0 18px 42px rgba(24, 38, 32, 0.08); +} + +.hooks-signal-board { + display: grid; + gap: 28px; + align-content: center; + min-width: 0; + padding: 28px; +} + +.hooks-board-header, +.hooks-panel-heading, +.hooks-observation-main, +.hooks-observation-list article dl, +.hooks-contract-strip { + min-width: 0; +} + +.hooks-board-header, +.hooks-panel-heading, +.hooks-observation-main { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.hooks-board-header > span, +.hooks-panel-heading > span { + color: #68746d; + font-size: 0.82rem; + font-weight: 650; + overflow-wrap: anywhere; +} + +.hooks-flow-line { + display: grid; + grid-template-columns: minmax(0, 1fr) 30px minmax(0, 1fr) 30px minmax(0, 1fr); + gap: 10px; + align-items: center; +} + +.hooks-flow-line > svg { + justify-self: center; + color: #51776d; +} + +.hooks-flow-line article { + display: grid; + align-content: center; + min-width: 0; + min-height: 120px; + padding: 18px; + border-left: 4px solid #d9a441; + background: #f4f0e4; +} + +.hooks-flow-line article:nth-of-type(2) { + border-left-color: #2d6f95; + background: #eef5f5; +} + +.hooks-flow-line article:nth-of-type(3) { + border-left-color: #1d7a5f; + background: #edf6f1; +} + +.hooks-flow-line strong, +.hooks-flow-line small, +.hooks-metric-grid strong, +.hooks-metric-grid small, +.hooks-metric-grid span, +.hooks-contract-strip strong, +.hooks-contract-strip small, +.hooks-guarantee-list span, +.hooks-boundary-list span, +.hooks-observation-main strong, +.hooks-observation-main small { + overflow-wrap: anywhere; +} + +.hooks-flow-line strong { + color: #12201b; + font-size: 1.05rem; +} + +.hooks-flow-line small { + margin-top: 6px; + color: #5f6d66; +} + +.hooks-board-facts { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 1px; + overflow: hidden; + margin: 0; + border: 1px solid #d9dfd8; + border-radius: 8px; + background: #d9dfd8; +} + +.hooks-board-facts div { + min-width: 0; + padding: 12px; + background: #fffef9; +} + +.hooks-board-facts dt, +.hooks-observation-list dt { + color: #68746d; + font-size: 0.72rem; + font-weight: 760; + text-transform: uppercase; +} + +.hooks-board-facts dd, +.hooks-observation-list dd { + margin: 4px 0 0; + color: #14221d; + font-weight: 720; + overflow-wrap: anywhere; +} + +.hooks-metric-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.hooks-metric-grid article { + display: grid; + gap: 7px; + min-height: 136px; + padding: 18px; +} + +.hooks-metric-grid span { + color: #68746d; + font-size: 0.78rem; + font-weight: 760; + text-transform: uppercase; +} + +.hooks-metric-grid strong { + color: #13211c; + font-size: 1.58rem; + line-height: 1.12; +} + +.hooks-metric-grid article:nth-child(3) strong { + font-size: 1.28rem; +} + +.hooks-metric-grid small { + align-self: end; + color: #59645e; + line-height: 1.35; +} + +.hooks-public-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 18px; +} + +.hooks-panel { + min-width: 0; + padding: 22px; +} + +.hooks-panel-wide { + grid-column: 1 / -1; +} + +.hooks-panel-heading { + margin-bottom: 16px; +} + +.hooks-panel-heading > div { + display: flex; + align-items: center; + gap: 9px; + min-width: 0; +} + +.hooks-panel-heading h2 { + margin: 0; + color: #13211c; + font-size: 1.12rem; + line-height: 1.2; +} + +.hooks-contract-strip { + display: grid; + grid-template-columns: 0.7fr 1.35fr 1fr; + border-top: 1px solid #dce2db; +} + +.hooks-contract-strip > div { + display: grid; + gap: 6px; + min-width: 0; + padding: 16px 14px 0 0; +} + +.hooks-contract-strip span { + color: #68746d; + font-size: 0.72rem; + font-weight: 760; + text-transform: uppercase; +} + +.hooks-contract-strip strong { + color: #13211c; + font-size: 1.02rem; +} + +.hooks-contract-strip small { + color: #5f6b65; +} + +.hooks-guarantee-list, +.hooks-boundary-list { + display: grid; + gap: 10px; +} + +.hooks-guarantee-list span, +.hooks-boundary-list span { + position: relative; + padding-left: 20px; + color: #30423a; + line-height: 1.35; +} + +.hooks-guarantee-list span::before, +.hooks-boundary-list span::before { + position: absolute; + top: 0.52em; + left: 0; + width: 8px; + height: 8px; + content: ""; + border-radius: 50%; + background: #1d7a5f; +} + +.hooks-boundary-list span::before { + background: #c37d21; +} + +.hooks-observation-list { + display: grid; + gap: 12px; +} + +.hooks-observation-list article { + display: grid; + gap: 14px; + min-width: 0; + padding: 14px 0; + border-top: 1px solid #dce2db; +} + +.hooks-observation-list article:first-child { + border-top: 0; + padding-top: 0; +} + +.hooks-observation-list article dl { + display: grid; + grid-template-columns: 0.45fr 1.2fr 1fr 1fr 0.35fr; + gap: 10px; + margin: 0; +} + +.hooks-observation-list article dl div { + min-width: 0; +} + +.hooks-observation-main { + justify-content: flex-start; +} + +.hooks-observation-main > div { + min-width: 0; +} + +.hooks-observation-main strong, +.hooks-observation-main small { + display: block; +} + +.hooks-observation-main small { + margin-top: 2px; + color: #68746d; + font-size: 0.82rem; +} + +.hooks-observation-list a { + display: inline-flex; + align-items: center; + gap: 5px; + color: #175e7d; + text-decoration: none; +} + +.hooks-live-path { + display: grid; + grid-template-columns: minmax(320px, 0.76fr) minmax(0, 1.24fr); + gap: 22px; + align-items: start; + padding: 28px; + border: 1px solid #d4ded8; + border-radius: 8px; + background: #eaf3ef; +} + +.hooks-live-path h2 { + margin: 8px 0 10px; + font-size: 2rem; + line-height: 1.08; + letter-spacing: 0; +} + +.hooks-live-path p { + margin: 0; + color: #40504a; + line-height: 1.5; +} + +.hooks-live-path ol { + display: grid; + gap: 1px; + margin: 0; + padding: 0; + counter-reset: hooks-path; + list-style: none; + overflow: hidden; + border: 1px solid #d4ded8; + border-radius: 8px; + background: #d4ded8; +} + +.hooks-live-path li { + display: grid; + grid-template-columns: 38px minmax(0, 0.42fr) minmax(0, 0.58fr); + gap: 12px; + align-items: center; + min-width: 0; + min-height: 72px; + padding: 14px; + background: #fffef9; + counter-increment: hooks-path; +} + +.hooks-live-path li::before { + display: grid; + place-items: center; + width: 32px; + height: 32px; + color: #fffef9; + border-radius: 50%; + background: #184c3f; + font-size: 0.86rem; + font-weight: 760; + content: counter(hooks-path); +} + +.hooks-live-path li strong, +.hooks-live-path li span { + min-width: 0; + overflow-wrap: anywhere; +} + +.hooks-live-path li span { + color: #53615b; + line-height: 1.35; +} + +.hooks-json-preview { + max-height: 420px; + margin: 0; + overflow: auto; + padding: 16px; + border: 1px solid #273831; + border-radius: 8px; + background: #13211c; + color: #e9fff4; + font-family: "JetBrains Mono", "SFMono-Regular", Consolas, monospace; + font-size: 0.78rem; + line-height: 1.55; + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +@media (max-width: 1120px) { + .hooks-hero, + .hooks-live-path { + grid-template-columns: 1fr; + min-height: auto; + } + + .hooks-hero-copy { + padding-bottom: 0; + } + + .hooks-metric-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .hooks-public-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 740px) { + .hooks-public-nav { + display: grid; + grid-template-columns: 1fr; + padding: 14px 16px; + } + + .hooks-public-nav nav { + justify-content: flex-start; + } + + .hooks-public-main { + padding: 22px 16px 38px; + } + + .hooks-hero-copy h1 { + font-size: 2.75rem; + } + + .hooks-hero-copy p { + font-size: 1.02rem; + } + + .hooks-signal-board, + .hooks-panel, + .hooks-live-path { + padding: 16px; + } + + .hooks-panel-heading { + display: grid; + grid-template-columns: 1fr; + gap: 6px; + } + + .hooks-panel-heading > span { + justify-self: start; + text-align: left; + } + + .hooks-flow-line { + grid-template-columns: 1fr; + } + + .hooks-flow-line > svg { + transform: rotate(90deg); + } + + .hooks-flow-line article { + min-height: 84px; + } + + .hooks-board-facts, + .hooks-metric-grid, + .hooks-contract-strip, + .hooks-observation-list article dl, + .hooks-live-path li { + grid-template-columns: 1fr; + } + + .hooks-live-path li::before { + width: 30px; + height: 30px; + } +} diff --git a/apps/dashboard/src/test/dashboardData.test.ts b/apps/dashboard/src/test/dashboardData.test.ts index f0be20d4..423f3d29 100644 --- a/apps/dashboard/src/test/dashboardData.test.ts +++ b/apps/dashboard/src/test/dashboardData.test.ts @@ -28,6 +28,7 @@ import { WalletView } from "../views/WalletView"; import { ExternalTesterLaunchView } from "../views/ExternalTesterLaunchView"; import { ExplorerView } from "../views/ExplorerView"; import { OpsView } from "../views/OpsView"; +import { UniswapHooksView } from "../views/UniswapHooksView"; import { WorkbenchView } from "../views/WorkbenchView"; describe("dashboard fixture", () => { @@ -588,6 +589,19 @@ describe("dashboard fixture", () => { expect(html).not.toContain(configuredButHidden); }); + it("renders the public Uniswap V4 hooks surface from canary evidence without secrets", () => { + const html = renderToStaticMarkup(createElement(UniswapHooksView, { data: canaryData })); + + expect(html).toContain("Uniswap V4 afterSwap hooks for FlowMemory"); + expect(html).toContain("afterSwap signals"); + expect(html).toContain("FlowMemoryHookAdapter"); + expect(html).toContain("flowmemory://uniswap-v4/after-swap"); + expect(html).toContain("https://basescan.org/tx/"); + expect(html).toContain("Not a production Uniswap v4 hook deployment."); + expect(html).toContain("Base Sepolia hook broadcast"); + expect(html).not.toContain("BASESCAN_API_KEY"); + }); + it("renders the critical workbench view labels from fixture fallback", () => { const workbench = buildWorkbenchSnapshot(data, { devnetState, diff --git a/apps/dashboard/src/views/UniswapHooksView.tsx b/apps/dashboard/src/views/UniswapHooksView.tsx new file mode 100644 index 00000000..0894030f --- /dev/null +++ b/apps/dashboard/src/views/UniswapHooksView.tsx @@ -0,0 +1,303 @@ +import { Activity, ArrowRight, Boxes, ExternalLink, GitBranch, RadioReceiver, ShieldAlert, ShieldCheck } from "lucide-react"; +import { EmptyState } from "../components/EmptyState"; +import { HashValue } from "../components/HashValue"; +import { StatusBadge } from "../components/StatusBadge"; +import { formatDateTime } from "../data/format"; +import type { DashboardData, MemorySignal } from "../data/types"; + +const BASESCAN_URL = "https://basescan.org"; +const HOOK_URI = "flowmemory://uniswap-v4/after-swap"; + +function isHookSignal(signal: MemorySignal): boolean { + return signal.signalType === "swap_memory_signal" || signal.uri === HOOK_URI; +} + +function basescanAddressUrl(address: string): string { + return `${BASESCAN_URL}/address/${address}`; +} + +function basescanTxUrl(txHash: string): string { + return `${BASESCAN_URL}/tx/${txHash}`; +} + +export function UniswapHooksView({ data }: { data: DashboardData }) { + const canary = data.metadata.canary; + const hookContract = canary?.contracts.find((contract) => contract.name.includes("Hook")); + const hookSignals = data.memorySignals.filter(isHookSignal); + const hookObservations = data.flowPulseObservations.filter((observation) => observation.uri === HOOK_URI); + const uniqueTransactions = new Set(hookObservations.map((observation) => observation.txHash)).size; + const latestHookObservation = [...hookObservations].sort((left, right) => Number(right.blockNumber) - Number(left.blockNumber))[0]; + + return ( +
+
+ + + + FlowMemory + Uniswap V4 hooks + + + +
+ +
+
+
+ first public surface +

Uniswap V4 afterSwap hooks for FlowMemory

+

+ The hook path turns swap activity into FlowPulse memory signals that can be indexed, verified, + and followed through Rootflow without custody, dynamic fees, or receipt metadata assumptions inside the hook. +

+
+ + Inspect afterSwap signals + + {hookContract ? ( + + Base contract + + ) : null} +
+
+ +
+
+ 0 ? "observed" : "pending"} compact /> + {data.chain.name} +
+
+
+ Swap + Pool activity +
+
+
+
+
permission target
+
afterSwap only
+
+
+
hook delta
+
zero return delta
+
+
+
custody
+
none
+
+
+
receipt metadata
+
indexed after execution
+
+
+
+
+ +
+
+ afterSwap signals + {hookSignals.length} + {uniqueTransactions} Base transaction{uniqueTransactions === 1 ? "" : "s"} +
+
+ read window + {canary?.readWindow.fromBlock ?? "unknown"}-{canary?.readWindow.toBlock ?? "unknown"} + finalized {canary?.readWindow.finalizedBlock ?? data.chain.finalizedBlock} +
+
+ current contract + {hookContract?.name ?? "pending"} + {hookContract ? "Base canary adapter" : "contract metadata missing"} +
+
+ production gate + {canary?.productionReady ? "ready" : "gated"} + PoolManager hook deployment is separate +
+
+ +
+
+
+
+
+ {formatDateTime(data.metadata.generatedAt)} +
+
+
+ chain + {data.chain.chainId} + {data.chain.environment} +
+
+ contract + {hookContract?.name ?? "unknown"} + {hookContract ? : not found} +
+
+ deployed block + {hookContract?.block ?? "unknown"} + {hookContract ? : no deploy tx} +
+
+
+ +
+
+
+
+ contract path +
+
+ PoolManager-gated afterSwap candidate + No token custody or fee override + FlowPulse event is the public memory boundary + Transaction hash and log index come from the indexer +
+
+ +
+
+
+
+ honest status +
+
+ {(canary?.boundaries ?? ["Production hook deployment remains gated."]).map((boundary) => ( + {boundary} + ))} +
+
+
+ +
+
+
+
+ {hookObservations.length} observations +
+ {hookObservations.length > 0 ? ( +
+ {hookObservations.map((observation) => ( +
+
+ +
+ {observation.summary} + {observation.uri} +
+
+
+
+
block
+
{observation.blockNumber}
+
+
+
tx
+
+ + + +
+
+
+
pulse
+
+ +
+
+
+
commitment
+
+ +
+
+
+
log
+
{observation.logIndex}
+
+
+
+ ))} +
+ ) : ( + + )} +
+ +
+
+ path to live +

What goes public first

+

+ The public hook surface starts with canary evidence and Base Sepolia planning, then graduates to the real + afterSwap-only hook address once source verification and owner launch gates are complete. +

+
+
    +
  1. + Canary evidence + {hookSignals.length} swap memory signals are already represented in committed Base canary data. +
  2. +
  3. + Hook candidate + FlowMemoryAfterSwapHook and FlowMemoryHookPlanner define the PoolManager-gated path. +
  4. +
  5. + Public launch gate + Base Sepolia hook broadcast, source verification, and bounded reader evidence remain explicit gates. +
  6. +
  7. + First live route + After the gate clears, this route becomes the public status surface for hook signals. +
  8. +
+
+ +
+
+
+
+ {latestHookObservation ? "available" : "empty"} +
+ {latestHookObservation ? ( +
{JSON.stringify(latestHookObservation, null, 2)}
+ ) : ( + + )} +
+
+
+ ); +}