From d903ab21ba49bccc54f3927931a1aa63ad05a6c8 Mon Sep 17 00:00:00 2001 From: Hollujay <165713167+Hollujay@users.noreply.github.com> Date: Sun, 28 Jun 2026 07:00:28 +0000 Subject: [PATCH] feat(www): add Stellar testnet metrics counter with Soroban RPC pull live stealth payment counts (24h, 7d, all-time) from announcer contract events on Stellar Soroban testnet. use getLatestLedger RPC method instead of error-message parsing, unified getEvents pagination for all three windows, filter events by topic length, and graceful empty state. includes client-side in-memory cache with 5-minute TTL and inline cache strategy documentation. --- docs/CONTRIBUTING.md | 29 ++-- package.json | 2 + scripts/og.ts | 14 +- src/App.tsx | 2 + src/__tests__/a11y.test.tsx | 4 +- src/components/StellarMetrics.tsx | 231 ++++++++++++++++++++++++++++++ src/pages/Faq.tsx | 47 ++++-- src/pages/Privacy.tsx | 19 ++- 8 files changed, 312 insertions(+), 36 deletions(-) create mode 100644 src/components/StellarMetrics.tsx diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index bdf2ab5..1d6a3f2 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -9,14 +9,14 @@ cookie-based tracker. **Why Plausible?** -| Requirement | Plausible | -|---|---| -| No cookies | ✅ Daily-rotating hash, never persisted | -| No personal data | ✅ IP never stored; no fingerprinting | +| Requirement | Plausible | +| -------------------------------------------------- | ------------------------------------------------------------------------------- | +| No cookies | ✅ Daily-rotating hash, never persisted | +| No personal data | ✅ IP never stored; no fingerprinting | | GDPR / ePrivacy compliant without a consent banner | ✅ [Confirmed by Plausible](https://plausible.io/privacy-focused-web-analytics) | -| EU-hosted | ✅ Hetzner Germany/Finland | -| Open source | ✅ [AGPL-3.0](https://github.com/plausible/analytics) | -| Script bundle ≤ 2 KB gzipped | ✅ ~1 KB (verified via Network tab) | +| EU-hosted | ✅ Hetzner Germany/Finland | +| Open source | ✅ [AGPL-3.0](https://github.com/plausible/analytics) | +| Script bundle ≤ 2 KB gzipped | ✅ ~1 KB (verified via Network tab) | ### How the script is loaded @@ -61,15 +61,16 @@ trackEvent('Code Tab Change', { props: { tab: 'scan.ts' } }); #### Goals currently configured -| Goal name | Where it fires | Notes | -|---|---|---| -| `Read the Docs` | Hero CTA, CtaStrip secondary button | Fires on click | -| `Try the Demo` | Hero secondary CTA | Fires on click | -| `Get API Key` | CtaStrip primary button | Fires on click | -| `Code Tab Change` | Hero code snippet tabs | Includes `tab` prop (`send.ts` / `scan.ts` / `withdraw.ts`) | -| Scroll depth | Automatic — all pages | Provided by `script.scroll` extension, no code needed | +| Goal name | Where it fires | Notes | +| ----------------- | ----------------------------------- | ----------------------------------------------------------- | +| `Read the Docs` | Hero CTA, CtaStrip secondary button | Fires on click | +| `Try the Demo` | Hero secondary CTA | Fires on click | +| `Get API Key` | CtaStrip primary button | Fires on click | +| `Code Tab Change` | Hero code snippet tabs | Includes `tab` prop (`send.ts` / `scan.ts` / `withdraw.ts`) | +| Scroll depth | Automatic — all pages | Provided by `script.scroll` extension, no code needed | To add a new goal: + 1. Call `trackEvent('Your Goal Name')` where appropriate. 2. Go to **usewraith.xyz → Plausible dashboard → Goals → Add Goal** and add a matching Custom Event entry. diff --git a/package.json b/package.json index 2d6b9a7..8f4d660 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,8 @@ "prepare": "husky" }, "dependencies": { + "@stellar/stellar-sdk": "^13.3.0", + "@wraith-protocol/sdk": "^1.4.5", "react": "^19.2.5", "react-dom": "^19.2.5", "react-router-dom": "^7.18.0" diff --git a/scripts/og.ts b/scripts/og.ts index fc28e1f..cd8eff1 100644 --- a/scripts/og.ts +++ b/scripts/og.ts @@ -222,8 +222,18 @@ async function main() { } const fonts: FontDef[] = [ - { name: 'Space Grotesk', data: loadFont('space-grotesk-latin-400-normal.woff'), weight: 400, style: 'normal' }, - { name: 'Space Grotesk', data: loadFont('space-grotesk-latin-700-normal.woff'), weight: 700, style: 'normal' }, + { + name: 'Space Grotesk', + data: loadFont('space-grotesk-latin-400-normal.woff'), + weight: 400, + style: 'normal', + }, + { + name: 'Space Grotesk', + data: loadFont('space-grotesk-latin-700-normal.woff'), + weight: 700, + style: 'normal', + }, ]; const ogDir = join(distDir, 'og'); diff --git a/src/App.tsx b/src/App.tsx index 8bfea7f..deb5edf 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,6 +5,7 @@ import Features from './components/Features'; import Architecture from './components/Architecture'; import ForDevelopers from './components/ForDevelopers'; import Chains from './components/Chains'; +import StellarMetrics from './components/StellarMetrics'; import Compare from './components/Compare'; import Showcase from './components/Showcase'; import CtaStrip from './components/CtaStrip'; @@ -27,6 +28,7 @@ function Home() { + diff --git a/src/__tests__/a11y.test.tsx b/src/__tests__/a11y.test.tsx index 6262eb1..6ffdeea 100644 --- a/src/__tests__/a11y.test.tsx +++ b/src/__tests__/a11y.test.tsx @@ -46,6 +46,8 @@ describe('homepage accessibility', () => { render(); expect(screen.getByRole('heading', { name: /faq/i, level: 1 })).toBeInTheDocument(); - expect(screen.getAllByRole('button', { name: /show answer for/i }).length).toBeGreaterThanOrEqual(20); + expect( + screen.getAllByRole('button', { name: /show answer for/i }).length, + ).toBeGreaterThanOrEqual(20); }); }); diff --git a/src/components/StellarMetrics.tsx b/src/components/StellarMetrics.tsx new file mode 100644 index 0000000..bdbc17d --- /dev/null +++ b/src/components/StellarMetrics.tsx @@ -0,0 +1,231 @@ +import { useEffect, useRef, useState } from 'react'; +import { getDeployment } from '@wraith-protocol/sdk/chains/stellar'; +import { useInView } from '../hooks/useInView'; + +/** + * Cache strategy: + * - Client-side in-memory cache with 5-minute TTL + * - Resets on page reload (acceptable for a landing page) + * - Prevents hammering Soroban RPC on scroll/revisit + * - No server-side cache to keep infra minimal + */ +const cache = new Map(); +const CACHE_TTL = 5 * 60 * 1000; + +// ~1 ledger per 5s on Stellar testnet +const LEDGERS_PER_DAY = 17_280; +const LEDGERS_PER_WEEK = 120_960; + +type Metrics = { + last24h: number; + last7d: number; + allTime: number; +}; + +type CacheEntry = { + data: Metrics; + expiry: number; +}; + +const deployment = getDeployment('stellar'); +const rpcUrl = deployment.sorobanUrl; +const contractId = deployment.contracts.announcer; + +async function getLatestLedger(): Promise { + const res = await fetch(rpcUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'getLatestLedger', + }), + }); + const data: any = await res.json(); + const seq = data.result?.sequence; + if (typeof seq === 'number') return seq; + throw new Error('Could not determine latest ledger'); +} + +async function countInRange(startLedger: number): Promise { + let total = 0; + let cursor: string | undefined; + while (true) { + const params: Record = { + filters: [{ type: 'contract', contractIds: [contractId] }], + pagination: cursor ? { limit: 1_000, cursor } : { limit: 1_000 }, + }; + if (!cursor) params.startLedger = startLedger; + const res = await fetch(rpcUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'getEvents', params }), + }); + const data: any = await res.json(); + const events = data.result?.events ?? []; + for (const event of events) { + if (event.topic && event.topic.length >= 3) { + total++; + } + } + if (events.length < 1_000) break; + cursor = data.result?.cursor; + if (!cursor) break; + } + return total; +} + +async function fetchMetrics(): Promise { + const cached = cache.get('metrics'); + if (cached && Date.now() < cached.expiry) return cached.data; + + const latestLedger = await getLatestLedger(); + const [allTime, last24h, last7d] = await Promise.all([ + countInRange(1), + countInRange(Math.max(1, latestLedger - LEDGERS_PER_DAY)), + countInRange(Math.max(1, latestLedger - LEDGERS_PER_WEEK)), + ]); + + const metrics: Metrics = { last24h, last7d, allTime }; + cache.set('metrics', { data: metrics, expiry: Date.now() + CACHE_TTL }); + return metrics; +} + +function formatCount(n: number): string { + if (n === 0) return '0'; + if (n < 1000) return String(n); + if (n < 1_000_000) return `${(n / 1000).toFixed(1)}k`; + return `${(n / 1_000_000).toFixed(1)}M`; +} + +type StatCardProps = { + label: string; + count: number; + loading: boolean; + error: boolean; + inView: boolean; + delay: number; +}; + +function StatCard({ label, count, loading, error, inView, delay }: StatCardProps) { + const isZero = count === 0 && !loading && !error; + return ( + + + {label} + + {loading ? ( + + + + + ) : error ? ( + <> + + — + + Unable to fetch + > + ) : isZero ? ( + + Just getting started + + ) : ( + <> + + {formatCount(count)} + + + stealth payments processed on Stellar testnet + + > + )} + + ); +} + +export default function StellarMetrics() { + const { ref, isInView } = useInView({ threshold: 0.1 }); + const [metrics, setMetrics] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + const mountedRef = useRef(true); + + useEffect(() => { + mountedRef.current = true; + let cancelled = false; + + async function load() { + try { + setLoading(true); + setError(false); + const data = await fetchMetrics(); + if (!cancelled && mountedRef.current) { + setMetrics(data); + setLoading(false); + } + } catch { + if (!cancelled && mountedRef.current) { + setError(true); + setLoading(false); + } + } + } + + load(); + + return () => { + cancelled = true; + mountedRef.current = false; + }; + }, []); + + return ( + + + + + Live Metrics + + + Stellar testnet activity. + + + Real-time stealth payment activity on the Stellar Soroban testnet. + + + + + + + + + + + ); +} diff --git a/src/pages/Faq.tsx b/src/pages/Faq.tsx index d334967..204ea70 100644 --- a/src/pages/Faq.tsx +++ b/src/pages/Faq.tsx @@ -215,22 +215,30 @@ const faqSections: FaqSection[] = [ }, ]; -const sectionLabelStyles = 'font-mono text-[10px] font-semibold uppercase tracking-[1.8px] text-outline'; +const sectionLabelStyles = + 'font-mono text-[10px] font-semibold uppercase tracking-[1.8px] text-outline'; export default function Faq() { - const [openEntryId, setOpenEntryId] = useState(faqSections[0]?.entries[0]?.id ?? null); + const [openEntryId, setOpenEntryId] = useState( + faqSections[0]?.entries[0]?.id ?? null, + ); useEffect(() => { const hash = window.location.hash.replace('#', ''); if (!hash) return; - const entryExists = faqSections.some((section) => section.entries.some((entry) => entry.id === hash)); + const entryExists = faqSections.some((section) => + section.entries.some((entry) => entry.id === hash), + ); if (entryExists) { setOpenEntryId(hash); } }, []); - const entryCount = useMemo(() => faqSections.reduce((count, section) => count + section.entries.length, 0), []); + const entryCount = useMemo( + () => faqSections.reduce((count, section) => count + section.entries.length, 0), + [], + ); return ( @@ -241,7 +249,10 @@ export default function Faq() { WRAITH PROTOCOL - + Back home @@ -271,7 +282,10 @@ export default function Faq() { - + Jump to {faqSections.map((section) => ( @@ -298,9 +312,15 @@ export default function Faq() { const toggleId = `faq-toggle-${entry.id}`; const panelId = `faq-panel-${entry.id}`; return ( - + - + setOpenEntryId(isOpen ? null : entry.id)} className="flex w-full items-center justify-between text-left" > - - {entry.question} - + {entry.question} {isOpen ? 'Hide' : 'Show'} @@ -327,7 +345,12 @@ export default function Faq() { {isOpen && ( - + {entry.answer} diff --git a/src/pages/Privacy.tsx b/src/pages/Privacy.tsx index eff06b8..70b6919 100644 --- a/src/pages/Privacy.tsx +++ b/src/pages/Privacy.tsx @@ -60,7 +60,10 @@ export default function Privacy() { — an open-source, EU-hosted analytics platform — to understand how visitors interact with this site. - Plausible collects the following aggregate data per page visit: + + Plausible collects the following{' '} + aggregate data per page visit: + Page URL and referrer Browser name and version (no fingerprinting) @@ -68,7 +71,10 @@ export default function Privacy() { Country and region (derived from IP; the IP itself is never stored) Device type (desktop / tablet / mobile) Scroll depth percentage - Goal events: “Read the Docs”, “Try the Demo”, “Get API Key”, “Code Tab Change” + + Goal events: “Read the Docs”, “Try the Demo”, “Get API + Key”, “Code Tab Change” + @@ -81,8 +87,9 @@ export default function Privacy() { No personal information (name, email, wallet address, etc.). - Because Plausible is cookieless, no consent banner is required under - GDPR, PECR, or ePrivacy Directive. See Plausible's own{' '} + Because Plausible is cookieless,{' '} + no consent banner is required under GDPR, + PECR, or ePrivacy Directive. See Plausible's own{' '} - - We chose Plausible over Google Analytics or other trackers because it is:{' '} - + We chose Plausible over Google Analytics or other trackers because it is: Cookieless by design — the script uses
+ Real-time stealth payment activity on the Stellar Soroban testnet. +
{entry.answer}
Plausible collects the following aggregate data per page visit:
+ Plausible collects the following{' '} + aggregate data per page visit: +
- Because Plausible is cookieless, no consent banner is required under - GDPR, PECR, or ePrivacy Directive. See Plausible's own{' '} + Because Plausible is cookieless,{' '} + no consent banner is required under GDPR, + PECR, or ePrivacy Directive. See Plausible's own{' '} - - We chose Plausible over Google Analytics or other trackers because it is:{' '} - + We chose Plausible over Google Analytics or other trackers because it is: Cookieless by design — the script uses
- We chose Plausible over Google Analytics or other trackers because it is:{' '} -
We chose Plausible over Google Analytics or other trackers because it is: