Web UX pass: unified sign-in, header wallet menu, /profile hub#13
Merged
Conversation
Consolidate the entry experience per a full UX review of the home page. Auth: replace the separate Connect Wallet + Sign in buttons with one AuthButton (RainbowKit ConnectButton.Custom). Connecting auto-switches to the server's expected chain and, on a wagering server, auto-prompts the SIWE signature — a single flow instead of a two-step process. Signed-in state shows a compact account chip. The session token is now bound to the wallet it was issued for and cleared on disconnect / account switch, fixing a latent stale-token bug. Bankroll: move deposit/withdraw into a top-right WalletMenu (balance pill + popover reusing BankrollPanel); remove the inline bankroll panels from Home, Tournament, and Gauntlet. Home: drop the browser-bot panel, bankroll panel, and player-lookup box. Profile: new /profile hub with stats + game history (shared ProfileStats), an Engine section combining browser-bot config and the connect-your-engine pairing flow (extracted to ConnectEngine), and the player-lookup box. Remove Connect Engine from the nav; /player/[address] stays public and /connect remains as a deep link. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
The leaderboard ran a correlated COUNT(*) per user over the whole games table — O(users × games) — with lower() on unindexed wallet columns. Rewrite it to count finished rated games per wallet in a single GROUP BY pass, then join the counts back to users: O(users + games). EXPLAIN confirms one hash join over a single grouped scan, no per-user subquery. COUNT(DISTINCT id) preserves the old OR semantics (a hypothetical self-play game counts once) and the inner join makes the "at least one game" filter implicit. Add functional lower(wallet) indexes (migration 0007) on games' white/black wallet columns and users.wallet, matching the lower() predicates used across the leaderboard join and the per-address profile queries (player_stats / player_games / player_rating / update_ratings). Add a DATABASE_URL-gated test covering case-insensitive count folding, exclusion of players with no finished games, pending games not counting, and Elo-driven ordering. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Winners of a root-settled (large) tournament and entrants of an
abandoned one had no in-app way to collect — they'd have to call the
escrow contract directly. Wire the buttons to ChessEscrow:
- TournamentClaim component reads on-chain state for the connected
wallet and shows the right action or nothing: a Merkle "Claim payout"
(fetches the proof from GET /tournaments/{id}/claim/{address}, then
claimTournament), a "Claim refund" past settleTimeout (claimRefund),
or an idle "claimed ✓" / "refund available in ~Nh" state. Renders
nothing for non-participants, disconnected wallets, or direct-settled
fields (already credited to bankroll).
- escrow.ts: extend ESCROW_ABI with claimTournament/claimRefund and the
tournaments/tournamentClaimed/tournamentEntered/settleTimeout getters;
add tidToBytes32 (matches the server's game_id_to_bytes32) and a
fetchClaimProof helper.
- Surface the widget in the lobby card and both finished play views;
claims credit the escrow bankroll, withdrawn via the Bankroll panel.
Mirrors the existing BankrollPanel write pattern (writeContractAsync →
waitForTransactionReceipt, chain guard, shortMessage errors).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes from the review of the prior commit. Reactive session (fixes the /profile pairing regression + related staleness): add a shared useAuthToken() hook backed by a same-tab "chess-auth-changed" event (dispatched by setAuth/clearAuth) plus the cross-tab storage event. ConnectEngine, Lobby, tournament, and gauntlet now consume it instead of snapshotting authToken() into per-component state, so a sign-in completed on the same page (e.g. the header AuthButton's auto-SIWE on /profile) or a session cleared on account switch propagates immediately — no dead pairing code or "sign in to stake" errors until reload. ConnectEngine also drops its minted code when the session changes so it can't pair an engine to a previous wallet. AuthButton hardening: - clear a legacy token (no bound chess_addr) or a token bound to a different wallet on account change, so pre-upgrade sessions and switches re-sign cleanly and don't disagree with other components. - guard the in-flight SIWE signature against an account switch: if the account changed while the signature was pending, clear instead of marking signed-in. - restore a "Wrong network — switch" affordance for a signed-in user who drifted off the expected chain (the custom chip had dropped what the default ConnectButton showed). Minor: gauntlet underfunded copy pointed "above" at the removed inline deposit panel → point at the header wallet menu. The header balance pill polls on a slow 30s interval (shares the query key, so an open BankrollPanel still drives 8s refreshes) instead of 8s on every route. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds an opt-in, #[ignore]d integration test (tournament_claim_and_refund_live)
and a runner (scripts/test-sepolia-tournament.sh) that verify the tournament
money paths against a real chain — closing the gap that the on-chain claim/
refund UI could only be reasoned about, not exercised end-to-end.
The test runs the exact production Merkle code (merkle_root / merkle_proof /
tournament_leaf) plus the server's EIP-712 root-settle, then:
- claimTournament: a Rust-built proof verifies against the deployed Solidity
_verifyProof and credits the winner's bankroll
- a double-claim reverts (AlreadyClaimed), asserted via eth_call
- claimRefund: an entrant reclaims their buy-in past the settle window
It deploys throwaway MockUSDC + escrows (short refund window), so it's
repeatable and needs only one funded key + a Base Sepolia RPC. Env-gated
(LEDGER_TEST_RPC / LEDGER_TEST_KEY / LEDGER_TEST_TIMEOUT) and #[ignore]d, so a
normal `cargo test` never runs it or spends testnet ETH.
Verified green end-to-end against a local anvil (same code path as Sepolia);
the existing Anvil test still covers the OnchainSettlement wrapper and the
happy-path claim.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Non-behavioral tidy-ups from the review's cleanup angles. Extract the copy-pasted mount gate (useState + useEffect + placeholder) into a useMounted() hook, used by AuthButton, WalletMenu, and the home / profile / gauntlet / tournament pages. Collapse AuthButton's two auto-effects (auto-switch chain, then auto-sign) into one: runSignIn already switches to the expected chain before signing, so a single guarded auto-sign effect covers the whole connect → switch → sign flow. Drops the redundant switchTried ref and the standalone switch prompt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fold PR 14's tournament claim/refund out of the tournament page and into the header wallet menu, where it sits alongside deposit/withdraw — claims credit the same escrow bankroll, so they belong together rather than scattered across the tournament page's cards and finished-game views. - New ClaimWinnings: discovers the connected wallet's settled/abandoned buy-in tournaments and renders a claim (Merkle payout) or refund per one that has something to collect. Rendered in the wallet popover under the bankroll panel; the discovery fetch is lazy (only while the popover is open). - TournamentClaim gains an optional `label` (tournament name) and an `onResolved` callback so the list hides its "Tournament winnings" header when nothing is actually claimable (e.g. small fields credited directly). - Remove the three inline TournamentClaim placements from the tournament page; its copy now points winners/entrants at the wallet menu to claim. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
De-duplicate cross-page logic surfaced while wiring the claim UI, so the pages compose shared primitives instead of each re-implementing them. - useOnchainConfig(): one source of the server config + `wagerOn` gate, replacing the fetchConfig-into-state + `!!wagerEnabled && !!escrow` pattern duplicated in Lobby, tournament, gauntlet, WalletMenu, and AuthButton. - useEnsureChain(): the single "switch to the expected chain before a write" step, replacing the inline ensureChain in BankrollPanel, AuthButton, and TournamentClaim. - lib/tournaments.ts: fetchTournaments()/fetchTournament() + the shared Tournament type, replacing the ids→details fan-out duplicated between the tournament lobby and the bankroll claim discovery (ClaimWinnings). No behavior change; tsc clean and all pages render. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes from the entire-PR review.
Finding 1/8 (high) — DB-backed claimable-tournaments endpoint. The claim UI
discovered tournaments via the in-memory GET /tournaments list, which isn't
rehydrated on restart, so settled/abandoned payouts/refunds became undiscoverable
in-app after any deploy. Add GET /tournaments/claimable/{address} sourced from
Postgres: migration 0008 adds a tournaments.name column, upsert_tournament now
persists it, and claimable_tournaments(address) returns the wallet's
finished (complete|settled|abandoned) buy-in tournaments via a JSONB players
containment query. ClaimWinnings now calls this (one request, server-filtered)
instead of an N+1 client enumeration — also fixes the O(all-tournaments) cost.
Finding 2 — stale publicClient. usePublicClient({ chainId: expected }) in
BankrollPanel + TournamentClaim, so a receipt read after ensureChain switches
doesn't hit an undefined/other-chain client and falsely report a succeeded tx
as failed.
Finding 3 — reset ClaimWinnings' resolved map on account change (not just
disconnect), so a prior wallet's claimable state can't keep the header shown.
Finding 4 — include 'complete' (settlement-enqueue-failed) tournaments in the
claimable set, so a stuck buy-in stays refundable via the UI. (Now server-side.)
Finding 5 — fetchTournaments tolerates a single failing detail fetch instead of
blanking the whole list.
Findings 6/7/9 — single-source TournamentClaim's state via one `kind`
discriminant (drops the duplicated hasAction conditions); remove the dead
onClaimed prop; already-claimed no longer counts toward showing the header.
Finding 10 — fmtUsdcSigned (BigInt, no float) in escrow.ts, used by ProfileStats.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
From the re-review of the review-fix commit.
- Rate-limit the claimable endpoint (CLAUDE.md: "a new HTTP route is
unthrottled unless you add it to a throttled router"). It's the one
tournament route that hits Postgres (an unindexed JSONB containment scan), so
move the handler from matchmaking (unthrottled) into players::routes(), which
already carries the per-IP rate_limit_reads layer.
- Drop the unused buy_in from the claimable response end-to-end (Rust row +
view, TS type) — on-chain buyIn is the source of truth; the WHERE still
filters buy_in IS NOT NULL.
- ProfileStats: stakes are non-negative, so use fmtUsdc(g.stake) directly
instead of fmtUsdcSigned(...).replace("+","").
- Fix a stale comment in the tournament page (Tournament type is no longer
"shared with the bankroll claim discovery" — that uses ClaimableTournament).
net/stake are Decimal::from(u128) (scale 0) → integer base-unit strings, so
fmtUsdcSigned's BigInt parse is correct (verified, no change needed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A full pass over the web app's entry UX, plus the two backend/feature changes from #14 (now closed and folded in here).
1. Auth — one button instead of two
Replaced the separate Connect Wallet + Sign in buttons with a single Sign in (
AuthButton.tsx,ConnectButton.Custom): connecting auto-switches to the server's expected chain and, on a wagering server, auto-prompts the SIWE signature — one flow, no second button. Signed-in state is a compact account chip; a "Wrong network — switch" control appears if a signed-in wallet drifts off-chain.The session token is now bound to the wallet it was issued for and cleared on disconnect / account switch, and it's exposed through a reactive
useAuthToken()hook (backed by achess-auth-changedevent + cross-tabstorage) so a sign-in on the same page propagates instantly instead of going stale until reload.2. Bankroll + claims — top-right wallet menu
Deposit/withdraw moved into a header
WalletMenubalance pill + popover; the inline bankroll panels were removed from Home, Tournament, and Gauntlet. Tournament payouts/refunds (ClaimWinnings+TournamentClaim) live in that same popover — they credit the same escrow balance, so they belong with the bankroll rather than scattered across the tournament page.3. Home — decluttered
Removed the browser-bot panel, bankroll panel, and player-lookup box from the home page.
4.
/profile— new hubprofile/page.tsx: stats + game history (sharedProfileStats), an Engine section combining browser-bot config + the connect-your-engine flow (ConnectEngine), and the player lookup./player/[address]stays public;/connectremains a deep link.5. Leaderboard query optimization (backend, from #14)
The lobby leaderboard ran a correlated
COUNT(*)per user over the wholegamestable — O(users × games) withlower()on unindexed columns. Rewritten as a single GROUP BY pass joined back tousers→ O(users + games);EXPLAINconfirms one hash join over a grouped scan. Added functionallower(wallet)indexes (migration 0007) and aDATABASE_URL-gated test.Shared abstractions (no per-page duplication)
useAuthToken,useMounted,useOnchainConfig,useEnsureChain,ProfileStats,ConnectEngine,BankrollPanel,TournamentClaim— extracted so the pages compose them rather than re-implementing config fetching, mount gates, chain-switching, or token snapshots.Verification
tsc --noEmitclean;cargo check -p persistence -p ledger -p server→ Finished (exit 0)./profile//player//tournament//gauntletrender with no new console errors (only pre-existing WalletConnect-preload noise from a missing devWC_PROJECT_ID).Needs a live smoke test (no wallet/chain in sandbox)
The connected-wallet paths — auto-SIWE, account-switch guard, wrong-network control, bankroll deposit/withdraw, and tournament claim/refund — need a real injected wallet + running game server + a settled/abandoned on-chain tournament. The merged
scripts/test-sepolia-tournament.shis built for the claim/refund path. E2E tests are planned for a later session.🤖 Generated with Claude Code