diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx index dc7c2bb..8120333 100644 --- a/frontend/src/app/dashboard/page.tsx +++ b/frontend/src/app/dashboard/page.tsx @@ -20,6 +20,7 @@ import { useTokenMetadata } from "@/hooks/use-token-metadata"; import type { StreamData } from "@/lib/stacks"; import { STREAM_STATUS, unresolvableTokenLabel } from "@/lib/constants"; import { TopUpDialog } from "@/components/stream/top-up-dialog"; +import { NetworkStats } from "@/components/layout/network-stats"; import { Skeleton } from "@/components/ui/skeleton"; import Link from "next/link"; import { PlusCircle, Zap, Users, Coins, TrendingUp } from "lucide-react"; @@ -47,17 +48,23 @@ export default function DashboardPage() { if (!isConnected) { return ( - } - title="Connect your wallet" - description="Connect a Stacks wallet to open streams and manage real-time payments, settled on Bitcoin." - /> +
+ + } + title="Connect your wallet" + description="Connect a Stacks wallet to open streams and manage real-time payments, settled on Bitcoin." + /> +
); } return (
- {/* Stats row */} + + + {/* Your streams */} +

Your streams

{isLoading ? ( Array.from({ length: 4 }).map((_, i) => ( diff --git a/frontend/src/app/organisations/page.tsx b/frontend/src/app/organisations/page.tsx index f92ee3f..9118eff 100644 --- a/frontend/src/app/organisations/page.tsx +++ b/frontend/src/app/organisations/page.tsx @@ -158,15 +158,27 @@ function Hero({ directory }: { directory: OrganisationDirectory | null }) { style={delay(2)} >
-

- Organisations registered -

-

- {total ?? "–"} -

+
+
+

+ Organisations +

+

+ {total ?? "–"} +

+
+
+

+ Streams +

+

+ {directory?.streamsCreated ?? "–"} +

+
+
-

Streams tracked

+

From organisations

{streams ?? "–"}

diff --git a/frontend/src/components/landing/hero.tsx b/frontend/src/components/landing/hero.tsx index 83d60f9..068af86 100644 --- a/frontend/src/components/landing/hero.tsx +++ b/frontend/src/components/landing/hero.tsx @@ -4,6 +4,7 @@ import Link from "next/link"; import { motion } from "framer-motion"; import { Button } from "@/components/ui/button"; import { LiveStreamCard } from "@/components/landing/live-stream-card"; +import { LiveStats } from "@/components/landing/live-stats"; import { ArrowRight, Zap } from "lucide-react"; export function Hero() { @@ -81,6 +82,16 @@ export function Hero() { + {/* Live usage, read from the contract */} + + + + {/* Animated streaming preview */} + + + + + + {data.streamsCreated} streams on + mainnet + + · + + {orgs}{" "} + {orgs === 1 ? "organisation" : "organisations"} + + · read live from the contract + + ); +} diff --git a/frontend/src/components/layout/network-stats.tsx b/frontend/src/components/layout/network-stats.tsx new file mode 100644 index 0000000..c02f395 --- /dev/null +++ b/frontend/src/components/layout/network-stats.tsx @@ -0,0 +1,54 @@ +"use client"; + +import Link from "next/link"; +import { ArrowRight } from "lucide-react"; +import { Skeleton } from "@/components/ui/skeleton"; +import { useNetworkStats } from "@/hooks/use-network-stats"; + +/** + * StackStream's live usage on mainnet, shown at the top of the dashboard + * whether or not a wallet is connected. Both figures come from the contract. + */ +export function NetworkStats() { + const { stats, isLoading } = useNetworkStats(); + + if (isLoading) return ; + if (!stats) return null; + + return ( +
+
+
+

+ + + + + StackStream on mainnet +

+ + See organisations + +
+
+
+

+ {stats.streamsCreated} +

+

Streams created

+
+
+

+ {stats.workspacesRegistered} +

+

+ {stats.workspacesRegistered === 1 ? "Organisation registered" : "Organisations registered"} +

+
+
+
+ ); +} diff --git a/frontend/src/hooks/use-network-stats.ts b/frontend/src/hooks/use-network-stats.ts new file mode 100644 index 0000000..8964930 --- /dev/null +++ b/frontend/src/hooks/use-network-stats.ts @@ -0,0 +1,33 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { OPENCLAW_API_URL } from "@/lib/constants"; + +export interface NetworkStats { + /** Every stream ever opened on mainnet, by anyone (stream-manager's stream counter). */ + streamsCreated: number; + /** Every organisation registered on stream-factory. */ + workspacesRegistered: number; + asOf: string; +} + +async function fetchNetworkStats(): Promise { + const res = await fetch(`${OPENCLAW_API_URL}/api/stats`); + if (!res.ok) throw new Error(`stats ${res.status}`); + return (await res.json()) as NetworkStats; +} + +/** + * Network-wide usage, read from the contract through /api/stats. Both numbers + * only go up: each new stream and each new registration adds one, whoever makes + * it. Refreshed every minute, which is also the API's cache window. + */ +export function useNetworkStats() { + const query = useQuery({ + queryKey: ["network-stats"], + queryFn: fetchNetworkStats, + refetchInterval: 60_000, + staleTime: 30_000, + }); + return { stats: query.data ?? null, isLoading: query.isLoading }; +} diff --git a/frontend/src/lib/organisations.ts b/frontend/src/lib/organisations.ts index 1a3af10..2984ad4 100644 --- a/frontend/src/lib/organisations.ts +++ b/frontend/src/lib/organisations.ts @@ -15,6 +15,7 @@ import { getDao, getDaoCount, getNetwork, + getStreamNonce, HIRO_API_BASE, STREAM_FACTORY_CONTRACT, } from "./openclaw-server"; @@ -36,6 +37,12 @@ export interface OrganisationDirectory { network: "mainnet" | "testnet"; /** `get-dao-count`, the authoritative total. */ total: number; + /** + * Every stream ever opened on mainnet (`get-stream-nonce`), by anyone, whether + * or not an organisation sent it. Distinct from the streams organisations + * have linked, which is the sum of `streamsTracked`. + */ + streamsCreated: number; /** Newest first. */ organisations: Organisation[]; /** @@ -148,7 +155,11 @@ let cached: { directory: OrganisationDirectory; expires: number } | null = null; export async function getOrganisationDirectory(): Promise { if (cached && Date.now() < cached.expires) return cached.directory; - const [rows, total] = await Promise.all([fetchFactoryTransactions(), getDaoCount()]); + const [rows, total, streamsCreated] = await Promise.all([ + fetchFactoryTransactions(), + getDaoCount(), + getStreamNonce(), + ]); const registrations = parseRegistrations(rows, STREAM_FACTORY_CONTRACT); // The contract is the source of truth for each record. A registration whose @@ -172,6 +183,7 @@ export async function getOrganisationDirectory(): Promise const directory: OrganisationDirectory = { network: getNetwork(), total, + streamsCreated, organisations, unlisted: Math.max(0, total - organisations.length), asOf: new Date().toISOString(),