perf: rebuild wallet route with server-first architecture#76
perf: rebuild wallet route with server-first architecture#76marcopesani wants to merge 1 commit intomainfrom
Conversation
…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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
| const enabledChainIds = new Set(allAccounts.map((a) => a.chainId)); | ||
| const hasAnyAccounts = | ||
| allAccounts.filter((a) => enabledChainIds.has(a.chainId)).length > 0; |
There was a problem hiding this comment.
🟡 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
useMutationwithonSuccessreplaces manualuseEffectinvalidationSessionKeyAuthCard(370 LOC + ZeroDev deps) dynamically imported with skeleton fallbackwallet-content.tsxclient orchestrator, removed double auth check, replaced server-action reads with direct data-layer callsBefore vs After
Files changed (12 files, +249 -244)
wallet/page.tsx— simplified, single auth checkwallet/wallet-page-content.tsx— rewritten as server orchestrator with direct data-layer callswallet/wallet-content.tsx— deleted (logic moved to server components)wallet/active-wallet-section.tsx— converted to server component with asyncBalanceValuesub-componentwallet/pending-grant-section.tsx— converted to server component with dynamicSessionKeyAuthCardwallet/no-account-card.tsx— kept as client, usesuseMutationwithunwrap()wallet/chain-refresher.tsx— new client component for chain-switch →router.refresh()wallet/wallet-forms-section.tsx— new client component wrapping fund/withdraw forms with balance hookhooks/use-wallet-balance.ts— added 15s polling, reduced staleTimecomponents/fund-wallet-form.tsx— scoped invalidation to current chainIdcomponents/withdraw-form.tsx— converted touseMutationwithonSuccessinvalidationcomponents/wallet-balance.tsx— simplified (removed client-side balance fetching, now receives server-streamed value)Test plan
npm run build— compiles successfullynpm run lint— passes cleannpm run test:run— all 72 unit tests pass🤖 Generated with Claude Code