Skip to content

Wallet cache v2 phase 1: emit currency wallets before their engines exist - #733

Merged
j0ntz merged 10 commits into
masterfrom
jon/wallet-cache-v2
Sep 23, 2026
Merged

j0ntz merged 10 commits into
masterfrom
jon/wallet-cache-v2

Conversation

@j0ntz

@j0ntz j0ntz commented Jul 18, 2026 •

Copy link
Copy Markdown
Contributor

Technical Design Document

edge-wallet-cache-design.md

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

Dependencies

none

Description

Login is slow because the GUI cannot render a wallet list until currency engines exist, and nothing about a wallet survives a logout. This branch caches what the list renders and moves everything else behind it: a warm login reads one file, seeds Redux, and emits the account and every wallet before the account repo syncs or any engine starts.

The design doc ships on this branch at src/docs/edge-wallet-cache-design.md and is the place to start: it carries the decisions, the rejected alternatives, the boot-outcome matrix, and a retrospective on where the design was wrong. Supersedes #703, which proved the ~5x login win but was rejected on architecture (a parallel EdgeCurrencyWallet implementation with a delegation and polling layer). One wallet implementation here, no mirror objects.

The ten commits are meant to be read in order. What each one carries:

  1. The design doc. The reasoning is not recoverable from the diff, so it ships with the code.
  2. accountCache.json and its cleaners. One file on the account's local disklet holds the account boot state plus, per wallet, name, fiat code, enabled tokens, last-known balances, receive addresses, otherMethods names, and the public keys that used to sit in each wallet's publicKey.json. Reads upgrade older versions in place. The disklet exposes no rename on either platform and Android truncates the target, so generations alternate between two slots and the reader takes the newest that still parses.
  3. The engine startup queue. Engine creation drains 8 wallets at a time instead of racing at login, with the wallet the user opened moved to the front. A wedged wallet cannot starve the pool (a watchdog force-releases and temporarily over-admits, degrading to today's unbounded behavior), and priority bumps carry a TTL so post-startup engine calls do not mark every wallet as asked-for by the next login.
  4. Redux seeding and deferred loads. Two seeding actions populate the same reducers the file loads populate, so there is no shadow copy of wallet state. Loads are marked fromFile, and each field that can race an in-window user change merges rather than replaces: custom tokens per token id, enabled tokens per toggled id, plugin settings per plugin id, wallet states per wallet id. balanceMap keeps its identity when an engine re-reports an unchanged balance.
  5. The bulk loader. One read produces one seed per wallet for a single batched dispatch, so a warm login costs two seeding dispatches rather than two per wallet. A device on the older per-wallet layout takes the per-wallet reads once, after which the saver folds them into the consolidated file.
  6. Pre-storage account surfaces. The account now emits before its repo exists, so disklets resolve lazily or fall back to disklets built straight from the keys, and repo-backed calls pend instead of throwing. Repo syncs serialize per storage wallet.
  7. Wallets emit before their engines. The walletApi gate drops its engine != null condition and the engine becomes a dependency the API awaits internally; repo-writing methods gate on the storage wallet instead, which lands much earlier. otherMethods is an object of delegating stubs built from cached names, keeping its identity while the known name set is unchanged. A rotating chain serves its cached receive address immediately, re-asks the engine in the background, and emits addressChanged when the two differ.
  8. The account emits from cache, and one saver writes it. The boot seeds and emits right after the plugins load, with the repo sync and file loads deferred behind it under bounded retries. One serialized throttled saver owns the whole account, so a sync window where 194 engines report balances costs one write instead of 194, and each write logs its generation, wallet count, and duration.
  9. A fake-server route fix, without which a repo's second sync inside makeFakeEdgeWorld 404s and the two-device tests cannot diverge the repo from the cache at all.
  10. The tests. 49 deterministic cases across three suites, driving ordering through a test-controlled engine gate rather than sleeping.

Semantic shift worth flagging in review: waitForCurrencyWallet and waitForAllWallets now resolve when the wallet object exists, which can be before its engine loads. Internal core callers want the object and are unaffected; the GUI call sites that consumed engine state at resolve time are patched in the companion PR, EdgeApp/edge-react-gui#6080.

Cold start is unchanged. With no cache the gate opens on exactly the conditions master uses, guarded by a regression test from the first commit.

Measured on hardware (Galaxy S9, release builds, edge-funds, 146 to 156 currency wallets; method and caveats in TDD section 8.4):

develop this branch
Balances visible after the last PIN digit ~57 s serial gap plus ~23 s of per-wallet file reads 341 ms mean (305-411 ms over five iterations)
Boot reads, 194 wallets 389 files 1 file
Writes across a warm login's first 3 minutes 363 22
Most writes in any 5 s span 100 1

Payload size is not what a write pays for: writes carrying no wallets took up to 11.4 s during the busy first-login window while a full 89.6 KiB write took 76 ms once it quieted, so the write-amplification tradeoff this design accepted is real in bytes and close to irrelevant in time. Booting with the newest slot truncated still emits from cache off the older slot with no crash, and the next write repairs the damaged slot.

Functional behavior was verified in-app on the iOS simulator across the branch's development, with evidence on this PR (1, 2, 3) and screenshots on #6080: the wallet list renders from cache before any engine exists, wallets drain through the queue with tap-to-front prioritization taking effect mid-drain, an enabled-token round trip persists through a cache-seeded relaunch, and the FioActions warm-boot TypeError (17 per session) dropped to zero.

Post-review followups

The three follow-ups the post-review runs recorded, plus the reviewer-bot findings on the pushed heads (design doc section 6.7):

  • Write generations replace the whole-value dirty flags. Each user change to the wallet name, fiat code, or wallet settings bumps that file's generation; the loader captures the generation before its disk read and dispatches it as loadGen; the reducer applies a loaded value only when the generations match. A load that began before the change is dropped, a load that began after it applies, so a rename pulled from another device lands without waiting for the sync server to echo the local write. Test: a rename from another device applies after a local rename.
  • The per-write log line ships at info, with warn only for a write slower than 5 s (accountCacheSaverConfig.slowWriteMs). The throttle bounds the line per window for the whole session, so the default level cannot carry every write.
  • saveTxAction and saveTxMetadata stay engine-gated on purpose: the write needs the transaction in Redux, and every reducer case that fills that map is an engine action, so a file-scan gate would turn a pending call into a "missing tx" error.
  • Reviewer-bot findings fixed: a wallet pixie's destroy releases its engine-startup slot, so a logout mid-startup no longer makes the next login queue behind the old session's awaits; the startup catch returns when the pixie is destroyed, so a failure that lands after logout is not surfaced as a wallet error; and the per-session boot-file memo remembers a miss, so a cold login goes straight to the per-wallet files instead of re-reading both empty slots per wallet. Tests: logout releases in-flight startup slots for the next login, logout during engine startup swallows a late startup failure.
  • Later Bugbot round on the pushed head fixed: a pixie destroyed by a logout during its fallback cache read returns before seeding, so the next session's pixie under the same wallet id keeps its own name, fiat and token list and the storage wallet is not re-added under it; and the saver replaces the per-session account-cache memo with each generation it writes, so a wallet created after boot seeds from the memo when it is unarchived instead of falling to per-wallet files this version no longer writes. Tests: a fallback read that outlives its logout does not seed the next session, a wallet created after boot seeds from the cache when unarchived.

Asana: Login Perf - Wallet Cache v2


Note

High Risk
Large changes to login ordering, wallet/engine lifecycle, and API timing (including waitForCurrencyWallet semantics) with intentionally stale cached UI data until authoritative loads complete.

Overview
Warm logins now read a consolidated accountCache.json (dual-slot generations for safe writes), seed Redux, and emit the account and wallet APIs before the synced repo finishes or any currency engine starts; repo sync and authoritative file loads run deferred with retries.

Cached wallets start engines through a limited-concurrency queue (with user-opened wallets bumped to the front). Wallet surfaces can show cached balances, receive addresses, and otherMethods stubs while engines load; getAddresses may serve cache first and emit addressChanged if the engine disagrees. waitForCurrencyWallet / waitForAllWallets resolve when the wallet object exists, not necessarily when its engine is ready—engine-backed account and wallet methods waitForCurrencyEngine instead of failing early.

Boot-window races are handled with per-field merge rules (custom tokens, enabled tokens, plugin/swap settings, wallet states) and generation counters for name/fiat/settings loads; plugin settings writes merge into on-disk files under a per-account queue, and one throttled account cache saver persists boot state. Supporting fixes include serialized storage syncs, lazy repo disklets for cache-seeded APIs, and a fake-server store route that accepts hash-suffixed paths.

Reviewed by Cursor Bugbot for commit 1c2e1c2. Bugbot is set up for automated code reviews on this repo. Configure here.

Test evidence

49e5a1f
Document the wallet cache v2 design

agent proof 1216673467164267 p3 03 warm fixed

agent proof 1216673467164267 p3 04 warm pin list

agent proof 1216673467164267 p3 05 final warm

agent proof 1216673467164267 p4 01 cold boot

agent proof 1216673467164267 p4 02 warm wallets

agent proof 1216673467164267 p4 03 warm final

agent proof 1216673467164267 p5 01 warm list

agent proof 1216673467164267 p5 02 receive

agent proof 1216673467164267 p5 03 warm final

warm login wallet list

receive address no affordance

warm login wallet list

warm login after bot fixes

warm login before create

probe wallet created

probe wallet archived

probe wallet restored from cache

probe wallet rearchived
27f9d94
fixup! Emit currency wallets before their engines exist
🪓 Uncommitted 240 s snooze before loadFiatFile in the wallet pixie startup block, so a logout could land inside the window; reverted

🪓 warm login list

🪓 logout during startup

🪓 relogin list

🪓 relogin list after stale loads
2450895
Accept hash-suffixed routes in the fake server
🪓 removed withWallet's waitForCurrencyWallet priority bump locally so the opened ETC wallet stayed in the engine queue while DAI was enabled; reverted.

logged out mid startup

relogin after mid startup logout

🪓 dai enabled while etc wallet still queued

🪓 dai row from cache after relaunch

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Comment thread src/core/currency/wallet/currency-wallet-api.ts
@j0ntz
j0ntz force-pushed the jon/wallet-cache-v2 branch from fd70e56 to 0fc4524 Compare July 18, 2026 01:08
Comment thread src/core/currency/wallet/currency-wallet-api.ts
Comment thread src/core/currency/wallet/currency-wallet-pixie.ts
@j0ntz
j0ntz force-pushed the jon/wallet-cache-v2 branch from 0fc4524 to ed985f4 Compare July 18, 2026 01:29
Comment thread src/core/currency/wallet/currency-wallet-pixie.ts Outdated
Comment thread src/core/currency/wallet/currency-wallet-api.ts
Comment thread src/core/currency/wallet/currency-wallet-api.ts
@j0ntz
j0ntz force-pushed the jon/wallet-cache-v2 branch from f615690 to 4783de3 Compare July 19, 2026 09:16
Comment thread src/core/currency/wallet/currency-wallet-api.ts
@j0ntz

j0ntz commented Jul 19, 2026

Copy link
Copy Markdown
Contributor Author

Phase 2 test evidence: live in-app verification (iOS sim, edge-funds, 194 wallets, warm login)

Captured the core's log stream during a warm login with verbose logging on. Three behaviors verified live:

  1. Staggered engine startup: 187 cached wallets acquired startup slots over ~12s at concurrency 8, instead of all racing at once. The remaining wallets have no cache (their engines fail on sim, so the saver never persisted one) and correctly bypassed the queue.
  2. Tap-prioritization: opening "My Sepolia" mid-drain moved it to the front of the queue; slot acquired 91ms after the bump, startEngine 145ms later, live balance on the detail scene.
  3. Organic bump via waitForCurrencyWallet: pending action-queue balance effects asked for their two wallets at login and both jumped the queue.
== Tap-prioritization: user opened My Sepolia during the drain ==
09:42:43 edge-core: X2yC3vU... engine startup bumped to front of queue
09:42:43 edge-core: X2yC3vU... engine startup slot acquired
09:42:44 edge-core: X2yC3vU... startEngine
09:42:44 sepolia-X2: X2yC3vU... syncRatio of: 0.5

== Organic bump: action-queue balance effects at login ==
09:40:47 edge-core: 2k9Bly5... engine startup bumped to front of queue
09:40:47 edge-core: jwjTT2k... engine startup bumped to front of queue
09:40:47 edge-core: 2k9Bly5... engine startup slot acquired
09:40:47 edge-core: jwjTT2k... engine startup slot acquired

Phase 1 re-verified on the same run: the wallet list rendered names and balances from walletCache.json seconds after PIN entry, before engines existed (screenshots on EdgeApp/edge-react-gui#6080).

@j0ntz

j0ntz commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

=== Phase 3 in-app evidence: edge-funds (194 wallets), iOS sim, phase-3 core bundle ===

--- 1. Cold boot (fresh install, no accountCache.json): master-identical sequence ---
[info] 07-21 01:34:01 edge-core: Login: decrypted keys for user uU6v6oNbuWDWphBVTz60M8qm8gnVTnvnYf7U6JyGf7o=
[info] 07-21 01:34:01 edge-core: Login: account exists for appId
[info] 07-21 01:34:01 edge-core: Login: currency plugins exist
[info] 07-21 01:34:01 edge-core: Login: synced account repos
[info] 07-21 01:34:01 edge-core: Login: loaded files
[info] 07-21 01:34:01 edge-core: Login: complete

--- 2. Fresh-process warm relaunch (accountCache.json present): account emits from cache before the deferred loads ---
[info] 07-21 01:40:22 edge-core: Login: decrypted keys for user uU6v6oNbuWDWphBVTz60M8qm8gnVTnvnYf7U6JyGf7o=
[info] 07-21 01:40:22 edge-core: Login: account exists for appId
[info] 07-21 01:40:22 edge-core: Login: emitted account from cache
[info] 07-21 01:40:22 edge-core: Login: currency plugins exist
[info] 07-21 01:40:22 edge-core: Login: synced account repos
[info] 07-21 01:40:22 edge-core: Login: loaded files
[info] 07-21 01:40:22 edge-core: Login: complete

--- 3. Warm PIN login with verbose logging: bulk-seeded wallets enter the startup queue before the loads land ---
[info] 07-21 01:43:23 edge-core: Login: decrypted keys for user uU6v6oNbuWDWphBVTz60M8qm8gnVTnvnYf7U6JyGf7o=
[info] 07-21 01:43:23 edge-core: Login: account exists for appId
[info] 07-21 01:43:23 edge-core: Login: emitted account from cache
[info] 07-21 01:43:23 edge-core: Y222zJlNNxqcy/i3xnGnWruGEBz4tjrSI/xxu0Mf9xg= engine startup slot acquired
[info] 07-21 01:43:23 edge-core: tIz3zg4KVh38F1lKBP4Ft/+QnLcLVrdLlc1rEVyGEkk= engine startup slot acquired
[info] 07-21 01:43:23 edge-core: PFpJNIrWnirbVqJU5wiRdnApSaH2MfyAA+MwfFhpF6k= engine startup slot acquired
[info] 07-21 01:43:23 edge-core: //j3WFpSPs14g6rNn6q0QI/ioh7m0AGfmDqDfst7rlY= engine startup slot acquired
[info] 07-21 01:43:23 edge-core: V5b+2aOIsWj6748Hme5EGuuWV26CKLBgWbBGb8v4p2Q= engine startup slot acquired
[info] 07-21 01:43:23 edge-core: dDMrVTRMMgWRmQ4M2FChJeRN2H43dPRwblK9R7cEaSk= engine startup slot acquired
[info] 07-21 01:43:23 edge-core: 2SDRqv1GBntaqRdNk0RQDJ7fxYv1oUIB48jiM3ydyag= engine startup slot acquired
[info] 07-21 01:43:23 edge-core: DRWdI0mngV/CNEzw0UxVFJzIGdUIMK+iQ1/dLoqwV5E= engine startup slot acquired
[info] 07-21 01:43:23 edge-core: Login: currency plugins exist
[info] 07-21 01:43:23 edge-core: Login: synced account repos
[info] 07-21 01:43:23 edge-core: Login: loaded files
...

--- 4. Queue drain: 174 cached wallets at concurrency 8 (cold monero/zano-family wallets bypass) ---
8 07-21 01:43:23
16 07-21 01:43:24
15 07-21 01:43:25
28 07-21 01:43:26
25 07-21 01:43:27
26 07-21 01:43:28
24 07-21 01:43:29
25 07-21 01:43:30
7 07-21 01:43:31

--- 5. accountCache.json shape on the sim (no plugin settings; privacy fix) ---
keys: customTokens, legacyWallets, version, walletStates | walletStates: 132 | customTokens plugins: 6 | legacyWallets: false | userSettings present: False

Comment thread src/core/currency/wallet/currency-wallet-reducer.ts Outdated
Comment thread src/core/currency/wallet/currency-wallet-pixie.ts
Comment thread src/core/storage/storage-api.ts
Comment thread src/core/currency/wallet/currency-wallet-api.ts
Comment thread CHANGELOG.md Outdated
Comment thread src/core/account/account-pixie.ts Outdated
Comment thread src/core/storage/storage-api.ts Outdated
Comment thread src/core/account/account-pixie.ts
Comment thread src/core/account/account-files.ts Outdated
Comment thread src/core/account/plugin-api.ts
Comment thread src/core/currency/wallet/currency-wallet-api.ts
@j0ntz

j0ntz commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

=== Phase 4 in-app evidence (edge-funds, 194 wallets, iOS sim, core webview bundle @ phase-4 HEAD) ===

--- Cold boot (fresh app data restored from pool image; master-identical ordering, no cache emit) ---
2026-07-21T23:36:26 edge-core: Login: decrypted keys for user uU6v...
2026-07-21T23:36:26 edge-core: Login: account exists for appId
2026-07-21T23:36:26 edge-core: Login: currency plugins exist
2026-07-21T23:36:26 edge-core: Login: synced account repos
2026-07-21T23:36:26 edge-core: Login: loaded files
2026-07-21T23:36:26 edge-core: Login: complete
(no "emitted account from cache" line: cold path, byte-identical ordering to master)
accountCache.json + 194 walletCache.json written after boot.

--- Warm relaunch (account emits from cache before repo sync/file loads) ---
2026-07-21T23:44:11.277 edge-core: Login: decrypted keys for user uU6v...
2026-07-21T23:44:11.334 edge-core: Login: emitted account from cache
2026-07-21T23:44:11.573 edge-core: Login: currency plugins exist
2026-07-21T23:44:11.589 edge-core: Login: synced account repos
2026-07-21T23:44:12.616 edge-core: Login: loaded files
2026-07-21T23:44:12.616 edge-core: Login: complete

--- Enabled-token toggle round trip through the new set-diff path (L3USD on My Fantom) ---
2026-07-21T23:51:25 edge-core: enabledTokenIds: zi changeEnabledTokenIds
2026-07-21T23:51:25 edge-core: enabledTokenIds: zi write to disk, add [], remove [5f0456f728e2d59028b4f5b8ad8c604100724c6a]
2026-07-21T23:53:28 edge-core: enabledTokenIds: zi write to disk, add [5f0456f728e2d59028b4f5b8ad8c604100724c6a], remove []

--- Final warm relaunch (token persisted through a cache-seeded boot) ---
2026-07-21T23:54:16 edge-core: Login: emitted account from cache
2026-07-21T23:54:17 edge-core: Login: complete
L3USD row present in the seeded wallet list with cached balance (screenshot p4-03).

@j0ntz

j0ntz commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

=== Phase 5 in-app evidence (edge-funds, 194 wallets, iOS sim, core webview bundle @ phase-5 HEAD) ===

--- Warm boot on phase-5 core (account emits from cache; wallet list renders pre-engine) ---
2026-07-22T21:51:15.185 edge-core: Login: decrypted keys for user uU6v...
2026-07-22T21:51:15.245 edge-core: Login: emitted account from cache
2026-07-22T21:51:15.530 edge-core: Login: currency plugins exist
2026-07-22T21:51:15.778 edge-core: Login: synced account repos
All 194 walletCache.json files rewritten as schema v2 with otherMethodNames within the first minute.

--- Receive/QR scene (My Fantom / L3USD): address renders, and the engine's answer lands in the cache ---
walletCache.json (Fantom wallet) after the receive query:
"addresses": {"": [{"addressType": "publicAddress", "publicAddress": "0x15aDA9aB27d7fCdE5c1806cd48683977656E3af0"}]}
"otherMethodNames": ["parseWalletConnectV2Payload", "txRpcParamsToSpendInfo"]
(hasStableAddresses ships defaulted OFF, so the scene still waits for the engine exactly as before; screenshot p5-02.)

--- otherMethods stub retirement: the FioActions warm-boot TypeError is gone ---
Phase-4 evidence logs: "TypeError: fioWallet.otherMethods.fetchFioAddresses is not a function" on every warm boot (17 occurrences in one session).
Phase-5 warm boots #1 and #2: 0 occurrences (the cached name exposes a delegating stub pre-engine; the call now waits for the engine instead of exploding).

--- Final warm relaunch (cache emit, clean boot) ---
2026-07-22T21:56:19.151 edge-core: Login: emitted account from cache
2026-07-22T21:56:19.166 edge-core: Login: complete
fetchFioAddresses TypeErrors: 0

Comment thread src/core/account/account-pixie.ts
Comment thread src/core/currency/wallet/engine-scheduler.ts
Comment thread src/core/account/account-files.ts
Comment thread src/core/account/account-reducer.ts
Comment thread src/core/account/account-pixie.ts
@j0ntz
j0ntz marked this pull request as draft August 1, 2026 01:02
@j0ntz
j0ntz force-pushed the jon/wallet-cache-v2 branch from c532ebe to 733c172 Compare August 1, 2026 01:02
@j0ntz

j0ntz commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

Approval noted, no code change for this round. Your correction on the sticky dirty flags is the accurate one: that thread reply predates the write-generation scheme, which landed in a later fixup and is what the branch ships (nameGen/fiatGen/walletSettingsGen plus a loadGen captured before each disk read). Branch head is unchanged at 28c09f5.

@j0ntz
j0ntz force-pushed the jon/wallet-cache-v2 branch from 28c09f5 to a7068d5 Compare September 22, 2026 22:59

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread src/core/currency/wallet/currency-wallet-api.ts
j0ntz added a commit that referenced this pull request Sep 22, 2026
Answer the Cursor Bugbot finding on PR #733: a logout destroys the pixie under the pending engine wait, so the background address reconcile rejected and reported a shutdown or wallet-missing error to the app. Swallow both, since the correction is moot once the wallet is gone.

Fixup-for: auto
@j0ntz
j0ntz force-pushed the jon/wallet-cache-v2 branch from a7068d5 to a1650f3 Compare September 22, 2026 23:29

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread src/core/account/account-pixie.ts Outdated
Comment thread src/core/currency/wallet/currency-wallet-pixie.ts
j0ntz added a commit that referenced this pull request Sep 22, 2026
Answer the Cursor Bugbot finding on PR #733: a logout destroys the pixie under the pending engine wait, so the background address reconcile rejected and reported a shutdown or wallet-missing error to the app. Swallow both, since the correction is moot once the wallet is gone.

Fixup-for: auto
@j0ntz
j0ntz force-pushed the jon/wallet-cache-v2 branch from a1650f3 to 4da4bc4 Compare September 22, 2026 23:41

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread src/core/currency/wallet/currency-wallet-pixie.ts
j0ntz added a commit that referenced this pull request Sep 23, 2026
Answer the Cursor Bugbot finding on PR #733: a logout destroys the pixie under the pending engine wait, so the background address reconcile rejected and reported a shutdown or wallet-missing error to the app. Swallow both, since the correction is moot once the wallet is gone.

Route the engine callbacks and the staking query's rejection through the destroy-guarded input, so a report that lands between a logout and the end of killEngine cannot write into the next login's wallet or raise an error for a wallet the user has left.

Fixup-for: auto
@j0ntz
j0ntz force-pushed the jon/wallet-cache-v2 branch 2 times, most recently from 7b68565 to 5451bec Compare September 23, 2026 00:21

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread src/core/currency/currency-selectors.ts
@j0ntz
j0ntz force-pushed the jon/wallet-cache-v2 branch from 5451bec to 0b281e5 Compare September 23, 2026 00:37

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0b281e5. Configure here.

Comment thread src/core/currency/wallet/currency-wallet-reducer.ts Outdated
@j0ntz
j0ntz force-pushed the jon/wallet-cache-v2 branch from 0b281e5 to af1b807 Compare September 23, 2026 02:15
The design spans two repos, seven phases, and a file-layout reversal,
and the reasoning behind each is not recoverable from the diff. The
doc ships with the code so it is reviewed alongside it and stays
current instead of drifting in a gist.
A warm login needs the account's boot state and every wallet's UI
state on disk in a form that survives a schema bump and a kill
part-way through a write.

The file carries wallet states, custom tokens, and one entry per
wallet holding its name, fiat code, enabled tokens, balances,
addresses, public keys, and otherMethods names. Reads accept older
versions by upgrading in place. The disklet exposes no rename on
either platform and Android truncates the target, so generations
alternate between two slots and the reader takes the newest that
still parses. The per-wallet walletCache.json reader stays for
devices on the old layout.
Once the wallet list renders from cache, every wallet's engine work
(repo sync, key derivation, makeCurrencyEngine) still races in the
seconds after login, which is where the post-login jank lives.

The scheduler admits 8 wallets at a time and lets a caller move one
to the front, so a wallet the user actually opened does not wait
behind the whole queue. A wedged wallet cannot starve the pool: a
watchdog force-releases its slot and temporarily over-admits, which
degrades to the unbounded behavior this replaces. Priority bumps
carry a TTL, since post-startup engine calls bump constantly and
would otherwise mark every wallet as asked-for by the next login.
The seeded state is a guess that the file loads overwrite moments
later, and a user change made inside that window would otherwise be
reverted by a load that read the file before it.

Two seeding actions, one for the account and one carrying every
wallet, populate the same reducers the file loads populate, so there
is no shadow copy of wallet state. The loads then run deferred, marked
fromFile so the reducers can tell an authoritative value from a user
change, and each racing field merges rather than replacing: custom
tokens per token id, enabled tokens per toggled id, plugin settings
per plugin id, wallet states per wallet id. A terminal load failure
rejects the waiters instead of leaving them pending forever, and
balanceMap keeps its identity when an engine re-reports an unchanged
balance, so yaob sees no phantom update.
Reading each wallet's cache from its own pixie costs two dispatches
per wallet, so a 194-wallet account paid ~400 store transits before
the list could render, and every pixie and watcher re-evaluated
against each one.

The loader reads the consolidated file once and, for a device still
on the per-wallet layout, falls back to reading those files
concurrently. Either way it produces one seed per wallet for a single
batched dispatch. This is batching, not notification suppression:
every consumer is still notified once, with the final state.
The account now emits before its repo is created, so every surface
that assumed a live repo could throw during the window the cache
exists to make usable.

Disklets resolve lazily or fall back to disklets built straight from
the keys, which are the same files under the same encryption. Repo-
backed calls pend rather than throw: changeWalletStates waits for the
repo, sync waits for the storage wallet, plugin-settings writes wait
for their load, and changeEnabledTokenIds waits for the plugin's
builtin definitions. The account-level engine methods wait through the
shared selector and read the wallet list after the wait, so wallets
that arrived during it are included. Repo syncs serialize per storage
wallet, so two concurrent syncs cannot interleave.
The wallet API refused to emit until its engine existed, so the GUI
could not render a name, a fiat code, or a balance until every
wallet's engine had loaded, which is the whole of the login delay.

The gate now opens on the cached state, and the engine becomes a
dependency the API awaits internally: engine-backed methods wait and
reject if the engine fails or the wallet is deleted, while repo-
writing methods gate on the storage wallet instead, which lands much
earlier. otherMethods is a permanent object of delegating stubs built
from the cached names, so its identity survives the engine landing.
A rotating chain serves its cached receive address immediately and
re-asks the engine in the background, emitting addressChanged when
the two differ. Without a cache the gate opens on the same conditions
as before, so first login is unchanged.
The account itself was the other half of the delay: nothing emitted
until plugins loaded, the account repos synced, and every key file was
read, and wallet pixies could not even start until that chain finished.

The boot now seeds from the cache right after the plugins load and
emits, running the repo sync and file loads deferred behind it. Writes
go the other way: one serialized throttled saver owns the whole
account, so a sync window where 194 engines all report balances costs
one write instead of 194, and generations alternate between the file's
two slots so an interrupted write costs one generation of staleness.
Each write logs its generation, wallet count, and duration, since the
write is this design's whole cost and nothing else reports it.
The fake server hands out hash-suffixed store URLs but only routed the
bare form, so a repo's second sync inside makeFakeEdgeWorld 404'd.
Two-device tests need that second sync to diverge the repo from the
cache at all.
Every claim here is a timing claim, so the suites drive the ordering
directly rather than sleeping: the fake plugin gets a test-controlled
engine gate, the savers get a 50 ms throttle, and the fake world is
relaunched from disk to prove a warm boot reads only what is on it.

Coverage runs from cold-start equivalence and cached emission through
engine-gated calls completing, failing, and being deleted mid-wait,
the concurrency queue draining with front-of-queue bumps, the four
two-device races the write-path audit found, the address and
otherMethods caches, and a torn slot degrading to the older
generation. A cache-coverage test asserts every EdgeCurrencyWallet
property is classified, so a new property forces a caching decision.
@j0ntz
j0ntz force-pushed the jon/wallet-cache-v2 branch from af1b807 to 1c2e1c2 Compare September 23, 2026 02:37
@j0ntz
j0ntz merged commit d3d1a61 into master Sep 23, 2026
6 checks passed
@j0ntz
j0ntz deleted the jon/wallet-cache-v2 branch September 23, 2026 02:45

@nisgroup1-ship-it nisgroup1-ship-it left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Approve

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.

3 participants