fix: derive hasWallet from the wallet list on load (closes #195) #200
Reference in New Issue
Block a user
Delete Branch "fix/issue-195-loadstate-haswallet"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #195.
loadState()tookhasWalletstraight from storage, so a profile persisted withthe flag out of step with
walletsstayed broken on every subsequent load, notjust until the next write.
Approach
loadState()setsstate.walletsfirst and then derivesstate.hasWallet = state.wallets.length > 0, discarding the persisted value.Derive rather than reconcile-and-persist: a load then has no storage side effect,
there is no migration bookkeeping, and the correction cannot itself fail to be
written. The next ordinary
saveState()normalizes the stored blob anyway. Nogeneral migration framework was added, and the write path is untouched.
Why deriving is safe: every read enumerated
#195 requires that this be
established rather than assumed. Deriving is only safe if no consumer wants
hasWalletto mean something other than "the wallet list is non-empty" — an"onboarding was completed" or "a wallet existed once" marker would be destroyed
by it.
Enumerated with
git grep -n hasWalletover tracked files at this branch'scurrent base (
b9bc226). There is no bracket-notation (state["hasWallet"]),destructured or case-variant access anywhere in the tree, so the literal grep is
exhaustive.
Reads — five, in four places:
src/popup/index.js:265if (!state.hasWallet)gates the welcome view against the wallet list at popup init, immediately afterloadState(). Means "the user has a wallet".src/popup/views/deleteWallet.js:73if (!state.hasWallet)chooses the post-delete view (welcome vs. back to main). Reads the flag immediately afterremoveWalletFromState()set it, so it means "any wallets remain".src/shared/walletDelete.js:37,41,54removeWalletFromState().state.hasWallet = state.wallets.length > 0) in the same invocation — same expression.src/shared/state.js:52saveState()persists the in-memory value.Writes — every one keeps the flag equal to
wallets.length > 0:src/popup/views/addWallet.js:144,200,252— settrueimmediately afterpushing a wallet.
src/shared/walletDelete.js:35— set from the remaining wallet count.src/shared/state.js:12— defaultfalse, alongsidewallets: [].src/shared/state.js:91— the derivation itself.No consumer depends on the two disagreeing, so the derivation preserves every
read.
Correction to the earlier revision of this description: it claimed
src/popup/index.js:265was the only read. That was false — the enumeration hadnot been re-run after the rebase onto
#156, which added
deleteWallet.js:73and thewalletDelete.jsreads. The table above is a freshenumeration at the current base.
Demonstrated failing first
tests/state.test.jsis new: it stubschrome.storage.localbefore requiringsrc/shared/state, resetting the module registry per case becausestateis amodule-level singleton. Against the unfixed
state.js(derivation reverted tostate.hasWallet = saved.hasWalletlocally, tests unchanged),make test:Exactly those four, and nothing else — no other suite is disturbed by the
revert. Re-run at the current base rather than carried over from the earlier
revision of this description, whose transcript predated
#156 and
#163 landing (it showed the
then-current 8 suites / 151 tests).
Both directions of the inconsistency are covered, plus the consistent cases,
empty storage, no write on load, and that the rest of the persisted fields still
load.
make checkRebased onto current
nextatb9bc226. TheTODO.mdCompleted Steps conflictwas resolved by keeping both entries.
make checkre-run after resolving, at2589473:All 157 executed, no cached results. Nine suites, so this includes
tests/walletDelete.test.jspassing unchanged against the derived flag.Not verified here: the popup landing on the welcome screen is asserted at the
state layer only.
src/popup/index.js:265is a single read ofstate.hasWalletwith no other input, so a correct derivation determines it, but no DOM-level or
make test-e2erun is included in this change.a451604e59to27e37b9976FAIL —
needs-rebase.1. Conflicts with current
next(blocking).nextmoved to19cb1ca(#163 landed) after this branch was rebased ontob882ced.git merge-tree --write-tree origin/next HEADgivesCONFLICT (content): Merge conflict in TODO.md— #163 added a Completed Steps entry at the same position as this one. Gitea now reportsmergeable: false. Acceptable: rebase onto19cb1ca, keep both entries, re-runmake check.2. The exhaustiveness claim is false (blocking; fix during the rebase). The PR body says "
src/popup/index.js:265— the only read", under "Every occurrence in the tree (grep -rn hasWallet, excludingnode_modules)", and the commit message states "The only consumer is the popup's welcome-vs-wallet-list gate insrc/popup/index.js". My owngrep -rn hasWalletat head27e37b9returns a second read:src/popup/views/deleteWallet.js:73—if (!state.hasWallet) {, introduced byb882ced(#156), i.e. by the very commit this branch was rebased onto.src/shared/walletDelete.js:37,41,54also read the flag (reads-after-write withinremoveWalletFromState).The body was updated post-rebase to add
walletDelete.jsas "a fourth writer", but the read enumeration was not re-run against the new base. This matters because #195 explicitly requires that no consumer depending on the flag differing fromwallets.length"must be established first rather than assumed" — the whole safety argument rests on the enumeration being complete, and the commit message is a permanent record of a claim that is not.No behavioural defect results: I verified
deleteWallet.js:73reads the flag immediately afterremoveWalletFromState()sets it tostate.wallets.length > 0, so it means "any wallets remain" — the same semantics as the derivation — andwalletDelete.js's reads are reads-after-write of that same expression. No consumer wants "onboarding completed" semantics. Acceptable: commit message and body enumerate both reads (src/popup/index.js:265,src/popup/views/deleteWallet.js:73) and state why each is safe.3. CI never observed green on the head commit.
check / check (push)on27e37b9is stillpending/ "Waiting to run" (run 460). Moot after the rebase, but the rebased head must go green. Disclosure: theclawbotaccount gets 403 on listing action runs, so I could not inspect the run directly.Verified and passing:
make checkon27e37b9green in my own clone (9 suites / 157 tests, all executed, no cached results;prettier --checkand fmt-check clean). Tests have teeth — reverting the derivation tostate.hasWallet = saved.hasWalletproduced exactly the 4 claimed failures and nothing else; no automock ormoduleNameMapperinpackage.json, so the real module is exercised, and stub-before-require is genuinely required sincestorageApiresolves at require time. Assignment order is safe (state.walletsdefaulted with|| []before.length); first-run/empty storage covered. Agrees withsrc/shared/walletDelete.js— identicalstate.wallets.length > 0invariant, write path untouched,walletDelete.test.jspasses unchanged. Single commit titled(closes #195), basenext, oneTODO.mdline, no scope creep, no attribution trailers, no Claude/Anthropic references in the diff.The disclosed caveat (welcome screen asserted at the state layer only) is accepted:
src/popup/index.js:265is the sole gate with no other input, so a correct derivation determines it.27e37b9976to2589473500PASS.
Independent re-enumeration at
2589473agrees exactly with the record: five production reads in four places (src/popup/index.js:265,src/popup/views/deleteWallet.js:73,src/shared/walletDelete.js:37,41,54,src/shared/state.js:52), every one meaning "the wallet list is non-empty", no consumer wanting "onboarding completed". Negatives confirmed independently: no bracket-notation, no destructuring offstate, no case variants, no spread of the persisted blob, andsrc/background/index.js:42reads theautistmaskblob separately but never touches the flag. Reverting the derivation myself reproduces exactly the claimed4 failed, 153 passed, 157 totaland nothing else.src/shared/state.jsandtests/state.test.jsare byte-identical to the pre-rework27e37b9blobs apart from the derivation line, and every landed Completed Steps entry survived the rebase.make checkgreen in my own clone (9 suites / 157 tests, all executed, no cached results). CIcheck / check (push)issuccesson2589473(run 477).Anomalies, none of them defects:
nexthas advanced tocf5f582since the enumeration was taken atb9bc226; it merges clean and I re-grepped it — README/TODO only, no newhasWalletsite.src/shared/state.js:36spreadsDEFAULT_STATEinto the livestate, an indirect propagation of the flag that a literal grep cannot surface; it seedsfalsealongsidewallets: [], so the invariant holds.README.md:383documents the flag with the same semantics.Disclosure: to reproduce the failing transcript I edited the derivation line with a scripted substitution in a throwaway clone; it was reverted and the tree left clean, and nothing in this PR was modified.