Skip to content

Release v1.2.9 - #59

Merged
BitHighlander merged 82 commits into
masterfrom
release/1.2.9
Mar 24, 2026
Merged

Release v1.2.9#59
BitHighlander merged 82 commits into
masterfrom
release/1.2.9

Conversation

@BitHighlander

Copy link
Copy Markdown
Collaborator

Summary

  • Performance: Pre-bundle backend (13,400 files → 393, Windows first-launch 56s → 2.1s)
  • Intel Mac support: make build-signed-intel cross-compiles via Rosetta
  • Zcash privacy: ZIP-317 dynamic fees, internal IVK scanning for change notes, targeted rescan, release block optimization
  • Security: Timing-safe API key validation, REST log sanitization for signing payloads, strict chainId validation
  • Build pipeline: Dependency audit (SBOM) integrated into make build-signed, CI device-protocol fix
  • UX: Incorrect PIN error overlay, bootloader reboot messaging, auto-update opens GitHub releases, verify seed PIN dialog fix, recovery cipher transport race guard
  • Reports: PDF text wrapping for table cells, CSV export guards for empty tx history
  • Bug fixes: Litecoin zpub receive tab, swap fee rejection messages, Windows BOM fix, macOS update path

Build & Infra Changes

  • bundle-backend.ts — new script pre-bundles all JS deps into single file (externals: node-hid, usb, google-protobuf, proto-tx-builder)
  • collect-externals.ts — slimmed to only 4 external packages (was 10+)
  • audit-deps.ts — fixed path lookup (_build/ not build/), added to make build-signed pipeline
  • Makefile — Intel Mac targets, proto-tx-builder build stamp, zcash-cli stamp force-clear in build-signed
  • CI workflow — device-protocol cp -r nesting fix, fail-hard on 0 JS files, version guard

Test plan

  • CI green (Linux AppImage uploaded)
  • macOS ARM64 DMG signed + notarized + stapled
  • macOS Intel DMG
  • Windows installer
  • Smoke test: fresh install, device connect, PIN entry, send tx
  • Verify seed flow with cipher recovery
  • Zcash shielded scan + send

Files changed

60 files, +1671/-303 lines

BitHighlander and others added 30 commits March 21, 2026 00:16
Settings drawer now has full update flow — Download, Restart & Install,
and a "Download from GitHub" fallback when auto-update fails (Windows
zig-zstd decompression issue).

Previously the settings drawer only showed "Version X available" text
with no actionable buttons. Users had to find the top UpdateBanner
(which scrolls off-screen on default Windows window size) to actually
download and install updates.

Changes:
- DeviceSettingsDrawer: add onDownloadUpdate, onApplyUpdate, updateVersion props
- Settings update section: Download Update button (available phase),
  Restart & Install button (ready phase), Download from GitHub link
  (error phase fallback), View Release link
- App.tsx: wire update.downloadUpdate/applyUpdate/info.version to both
  drawer instances
* fix: Windows auto-update — download setup exe directly, bypass zig-zstd

Electrobun's zig-zstd decompressor crashes on Windows with
"undefined is not an object" because updateInfo is uninitialized.
Rather than patching Electrobun, bypass it entirely on Windows:

1. downloadUpdate detects Windows, fetches setup exe from GitHub release
2. Streams with progress → rpc update-status messages → UI progress bar
3. Saves to temp dir
4. applyUpdate launches the installer via cmd /c start (detached)
5. App exits, installer replaces old version, relaunches

Also adds Download/Install/Download-from-GitHub buttons to Settings
drawer so users don't need the top banner (scrolls off-screen on
default Windows window size).

* fix: guard against downloading current version as update
Bootloader flash auto-reboots — show "Device is rebooting..." with spinner.
Firmware flash requires manual action — keep "Please disconnect and reconnect."
Previously both showed identical disconnect/reconnect text (QA-1.2.0 #1).

Also adds wizard-messaging.test.ts (17 tests) covering reboot message
differentiation, elapsed-time escalation, and seed phrase warning visibility.
Adds engine-state-machine.test.ts and wizard-messaging.test.ts to make test-unit.
…ompt

When user enters wrong PIN, the overlay now shows an error banner with
shake animation instead of silently dismissing and reappearing. Backend
emits pin-error event when promptPin fails and device is still locked.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix timing race: reset pinFailed before setting true via queueMicrotask
  so useEffect fires even when PinEntry remounts after unmount gap
- Clear pinFailed on cancel and wipe (prevents stale error on re-open)
- Simplify pin-error schema to Record<string, never> (attemptsRemaining
  was declared but never populated)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Timing-safe API key validation — iterate all keys with constant-time
   comparison to prevent timing-based key guessing (auth.ts)
2. Sanitize audit log — redact signatures, serializedTx, typedData, and
   other sensitive fields from signing route logs (rest-api.ts)
3. localStorage try/catch in i18n init — prevents crash in private
   browsing or when storage is blocked (i18n/index.ts)
4. ChainId range validation — reject NaN, negative, or >uint32 values
   before passing to device for signing (rest-api.ts)
5. Makefile build target now depends on build-zcash-cli to prevent
   incomplete builds missing the zcash binary
Sanitize: recurse into nested objects (depth-limited to 8) so nested
signing payloads like signDoc.msgs, txMetadata.signedPayload are
redacted. Added data, signedPayload, msgs, memo, tx, txBytes, signDoc,
authInfoBytes, bodyBytes to sensitive key set. Switched to Set for O(1)
lookup.

ChainId: reject non-integer strings ("1abc" → 400, "1.5" → 400,
"" → 400) with regex validation before parseInt. Reject fractional
numbers (1.5 → 400) with Number.isInteger check. Prevents silent
coercion to wrong chain.
fix: differentiate bootloader vs firmware reboot messaging
windowsDownloadAndInstall() read version from Updater.updateInfo()
which is null for pre-release updates found via GitHub API. The
function threw "No update version available" and the UI fell back
to opening the GitHub releases webpage.

Fix: cache the update version in pendingUpdateVersion when found
by either the pre-release GitHub check or the Electrobun stable
check. windowsDownloadAndInstall() reads this first.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: Incorrect PIN error UI in PIN overlay
fix: Windows auto-update fails for pre-release channel
After bootloader flash, the OLD bootloader does NOT auto-reboot — device
screen shows "FIRMWARE UPDATE COMPLETE — Please disconnect and reconnect."
The app was showing a blue "Device is rebooting... will restart
automatically" box, causing users to wait indefinitely.

Changed bootloader step's reboot UI to yellow "Please disconnect and
reconnect your KeepKey" with time-based escalation, matching the actual
device behavior. Firmware step remains separate — NEW bootloader DOES
auto-reboot via board_reset().

Updated wizard-messaging tests to properly distinguish bootloader
(manual-disconnect) from firmware (auto-reboot).
After bootloader flash, OLD bootloader does NOT auto-reboot — device says
"Please disconnect and reconnect." After firmware flash, NEW bootloader
calls board_reset() — device auto-reboots.

rebootPhase was a single 'rebooting' state, so both steps rendered the
same UI. Now split into 'bootloader-rebooting' (yellow disconnect box)
and 'firmware-rebooting' (blue spinner + "restarting automatically").

Derived `isRebooting` boolean replaces all guard checks. Render blocks
use the specific phase to show the correct UI for each step.

Evidence: firmware source — bootloader/main.c:update_fw() calls
board_reset(RESET_PARAM_NONE), blupdater/main.c:success() calls
board_reset(RESET_PARAM_REQUEST_UPDATE). PR #37 logs confirm firmware
auto-reboots in ~7s.
…messaging

fix: bootloader step shows rebooting when device needs manual unplug
Zig 0.15 no longer implicitly coerces [:0]const u16 (sentinel-terminated
slice from utf8ToUtf16LeStringLiteral) to [*]const u16. Use .ptr to
explicitly get the many-pointer from the slice.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Electrobun's Updater.checkForUpdate() fails on Windows (no update.json
published in releases) and returns stale versions on macOS (update.json
is generated before the release is published, so it's always one version
behind). Replace both the RPC checkForUpdate handler and the startup
background check with GitHub releases API, which works cross-platform
and always returns the correct latest version.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: Zig 0.15.2 DrawTextW compat for wrapper launcher
…launch)

Electrobun's Updater.downloadUpdate() uses releases/latest/download which
ignores pre-releases and has stale update.json. macosDownloadAndInstall()
downloads the tar.zst directly from the specific release tag, decompresses,
swaps the .app bundle, and relaunches — same pattern as Windows bypass.
1. Asset name now uses process.arch to select arm64 vs x64 tar.zst
   (was hardcoded to arm64, breaking Intel Macs)
2. Use zig-zstd from Contents/MacOS/ instead of system zstd
   (stock macOS doesn't ship zstd; falls back to system zstd if
   bundled binary isn't found)
fix: use GitHub API for update checks instead of Electrobun native
zig-zstd uses different CLI flags than zstd, stock macOS has no zstd.
Replace macosDownloadAndInstall() with macosOpenReleasePage() — opens
the GitHub release tag page so user can download the DMG directly.
…Mac)

- Remove windowsDownloadAndInstall(), windowsLaunchInstaller(),
  macosOpenReleasePage() — replace with single openReleasePage()
- Both Win + Mac downloadUpdate/applyUpdate now open the releases page
- UpdateBanner: merge available+ready phases into one "Download from GitHub" button
- DeviceSettingsDrawer: simplify to single button for all update states
- PowerShell BOM fix: use .NET WriteAllText to avoid UTF-8 BOM in version.json
The collapsed available+ready button broke Linux which still uses
Electrobun's Updater.downloadUpdate()/applyUpdate(). Restore separate
phases: available → Download, ready → Restart & Install, error → fallback.
On Win/Mac the backend redirects both to openReleasePage() so the
phase never reaches 'ready', but Linux reaches it via Electrobun.
fix: replace in-app auto-update with GitHub releases redirect (Win + Mac)
Root cause (proven by ALPHA-1 through ALPHA-6 on Windows):
device-protocol submodule has lib/ in .gitignore. On fresh checkout,
lib/ is empty. collect-externals copies empty lib/ into the bundle.
Bun crashes on require('device-protocol/lib/messages_pb') before any
Electrobun code runs — silent blank window, zero logs, zero diagnostics.

Fixes:
1. collect-externals.ts: verify messages_pb.js exists after copy,
   attempt auto-build if missing, fail hard if still missing
2. build-windows-production.ps1: init device-protocol submodule,
   build lib/ if absent; fix em-dashes that break PowerShell 5
3. vite.config.ts: fixed filenames (no content hashes) — prevents
   WebView2 cache mismatch on over-install upgrade
4. db.ts: strip BOM from version.json (PowerShell 5 writes BOM,
   breaks Electrobun's JSON.parse, SQLite never initialized on Windows)
5. index.ts: deferred init (window first, backend second), file
   logger before all imports, perf markers
6. auth.ts: reloadPairings() after DB init (deferred init regression)
7. installer.iss: remove [InstallDelete] (was nuking files on upgrade),
   add SetupLogging=yes

Version bump: 1.2.1 -> 1.2.6

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Single doc capturing all learnings from the release-cycle investigation:
root cause, secondary issues, fixes applied, diagnostic patterns, and
Windows build checklist. Replaces 13 investigation docs from release-cycle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…uild

1. index.ts: logger comment now states it only catches runtime errors,
   not import-time crashes. Build-time messages_pb.js check is the real
   guard. No false claims about pre-import diagnostics.

2. collect-externals.ts: removed auto-build fallback (device-protocol's
   build:postprocess uses BSD sed -i '' which fails on Windows). Now
   just fail-fast with clear instructions to build on macOS first.

3. build-windows-production.ps1: same — check-only, no auto-build.
   Clear error message pointing to macOS build step.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
BitHighlander and others added 24 commits March 23, 2026 16:04
…l sources

1. VAULT_INSTALL_STAMP now depends on PROTO_BUILD_STAMP (not just
   PROTO_INSTALL_STAMP), so `make vault` / `make install` cannot
   proceed without a built proto-tx-builder/dist/index.js.

2. PROTO_BUILD_INPUTS now uses find to track all .ts/.js files under
   modules/proto-tx-builder/src/, not just tsconfig.json and index.ts.
   This prevents stale stamps after branch switches or edits to proto/,
   amino/, or any other subdirectory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
perf: pre-bundle backend — first launch 56s to 2.1s
Zcash NU5+ requires ZIP-317 conventional fees: 5000 zats per logical
action, minimum 2 actions (10,000 zats / 0.0001 ZEC). The UTXO
builder used coinselect with sat/byte fee rates (1-5 sat/byte) which
produced fees of ~400-1000 zats — 10-22x below the ZIP-317 floor.

Nodes reject these with "tx unpaid action limit exceeded: 1 action(s)
exceeds limit of 0".

Added ZIP-317 fee enforcement after coin selection (same pattern as
DOGE minimum fee). Absorbs the deficit from the change output, or
reduces the spend output if no change exists.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: ZEC broadcast rejected — enforce ZIP-317 minimum fee
verifySeed() was setting setupInProgress=true, which caused the
PIN_REQUEST handler to classify the unlock PIN as 'new-first'
(Create a PIN) instead of 'current' (Enter PIN). Introduced a
separate verifyInProgress flag so dry-run verification doesn't
affect PIN type classification while still guarding sendCharacter*
and sendPin from racing with the transport.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…release block start

- Scan with both External and Internal (change) IVKs so spent-note change
  outputs are properly detected and reflected in balance
- Support targeted rescan from a specific block height by resetting the
  scan cursor (preserves existing notes via INSERT OR IGNORE)
- Start first scan from KeepKey release block (3282941) instead of Orchard
  activation (1687104) — skips ~1.6M irrelevant blocks
- Return synced_to height in balance response for UI sync indicator
- Clickable block heights in ZcashPrivacyTab to set as rescan target

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the DEX node rejects a swap because outbound fees exceed the output
value, show a user-friendly message instead of the raw Go error stack.
Covers "not enough asset to pay for fees" and "pool not available".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…release block constant

- Define KEEPKEY_RELEASE_BLOCK constant in frontend (was referenced but missing)
- Add needsScan state — detects unscanned wallets and shows initial scan prompt
- Balance fetch now reads synced_to from sidecar to detect scan state
- Sidecar startup ready signal includes synced_to + keepkey_release_block
- "Scan from block 3,282,941" button guides first-time users
Review fixes for PR #56:

1. ZIP-317 fee (utxo.ts): The fallback path that reduces the recipient
   output to cover the fee deficit is now gated to isMax or memo-present
   (swap) flows only. Exact-amount user sends fail with a clear message
   instead of silently sending less than requested.

2. Sidecar scan state (zcash-sidecar.ts, index.ts, ZcashPrivacyTab.tsx):
   The Rust sidecar ready signal now includes synced_to — capture it in
   the TS sidecar module and expose via zcashShieldedStatus RPC. The
   frontend reads it on mount so needsScan is set immediately, not only
   after a separate balance RPC. Also updates cachedSyncedTo after scans.

3. i18n: Replace hardcoded English strings in needsScan UI block with
   t() calls (needsScanPrompt, scanFromBlock keys in asset namespace).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add synced_to and keepkey_release_block to zcashShieldedStatus response
type. Add synced_to, notes_total, notes_unspent, keepkey_release_block
to zcashShieldedBalance response type. Aligns shared types with the
actual RPC handler responses.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: Zcash broadcast fees, scanner improvements, swap UX
fix: verify seed shows Enter PIN instead of Create PIN
…ivacy tab

- Add #3,282,941 reference height next to "Scan for Payments" label —
  click to populate the scan input with KeepKey release block
- TX history heights: single-click copies to clipboard, double-click
  sets as rescan height

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The hardcoded 10,000 ZAT fee only covered ≤2 actions. Transactions
spending >2 notes were rejected by consensus with "Unpaid actions
is higher than the limit". Now computes fee = 5000 × max(2, actions)
per ZIP-317, matching the existing shield and transparent paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: dynamic ZIP-317 fee for Orchard privacy sends
…uption

syncState() had no guard for setupInProgress/verifyInProgress. A USB
attach event during cipher recovery would call getFeatures() on the
transport — which is locked by the recovery session. The device received
GetFeatures instead of the expected CharacterAck, corrupting the decoded
word buffer and causing firmware to reject valid words with "Word not
found in BIP39 wordlist".

Two guards added:
- syncState(): return early if recovery/verify in progress
- USB attach handler: ignore spurious re-attach during cipher session
fix: recovery cipher transport race — syncState guard
Wires `make audit` (dep manifest + SBOM) into build-signed between
build-stable and prune-bundle. Also adds audit step to CI workflow
so Linux builds include the SBOM artifact.
rm -rf target dir before cp -r to prevent nested lib/lib/ when
target already exists as empty directory. Also fail hard if 0 JS
files are copied instead of silently continuing.
build/_ext_modules is consumed by Electrobun packaging, so audit
may not find it in CI. Real audit runs locally via make build-signed.
Audit needs build/_ext_modules which is consumed during Electrobun
packaging. CI can't run it post-build. Signed release artifacts
are built locally via make build-signed which runs audit correctly.
collect-externals outputs to _build/_ext_modules, not build/.
Stale .make/zcash-cli.stamp from unsigned builds skips Developer ID
signing, causing Apple notarization to reject the binary. Now
build-signed always clears the stamp first.
Electrobun creates tar.zst BEFORE patch-bundle.ts runs, so the
archive always has unpatched bun/index.js. The prune step extracts
from tar.zst, so it must apply the patch after extracting.
# Conflicts:
#	projects/keepkey-vault/electrobun.config.ts
#	projects/keepkey-vault/package.json
@BitHighlander
BitHighlander merged commit 0f8b037 into master Mar 24, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant