Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

## 🧪 Quick Start

1. **Try the Demo:** [🌐 Live Demo](https://bio-verify-ai-dapp.vercel.app/)
1. **Try the Demo:** [🌐 Live Demo](https://bio-verify-agentic-dapp.vercel.app)
2. **Get Testnet Sepolia ETH:** [Sepolia Faucet](https://sepolia-faucet.pk910.de/)
3. **Swap for Base Sepolia ETH:** [Superbridge](https://superbridge.app/base-sepolia) (only if you want to use Base)
4. **Connect your wallet** to the DApp (Base Sepolia or Ethereum Sepolia)
Expand Down Expand Up @@ -71,7 +71,7 @@ graph TD

### Event-Driven Data Flow

The contract uses a getter-less design: all state mutations emit events. These are projected off-chain into a Postgres read model, which powers all frontend queries. No on-chain reads required. In parallel, the frontend subscribes to `NewPublicationStatus` events via standalone viem WebSocket clients (Alchemy WSS), independent of wallet connection state, debouncing cache invalidations so the publications table stays in sync without polling.
The contract uses a getter-less design: all state mutations emit events. These are projected off-chain into a Postgres read model, which powers all frontend queries. No on-chain reads required. In parallel, the frontend subscribes to `NewPublicationStatus` events via standalone viem WebSocket clients (Alchemy WSS), independent of wallet connection state, debouncing cache invalidations so the publications list, global stats strip, and related TanStack Query caches stay in sync without polling the table.

```mermaid
graph LR
Expand Down
2 changes: 1 addition & 1 deletion apps/contracts/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@apps/contracts",
"version": "0.3.0",
"version": "0.4.0",
"private": true,
"author": "Siegfried Bozza",
"license": "MIT"
Expand Down
14 changes: 7 additions & 7 deletions apps/fe/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ graph TD
Two provider trees serve different route groups:

- **`RootProviders`** (home): ThemeProvider -> CustomWagmiProvider (Reown AppKit + TanStack QueryClientProvider + NuqsAdapter) -> TooltipProvider
- **`SideProvider`** (publications): Same stack plus `AppSideBarProvider` wrapping a collapsible sidebar layout
- **`SideProvider`** (publications): Same stack plus `AppSideBarProvider` wrapping a collapsible sidebar layout, and **`PublicationsRealtimeProvider`** (under `app/(routes)/publications/layout.tsx`) which mounts `useWatchNewPublicationStatusEvent` so Alchemy WSS subscriptions stay active on every `/publications/*` route (list, new submission, detail, assignments)

`CustomWagmiProvider` initializes Reown AppKit with `baseSepolia` and `sepolia` networks, configures Alchemy HTTP transports for wallet-connected operations, and hydrates wallet state from cookies for SSR.

Expand All @@ -85,9 +85,9 @@ sequenceDiagram
TQ-->>Client: live data with smart polling
```

**Smart polling**: Query hooks like `usePublicationDetail` use a dynamic `refetchInterval` that polls every 5 seconds while a publication is in a pending state, and automatically stops polling once it reaches a terminal status (`PUBLISHED`, `SLASHED`, or `EARLY_SLASHED`). The `PublicationDetailsProvider` context wraps this pattern, exposing live publication data and a syncing indicator to all child components.
**Smart polling**: Query hooks like `usePublicationDetail` use a dynamic `refetchInterval` that polls every **10 seconds** while a publication is in a pending state, and automatically stops polling once it reaches a terminal status (`PUBLISHED`, `SLASHED`, or `EARLY_SLASHED`). This complements WebSocket-driven status updates: reviewer assignment (`Agent_PickReviewers`) and per-review progress (`Agent_RecordReview`) do not emit `NewPublicationStatus`, so polling still refreshes detail data between status transitions. The `PublicationDetailsProvider` context wraps this pattern, exposing live publication data and a syncing indicator to all child components.

**WebSocket cache invalidation**: The `/publications` table uses `useWatchNewPublicationStatusEvent` to subscribe to `NewPublicationStatus` on-chain events on both chains via standalone viem WebSocket clients (`eth_subscribe` over Alchemy WSS), independent of wagmi's wallet connection state. This means all visitors see real-time updates — even without a connected wallet. When events arrive, a debounced invalidation (3-second delay for CQRS eventual consistency) triggers a TanStack Query refetch, keeping the table in sync without polling or manual refresh.
**WebSocket cache invalidation**: `PublicationsRealtimeProvider` calls `useWatchNewPublicationStatusEvent` to subscribe to `NewPublicationStatus` on both chains via standalone viem WebSocket clients (`eth_subscribe` over Alchemy WSS), independent of wagmi's wallet connection state. When events arrive, a debounced invalidation (3-second delay for CQRS eventual consistency) triggers TanStack Query refetches for **`publicationsKeys.all`** (list + detail queries under that prefix) and **`statsKeys.all`** (global stats strip on `/publications`), so the table and metric cards stay in sync without manual refresh. The publications list query also uses `staleTime: 0` so navigating back to `/publications` always background-refetches.

### CQRS Bridge

Expand Down Expand Up @@ -130,9 +130,9 @@ The server action verifies the EIP-712 signature, then resumes the LangGraph rev
| Route | Description |
|-------|-------------|
| `/` | Landing page with protocol mechanism walkthrough and CTAs |
| `/publications` | Server-side paginated and filtered publications table. nuqs syncs filters (chain, status, page) to URL search params; server component passes them to the CQRS query. TanStack Table in manual mode. Real-time updates via WebSocket subscription to `NewPublicationStatus` on-chain events. |
| `/publications` | Server-side paginated and filtered publications table plus global stats cards. nuqs syncs filters (chain, status, page) to URL search params; server component passes them to the CQRS query. TanStack Table in manual mode. Real-time updates: layout-mounted WebSocket on `NewPublicationStatus` invalidates publications + global stats (debounced). |
| `/publications/new` | Submit publication form (react-hook-form + zod, IPFS manifest pinning via Pinata, on-chain `submitPublication`) |
| `/publications/[chainId]/[pubId]` | Publication detail with live smart-polling, verdict timeline, economics sidebar, participants list |
| `/publications/[chainId]/[pubId]` | Publication detail with live smart-polling (10s while non-terminal), verdict timeline, economics sidebar, participants list |
| `/publications/[chainId]/[pubId]/review` | Reviewer form with EIP-712 signing and agent handoff |
| `/publications/assignments` | Reviewer dashboard: stake management, assigned publications table, review status tracking |

Expand All @@ -145,8 +145,8 @@ The server action verifies the EIP-712 signature, then resumes the LangGraph rev

## Key Patterns

- **Smart polling** -- `refetchInterval` dynamically stops when a publication reaches a terminal status, eliminating unnecessary network requests
- **WebSocket real-time invalidation** -- `useWatchNewPublicationStatusEvent` subscribes to `NewPublicationStatus` on-chain events via standalone viem WebSocket clients (Alchemy WSS) on both chains, independent of wallet state; rapid-fire events are debounced into a single TanStack Query cache invalidation (3-second delay for CQRS eventual consistency)
- **Smart polling** -- `refetchInterval` (10s while non-terminal) dynamically stops when a publication reaches a terminal status, covering DB fields not tied to `NewPublicationStatus`
- **WebSocket real-time invalidation** -- `useWatchNewPublicationStatusEvent` (via `PublicationsRealtimeProvider` in the publications layout) subscribes to `NewPublicationStatus` on-chain events via standalone viem WebSocket clients (Alchemy WSS) on both chains, independent of wallet state; rapid-fire events are debounced into TanStack Query invalidations for publications and global stats (3-second delay for CQRS eventual consistency)
- **Optimistic updates + delayed invalidation** -- immediate UI feedback on transactions, with a 3-second delayed cache invalidation to account for Alchemy webhook -> CQRS projection latency
- **Server-first data loading** -- RSC fetches from Neon via `@packages/cqrs`, hydrates client hooks via `initialData` for zero-loading-state initial renders
- **`"use server"` CQRS bridge** -- all Drizzle DB queries stay server-only, exposed to client hooks through Next.js Server Actions
Expand Down
5 changes: 3 additions & 2 deletions apps/fe/_hooks/cqrs/queries/use-publication-detail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,16 @@ export const usePublicationDetail = (params: Params) => {
initialData,
enabled: !!id,

// Smart Polling: Only poll every 5s if the publication is NOT finalized
// Smart Polling: Only poll every 10s if the publication is NOT finalized
// (WebSocket handles NewPublicationStatus; polling covers Agent_PickReviewers)
refetchInterval: (query) => {
const status = query.state.data?.status
const isFinalized =
status === PublicationStatusSchema.enum.PUBLISHED ||
status === PublicationStatusSchema.enum.SLASHED ||
status === PublicationStatusSchema.enum.EARLY_SLASHED

return isFinalized ? false : 5_000 // Poll every 5s if NOT finalized
return isFinalized ? false : 10_000
},
})

Expand Down
1 change: 1 addition & 0 deletions apps/fe/_hooks/cqrs/queries/use-publications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const usePublications = (props: Props) => {
queryKey: publicationsKeys.byQueryParams(searchQueryParams),
queryFn: () => getPublications(searchQueryParams),
initialData,
staleTime: 0,
})

return { data, isFetching, isError, refetch }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* @title Real-Time Publication Status Watcher
* @notice Subscribes to on-chain `NewPublicationStatus` events via WebSocket
* (eth_subscribe) on all supported chains, then invalidates the TanStack Query
* publications cache so the UI refreshes automatically.
* publications and stats caches so the list and metric cards refresh together.
*
* @dev Uses standalone viem WebSocket clients (not wagmi) so that subscriptions
* remain active regardless of the user's wallet connection state. This is
Expand All @@ -30,6 +30,7 @@ import { useQueryClient } from "@tanstack/react-query"
import { useCallback, useEffect, useRef } from "react"
import type { Log, WatchContractEventReturnType } from "viem"
import { publicationsKeys } from "../cqrs/query-keys/publications-keys"
import { statsKeys } from "../cqrs/query-keys/stats-keys"

type NewPublicationStatusArgs = {
pubId?: bigint
Expand Down Expand Up @@ -77,9 +78,14 @@ export const useWatchNewPublicationStatusEvent = () => {
queryClient.invalidateQueries({
queryKey: publicationsKeys.all,
})
queryClient.invalidateQueries({
queryKey: statsKeys.all,
})

if (process.env.NODE_ENV === "development") {
console.log(`[WS] Cache invalidated for pubIds: ${pubIds.join(", ")}`)
console.log(
`[WS] Cache invalidated (publications + stats) for pubIds: ${pubIds.join(", ")}`,
)
}
}, INVALIDATION_DELAY_MS)
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"use client"

import { useWatchNewPublicationStatusEvent } from "@/_hooks/websockets/use-watch-new-publication-status-event"
import type { FC, PropsWithChildren } from "react"

export const PublicationsRealtimeProvider: FC<PropsWithChildren> = ({
children,
}) => {
useWatchNewPublicationStatusEvent()

return <>{children}</>
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"use client"

import { usePublications } from "@/_hooks/cqrs/queries/use-publications"
import { useWatchNewPublicationStatusEvent } from "@/_hooks/websockets/use-watch-new-publication-status-event"
import { FetchError } from "@/app/_components/fetch-error"
import type { PublicationsQueryParams } from "@packages/cqrs"
import type { PublicationsResponse } from "@packages/schema"
Expand All @@ -16,10 +15,7 @@ type Props = {
export const PublicationsTableContainer = (props: Props) => {
const { initialData, searchQueryParams } = props

// Alchemy Websockets (invalidate tanstack query publications keys on NewPublicationStatus on-chain event)
useWatchNewPublicationStatusEvent()

// Fetch publications data
// Fetch publications data (WebSocket invalidation lives in PublicationsRealtimeProvider in layout)
const {
data: publicationsResponse,
isFetching,
Expand Down
15 changes: 9 additions & 6 deletions apps/fe/app/(routes)/publications/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { SideProvider } from "@/_context/side-provider"
import { headers } from "next/headers"
import { Toaster } from "sonner"
import { SideProvider } from "@/_context/side-provider"
import { PublicationsRealtimeProvider } from "./_components/publications-realtime-provider"

export default async function Layout({
children,
Expand All @@ -14,11 +15,13 @@ export default async function Layout({

return (
<SideProvider cookies={cookies}>
{breadcrumbs}
<main className="max-w-7xl flex flex-col gap-6 pt-6 pb-2">
{children}
<Toaster />
</main>
<PublicationsRealtimeProvider>
{breadcrumbs}
<main className="max-w-7xl flex flex-col gap-6 pt-6 pb-2">
{children}
<Toaster />
</main>
</PublicationsRealtimeProvider>
</SideProvider>
)
}
2 changes: 1 addition & 1 deletion apps/fe/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@apps/fe",
"version": "0.3.0",
"version": "0.4.0",
"private": true,
"author": "Siegfried Bozza",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bio_verify",
"version": "0.3.0",
"version": "0.4.0",
"description": "Durable AI Agent Protocol for Scientific Integrity",
"private": true,
"author": "Siegfried Bozza",
Expand Down
2 changes: 1 addition & 1 deletion packages/agents/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@packages/agents",
"version": "0.3.0",
"version": "0.4.0",
"private": true,
"author": "Siegfried Bozza",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion packages/cqrs/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@packages/cqrs",
"version": "0.3.0",
"version": "0.4.0",
"private": true,
"author": "Siegfried Bozza",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion packages/db/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@packages/db",
"version": "0.3.0",
"version": "0.4.0",
"private": true,
"author": "Siegfried Bozza",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion packages/notifications/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@packages/notifications",
"version": "0.3.0",
"version": "0.4.0",
"private": true,
"author": "Siegfried Bozza",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion packages/schema/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@packages/schema",
"version": "0.3.0",
"version": "0.4.0",
"private": true,
"author": "Siegfried Bozza",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion packages/utils-server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@packages/utils-server",
"version": "0.3.0",
"version": "0.4.0",
"private": true,
"author": "Siegfried Bozza",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion packages/utils/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@packages/utils",
"version": "0.3.0",
"version": "0.4.0",
"private": true,
"author": "Siegfried Bozza",
"license": "MIT",
Expand Down
Loading