Money E2E harness + security hardening + production-grade UX pass#17
Conversation
Live E2E money-path smoke test (real Anvil + Postgres + wallets): - scripts/tournament-e2e.py drives the full staked-tournament loop and asserts fund conservation for both a SETTLED tournament (pool distributed on-chain by standings) and an ABANDONED one (server restart -> claimRefund recovers every buy-in). Fixes the stale tournament-demo.sh, which 403'd (no organizer auth on /start) and expected launch tokens the server now serde-skips. Backend security hardening (crates/server, crates/persistence): - Per-IP `create` throttle on the previously-unthrottled game/tournament creation routes (POST /games, /queue, /gauntlet/start, /tournaments + /join /start) — closes a resource-exhaustion DoS. - Global room ceiling in start_game (before any escrow opens) so a creation flood can't exhaust the single node. - Per-organizer cap on open buy-in tournaments so one authed wallet can't drain oracle gas by looping openTournament. - SIWE verify fails closed when on-chain settlement is configured but SIWE_DOMAIN is unset (no localhost-default acceptance in prod). - UNIQUE(game_id) idempotency backstop on settlement_outbox (migration 0009). Frontend security (apps/web): - Content-Security-Policy + X-Frame-Options/nosniff/Referrer-Policy/ Permissions-Policy (frame-ancestors blocks deposit/sign-in clickjacking). - Validate + encodeURIComponent wallet path segments in ProfileStats. Production-grade UX: - Board parity: player name-plates (name, engine, clock, captured material, active-turn highlight), last-move highlight, check indicator — shared across all three game views (Chessboard/PlayerBar/lib/board). - Rake/payout disclosure at every wager point; rated/casual tags in lobby. - Spectator page shows players/stake/time-control + nav (was a bare UUID); SeatGame spectator socket reconnects instead of freezing mid-wager. - Fixed cryptic 0.5+0 time-control formatting; responsive header; :focus-visible + reduced-motion; how-it-works + trust footer with a live escrow-contract link; profile-history opponents link; /park redirects home; Gauntlet/Tournament in nav. Verified: full Rust test suite + backend E2E green against the modified binary; tsc + production next build clean; CSP + engine confirmed in preview. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Correctness fixes surfaced by the review: - payoutForStake charged the fee on the whole pot (2*stake), but ChessEscrow.settleGame rakes ONE stake — winner nets 2*stake - rake. The UI under-quoted the payout (1.98 vs 1.99 USDC at 1%/1 USDC). Fixed the formula to mirror the contract and corrected the "1% fee on the pot" copy to "on the winnings" (footer, stake modal, SeatGame, how-it-works). - Spectator page (/game/[id]) reconnected forever after game-over — added the `finished` guard SeatGame already had, plus a give-up cap (~8 dead reconnects) on both views so a reaped room can't cause endless churn. - Spectator metadata showed "Loading game details…" forever for a finished or not-yet-live game; now retries a few times then degrades to a clear message. - PlayerBar suppressed the material edge badge when a side was up material with no captures (e.g. after a promotion) — decoupled the +N edge from the captured-pieces list. - Migration 0009: a pre-existing duplicate game_id would make CREATE UNIQUE INDEX abort and brick server boot. Added a safe defensive de-dup first (keep a settled row, else newest) so the index can't fail. Documentation: - Noted the global room ceiling and per-organizer tournament cap are best-effort (non-atomic check-then-insert, bounded by the create throttle). Verified: tsc + production next build clean; migration re-applies cleanly on boot; payout math matches the contract (1->1.99, 100->199.00, 25->49.75); spectator graceful-fallback confirmed in preview. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Code review (8-angle, high effort) — findings + resolutionRan 8 finder angles (line-by-line, removed-behavior, cross-file, reuse, simplification, efficiency, altitude, conventions) with per-candidate verification. Findings, most-severe first. All confirmed correctness bugs are fixed in 490a65c. Fixed
Accepted / documented (not code-changed)
Verified cleanNo f64 money ( Minor / optional polish (left as-is)
|
… loop Correctness (edge cases the round-1 give-up cap / meta-latch introduced): - The reconnect give-up counted a game-not-yet-started window (no frames sent, socket held open) toward the cap, so a few pre-start drops could permanently stop the spectator socket — and since onResult fires only from that socket's game_over, a gauntlet/tournament could stall. The give-up now only counts IMMEDIATE closes (an absent/reaped room); a socket that stays open a while (>3s) is treated as a transient drop and keeps retrying. - The spectator page latched a "not in live list" message from the one-shot meta poll, which then showed next to a board that had since gone live. The fallback message is now derived from the live WS status (only shown once the socket has given up), and the meta poll keeps trying until the game appears. Simplification (removes the duplication that caused the round-1 drift): - Extracted the two near-identical spectator reconnect loops (SeatGame + /game/[id], which had already drifted on onerror handling) into one lib/spectatorSocket.ts helper. Fixes the give-up heuristic in one place instead of two. - Removed PlayerBar's dead `rating` prop (no caller passed it). Verified: tsc + production next build clean; live spectate via the helper confirmed in preview (board/clocks/active-turn/last-move/material edge all update); a non-live game reconnects then degrades to "not live" without the wrong latch or infinite churn. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- spectatorSocket: openedAt was set once on the first onopen and never reset, so any later reconnect where onopen never fires (server unreachable, or the reconnect churn tripping the per-IP WS rate limit → 429 before upgrade) was misclassified as a "lived" connection. fastFails could never accumulate, so the tab reconnected forever every 500ms and never reached "disconnected" — a regression from round 2. Reset openedAt at the start of each connect attempt. - CSP: connect-src/frame-src omitted Coinbase Wallet's origins (walletlink.org relay + *.coinbase.com APIs + keys.coinbase.com Smart-Wallet frame), which RainbowKit ships as a default connector — pairing Coinbase Wallet would have been blocked. Added them. Verified: tsc + production next build clean; CSP header now carries the Coinbase origins; a live spectator whose server is killed reconnects with backoff and reaches "disconnected" (gives up) instead of churning forever. Known cosmetic (accepted, not fixed): material() infers captures from FEN counts, so a promotion shows a phantom captured pawn — a correct fix needs move history and isn't worth the added complexity for a display-only glitch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review→fix loop — converged after 4 roundsRan the multi-angle review repeatedly, fixing findings each round (with a standing check that the fixes didn't add complexity or new bugs), until a round surfaced nothing worth fixing.
Notably, rounds 2 and 3 caught real regressions introduced by the earlier rounds' own fixes (the give-up cap's edge cases, then the consolidated helper's On complexity: the only structural change the loop made was extracting the duplicated spectator-reconnect loop into one helper (a net simplification that also removed a real drift). The reviewer explicitly judged Accepted (not fixed): Every round: |
Comprehensive pass across three fronts: the outstanding live money-path E2E, a deep backend+frontend security review, and a production-grade UX upgrade. Main checkout untouched; all work verified.
1. Live E2E money-path smoke test (the outstanding item)
Real-stack harness — local Anvil + real Postgres + real wallets + the real server (SIWE, durable outbox, on-chain settlement). All money paths pass:
scripts/onchain-demo.sh) — draw returns stakes, funds conserved.[10.95, 9.75, 9.30], sum conserved at 30 USDC.abandonedin Postgres → time jumps past the settle window → every entrant recovers their buy-in via on-chainclaimRefund.Committed as
scripts/tournament-e2e.py, and fixed the staletournament-demo.sh(it 403'd — never sent the organizer's SIWE session to/start— and expected launch tokens the server now#[serde(skip)]s).2. Security hardening
Backend:
createthrottle to the creation routes, a global room ceiling instart_game(before any escrow opens), and a per-organizer cap on open buy-in tournaments (stops one wallet loopingopenTournamentto burn oracle ETH).SIWE_DOMAINis unset (nolocalhost:3000default in prod).UNIQUE(game_id)idempotency backstop onsettlement_outbox(migration0009, verified it applies cleanly to the live-schema DB).Frontend:
frame-ancestors 'none'blocks the deposit/sign-in clickjacking the audit flagged, plusobject-src/base-uri, scopedconnect-src,X-Frame-Options/nosniff/Referrer-Policy/Permissions-Policy). Verified it doesn't break the app or the WASM engine.encodeURIComponentwallet path segments inProfileStats.Audits confirmed the code is otherwise solid (no XSS sinks, seats bound to the SIWE wallet never a request body, admin owner-gated + fail-closed, no key leakage). Documented follow-ups: session token off
localStorage;/ws/gametoken in a first frame not the URL; build-time escrow pinning; 1v1 restart recovery.3. Production-grade UX (verified in-browser)
Board now at Chess.com/Lichess parity for core cues (verified live): player name-plates (name, engine, clock, captured material, active-turn highlight), last-move highlight, check indicator — shared across all three game views via
Chessboard/PlayerBar/lib/board.Also: rake/payout disclosure at every wager point; rated/casual tags; spectator page shows players/stake/TC + nav (was a bare UUID); SeatGame spectator socket reconnects instead of freezing mid-wager; fixed the cryptic
0.5+0time-control bug; responsive header +:focus-visible+ reduced-motion; how-it-works + trust footer with a live escrow-contract link; profile-history opponents link;/parkduplicate redirects home; Gauntlet/Tournament in nav.Deferred (need server support / larger refactor): game replay for finished games, clickable move-list scrubbing, settlement resolved/failed state, ratings-on-board.
Verification
tsc --noEmitclean, productionnext buildclean (all 10 routes), polyglot book test passes, CSP + engine confirmed in the live preview.🤖 Generated with Claude Code