Skip to content

perf: rebuild wallet route with server-first architecture#76

Open
marcopesani wants to merge 1 commit intomainfrom
rebuild-account-route-fast
Open

perf: rebuild wallet route with server-first architecture#76
marcopesani wants to merge 1 commit intomainfrom
rebuild-account-route-fast

Conversation

@marcopesani
Copy link
Owner

Summary

  • Server-component-first architecture — server determines account state and renders the correct view immediately; balance streams independently via granular Suspense boundaries
  • Fixed balance refresh — 15s React Query polling, chainId-scoped invalidation after fund/withdraw, useMutation with onSuccess replaces manual useEffect invalidation
  • Reduced initial bundleSessionKeyAuthCard (370 LOC + ZeroDev deps) dynamically imported with skeleton fallback
  • Eliminated waste — deleted wallet-content.tsx client orchestrator, removed double auth check, replaced server-action reads with direct data-layer calls

Before vs After

Before After
Single Suspense blocks entire page Granular Suspense — balance streams independently
Client component orchestrator Server determines layout, client islands for interactivity
Server actions for reads (unnecessary hop) Direct data-layer calls from server components
No balance polling 15s React Query polling
Fund form invalidates ALL chains Scoped to current chainId
Withdraw form uses manual useState useMutation with onSuccess invalidation
SessionKeyAuthCard always loaded Dynamic import with skeleton fallback
Double auth check Single auth in page.tsx

Files changed (12 files, +249 -244)

  • wallet/page.tsx — simplified, single auth check
  • wallet/wallet-page-content.tsx — rewritten as server orchestrator with direct data-layer calls
  • wallet/wallet-content.tsxdeleted (logic moved to server components)
  • wallet/active-wallet-section.tsx — converted to server component with async BalanceValue sub-component
  • wallet/pending-grant-section.tsx — converted to server component with dynamic SessionKeyAuthCard
  • wallet/no-account-card.tsx — kept as client, uses useMutation with unwrap()
  • wallet/chain-refresher.tsxnew client component for chain-switch → router.refresh()
  • wallet/wallet-forms-section.tsxnew client component wrapping fund/withdraw forms with balance hook
  • hooks/use-wallet-balance.ts — added 15s polling, reduced staleTime
  • components/fund-wallet-form.tsx — scoped invalidation to current chainId
  • components/withdraw-form.tsx — converted to useMutation with onSuccess invalidation
  • components/wallet-balance.tsx — simplified (removed client-side balance fetching, now receives server-streamed value)

Test plan

  • npm run build — compiles successfully
  • npm run lint — passes clean
  • npm run test:run — all 72 unit tests pass
  • Manual: verify page renders card shells before balance loads
  • Manual: verify fund → balance refreshes (current chain only)
  • Manual: verify withdraw → balance refreshes via onSuccess
  • Manual: verify chain switching re-renders server components
  • Manual: verify polling updates balance without user interaction

🤖 Generated with Claude Code

…r Suspense

Restructure the wallet (account) route from a client-orchestrator pattern to a
server-component-first architecture with streaming, matching the dashboard page
pattern. This eliminates the single Suspense boundary that blocked the entire
page and fixes balance refresh gaps.

Key changes:
- Server determines account state and renders correct view immediately
- Balance streams independently via its own Suspense boundary
- 15s React Query polling for wallet balance (matches pending payments)
- Fund/withdraw forms scope invalidation to current chainId
- Withdraw form converted to useMutation with onSuccess invalidation
- SessionKeyAuthCard dynamically imported (reduces initial bundle)
- wallet-content.tsx deleted (logic moved to server components)
- Single auth check (removed double auth)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vercel
Copy link

vercel bot commented Feb 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
brevet Building Building Preview Feb 28, 2026 11:36pm

Request Review

Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 7 additional findings in Devin Review.

Open in Devin Review

Comment on lines +35 to +37
const enabledChainIds = new Set(allAccounts.map((a) => a.chainId));
const hasAnyAccounts =
allAccounts.filter((a) => enabledChainIds.has(a.chainId)).length > 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 hasAnyAccounts is a tautology — always equals allAccounts.length > 0

The hasAnyAccounts computation builds enabledChainIds from allAccounts itself, then filters allAccounts by that same Set. Every account's chainId is guaranteed to be in the Set, so the filter is a no-op and hasAnyAccounts always equals allAccounts.length > 0.

Root Cause and Impact

The old code in the deleted wallet-content.tsx:36 built enabledChainIds from supportedChains (the user's enabled chains from the chain context):

const enabledChainIds = new Set(supportedChains.map((c) => c.chain.id));

Then filtered accounts against that set:

hasAnyAccounts={(allAccounts?.filter((a) => enabledChainIds.has(a.chainId)).length ?? 0) > 0}

The new code at wallet-page-content.tsx:35-37 instead does:

const enabledChainIds = new Set(allAccounts.map((a) => a.chainId));
const hasAnyAccounts = allAccounts.filter((a) => enabledChainIds.has(a.chainId)).length > 0;

This is a tautology — the Set is derived from the same array being filtered. The intent was to check whether the user has accounts on any of their enabled chains, but the enabled chain IDs are never fetched in this server component.

Impact: When a user has smart accounts on chains that are not in their currently enabled set, hasAnyAccounts will be true when it should be false, showing the wrong onboarding message ("Set Up Smart Account on {chainName}" instead of "Set Up Your First Smart Account").

Prompt for agents
In src/app/(dashboard)/dashboard/wallet/wallet-page-content.tsx, the hasAnyAccounts computation at lines 35-37 is a tautology. The enabledChainIds Set is built from allAccounts' chainIds, then allAccounts is filtered by that same Set, which always passes.

To fix this, you need to get the user's enabled chain IDs (from getUserEnabledChains in src/lib/data/user.ts) and filter allAccounts against those. For example:

1. Import getUserEnabledChains from @/lib/data/user
2. Fetch the enabled chains in the Promise.all alongside the other data fetches
3. Build enabledChainIds from the user's enabled chains, not from allAccounts
4. Filter allAccounts against the user's enabled chain IDs

Alternatively, if the intent is simply to check whether the user has any accounts at all (regardless of enabled chains), simplify to: const hasAnyAccounts = allAccounts.length > 0; which is what the current code effectively does but without the misleading intermediate variable.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant