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 37 commits into
Conversation
Add walletCache.json (name, fiat code, enabled token IDs, last-known balances) with a versioned cleaner, plus a CURRENCY_WALLET_CACHE_LOADED action that seeds the wallet's Redux slice. Cached balances never overwrite live engine data, and the later authoritative file loads replace the cached values exactly as they replace initial state today.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
fd70e56 to
0fc4524
Compare
Hoist a read of publicKey.json + walletCache.json to the top of the
engine pixie, ahead of the storage-wallet sync, and seed Redux from it,
so the walletApi gate (which drops its engine condition) opens within
one pixie tick on a warm login. Without the cache the gate opens on the
same conditions as before, keeping cold-start behavior unchanged.
makeCurrencyWalletApi loses its engine and tools constructor parameters.
Engine-backed methods wait internally via getEngine(), which bails out
if the wallet is deleted mid-wait and rethrows engineFailure so a broken
plugin surfaces as a rejected call instead of a hang. Mutations that
write synced-repo files gate on the storage wallet instead, which loads
well before the engine. otherMethods is guaranteed to be {} pre-engine
and switches to the engine's bridgified methods once it lands.
A cacheSaver sub-pixie watches the cache-relevant Redux slice and
persists it per wallet, throttled to one trailing-edge write per 5s,
guarded against post-logout writes, and giving up after 3 consecutive
failures. Engine creation and start scheduling are unchanged.
The fake currency plugin gains a test-controlled engine gate, so "before the engine exists" is a controlled state instead of a race, and the cache saver throttle drops to 50ms under test. The suite covers cold-start equivalence, cached emission, live-data overwrite, pending engine-gated calls (completion, engine failure, wallet deletion), renames inside the cache window, cancelled post-logout writes, corrupt cache files, saver behavior, and the otherMethods pre-engine guarantee.
0fc4524 to
ed985f4
Compare
…queue Wallets that emit from walletCache.json no longer race every other wallet through repo sync, key derivation, and engine creation in the seconds after login. Their heavy startup work now waits in a per-context queue (8 at a time), while wallets without a cache bypass the queue because they cannot emit at all until that work runs, keeping first login identical to before. Asking for a wallet moves it to the front of the line: the account's waitForCurrencyWallet, the internal engine/storage waiters, and changePaused(false) all bump the wallet's queued startup.
Engines re-report balances they already reported, and each report used to allocate a fresh Map. An unchanged balance now keeps the existing Map, so memoized reducers, the wallet cache saver, and yaob's === diffing see no phantom update.
The fake plugin reports each makeCurrencyEngine call through an onEngineCreate hook, so tests can observe creation order. Cases cover concurrency-limited draining, waitForCurrencyWallet bumping a queued wallet to the front, cold wallets bypassing the queue, a deleted queued wallet giving up its place, and balanceMap identity across unchanged balance reports.
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 |
A warm login reads accountCache.json (wallet states, custom tokens, plugin settings) from the account's local disklet right after waitForPlugins, seeds Redux through a new ACCOUNT_CACHE_LOADED action, and emits the account API object immediately. The repo sync and file loads still run and overwrite the seeded state authoritatively, with dirty-wins guards so user changes made during the window survive. Once the account cache seeds currencyWalletIds, one bulk loader reads every active wallet's cache files concurrently and seeds them all in a single CURRENCY_WALLETS_CACHE_LOADED dispatch, so a warm login costs two seeding dispatches total instead of two per wallet. The per-wallet read inside the wallet pixie remains as the fallback for cold logins and wallets activated after login. A throttled account cache saver persists the state after the authoritative loads land; its dirty set includes account-level custom tokens. The cold path (no cache file) boots exactly as before.
The cache seeding path already reads publicKey.json before the wallet enters the startup queue, and the seeded public info lands in Redux. Pass it into getPublicWalletInfo so the queued block does not read the same file a second time. Cold wallets still read the file as before.
Covers the design's test cases 12-14: the cache-coverage exhaustiveness test classifying every EdgeCurrencyWallet property, warm account login emitting before the deferred loads land (with the cold path blocking exactly as on master), and the bulk seed dispatch count with the pixie fallback for wallets activated after login. The fake plugin gains a builtinTokensGate, which blocks the deferred account loads at their head, making both states deterministic.
|
=== 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) --- |
A wallet emits from its cache before its name, fiat, and settings files load, so a rename (or fiat/settings change) made during that window could be overwritten when the in-flight load dispatched its stale value. The mutation writes the file before dispatching, so the change was already on disk; only Redux regressed. File loads now tag their dispatches, and a dirty flag lets the user's value win over one racing load, mirroring the enabledTokenIds and account-level guards.
On a warm login, engine startup runs in parallel with the deferred account file loads. The pixie watcher could observe freshly-loaded plugin settings or custom tokens while the engine was still null, adopt them, and then never deliver them once the engine appeared, leaving it on empty settings for the whole session. The watcher now never adopts a value it could not deliver, and engine creation reads the account state fresh instead of from an earlier snapshot.
A cache-seeded login emits the account and wallet API objects before addStorageWallet attaches their repos, so sync() now waits for the repo instead of throwing during that window, matching the disklet fallbacks and the pending repo-backed mutations.
changeEnabledTokenIds filters the requested ids against the plugin's known tokens, but on a warm login the builtin definitions load after the wallet exists, so a toggle made in that window silently dropped enabled builtin tokens. The method now waits for the plugin's builtin tokens (still working when the engine has failed, bailing only on wallet deletion).
changeEnabledTokenIds waits for the plugin's builtin token definitions, which never arrive if the deferred boot loads failed terminally; re-throw the recorded failure like the other repo waiters.
📸 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). |
A cache-seeded login holds possibly-stale wallet states, so a change made in that window could no-op against a stale record and then be silently reverted when the authoritative load lands. Gating on walletStatesLoaded also bases the written record on loaded state.
The saver rebuilds the whole CustomTokens.json file from Redux, so a write in the cache-seeded window would wipe tokens another device added to the file. Return without adopting the diff, so the write retries once the authoritative load has merged.
The dirty guard for custom tokens was a whole-map boolean, so a load landing after an in-window edit kept the entire stale map, dropping tokens another device had synced to the file. Track the edited token ids instead, and have the load merge everything else from the file. The ids clear once the saver writes them to disk.
changeEnabledTokenIds replaced the whole enabled list, so a call made against a cache-seeded list erased enablement changes another device had synced to the file. Diff the request against the list the caller saw, apply those toggles to the current list after the builtin-token wait, and have a racing file load preserve exactly the toggled ids.
One boolean covered both settings maps, so a load landing after an in-window change discarded the entire loaded payload, dropping settings another device had synced. Track the changed plugin ids per map instead, and merge the rest from the loaded file. The writers now also merge into the freshly read file rather than rebuilding the whole on-disk map from Redux.
getActivationAssets, activateWallet, getDisplayPrivateKey, and getDisplayPublicKey read the engine output directly and threw when it was missing, unlike every wallet-level engine method. Route them through a shared engine waiter, which also bumps the wallet to the front of the startup queue and rejects on deletion or engine failure.
The saver derives the legacyWallets flag from currencyWalletIds at write time but never re-compared it, so a late plugin load could leave a stale flag in the cache file.
The fake server handed out a sync hash on every update but rejected the hash-suffixed routes clients build from it, so any repo's second sync inside a fake world failed with a 404. Reads ignore the hash and return the whole repo, which clients treat as a superset of the changes they asked for.
Cover the four confirmed audit scenarios: a boot-window custom-token add no longer deletes a token synced from another device, a toggle against a stale cached enabled list preserves the loaded list, a wallet-state change that matches the stale cache waits for the load instead of silently no-oping, and a settings write merges into the freshly read file instead of wiping another plugin's synced settings. The fake plugin gains a public-key check gate, so tests can hold a wallet's file loads while its cache-seeded API stays usable.
|
=== 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) --- |
The receive path always waited on the engine, even for chains whose addresses never rotate. The engine's answer to the default address query is now remembered in Redux (balances stripped), persisted into walletCache.json (schema version 2, with old files upgraded on read so nobody loses a warm boot), and seeded on the next login. A new optional EdgeCurrencyInfo.hasStableAddresses hint gates the pre-engine serve; it defaults to false, so rotating (UTXO-style) chains and unflagged plugins keep exactly today's engine wait.
The wallet's otherMethods object was {} pre-engine and swapped to the
engine's bridgified methods when it landed, so a method was invisible
until then even though its name never changes. The engine's method
names now persist in walletCache.json, and the wallet exposes one
permanent object of delegating stubs: each waits for the engine and
forwards, resolving the real method once. A warm login can therefore
call a known method before the engine exists, a stale cached name
rejects cleanly at call time, and the object-swap plus update() dance
is gone. Cold logins with no cache keep the {} guarantee.
CurrencyConfig.otherMethods now mirrors the wallet's model: one permanent object of delegating stubs whose names come from the live plugin, with the account cache as a fallback. Each plugin's names persist in accountCache.json, so the config surface stays complete even if plugin loading ever defers past the account emit.
Cover the phase-5 scope: a stable-flagged chain serves its cached addresses pre-engine (and getReceiveAddress derives from them), a rotating chain still waits for the engine, the engine's address answer reaches the cache file with balances stripped, a cached otherMethods name is callable before the engine exists, a stale cached name rejects cleanly when the loaded engine lacks the method, config-level names persist and delegate, and the cache-coverage classification gains a cache-assisted set for the conditional surfaces. The fake plugin gains an identity-stable currencyInfo patch hook and an omit-otherMethods switch to stage these states.
Review findings on the new caches. The address cache was one flat array per wallet, so a token query overwrote the parent chain's answer and a warm boot could serve the wrong asset's address; it is now keyed by tokenId end to end (Redux, file schema, the pre-engine serve), with an identity guard so a repeated identical answer causes no phantom update. The otherMethods object cannot gain properties after it first crosses the yaob bridge (update() only re-serializes properties that existed then, verified empirically), so instead of mutating one permanent object, the getter rebuilds it as a new bridgified object whenever a name first appears; identity still holds for the common warm case where the cache already names everything. Stubs also resolve against the live engine on every call, so an engine rebuilt by a resync never leaves a stale capture, and calls go through the source object to preserve the plugin's this binding.
Review findings: routing every config otherMethods call through a stub dropped the plugin's this binding and any non-function property, on every login. When the plugin is loaded (always the case today) the config exposes the plugin's own otherMethods object verbatim, exactly as before; the cached names only build delegating stubs in the so-far-unreachable case where plugin loading defers past the account emit, and those stubs call through the object to preserve this.
|
=== 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) --- |
Bugbot findings. A warm-boot retry re-ran addStorageWallet for repos a prior attempt had already attached; STORAGE_WALLET_ADDED replaces the whole entry (wiping lastChanges) and starts a second sync that can race the one still in flight, so retries now attach only the repos that are still missing. The engine scheduler's watchdog called its logging callback before freeing the slot, so a throw from stale props after a pixie destroy could permanently shrink the pool; the slot is now released first and the callback is contained.
Bugbot finding: storageWallets entries survive logout, so on a same-context re-login the repo waiters resolve against the prior session's entry and a user-facing sync can run concurrently with the boot's own addStorageWallet sync. Concurrent syncRepo calls on one repo are unsafe (the changes-folder snapshot is deleted after the round trip, so a racing write can be dropped or double-uploaded), and the same overlap already existed between the periodic timer and a user sync. All callers funnel through syncStorageWallet, which now queues per repo, and a sync that dequeues after deletion or logout rejects cleanly.
The receive scene wants to show an address the instant a warm login finishes, even on a chain whose addresses rotate. getAddresses and getReceiveAddress now accept allowCached: passing it serves the cached answer before the engine loads on any chain, not just stable-address ones. Programmatic callers leave it unset and keep waiting for the engine, so only the receive scene shows a provisional address (which it reconciles once the engine returns the fresh one) and no payment path ever latches a reused address.
ACCOUNT_CACHE_LOADED sets bulkWalletSeedPending so wallet pixies hold their own cache reads until the bulk seed dispatches. It cleared only on CURRENCY_WALLETS_CACHE_LOADED or ACCOUNT_KEYS_LOADED, so a terminal deferred-load failure (ACCOUNT_LOAD_FAILED after the account already emitted) left the flag stuck true and every wallet pixie returning early forever, never starting an engine or emitting an API object. Clear the flag on ACCOUNT_LOAD_FAILED too, matching the existing ACCOUNT_KEYS_LOADED backstop, so the wallets fall back to their own reads.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default 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 c532ebe. Configure here.










CHANGELOG
Does this branch warrant an entry to the CHANGELOG?
Dependencies
none
Description
Implements phase 1 of the wallet cache v2 TDD (immutable snapshot, live): the wallet list can render names, fiat codes, enabled tokens, and last-known balances as soon as per-wallet cache files load, before any currency engine exists. Supersedes #703, which proved the ~5x login win but was rejected on architecture (parallel
EdgeCurrencyWalletimplementation, delegation/polling layer).One wallet implementation, three changes:
walletCache.jsonon the wallet's local disklet (name, fiat code, enabled token IDs, last-known balances), validated by a versioned cleaner. Missing or invalid file (first login, schema bump, corruption) falls through to the exact cold path. Balances are allowed to be stale; the engine overwrites them within seconds of starting. Token definitions, tx history, and addresses are intentionally not cached (TDD decisions 9.3, 9.4). Privacy coins cache uniformly:publicKey.jsonalready stores viewing keys on the same plain disklet, so encrypting only this file would protect nothing (TDD 9.2).publicKey.json+walletCache.jsonfirst, ahead of the storage-wallet sync, and seeds Redux via a newCURRENCY_WALLET_CACHE_LOADEDaction (cached balances never overwrite live engine data). ThewalletApigate drops itsengine != nullcondition, so the wallet object emits within one pixie tick of the cache read. Without a cache, the gate opens on the same conditions as master (name loads after engine creation), so cold-start behavior is unchanged. Engine creation and start scheduling are untouched.makeCurrencyWalletApidrops itsengine/toolsconstructor parameters. Engine-backed methodsawait getEngine()internally (thewaitForCurrencyWalletpattern, keeping the deleted-wallet bailout and theengineFailurerethrow); repo-writing mutations (renameWallet,setFiatCurrencyCode,changeWalletSettings,sync) gate on the storage wallet instead, which loads well before the engine.otherMethodsis guaranteed{}pre-engine and switches to the engine's bridgified methods when it lands.publicWalletInfois served from Redux so a later key-cache upgrade propagates.A per-wallet
cacheSaversub-pixie persists the cache-relevant Redux slice: trailing-edge throttled to one write per 5s per wallet, guarded against post-logout writes, stops after 3 consecutive failures. It only writes once the authoritative name/fiat/token files have loaded, so a cold start never caches placeholder values. Stale cache files for deleted wallets are dead data on disk, never resurrected wallets: the cache is only read for wallet IDs in the account's encrypted key state.Semantic shift:
waitForCurrencyWallet/waitForAllWalletsnow mean "wallet object exists," which can be pre-engine. Internal core callers want the object, not the engine, and are unaffected; the two GUI call sites that consumed engine state at resolve-time are patched in the companion PR (EdgeApp/edge-react-gui).Tests adopt #703's determinism mechanisms (a test-controlled engine gate on the fake plugin plus a 50ms saver throttle) and cover the 11 TDD section-6 cases: cold-start equivalence, cached emission, live overwrite on the same object, pending engine-gated calls (completion / engine failure / wallet deletion), renames inside the window, cancelled post-logout writes, corrupt cache files, saver timing, and the
otherMethodspre-engine guarantee. One deviation from the TDD's test 6 wording: when a wallet is deleted mid-wait its pixie tree is destroyed, so a pending call rejects with redux-pixies' shutdown error rather than the does-not-exist message; the guard for the does-not-exist path is still in the waiter, and the test asserts rejection (no dangling promise) either way.Phase 2: gentle engine scheduling (TDD section 8)
Cached wallets no longer race all of their heavy startup work (repo sync, key derivation,
makeCurrencyEngine) in the seconds after login. A per-context scheduler runs that work for at most 8 wallets at a time; wallets without a cache bypass the queue entirely because they cannot emit until the work runs, keeping first login byte-identical to the cold path above. A wallet moves to the front of the queue when the app asks for it:waitForCurrencyWallet(both the account method and the internal selector), the internal engine/storage waiters behind every engine- and repo-backed method, andchangePaused(false). A wallet deleted or logged out while queued gives up its slot without creating an engine.This PR also carries the one #709 commit William called correct: the
balanceMapreducer keeps the existingMapwhen an engine re-reports an unchanged balance, so memoized reducers, the cache saver, and yaob's===diffing see no phantom update.New deterministic tests (
engine-scheduler.test.ts) cover concurrency-limited draining, front-of-queue bumping, cold-wallet bypass, deleted-while-queued, andbalanceMapidentity.Phase 3: account startup cache (TDD section 8.1)
The account boot itself now bypasses the repo sync and file loads on a warm login. A new
accountCache.jsonon the account's local disklet holds what the deferred loads would produce (wallet states, custom token definitions, plus alegacyWalletsflag); right afterwaitForPlugins, a newACCOUNT_CACHE_LOADEDaction seeds that state (includingkeysLoaded) andmakeAccountApiemits immediately.loadBuiltinTokens,addStorageWallet, and the file loads still run and overwrite the seeded state authoritatively, with dirty-wins guards (per-id for wallet states, whole-map for custom tokens and plugin settings) so user changes made during the window survive the overwrite. The deferred chain retries transient failures up to 3 times, since the GUI already holds the account. The cold path (no cache file) boots exactly as before, regression-guarded by a gated test.currencyWalletIds, one loader reads every active wallet'spublicKey.json+walletCache.jsonconcurrently and seeds them all in a singleCURRENCY_WALLETS_CACHE_LOADEDdispatch (the wallet reducer's filter hands each wallet its own seed), so a warm login costs two seeding dispatches total instead of two per wallet. The per-wallet read inside the wallet pixie stays as the fallback for cold logins, bulk misses, and wallets activated after login, and now costs one dispatch instead of two.EdgeDataStoreresolves its disklet lazily, the account/walletdisklet/localDiskletgetters fall back to disklets built directly from the keys (same files, same encryption), and repo-backed calls pend instead of throwing:changeWalletStateswaits for the repo,syncwaits foraddStorageWallet(rejecting on logout/deletion or a terminal boot failure), plugin-settings writes wait for the settings load (writing earlier would rebuild the on-disk map from an incomplete Redux map), andchangeEnabledTokenIdswaits for the plugin's builtin token definitions. The accounttokenSaverdefers (never drops) a custom-token write made before the repo exists, the pixie watcher never adopts account state it could not deliver to a not-yet-created engine, and user changes made while a boot-time file load is in flight win over the value the load read (name, fiat, wallet settings, wallet states, custom tokens, plugin settings).userSettings/swapSettingsare deliberately NOT cached: unlike wallet states and token definitions, they can hold credentials (custom node auth, API keys) and previously never left the encrypted repo. Cached wallet states are unauthenticated local data; a tampered file can only transiently hide or reorder wallets until the authoritative load lands (same attacker model as the existing plaintextpublicKey.json/walletCache.json).legacyWallets: trueand the next login boots cold rather than briefly hiding those wallets.publicKey.jsona second time; the seeded public info (or the fallback read) is passed intogetPublicWalletInfo.New tests cover TDD cases 12-14: the cache-coverage exhaustiveness test (every
EdgeCurrencyWalletproperty must be classified cache-seeded, engine-gated, or engine-free, so new properties force a caching decision), warm account login with the cold path blocking exactly as on master, the two-dispatch bulk seed count with fallback seeding for wallets activated after login, plus fresh-process warm login (a brand-new context with only the cache files on disk), stale-cache overwrite, corrupt-file fallback, and cancelled post-logout saves.Phase 4: write-path staleness fixes (TDD section 8.2)
A write-path audit of the boot window confirmed four gaps where a load racing an in-window user change could lose data; all reuse existing patterns, no new surfaces:
CustomTokens.jsonwholesale from Redux, so a write beforecustomTokensLoadedwould delete tokens another device had synced. It now returns without adopting the diff until the load lands (d8c48b20).ACCOUNT_CUSTOM_TOKENS_SAVEDonce the saver writes (7a809228); enabled tokens merge per toggled id, andchangeEnabledTokenIdsapplies the caller's change as toggles over the current list, so a call built against a stale cached list cannot erase another device's enablement (3bbc97fd); plugin/swap settings track dirty plugin ids per map, and the writers merge into the freshly read file instead of rebuilding it from Redux, serialized per account so concurrent local writes cannot clobber each other (e9027da6).changeWalletStateswaits forwalletStatesLoaded(audit 5.1.3): a change diffed against cache-seeded records could no-op and then be silently reverted by the load; it now bases the written record on loaded state (dc1cb707).getActivationAssets,activateWallet,getDisplayPrivateKey, andgetDisplayPublicKeywait via a sharedwaitForCurrencyEngineselector (also backing the wallet's owngetEngine) instead of throwing pre-engine, and read the account's wallet list after the wait so wallets that loaded during it are included (987632ca).currencyWalletIdsjoined the account-cache saver's ref-compare set (audit 5.3, theoretical;9b4fe78b).New tests cover the audit's four two-device scenarios (TDD cases 15-18), producing the repo-ahead-of-cache divergence deterministically by stalling the cache saver for a session (
16ff1fef). Reaching them needed the fake sync server to accept the hash-suffixed store routes it hands out, which previously 404'd every repo's second sync inside a fake world (1e2d4808).Phase 5: receive-address cache + otherMethods name cache (TDD sections 5.1/5.6, decisions 9.4/9.8)
Review feedback flagged that the receive/QR path always waits on the engine, and neither implementation ever cached addresses. Both additions follow the established observe-dispatch-persist-seed pattern:
2ea1c016, hardened inbb3a1690): the engine's answer to the defaultgetAddressesquery dispatches into Redux (keyed per tokenId, balances stripped), persists inwalletCache.json(schema v2; version-1 files upgrade on read so no device loses its warm boot), and seeds on login. A new optionalEdgeCurrencyInfo.hasStableAddresseshint gates the pre-engine serve; it defaults to false, so rotating (UTXO-style) and unflagged chains keep exactly today's engine wait. No plugin sets the hint yet: this lands the core mechanism only, and flagging account-based chains is a follow-up in their own repo with its own in-app proof.e1f7c428, hardened inbb3a1690): the engine's method names persist in the wallet cache, andwallet.otherMethodsexposes one delegating stub per known name: each awaits the engine, resolves against the live engine on every call (a resync never leaves a stale capture), forwards through the source object (preservingthis), and rejects cleanly if the loaded engine lacks the method. The object keeps its identity when the cache already names every method (the common warm boot); a newly discovered name rebuilds it once, because yaob facades cannot gain properties after first crossing (verified empirically in review). On warm logins the FioActions "fetchFioAddresses is not a function" class retires: the stub exists before the engine does.f5f0e64c,eeabe437): each plugin's otherMethods names persist inaccountCache.json; the live plugin's object stays exposed verbatim (identicalthisand non-function properties), with the cached names only building fallback stubs if plugin loading ever defers past the account emit.New tests (
b0346238) cover the stable-flagged serve, the rotating-chain gate, the pre-engine stub call, the stale-name rejection, the version-1 upgrade + post-engine stub growth through the bridge, config-level persistence, and the cache-coverage classification's new cache-assisted set.Phase 6: provisional receive address for rotating chains (TDD sections 5.4/7.5, decision 9.9)
Review feedback flagged the receive/QR path as slow on rotating chains, which wait on the engine for a fresh address. Phase 6 lets the receive scene show the cached address immediately and reconcile.
allowCachedopt-in:getAddresses/getReceiveAddresstake anallowCachedoption that serves the cached address pre-engine on any chain, not justhasStableAddressesones. The flag is stripped before the engine call and only the receive scene passes it, so programmatic callers (payments, action queue, loans) stay engine-gated and never latch a reused address. Paired with a fix for a terminal-boot-failure wedge (c532ebee: clearbulkWalletSeedPendingonACCOUNT_LOAD_FAILEDso wallet pixies fall back to their own reads instead of never starting).hasStableAddresseson every account-based chain; UTXO chains stay unflagged and use the provisional path. It is a deliberate product change accepting informed reuse in the short pre-engine window for an instant receive screen (decision 9.9).New tests cover the rotating-chain
allowCachedserve, the programmatic gate, andforceIndexbypass (TDD cases 26-27).TDD (pinned, live): implementation divergences and the decisions are documented inline in the affected sections.
Asana: https://app.asana.com/1/9976422036640/project/1213843652804305/task/1216673467164267
Note
High Risk
Large login/boot refactor touches wallet lifecycle, multi-device merge semantics, and address serving before engines load; regressions could affect funds display, receive QR, or synced settings.
Overview
Warm boot seeds Redux from
accountCache.jsonand per-walletwalletCache.json, emits the account and wallet APIs before repo sync and engines finish, then lets authoritative file loads overwrite cache while dirty-field merges preserve in-window user edits and cross-device sync.Engines start behind an 8-wide queue for cache-hit wallets only;
waitForCurrencyWallet, engine/repo-backed methods, and un-pause bump priority. Wallet methods await the engine or storage repo internally instead of throwing pre-load;waitForCurrencyWalletmeans “API exists,” not “engine ready.”Also adds
hasStableAddresses/allowCachedfor provisional receive addresses, delegatingotherMethodsstubs from cached method names, throttled cache savers, serialized plugin-settings and storage sync writes, and fixes (custom-token write gating, fake sync hash routes, stablebalanceMapidentity).Reviewed by Cursor Bugbot for commit c532ebe. Bugbot is set up for automated code reviews on this repo. Configure here.