release/1.4.4 → master#243
Merged
Merged
Conversation
v1.4.3 published as prerelease.
…-derives (#234) Enabling/disabling BIP-39 passphrase protection from Device Settings left every wallet address — including 'view on device' — showing the previous (standard) wallet until a physical reconnect. Root cause is in firmware: storage_setPassphraseProtected() only flips the passphrase_protection flag and does NOT invalidate session.seedCached. The seed derived earlier in the session (empty passphrase) stays cached, so storage_getRootNode() keeps returning it even after the user enters a new passphrase. A USB unplug power-cycles the device and clears the cache, which is why reconnecting 'fixed' it. Fix without requiring a reconnect: engine.applySettings() now sends ClearSession after toggling usePassphrase, dropping the cached seed + passphrase + PIN so the device re-prompts and re-derives from the correct seed. The device routes back through needs_pin -> needs_passphrase, which also resets the in-memory account managers and remounts the dashboard, so stale addresses/balances in the UI clear too. Skipped on the emulator. DeviceSettingsDrawer: both enable and disable now re-auth, so neither path calls getFeatures inline (would race promptPin's getPublicKeys); the drawer re-fetches features when it next opens.
…n toggle (#235) * fix(passphrase): clear device session on passphrase toggle so seed re-derives Enabling/disabling BIP-39 passphrase protection from Device Settings left every wallet address — including 'view on device' — showing the previous (standard) wallet until a physical reconnect. Root cause is in firmware: storage_setPassphraseProtected() only flips the passphrase_protection flag and does NOT invalidate session.seedCached. The seed derived earlier in the session (empty passphrase) stays cached, so storage_getRootNode() keeps returning it even after the user enters a new passphrase. A USB unplug power-cycles the device and clears the cache, which is why reconnecting 'fixed' it. Fix without requiring a reconnect: engine.applySettings() now sends ClearSession after toggling usePassphrase, dropping the cached seed + passphrase + PIN so the device re-prompts and re-derives from the correct seed. The device routes back through needs_pin -> needs_passphrase, which also resets the in-memory account managers and remounts the dashboard, so stale addresses/balances in the UI clear too. Skipped on the emulator. DeviceSettingsDrawer: both enable and disable now re-auth, so neither path calls getFeatures inline (would race promptPin's getPublicKeys); the drawer re-fetches features when it next opens. * feat(onboarding): make passphrase tip non-skippable with inline opt-in toggle The post-setup 'Hidden Wallets' tutorial card is now mandatory and carries the real passphrase toggle, so every new user makes a deliberate choice instead of skipping past the explanation. - TutorialCards: cards can be marked interactive:'passphrase' + nonSkippable. The passphrase card renders an on/off toggle and a 'change anytime in Settings' hint, and hides its Skip button. Next shows a spinner while the choice is applied. - OobSetupWizard: the toggle choice is held locally (default OFF) and applied via applySettings({usePassphrase:true}) only when the user clicks Continue, then the wizard advances to 'complete' (the device's PIN/passphrase re-prompt overlays follow naturally). Skipping an earlier tip now lands on the passphrase card so it cannot be bypassed. - en/setup.json: toggleLabel + settingsHint strings (defaultValue fallbacks keep other locales working until translated).
…#236) * feat(onboarding): one-time passphrase intro dialog on first dashboard Every new Vault install now sees a one-time, informational hidden-wallet / passphrase dialog the first time they reach the dashboard — so users with an already-initialized device (who never go through the OOB flow) still learn the feature exists and how to enable it. - New persisted setting passphrase_intro_shown, exposed via getAppSettings (AppSettings.passphraseIntroShown) + a markPassphraseIntroShown RPC. - PassphraseIntroDialog: info-only modal (reuses the hidden-wallets explainer + 'change anytime in Settings' hint, 'Got it' to dismiss). z-index below the device PIN/passphrase overlays so prompts always win. - Dashboard shows it once when settings load with passphraseIntroShown=false (skipped in watch-only); dismiss persists the flag so it never reappears. - OOB: completing the passphrase tip card marks the intro shown, so new-device users aren't educated twice. * fix(onboarding): await intro-shown persistence before completing OOB Awaiting markPassphraseIntroShown before setStep('complete') closes a race where the dashboard's on-mount getAppSettings read could beat the fire-and- forget write and re-show the one-time passphrase intro to a user who just saw it in the OOB flow. Best-effort: on RPC failure we still complete.
…unresolved The quote is gated by canQuote, but the quote effect only surfaces a reason for two of its failure modes (destAddressError, toAddressIsXpub). Every other false condition returns silently, leaving the YOU RECEIVE panel blank — no quote, no error, no loading state. The trigger is an asymmetry: fromAddress falls back to the device-derived evmAddresses (independent of the balance cache), but the destination address read only the balance cache and the address resolver runs for UTXO chains only. When the balance cache is empty (e.g. balance server unavailable), an EVM destination's toAddress is '' with no error, so canQuote is false and the quote is never attempted. - cachedToAddress falls back to the EVM device address for EVM destinations, mirroring fromAddress — fixes the blank panel at the source. - Add a fallback "Get Quote" button + reason text in the empty receive slot, shown only in the address dead-end; it derives a missing non-EVM destination address on demand, refreshes balances, and re-fires the quote. - Show the enter-amount hint for 0/invalid amounts instead of a blank panel. - New i18n keys: resolvingSendAddress, resolvingReceiveAddress.
… button to dest Addresses two review findings on the manual Get Quote fallback: - Stale-address race: handleManualQuote could write a destination address for the wrong chain if the user switched the output asset while the (up to 60s) address-derivation RPC was still pending. Capture the requested chainId and discard a late result (and a late error) if the output asset changed — the auto-resolver useEffect already had this guard via a cancel flag. - Button scope: the fallback button now renders only when a DESTINATION address is missing (the case handleManualQuote actually derives). A missing SOURCE address is a transient the quote effect re-runs automatically once fromAddress populates, so it shows the reason text without an inert button.
The destination-address resolver ran for UTXO chains only, so for non-EVM non-UTXO destinations (Solana, Cosmos, Tron, XRP, ...) the receive address stayed '' whenever the balance cache was empty — canQuote never went true and the quote was never attempted automatically. Only the manual "Get Quote" button (which derives on demand) produced a quote. Resolve from the device for any non-EVM destination (EVM still uses the cache-free evmAddresses fallback in cachedToAddress), so auto-quoting works without the button. The existing cancel flag already discards a stale result if the output asset changes mid-derivation.
…-memory swap/activity for hidden wallets **Root cause:** isPassphraseWallet gates block LIVE/DISPLAY state, not just disk persistence. The privacy invariant is 'never write hidden-wallet data to disk, but operate live from the device.' Three consumers read only the persistence-coupled layer (intentionally empty for hidden wallets) instead of falling through to live device/server paths. **Symptom 1 — 'Your KeepKey is empty' swap picker:** SwapDialog seeds balances only from getCachedBalances (null for hidden wallets). Added live getBalances fallback on open, mirroring the Dashboard pattern (Dashboard.tsx:903-921). getBalances derives addresses live from device and persists nothing for hidden wallets. Also fixes non-EVM FROM address dead-end (fromAddress reads balances.find()?.address which is empty for hidden wallets). **Symptom 2 — 'No supported routes — only NEAR Intents':** parseQuoteResponse picked quotes[0] unconditionally, so a single unbuildable head quote killed the pair even with a buildable route behind it. Now scans for the first buildable quote (using the exact throw predicate), logs loudly when skipping higher-ranked unbuildable quotes (no silent rate downgrade). Re-throws the head-quote error when none are buildable (preserves the existing user-facing message). **Symptom 3 — In-session hidden-wallet swaps invisible in Activity:** getPendingSwaps short-circuited to [] before reading the in-memory map. ActivityTracker skipped fetchSwaps() for hidden wallets. Relaxed both to read the RAM-only pending swaps (scoped to walletId, never leaks to DB). **Symptom 4 — Fresh passphrase entry leaves getWalletDbScope() null:** sendPassphrase() nulls seedEthAddress and never re-derives it; reconnect probe sets it but is gated on !passphraseSetThisSession. Added in-memory ETH address derivation in sendPassphrase for hidden wallets (RAM only, NO setSetting disk write — preserves plausible deniability). **Symptom 5 — Activity panel empty / rescan no-ops for hidden wallets:** Added write-free getLiveActivity path: scanChainHistory with dryRun+collectRows fetches live from Pioneer, returns rows via in-memory session store (never written to disk). getRecentActivity serves session store for hidden wallets. Session store cleared on needs_passphrase/disconnect/seed-changed. Privacy invariant preserved: every DB-write gate stays. Only display/live-lookup gates are relaxed. All write consumers of getWalletDbScope() are correctly double-gated on !isPassphraseWallet. Tests: 3 new B1 tests pinning buildable-quote selection behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pioneer's /swap/available-assets carries no SPL tokens, so the backend decimals lookup misses for e.g. USDT-on-Solana and buildTx throws "tokenDecimals required for SPL token transfers" — the preview/build dead-ends. Forward the picker asset's decimals through ExecuteSwapParams (Pioneer's canonical value still wins when present); the SPL builder hard-throws when both are missing rather than guessing scale. Adds solana-spl-decimals.test.ts pinning the build contract and wires it into the test-unit suite. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
getSwapByTxid and refreshSwap had a blanket isPassphraseWallet block, but hidden-wallet swaps are in-memory only (skipPersist — no DB row), so the block left a hidden session unable to read its OWN swap (detail view stuck "pending") or advance it (the live poll is the only thing that can move a skipPersist swap to completed, e.g. a NEAR Intents Solana deposit on 1Click). Replace the blanket block with walletId-scoping plus a live tracker fallback: a hidden walletId (deviceId:hiddenSeedEthAddr) never matches a standard swap's walletId, so standard-wallet swaps stay invisible from a hidden session — same posture as getPendingSwaps. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Show a spinner in the FROM picker and asset list while balances/assets load, instead of a bare empty state. - Surface a "Get Quote" button in the input view when a quote can fire but none is present (e.g. after returning from the confirm step), so the receive panel is never a silent blank. - Forward SPL picker decimals from the dialog's preview and execute calls (pairs with the backend tokenDecimals fix). - Harden canQuote (explicit boolean) and key the quote effect on asset CAIP rather than asset id so a switch between same-id assets requotes. - Back button resets quote state and re-fires the quote via requoteTick. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(swap): resolve silent quote dead-end when destination address is unresolved
…er.iss comment (#233) The per-user-install warning comment in InitializeSetup wrapped onto a line beginning (after indentation) with "[Code]". Inno Setup splits sections by scanning for lines that start with "[" before it parses Pascal { } comments, so it read "[Code] MsgBox, ..." as a malformed section header and aborted with "Error on line 96 ... Invalid section tag", breaking the Windows installer build (ISCC exit 2). Reword the comment so no line starts with "[". Comment-only change; no installer behavior change. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GetPendingSwap on a freshly-created swap reliably 404s ("Pending swap not
found") because Pioneer registers the row asynchronously — noisy in logs
with nothing to gain, since the swap already renders 'pending' from the
in-memory record. Skip the Pioneer poll for a 30s grace window after the
swap's createdAt; a manual rescan bypasses it. Hydrated/old swaps fall
through normally (createdAt is well past the window).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sending to an address not yet in the Address Book now pops a dialog after broadcast: warns that funds are leaving the wallet, shows the external address, and offers to save + label it. Saving is explicit (R4 opt-in) — "Not now" leaves the address out of the book. - recordOutbound now leaves auto-recorded external rows with saved_at=null (history-only) and reports `unsaved`; the book + send picker only show explicitly-saved contacts (listAddressBook savedOnly). - addExternalEntry stamps saved_at on explicit save; migration backfills existing labeled / manually-added rows so they stay visible. - Address Book rows display the date a contact was added. - Replaces the prior subtle inline label prompt with the SaveRecipientDialog.
…en wallets) The Address Book is public, non-secret data — your watch-only wallet addresses + saved contacts — so it should not be tied to which wallet is active. Previously every address-book RPC blanket-returned empty in a passphrase/hidden session, so the book showed nothing (no device wallets, no contacts) and sends couldn't be saved. - listAddressBook no longer gates on passphrase; own (cross-device) and external (cross-wallet) entries load in any session. Only seedOwnFromCache (a write) stays standard-only. - add/update/delete/history RPCs de-gated; mutations act globally by id (wallet_id is retained as provenance/dedup, no longer a read/edit scope). - broadcastTx: split api_log (kept standard-only for tx deniability) from the address-book capture, so recipients can be saved from hidden-wallet sends. Note: a contact saved while in a hidden wallet is therefore visible from the standard wallet — the address book is intentionally outside hidden-wallet deniability (api_log/balances writes remain gated).
Move address-book detection to where it's useful — while entering the
recipient — instead of after the funds have already left.
- New matchAddress RPC + matchAddressBook() (exact networkId equality, R5):
returns the matching own wallet / saved contact, or null for a new address.
- SendForm runs a debounced matchAddress as the recipient is entered:
- known → show the contact's identicon + label (gold chip)
- new valid address → rose "New address" chip + auto-open the save dialog
(once per address; also reachable via the chip's Save link)
- Saving now happens at form-fill (before sending); the post-broadcast save
dialog + its confirmation are removed. recordOutbound still records
outbound history on broadcast.
Dialog warning reworded for the pre-send context (verify the address).
…dialog feat(addressbook): opt-in save dialog for new external recipients
…s underneath them Recurring customer bug: the UI showed one wallet's addresses/balances while the device OLED showed another's (field report: UI 0x4C1922..., device 0x27de622c...). The in-memory managers (btcAccounts/evmAddresses) are kept across disconnect for watch-only and only re-derive when empty; the resets that should empty them are event-INFERRED and every trigger has a blind spot: - needs_passphrase reset: skipped on reconnect with a pre-cached passphrase (device goes straight to ready) - device-switch reset: skipped when deviceId is unchanged - seed-changed: skipped on hidden->standard transitions — hidden wallets never persist seed_eth_<id> (privacy), so the stored identity is the standard wallet's and MATCHES once the device returns to it; no event fires - passphrase toggle: applySettings+clearSession resets the engine's fingerprint/identity but never the managers Fix checks the RESULT instead of the cause. The seed-identity address (ETH m/44'/60'/0'/0/0) is bit-for-bit evmAddressPath(0), so the EVM manager's index-0 address must equal the device-derived identity. One invariant covers every cause, including future ones. Two legs: - getBalances: derive the identity FRESH from the device (new RAM-only engine.deriveSeedIdentity) on every fetch and reconcile before the init guards — every balance fetch is now self-verifying against the device - state-change(ready): reconcile against engine.currentSeedEthAddress once checkSeedIdentity / hidden-scope derive / reconnect probe classify the session (probe's confirmed-hidden path now re-emits state-change so the guard actually runs there) On purge: reset both managers (init guards re-derive), push empty account sets, wipe the deviceId-scoped DB cache (standard sessions only — a stale fetch classified as standard may have already persisted the WRONG wallet's rows; hidden sessions never persist and their rows must not be touched), and notify the frontend via new 'wallet-data-purged' message. Dashboard clears the displayed balances immediately and force-refreshes (in-flight fetches self-heal — the purge runs at their start). Never purges on uncertainty: unknown identity or uninitialized managers no-op, so cold start and watch-only keep their data. evmAddressPath moved to shared/chains.ts (pure, test-importable — src/bun pulls ./db -> electrobun import side effects that break bun test) and re-exported from evm-addresses for existing importers. Tests: __tests__/seed-reconcile.test.ts pins the path equivalence the invariant rests on, the never-purge-on-uncertainty rule, both transition directions, and the exact field-report scenario. Full suite 365 pass / 2 pre-existing environmental failures (live :1646 vault, live Solana RPC) — identical on baseline.
…sumers Review follow-up to the stale-wallet purge. Three gaps where stale account managers could still be read before a full getBalances ran: 1. getBalance (single-chain) and buildTx read btcAccounts/evmAddresses with no reconciliation. getBalance backs AssetPage / tx-push / post-send refreshes; buildTx is the SIGNING path — a stale xpub/evmAddressIndex would build a tx against the wrong wallet's context. Added a shared ensureManagersForSeed() boundary (derive seed identity fresh from device → reconcile → purge if stale) and call it at every consumer: getBalances, getBalance, getBtcAccounts, getEvmAddresses, and buildTx. buildTx THROWS on purge so the half-prepared send is abandoned and the user re-initiates against refreshed balances. 2. The DB-cleanup on purge was gated on !isPassphraseWallet, so during the conservative cached-passphrase reconnect window it was skipped — and when the probe later reclassified to standard the managers already matched, so no second purge ever cleared the stale rows. Clearing only REMOVES rows (never writes hidden data), so it's privacy-safe: run it unconditionally. 3. reconcileSeedManagers only compared the EVM index-0 address, so a BTC-only initialized stale state (getBtcAccounts initializes BTC independently) had no proof to compare and survived. Added a seed-owner STAMP (managersSeedOwner): the seed the managers were derived under, set after every init and checked alongside the EVM index-0 proof. Either mismatch purges. All reset paths now go through resetSeedManagers() so the stamp stays in lock-step. The engine reconnect probe's confirmed-hidden branch already re-emits state-change (prior commit) so the ready-handler leg re-runs there too. Tests: seed-reconcile.test.ts adds the seed-owner-stamp (BTC-only) cases. Project suite 344 pass / 2 pre-existing env failures (live :1646, live Solana RPC). Differential tsc clean; bun + vite build clean.
fix(wallet): purge stale account managers when the device seed changes underneath them
The vault discarded Pioneer's portfolio `meta`, so a chain that failed its fresh fetch (node down/timeout) or served cache >5min old showed $0 / a stale value with no way to tell "unavailable" from "genuinely empty." - bun: preserve `meta` per chunk (unwrapPortfolioResponse) and merge across chunks (mergeMetas); after each fetch, push degraded + stale chain names to the webview through the existing pioneer-error channel (severity warning/none) - schema: extend pioneer-error with optional severity + degraded/stale fields (missing severity stays a hard error — back-compatible) - Dashboard: amber soft-fault banner (separate state from the hard-error banner, which supersedes it); window-focus refetch when idle >5min; degraded-chain background retry with 30/60/120/300s backoff - i18n: English strings only; other languages fall back via fallbackLng Server meta is live on api.keepkey.info (pioneer release/v1.3.115).
…ften degraded copy - Banner rendered only the degraded branch; a response with both degraded and stale chains silently dropped the stale warning. Render both lines. - lastFetchAttemptRef seeded at 0 made the first focus event read as 'idle > 5 min' and force-refresh on startup. Seed with mount time. - degradedDesc promised 'last known values', but vault-side chunk failures can render as zeroes. Soften the copy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(portfolio): surface degraded/stale chains instead of silent $0
Adds a discoverable front door for users who expect a larger portfolio than the Vault shows. Diagnoses then helps recover funds across chains, reusing existing engines rather than rebuilding: - BTC on non-standard paths / un-added higher accounts (sweep-engine) - EVM funds on higher address indices (EvmAddressManager.autoDiscover) - chains that silently failed to price/fetch (three-state coverage) Entry points: an "Audit" button by the portfolio total and a "Run audit" CTA in the existing degraded/stale soft-fault banner. Diagnose-first, then inline recovery (sweep to main address, track accounts) on device. Safety (no device-lease refactor — transport already serializes IO): - Identity phase is read-only (deriveSeedIdentity) — never the purge-capable ensureManagersForSeed. - BTC scan runs a gen-guarded forked loop: aborts to 'aborted' (never 'complete') on wallet-handle swap, sticky stale flag, or N consecutive device failures. End-of-scan + auditSweep re-derive the seed identity so a same-handle passphrase toggle can't finalize clean or sign stale-seed UTXOs. - Honesty model: degraded/stale -> 'unverified' (matched by chainId, not symbol); unresolvable faults -> residual 'unverified'; single-address chains -> 'checked-shallow'. The green all-clear is gated on both, so the wizard never folds a fault into "$0/clean". - Hidden wallet: full diagnosis, recovery framed session-scoped, no PDF. Backend: audit-engine.ts (orchestrator) + audit-coverage.ts (pure classifier, unit-tested) + auditStart/auditGetStatus/auditSweep/ auditDismiss RPCs. Frontend: poll-driven AuditDialog. pioneer-error gains additive chainId-granular fault fields (banner unchanged).
…stom paths Expands the Audit wizard from a flat summary into a chain-by-chain walkthrough. Each chain is an escalation ladder the user drives: verify (balance + block-explorer link) → "Expected more?" → auto-scan 3 account/index levels on-device → "Search 3 more" → custom paths (guided steppers + advanced raw BIP32) → support handoff (copy report + open support link) Recovery is on-device: BTC track account (addBtcAccount), EVM track index (addEvmAddressIndex), BTC non-standard sweep (carried from v1). Chains without multi-account infra (cosmos/xrp/solana/…) surface the address + explorer + handoff rather than a false dead-end. Generalized, reused infra (no per-family reinvention): - chain-scan.ts: family-aware derivation (rpcMethod + ripple/ton/btc quirks), Pioneer GetBalanceAddressByNetwork, explorer-URL templates, BIP32 parse/format. Pure — unit-tested in isolation. - auditScanLevels / auditDeriveCustom RPCs (read-only, gen-guarded). Honesty fixes from adversarial review (8 findings): - UTXO excluded from the single-address level scan (would misread a funded account tree as empty); BTC discovery stays the sweep's job, and "Expected more" on BTC goes straight to custom paths + handoff. - A failed per-level balance lookup is flagged (balanceError) and shown as "could not verify", never a confident "0" — in the UI and handoff. - EVM custom guided stepper varies the same account element [2] the scan and addEvmAddressIndex use (was varying the trailing index element). - Coverage no longer labels an EVM chain "empty/$0" when index discovery found funds the pre-scan snapshot predates (→ checked-shallow). - Funded custom/non-trackable paths get an explicit "send to support" note instead of a silent dead-end; EVM discovery banner shown once; checked-shallow copy is family-aware; chainLevelPath doc corrected. Hidden wallet: handoff redacts addresses; recovery framed session-scoped.
… scanners Turns the per-chain walkthrough into a high-feature recovery/debug tool. UX redesign: - Top network-logo filmstrip stepper (ChainLogo via getAssetIcon) — done checks, current/funded/unverified status rings, click-to-jump, progress line. Cleaner, less-dense hero card; pulsing-ring scan animation. - Accounts 1-3 now AUTO-SCAN on arrival (lazy) for non-EVM chains — fixes the bug where the first levels only scanned after "Expected more". The involuntary scan never blocks the close button (separate autoScanning flag). Opt-in deep scanners under "Dig deeper": - EVM known-paths grid (AuditKnownPaths) — MyEtherWallet-style check of the well-known schemes (Ledger Live, Ledger Legacy/MEW, MetaMask/Trezor, legacy, old Ledger, testnet) via auditScanPaths; funded/unverified rows route to the support handoff (can't auto-track non-standard EVM paths). - BTC wrong-script-type + gap-limit (AuditBtcDeep) — drives the existing sweepScan/sweepExecute with custom config (new gapLimitReceive/ gapLimitChange/higherReceiveLimit on SweepScanConfig; defaults preserve the prior matrix). higherAccountScanLimit:0 so the panel never counts higher-account funds it can't show. - Raw-path inspector (AuditInspector) — derive any path, read back address + pubkey + xpub + balance (auditInspectPath, getPublicKeys). Honesty (carried + reinforced by adversarial review): - New scans are read-only + gen-guarded; nothing signs but the existing device-confirmed sweep/track. - Failed balance lookups → "could not verify" everywhere (grid, inspector, ladder, handoff) — never a confident "0". Unverified known-path results route to the handoff rather than dead-ending. - Filmstrip done-check suppressed on unverified chains. EVM_KNOWN_SCHEMES live in src/shared/evm-paths.ts (shared by RPC caller + grid + tests). 210 unit tests pass; typecheck clean.
Reworks the audit into a proper guided wizard matching the onboarding flow's feel, in a large wide dialog (not the cramped 480px card): - ~880px dialog, var(--ink-1) surface, 2px gold border — the onboarding wizard's shell. Two-column body: chain hero (logo in a glowing ring, symbol, balance, coverage) on the left, explanation + results on the right. - Conversational copy at every step — explains what we're checking and what was/wasn't found per chain, instead of bare labels. - Walks every configured chain; progress bar + logo filmstrip (click-to-jump). - Replaces the single "Dig deeper" with four explicit actions: Looks right ✓ (primary, advances) · Scan more accounts · Scan common wallet paths (EVM) / Scan unusual paths (BTC) · Scan a custom path. Plus "Still missing?" handoff. Known-paths clarity fix (the "same address under different paths" confusion): the schemes collapse at index 0 — BIP44 and Ledger Live both derive m/44'/60'/0'/0/0, the user's main address. AuditKnownPaths now DEDUPES paths across schemes (by bip32 string, which matches the backend pathToBip32), groups the wallet labels per unique path, tags the main address, and shows the full derived path on every row so it's verifiable. Funded / couldn't-verify non-default paths route to the support handoff. Adversarial review: 1 low finding fixed (flex truncation). Honesty invariants preserved (couldn't-verify never shown as 0). 210 unit tests pass; typecheck clean.
auditStart was doing the full BTC sweep + EVM auto-discovery up front, then the per-chain pages re-scanned the same accounts. Now it goes STRAIGHT into the walkthrough and scans lazily per page: - auditStart does only identity (one device call) + coverage classification (pure). No device sweeps. The walkthrough opens immediately. - Bitcoin paths scan lazily when the user opens the Bitcoin page (new auditScanBtc RPC → startBtcScan; report.btcScanState idle→scanning→done; the page polls for progress and shows findings/sweep when done). - EVM (and other) accounts scan lazily per page via the existing auditScanLevels auto-scan; the up-front autoDiscover phase is removed. - classifyCoverage gains a `lazy` mode: at start nothing is deep-confirmed, so non-funded non-degraded chains read "primary address only" (checked-shallow) rather than a premature "empty" — the page confirms them. Degraded still reads "couldn't verify". Adversarial review (1 high + 2 low, all fixed): - startBtcScan now restarts from 'error' (not just 'idle') and resets accumulators, so the Retry button actually re-runs after a failed scan. - auditScanBtc gets the device-ready guard its siblings have (fail fast vs churn through device-IO retries mid-reconnect). - a rejected scan trigger now surfaces a Retry instead of an infinite spinner. 212 unit tests pass (2 new lazy-coverage); typecheck clean.
…alk, finish Reskin/restructure AuditDialog to the design handoff while keeping all real RPC wiring and three-state honesty intact: - Scan: minimized to an honest beat (sonar + constellation + "Walk me through it" gate); drops the fake "scanning BTC/EVM paths" step ladder since that work runs lazily per page. No more auto-advance. - Walkthrough: centered single column with a windowed coin-roll carousel, coverage pill, persistent "Expected more?" action bar, footer nav. Real sweep / track / scan-levels / common / custom / handoff preserved. - Finish: new summary screen (total + recovered + per-chain status). - Gold is now the primary accent; teal reserved for funded/verified. - Honesty: no fabricated USD on finds; "Total verified" downgrades to "Total balance" when any chain is unverified; added a real "check it directly" primary-address re-derive for unverified chains.
- Order the per-chain walk by what people actually hold (BTC/ETH lead via
WALK_ORDER) instead of by coverage rank.
- EVM account scans now classify native + ERC-20 tokens (parseEvmScanResult)
so a token-only address ($0 ETH, $500 USDC) is funded, not a false empty.
- Honesty fix: a degraded (200-but-failed) single-caip portfolio fetch with
nothing found maps to balanceError ("couldn't verify"), never a clean $0.
- Show full chain name + CAIP-19 chip with a tap-to-open "what's a CAIP?"
popover; render token rows with icon/amount/USD.
- BTC level-add is session-only (not persisted yet) — recovery copy says so.
The guided custom-path editor kept defaultPath[0] (purpose 84') fixed when the user changed the script type, so picking Legacy/SegWit derived a DIFFERENT address than the displayed m/84' path implied (purpose and scriptType must agree). Remap [0] to the script's purpose (p2pkh→44', p2sh-p2wpkh→49', p2wpkh→84') via the canonical BTC_SCRIPT_TYPES table.
…ound nothing)
checkAddressBalance queried Pioneer's GetBalanceAddressByNetwork with a
bip122 networkId. Despite the name that endpoint is EVM-only (route
/evm/balance/{networkId}/{address} → ETH JSON-RPC): a Bitcoin address routes
into the ETH provider, throws on the non-hex address, and the call always
returned 0. Both the audit and the manual sweep gate on `balanceSats > 0`,
so the gate never opened — funded BTC addresses on custom/mismatch/higher
paths were silently missed (the exact "where's my money" case). The
magnitude heuristic was dead code on a broken call.
Source the per-address balance from ListUnspent instead (the only Pioneer
endpoint that serves Bitcoin); its UTXO `value` is integer satoshis, so no
unit guessing is needed.
Also forward the BTC gap-limit depths (gapLimitReceive/gapLimitChange/
higherReceiveLimit) from AUDIT_CONFIGS.deep into generatePathMatrix — the
deep audit was silently using the shallow defaults (5/1/3) and missing funds
past the gap. Verified Pioneer contract: the server `depth` query param
(ListUnspent/GetBalanceByXpub, gap=20 default, capped 200) is a separate
xpub-gap-scan knob, not used by the per-address audit path.
Requires live-device verification: a known-funded non-standard BTC address
must now report correct sats.
…C custom read 0)
The audit checked single-address balances via GetBalanceAddressByNetwork at
four sites (auditScanLevels, auditDeriveCustom, auditScanPaths,
auditInspectPath). Despite the name that endpoint is EVM-only — route
/evm/balance/{networkId}/{address} → ETH JSON-RPC. A non-EVM networkId routes
a foreign address into the Ethereum provider, which rejects it; live prod
repro:
GET /evm/balance/ripple:.../rGdmDZ... → 500 "Invalid Ethereum address"
So XRP/Cosmos/THOR — and BTC/DOGE/LTC/BCH custom-path checks — all read 0.
Route every family through the cross-family GetPortfolioBalances path (the
one the dashboard already uses): new parseNativeScanResult(entries, caip) +
an auditNativeBalance(chain, address) helper. Removed the EVM-only
parseNativeBalance. Same degraded→balanceError honesty as the EVM branch.
…CH/…) UTXO altcoins offered only a custom-path search — no "accounts 1/2/3" escalation — because chainSupportsLevelScan excludes UTXO (a single-address check misreads a UTXO account tree) and runBtcScan is Bitcoin-only. So funds on a higher DOGE/LTC account were undiscoverable in the audit. Add an xpub-based per-account scan, reusing the dashboard's exact derivation: for each account N derive m/purpose'/coinType'/N' (all script types — LTC walks 44/49/84), then balance-check the xpub(s) via GetPortfolioBalances so Pioneer gap-scans receive+change server-side. New auditScanUtxoAccounts RPC returns the existing AuditDerivedAddress shape, so the walkthrough renders it with no UI rework: UTXO altcoins now auto-scan accounts 1-3 and offer "Scan more accounts" like BTC/EVM. Extracted utxoAccountScriptPaths (shared with the dashboard's account-0 derivation so they can't drift). Found funds on a non-trackable account route to the existing support handoff (in-app tracking for altcoin accounts is a follow-up). Bitcoin keeps its own deep scan.
The landing-page support link was a raw <a target="_blank">, which the Electrobun WebView cannot navigate — no window opened. Route the click through the existing openUrl RPC (open/xdg-open/start), matching every other external-link call site in the app.
…s can launch KeepKeyVault.exe is our own wrapper-launcher.zig. The Windows build compiled it with no -mcpu, so Zig targeted the build box native AVX2 CPU and baked a VEX vmovdqa into the main() prologue. On no-AVX CPUs (e.g. Intel Pentium Silver N5030 Gemini Lake, SSE4.2 only) the app died instantly at launch with 0xC000001D STATUS_ILLEGAL_INSTRUCTION at +0x1b96, before bun.exe ever ran. Pin -target x86_64-windows -mcpu=baseline (x86-64-v1, runs everywhere). This is not an Electrobun/Bun bug; both ship baseline-clean binaries and the wrapper faults first. UNVERIFIED on the build box -- see docs/handoff-windows-non-avx-launcher-crash.md for build + verify steps.
…p progress
- BTC now uses the same xpub-based per-account scan as the other UTXO chains;
drops the lazy path-matrix (auditScanBtc / btcScanState) flow from the
walkthrough UI in favor of accounts 0/1/2 via getPublicKeys.
- Account #0 (the main wallet) is shown as a verified primary with its xpubs
as derivation proof — not folded into the "found money" reveal or the
empty-accounts summary.
- Per-account xpubs (legacy / segwit / native-segwit) rendered as proof rows.
- Stream each path the sweep ("unusual paths") scan checks to the WebView
(sweepScan streamProgress -> audit-sweep-progress) so the panel shows live
progress + a found log instead of a bare counter.
- Dedupe auto-scanned accounts by level; slow eased scroll reveal on first
scan completion; show-empties defaults on (proof we checked them).
… honesty copy
Addresses PR review findings (P1–P3):
- P1 sweep signing guard: the live BTC recovery path (sweepScan/sweepExecute)
signed with no captured-wallet/seed check, bypassing the guard the dead
auditSweep path had. sweepScan now captures {wallet, seedIdentity} (derived
before startScan to avoid racing the serial USB worker) onto the scan record;
sweepExecute revalidates both before btcSignTx (dry-run unaffected) and FAILS
CLOSED when the identity couldn't be captured — never sign what we can't verify.
- P1 EVM token-only funds: the known-paths grid (auditScanPaths) and custom-path
search (auditDeriveCustom) read native-only, so a Ledger/MEW address holding
USDC with 0 ETH showed "empty" and was dropped from the support handoff. New
shared auditBalanceForAddress() routes EVM through GetPortfolioBalances +
parseEvmScanResult (token-aware, degraded→balanceError); all three audit scans
use it and surface tokens. Grid row + support clipboard now show the tokens.
- P2 all-clear honesty: finish headline/label ignored anyShallow, so a wallet
with only-primary-checked chains read "Everything's accounted for / Total
verified". Added the shallow middle state.
- P2 stale audits: direct seed/passphrase resets didn't invalidate an open audit.
seed-changed now emits wallet-data-purged + markAuditsStale; needs_passphrase
marks stale (no purge emit — avoids dashboard churn on standard unlocks).
- P3 inspector: auditInspectPath returned no symbol (rendered "undefined").
Added symbol to AuditInspectResult + the handler return.
markAuditsStale only flips RUNNING reports; a completed report keeps status 'complete' and the dialog stops polling once non-running, so the needs_passphrase handler's markAuditsStale set an internal bit the UI never observed — the user could keep walking the wizard with old coverage while lazy scans derived against the post-passphrase seed. Add a dedicated 'audit-stale' push (only AuditDialog consumes it, so no dashboard cache churn like wallet-data-purged would cause on every standard-wallet unlock). needs_passphrase now emits it after markAuditsStale; the dialog reacts with setStale(true)+stopPoll, which gates the whole wizard behind the re-run banner. seed-changed already covers this via its wallet-data-purged emit.
feat(audit): multi-chain "Audit balances" wizard with guided recovery
fix(windows): build splash wrapper baseline so non-AVX CPUs can launch
Second half of the non-AVX launch fix (the -mcpu=baseline wrapper from PR #242 is already in 1.4.4). Electrobun 1.13.1 defaults to Bun 1.3.9, which sits in an upstream non-AVX regression window. Pin build.bunVersion to 1.3.14 (first release combining baseline WebKit + JSC AVX-gating fix); on Windows the override always pulls bun-windows-x64-baseline.zip. Also update WINDOWS-BUILD-AND-SIGN.md so the SOP matches reality: - step 9 now documents the load-bearing -mcpu=baseline flag + the "0 AVX instructions / .text VSize 0x5A46" post-build verification - step 6 documents the bunVersion baseline pin + bun.exe banner check - release checklist gains the two static non-AVX gates and the optional no-AVX hardware smoke test Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
One-shot elevated provisioning of the Windows build toolchain (Node LTS, Yarn, Bun, Rustup, Inno Setup, VS 2022 C++ BuildTools) via winget. Deliberately does NOT install Zig via winget -- the build needs Zig 0.15.x and winget ships 0.16 (breaks wrapper-launcher.zig, see WINDOWS-BUILD-QUIRKS). Log path is $PSScriptRoot-relative; _provision.log is already gitignored. Co-Authored-By: Claude Opus 4.8 (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.
Release 1.4.4
Promotes
release/1.4.4tomaster. CI green on the branch tip; version bumped to 1.4.4; submodule pins unchanged.Highlights
-mcpu=baselinesplash wrapper + Bun 1.3.14 baseline pin so non-AVX CPUs launch; winget build-toolchain provisioning script.Test plan
Build KeepKey Vaultgreen on tip