Skip to content

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

Open
j0ntz wants to merge 10 commits into
masterfrom
jon/wallet-cache-v2
Open

Wallet cache v2 phase 1: emit currency wallets before their engines exist#733
j0ntz wants to merge 10 commits into
masterfrom
jon/wallet-cache-v2

Conversation

@j0ntz

@j0ntz j0ntz commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

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 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 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 196 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.

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 waitForCurrencyWallet resolve 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), and otherMethods stubs that forward once the engine exists.

API semantics: waitForCurrencyWallet / waitForAllWallets resolve when the wallet object exists, not when the engine is ready; activation/key helpers and several account methods now waitForCurrencyEngine instead 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, changeEnabledTokenIds stale-list erasure, balanceMap identity 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.

@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 test evidence (in-app, edge-funds, 194 wallets)

agent proof 1216673467164267 p3 03 warm fixed

agent proof 1216673467164267 p3 03 warm fixed

agent proof 1216673467164267 p3 04 warm pin list

agent proof 1216673467164267 p3 04 warm pin list

Captured by the agent's in-app test run (build-and-test).

@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
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

📸 Test evidence (after review fixes, final HEAD d7a0ed0)

agent proof 1216673467164267 p3 05 final warm

agent proof 1216673467164267 p3 05 final warm

Captured by the agent's in-app test run (build-and-test).

@j0ntz

j0ntz commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

📸 Phase 4 test evidence (write-path staleness fixes)

agent proof 1216673467164267 p4 01 cold boot

agent proof 1216673467164267 p4 01 cold boot

agent proof 1216673467164267 p4 02 warm wallets

agent proof 1216673467164267 p4 02 warm wallets

agent proof 1216673467164267 p4 03 warm final

agent proof 1216673467164267 p4 03 warm final

Captured by the agent's in-app test run (build-and-test).

@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 test evidence (address cache + otherMethods stubs)

agent proof 1216673467164267 p5 01 warm list

agent proof 1216673467164267 p5 01 warm list

agent proof 1216673467164267 p5 02 receive

agent proof 1216673467164267 p5 02 receive

agent proof 1216673467164267 p5 03 warm final

agent proof 1216673467164267 p5 03 warm final

Captured by the agent's in-app test run (build-and-test).

@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
@j0ntz

j0ntz commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

📸 Test evidence: warm login from the consolidated cache

warm login wallet list

warm login wallet list

receive address no affordance

receive address no affordance

Captured by the agent's in-app test run (build-and-test).

@j0ntz
j0ntz marked this pull request as ready for review August 1, 2026 02:20
@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.

@j0ntz
j0ntz force-pushed the jon/wallet-cache-v2 branch from 4b81ab9 to 0f8f4a0 Compare August 1, 2026 02:24
@cursor

cursor Bot commented Aug 1, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot 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.

@j0ntz
j0ntz force-pushed the jon/wallet-cache-v2 branch from 0f8f4a0 to 9dc46e6 Compare August 1, 2026 02:27
Comment thread src/core/currency/wallet/currency-wallet-pixie.ts
@j0ntz
j0ntz force-pushed the jon/wallet-cache-v2 branch 2 times, most recently from 683a5bd to b589454 Compare August 4, 2026 10:45

@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 high 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 b589454. Configure here.

Comment thread src/core/account/account-cleaners.ts Outdated
@j0ntz
j0ntz force-pushed the jon/wallet-cache-v2 branch 2 times, most recently from 6438c41 to 093c043 Compare August 4, 2026 10:53
@j0ntz
j0ntz marked this pull request as draft August 5, 2026 00:26
@j0ntz

j0ntz commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

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:

  • aa60a85f logs each account-cache write with its generation, wallet count, and elapsed ms. The saver's existing 5 s throttle bounds it to the same volume as the Login: breadcrumbs it sits next to.
  • dbfebb3e fills in the one derived number left in the design doc. Section 8.4 claimed "up to 194 writes per 5 s window becomes 1" from reading the code. A release build at cae1f073, the last pre-consolidation commit, measured on the same Galaxy S9 and the same account: 363 per-wallet writes across a warm login's first 3 minutes against 22 for the consolidated file, and 100 writes in the worst 5 s span against 1.

@j0ntz
j0ntz force-pushed the jon/wallet-cache-v2 branch from dbfebb3 to e07528d Compare August 5, 2026 00:34
@j0ntz
j0ntz force-pushed the jon/wallet-cache-v2 branch 2 times, most recently from dbe3563 to 23bc602 Compare August 17, 2026 18:29
@j0ntz
j0ntz marked this pull request as ready for review August 17, 2026 18:30
@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.

j0ntz added 10 commits August 17, 2026 11:36
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 23bc602 to 3092b88 Compare August 17, 2026 18:36
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