Wallet cache v2 phase 1: emit currency wallets before their engines exist - #733
Wallet cache v2 phase 1: emit currency wallets before their engines exist#733j0ntz wants to merge 10 commits into
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
fd70e56 to
0fc4524
Compare
0fc4524 to
ed985f4
Compare
f615690 to
4783de3
Compare
|
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:
Phase 1 re-verified on the same run: the wallet list rendered names and balances from |
|
=== 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 --- --- 2. Fresh-process warm relaunch (accountCache.json present): account emits from cache before the deferred loads --- --- 3. Warm PIN login with verbose logging: bulk-seeded wallets enter the startup queue before the loads land --- --- 4. Queue drain: 174 cached wallets at concurrency 8 (cold monero/zano-family wallets bypass) --- --- 5. accountCache.json shape on the sim (no plugin settings; privacy fix) --- |
📸 Test evidence (after review fixes, final HEAD d7a0ed0)agent proof 1216673467164267 p3 05 final warm Captured by the agent's in-app test run (build-and-test). |
|
=== 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) --- --- Warm relaunch (account emits from cache before repo sync/file loads) --- --- Enabled-token toggle round trip through the new set-diff path (L3USD on My Fantom) --- --- Final warm relaunch (token persisted through a cache-seeded boot) --- |
|
=== 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) --- --- Receive/QR scene (My Fantom / L3USD): address renders, and the engine's answer lands in the cache --- --- otherMethods stub retirement: the FioActions warm-boot TypeError is gone --- --- Final warm relaunch (cache emit, clean boot) --- |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
4b81ab9 to
0f8f4a0
Compare
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
0f8f4a0 to
9dc46e6
Compare
683a5bd to
b589454
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b589454. Configure here.
6438c41 to
093c043
Compare
|
Back in draft for this turn on purpose. The push is a log line and a doc update, and Bugbot skips draft PRs, so neither change spends a review credit. What landed:
|
dbfebb3 to
e07528d
Compare
dbe3563 to
23bc602
Compare
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
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.
23bc602 to
3092b88
Compare












CHANGELOG
Does this branch warrant an entry to the CHANGELOG?
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.mdand 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 parallelEdgeCurrencyWalletimplementation 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:
accountCache.jsonand 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,otherMethodsnames, and the public keys that used to sit in each wallet'spublicKey.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.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.balanceMapkeeps its identity when an engine re-reports an unchanged balance.walletApigate drops itsengine != nullcondition and the engine becomes a dependency the API awaits internally; repo-writing methods gate on the storage wallet instead, which lands much earlier.otherMethodsis a permanent object of delegating stubs built from cached names. A rotating chain serves its cached receive address immediately, re-asks the engine in the background, and emitsaddressChangedwhen the two differ.makeFakeEdgeWorld404s and the two-device tests cannot diverge the repo from the cache at all.Semantic shift worth flagging in review:
waitForCurrencyWalletandwaitForAllWalletsnow 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 196 wallets; method and caveats in TDD section 8.4):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.
Asana: Login Perf - Wallet Cache v2
Note
High Risk
This rewrites the login boot path, wallet API timing, and multi-device merge behavior for tokens/settings/wallet states; regressions could show as stale UI, lost sync data, or callers assuming engines exist at
waitForCurrencyWalletresolve time.Overview
Warm login now reads a single consolidated
accountCache.json(alternating two slots for safe writes on Android), seeds Redux in bulk, and emits the account and currency wallet APIs before the account repo syncs or any engine starts. Deferred file loads still run afterward and merge per field so in-window user edits and cross-device syncs are not wiped.Wallet startup changes materially: cached wallets enter a 8-wide engine queue (user-open /
waitForCurrencyWallet/ engine-backed calls bump priority); wallets without cache skip the queue like a cold login. Wallet APIs expose cached balances, names, fiat, tokens, receive addresses (with background reconcile +addressChanged), andotherMethodsstubs that forward once the engine exists.API semantics:
waitForCurrencyWallet/waitForAllWalletsresolve when the wallet object exists, not when the engine is ready; activation/key helpers and several account methods nowwaitForCurrencyEngineinstead of throwing early. Repo-backed account/data-store and plugin-settings writes wait for authoritative loads, serialize settings writes, and merge into on-disk files.Also fixes custom-token first-write data loss,
changeEnabledTokenIdsstale-list erasure,balanceMapidentity on unchanged balances, per-repo sync serialization, and fake sync server routes for hash-suffixed store URLs.Reviewed by Cursor Bugbot for commit 3092b88. Bugbot is set up for automated code reviews on this repo. Configure here.