fix: detect KeepKey hot-swap + drop stale cache on device change#49
Merged
BitHighlander merged 3 commits intodevelopfrom Apr 24, 2026
Merged
fix: detect KeepKey hot-swap + drop stale cache on device change#49BitHighlander merged 3 commits intodevelopfrom
BitHighlander merged 3 commits intodevelopfrom
Conversation
The extension treated any reachable vault as a reachable device, with no
validation that the device currently paired with the vault is the same
one whose pubkeys we cached. That made three failure modes silent:
1. Hot-swap on same vault — checkKeepKey kept state.deviceConnected=true
and never re-probed, so addresses/balances pinned to the old device
indefinitely.
2. Extension reload with a new device — init() loaded the old pubkey
cache without comparing deviceId against the freshly-probed one, so
cached addresses survived even a full BEX reload.
3. Vault auth rejection (stale apiKey after re-pair) — fetchPubkeys
caught the error and silently fell back to cached pubkeys from the
previous device, masking the re-pair requirement.
Four narrow changes:
- wallet.init() now compares cached deviceInfo.deviceId to the freshly-
probed deviceId when a device is reachable. Mismatch → clear storage
before fetchPubkeys runs. (Covers case 2.)
- wallet.handleDeviceSwitch() primitive clears in-memory pubkeys/paths,
resets the deviceConnected flag, and wipes pubkeyStorage so the next
fetch rebuilds from the new device. Keeps SDK alive — only the
device-specific state is dropped.
- background/index.ts checkKeepKey now runs a periodic (30s, throttled)
getFeatures re-probe while deviceConnected. Compares before/after
deviceId; on mismatch calls handleDeviceSwitch which clears balance
cache, per-chain address caches (Solana/Tron/TON), and triggers a
fresh pubkey+balance refresh. (Covers case 1.)
- fetchPubkeys no longer silently falls back to cache on auth errors.
Detects 401 / "unauthorized" / "not paired" shapes, clears the stored
apiKey, and throws a user-facing message for the sidebar to surface.
Non-auth failures (network blip, device busy) still fall back to cache
as view-only — that path is correct. (Covers case 3.)
No user-visible UX change on the happy path. Recovery from device swap
is now automatic within ~30s on steady state, immediate on extension
reload.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…atch handleDeviceSwitch cleared state.paths to [], but the caller (background checkKeepKey) then calls wallet.refreshPubkeys() which only probes + fetches — it does not repopulate paths. fetchPubkeys maps state.paths into the batch sent to the device, so an empty paths array meant an empty batch, meaning the hot-swap recovery finished with zero pubkeys and zero balances instead of the new device's view. Reset to getDefaultPaths() on switch so refreshPubkeys has something to query against. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
refreshPubkeys only re-runs the default-path batch via
wallet.getPublicKeys — that batch covers BTC / LTC / ETH / Cosmos /
etc., but *not* SOL / TRX / TON. Those three are derived dynamically
at onStart via prefetchSolanaPubkey / prefetchTronPubkey /
prefetchTonAddress (each calls its chain's `*GetAddress` method and
stashes the result in a per-chain cache + adds a pubkey to state).
After a hot-swap:
- handleDeviceSwitch clears the per-chain caches (resetXState)
- refreshPubkeys repopulates the default batch
- but nothing re-runs the three prefetches
→ SOL/TRX/TON vanish from the network dropdown / asset list / balance
fetch until the user reloads the extension or manually navigates to
those chains, which racy-triggers the lazy getAddress in each handler.
Fan out all three prefetches in parallel via Promise.allSettled after
refreshPubkeys. Non-throwing by design, so if one chain's derivation
fails (minFirmware mismatch, etc.) the other two still recover.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The extension treated any reachable vault as a reachable device, with no validation that the device currently paired with the vault is the same one whose pubkeys we cached. Three failure modes became silent:
Fix (four narrow changes)
Recovery from device swap is now automatic within ~30s on steady state, immediate on extension reload.
Test plan
Dependencies
None — sits cleanly on develop. Independent of PR #46, #47, #48.
🤖 Generated with Claude Code