Every extension page (the toolbar popup, a dApp approval window, the background's backgroundRefresh()) holds its own in-memory `state`, loaded once, and showView() saves on every navigation. saveState() wrote the entire state blob, so any second page that saved overwrote whatever another page had written since -- a whole wallet, name, addresses and encrypted secret included, with no attacker and no unusual input. saveState() now re-reads storage, diffs the persisted fields against a deep-cloned baseline snapshot taken at this page's last loadState()/saveState(), and writes only the fields that differ. Every other field is carried forward from storage in its loaded-and-normalized shape (normalizePersisted(), shared with loadState()), so a legacy or malformed record a load has always self-healed in memory keeps getting written back even on a save that touched something unrelated. showView() fires saveState() without awaiting it, so two saves from the SAME page can be in flight at once; a FIFO queue serializes them. Deliberately not done, a documented deviation from the plan on the issue: the live `state` of a field this page does not own is not rehydrated from what another page wrote, only the persisted record is. Adopting a concurrently-written value into `state` reintroduced the same clobber one page later, under the fire-and-forget saveState() calling convention every view uses -- caught red by tests/txStatus.test.js. Two writers of the same field still resolve last-writer-wins, documented at the merge point. tests/stateMerge.test.js covers both required cases against the real state.js and showView(): a save from a page loaded before a wallet was added elsewhere, and the approval-window reproduction from the issue. Both were confirmed failing against the prior full-blob write before this fix landed.
8.0 KiB
8.0 KiB