Skip to content

fix(wallet): one wallet refresh at a time, not one per menu open - #418

Open
QSchlegel wants to merge 1 commit into
preprodfrom
claude/daily-2026-10-20-wallet-refresh-dedupe
Open

QSchlegel wants to merge 1 commit into
preprodfrom
claude/daily-2026-10-20-wallet-refresh-dedupe

Conversation

@QSchlegel

Copy link
Copy Markdown
Collaborator

What and why

The layout mounts WalletDataLoaderWrapper twice: the desktop refresh button (CSS-hidden on phones, but still mounted) and the item in the mobile More actions menu. Radix unmounts the menu's content while it is closed. Each instance kept its own prevWalletIdRef and fetchingTransactions flag, which caused three bugs. #266a74e made all three worse: a refresh of a busy wallet now runs at 5 req/s, so up to ~80 s for 400 transactions.

  1. Every time the mobile menu opened, a second full refresh started. That is up to 4 transaction pages, plus one Blockfrost lookup per transaction and one per asset. It ran on its own throttle, at twice the rate the throttle exists to hold.
  2. A wallet switched to while a refresh was running never loaded. refreshWallet() returned early on the in-flight flag, but the ref had already moved to the new wallet. Its data only arrived after a manual refresh.
  3. The old wallet's results landed on the new wallet. walletAssets and drepInfo are not keyed by wallet. A refresh of A that finished after the switch showed A's assets (send form, balance card) and A's DRep (vote and retire buttons) on B.

Fix

Refresh state now lives at module level in src/lib/wallet-refresh.ts, so remounts don't reset it and every instance shares it:

  • showWallet(id) is true only on a wallet switch (or the first wallet). Remounting a control does not trigger a load.
  • run(id, task) joins a run already in flight for the same wallet, so the menu item's spinner reflects it. Runs for different wallets do not wait on each other.
  • A run for a wallet the user has left is stale, and stays stale even after A→B→A. A stale run:
    • stops issuing lookups;
    • writes neither walletAssets/drepInfo nor a cut-short transaction history. The cached one is kept, and switching back loads afresh.
  • The component change is thin: loadWallet is the old body, and the isStale() guards sit in front of each write to a field that isn't keyed by wallet.

Behaviour changes

  • Clicking refresh while a run is in flight joins it. Before, the click was silently ignored.
  • Clicking refresh with no wallet open now only invalidates the queries. Before, it fetched /addresses/undefined/ and wiped walletAssets/drepInfo.
  • If the header remounts on the same wallet without a page reload, the wallet is not reloaded automatically. Logout already does router.reload(), so module state does not cross sessions.

Tests

  • npx tsc --noEmit: clean.
  • npx jest src/__tests__/walletRefresh.test.ts: 10/10. Covers joining, the slot being freed after an async or sync failure, staleness on a switch, A→B→A staying stale, and an old run not freeing a newer run's slot.
  • node scripts/run-tests.mjs: CJS 1666 passed / 2 skipped (151 suites), ESM 97 passed (11 suites).
  • next build --webpack + check-bundle-budget.mjs: within budget. Shared JS 193.6 KB gz, page sizes unchanged (e.g. /wallets/[wallet] 2210.5 KB).
  • An independent cold review of the diff found no correctness issues. Its one actionable nit, an unguarded setDrepInfo(undefined) in the no-DRep branch, is fixed.

Not verified

  • No component test. The repo has no React Testing Library, so the effect's showWallet || isRunning condition and the store-write guards are not covered.
  • Not exercised in a browser against live Blockfrost.
  • Window not covered, same as before: between the URL changing to wallet B and B's getWallet query resolving, appWallet is undefined. During that window A's run is not yet stale. B's own load overwrites the result afterwards.

Generated by Claude Code

The layout mounts the refresh control twice: the desktop button (only
CSS-hidden on phones) and an item in the mobile "More actions" menu,
whose content Radix unmounts when closed. Each instance kept its own
"last wallet" ref and in-flight flag, so:

- every open of the mobile menu started another full wallet refresh
  (up to 4 transaction pages + one Blockfrost lookup per transaction
  and per asset), on its own throttle — double the rate 266a74e set;
- switching wallet while a refresh ran skipped the new wallet's load
  entirely (the in-flight flag returned early, the ref moved on);
- the old wallet's late results overwrote the wallet-agnostic
  `walletAssets` / `drepInfo`, showing its assets and DRep on the new one.

The refresh state now lives in src/lib/wallet-refresh.ts at module
level: only a wallet switch triggers an automatic load, a second
control joins the run in flight, and a run for a wallet the user has
left goes stale — it stops issuing lookups and writes neither the
global fields nor a cut-short transaction history.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U11V2R2X68fxeDTVdaNYT2
@vercel

vercel Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
multisig Ready Ready Preview Oct 20, 2026 5:12am UTC

Request Review

This branch was successfully deployed

1 active deployment
Preview — ea97892f Deployed Oct 20, 2026 by vercel[bot]
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.

2 participants