Skip to content

release: v1.4.0#198

Merged
BitHighlander merged 95 commits into
masterfrom
release/1.4.0
May 28, 2026
Merged

release: v1.4.0#198
BitHighlander merged 95 commits into
masterfrom
release/1.4.0

Conversation

@BitHighlander
Copy link
Copy Markdown
Collaborator

Summary

  • Dynamic Pioneer chain coverage + Solana/LiFi swap routes
  • Hive blockchain integration (address, signing, firmware-gated minFirmware 7.16+)
  • Zcash Orchard clear-signing confirmed on mainnet (shield, private send, deshield)
  • NEAR Intents prover — BCH, ZEC, DOGE, LTC, BIP122 sendMax
  • BIP44 multi-account discovery + per-account drilled view
  • Dashboard redesign: sidebar, galaxy/donut/heatmap/stack charts, ⌘K search
  • Asset picker v2, activity table, SSE real-time tx notifications
  • Firmware v7.14.1 bundled (TON off-chain signing improvements)
  • device-protocol rolled to keepkey upstream master; @bithighlander/device-protocol@7.16.0 published (Hive, Zcash clear-signing, THORChain denom, Ripple memo)
  • Numerous swap, UI, and balance bug fixes

Release

Pre-release: https://github.com/keepkey/keepkey-vault/releases/tag/v1.4.0

Develop merge-back: 5bb2de9

Test plan

  • macOS arm64 DMG — stapled, Gatekeeper accepted, app launches
  • macOS x64 DMG — stapled, Gatekeeper accepted
  • Linux AppImage — launches, device detected
  • Windows installer — installs, app launches
  • Smoke test: backend starts, no missing module errors

sktbrd and others added 30 commits May 18, 2026 03:07
- Sidebar chain list replaces the cards grid; main area centers a
  flex-1 hero region with stable sun/donut y-position.
- Chain-detail orbital (chain icon as the sun, clean tokens orbiting)
  with per-token glow extracted from each icon's dominant color.
- Always-on Receive/Send/Swap action row in drilled mode; Swap opens
  the SwapDialog directly via a lazy import (no AssetPage detour).
- DonutChart enlarged and restyled to match the rest of the app;
  legend pill uses ink palette + bigger USD amount.
- View toggle (orbital/donut) moved to the TopNav, shared with
  Dashboard through a tiny context.
- Mono is the app-wide default font; serif aliases collapse to mono
  and decorative italic headers are dropped.
- Full-screen ambient radial glow, removed per-card chrome around
  the orbital so the glow reads as the page background.
- Refresh + Reports moved to top-right of the main canvas.
- Icon preloader keeps chain/token logos warm so they don't
  re-fetch when switching chains in the sidebar.
- Dogecoin easter egg: bottom-right Shiba that slides in, bobs,
  barks (synthesized), spouts random doge speak, and auto-dismisses
  after 8s (timer resets on click).
Pull in develop's portfolio reliability + cache fixes
(failedPubkeySet, no-walk-backwards cache, api-blue selector,
forceRefresh changes, 1.3.6 prep) so the design branch stays
current and minimizes future conflicts.

# Conflicts:
#	projects/keepkey-vault/src/mainview/components/Dashboard.tsx
- ⌘K / Ctrl+K command palette (CommandPalette.tsx) for fuzzy-jumping
  to any chain or token. Substring matching across name/symbol/CAIP
  with priority (exact symbol > startsWith > contains). Arrow-key
  navigation, Enter to select, Esc/backdrop to close.
- Tiny commandBus.ts pub/sub bridges the palette (lives at the App
  level) to Dashboard's drilldown state and serves Dashboard's
  balances Map for token search without lifting state.
- AssetPage accepts an `initialToken` prop. Clicking a token in the
  orbital (satellite or token list row) now lands directly on the
  token detail view — no more select-again after entering the chain.
- Token hover card in the chain-detail orbital uses the icon's
  extracted dominant color: tinted inset/outer shadow, colored
  symbol text. Falls back to white when extraction returned the
  fallback color.
- Doge easter egg picks a random phrase on initial appearance so
  every remount feels fresh instead of always saying "wow such doge".
- Add a third "heatmap" portfolio view next to orbital/donut in the
  TopNav pill. Renders a squarified treemap (Bruls et al, 2000) where
  each chain (or each token in chain-detail mode) is a tile with area
  proportional to its USD value, tile color from chain.color (or a
  rotating token palette). Click a tile to drill in / open the token.
- DashboardView type extended to 'orbital' | 'donut' | 'heatmap',
  persistence respects the new value.
- Tighten CommandPalette styles: kill the browser default focus ring on
  the search input, restyle the esc badge as a small ink-3 pill,
  smaller magnifying-glass icon in muted text-3 for a calmer look.
- Replace the three-button orbital/donut/heatmap pill in the TopNav with
  a single eye-icon button that drops a 260px menu (ViewPickerMenu.tsx).
  Each menu row carries a small SVG "thumbnail" of the view so the
  selection is visually recognizable, plus a one-line description and
  a gold check on the currently active view.
- Add a fourth view "stack": a horizontal stacked bar (StackedBarView.tsx)
  where each chain (or each token, when drilled) is a colored segment
  proportional to its USD share. Segments above 5% get inline labels
  (name + percentage + value); smaller items collapse into a chip
  legend below the bar. Optional 24h delta slot is wired but not fed
  data yet — pass `deltaUsd`/`deltaPct` once the engine surfaces it.
- DashboardView type extended to 'orbital' | 'donut' | 'heatmap' | 'stack'.
- HeatmapView measures its parent via ResizeObserver and lays out
  squarified tiles to the actual rendered dimensions, instead of the
  prior hardcoded 520×420. Explicit width/height props still supported
  for embedded usages.
- Dashboard wraps the heatmap in a flex-stretch container when the
  user is on the heatmap view, and collapses the bottom slot's reserved
  height when no per-chain content needs to render — gives the heatmap
  the entire area to the right of the sidebar.
…bar scrollbar

- Heatmap squarified layout now applies a value^0.65 compression curve
  so tiny-balance chains still get a usable tile next to five-figure
  positions. Pure proportional sizing made $5 chains effectively
  invisible against $5k chains; this trades exact area accuracy for
  glanceability.
- Heatmap container clamps to `calc(100vh - 90px)` (h + maxH) so the
  layout area equals the visible canvas — small chains no longer fall
  below the viewport fold.
- Outer hero region drops its 60–70vh minH when on the heatmap view
  so the column doesn't push beyond the viewport.
- Sidebar scrollbar is hidden by default and only fades in on hover
  (kk-sidebar-scroll class + scrollbar-width + webkit-scrollbar CSS).
The previous calc(100vh - 90px) guess was wrong because the actual
available height varies with the Dashboard's top-right utility row,
banners, and the App's nav padding. Introduce HeatmapHost which:

- on mount + window resize, reads the container's top via
  getBoundingClientRect and subtracts from window.innerHeight with a
  12 px breathing margin, then sets that as the explicit px height
- re-measures one frame later so it picks up flex-layout-settled rects

Net result: smaller tiles no longer fall below the fold; the heatmap
fills exactly the area between its top edge and the window bottom.
v1.3.6 published as prerelease.
Previously the scrollbar width toggled 0 → 6px on hover, which
re-flowed the chain rows. Now the 6px gutter is reserved at all
times and only the thumb opacity changes:
- resting: gutter present, thumb transparent (invisible)
- hover: thumb at rgba(255,255,255,0.06)
- thumb-hover: rgba(255,255,255,0.14)

Firefox `scrollbar-color` mirrors the webkit treatment.
Token page (AssetPage):
- When `initialToken` is set, the header now actually renders the
  token as the primary identity — icon, name, symbol, balance, and
  USD value all switch to the token. The chain shows up as a small
  "on Ethereum" subtitle with a "← chain" pill to drop the token
  context and view the chain.
- Mobile balance row and right-side balance both follow the same
  rule. Send/receive flows already consumed selectedToken, so they
  continue to work; this just makes the page visually feel like a
  token page instead of "chain page with a token internally tracked".

Sidebar scrollbar:
- Dropped `scrollbar-width: thin` — on WebKit it overrode the
  ::-webkit-scrollbar pseudo-elements and let the system thin
  scrollbar paint bright/white over the gutter. Now we only declare
  ::-webkit-scrollbar (6px reserved gutter, transparent track and
  thumb until hover) and gate the Firefox treatment behind a
  `@supports (-moz-appearance: none)` block. `colorScheme: dark`
  on the box for good measure.
…s alias fix

- ActivityPage: replace homemade CSV export with ReportDialog (PDF + CSV)
- ActivityPage: chain dropdown now shows all supported networks, not just
  networks that happen to appear in current activity
- ActivityPage: Rescan now works without selecting a network — iterates all
  chains when no filter is active; removes the "select a network to scan" gate
- swap-tracker: map nearIntents → shapeshift in PIONEER_INTEGRATION_ALIAS so
  NEAR Intents BTC→EVM swaps register with Pioneer (validator rejects nearIntents)
- ActivityTracker/ActivityPanel: moved bubble and slide-up panel to right side
  (avoids overlapping Vlad's left sidebar)
- docs/handoffs: pioneer-near-intents-tracking.md documenting Pioneer's two bugs
  (integration enum validator, protocol misidentification as thorchain)
- Additional handoff docs from PR #178 diagnostic work

NEAR Intents BTC broadcast confirmed (txid 292ddbb5, 7 confirmations as of today).
Pioneer registration was failing silently on every NEAR Intents swap — now fixed.
Root cause: getSwapQuote(balance) commits NEAR Intents to receiving the
full balance, but buildUtxoTx(isMax=true) only delivers balance-miner_fee.
NEAR Intents hard-fails with PARTIAL_DEPOSIT on any shortfall — slippage
tolerance only applies to the ETH output, not the BTC input amount.

Fixes:
- Add estimateUtxoFee() to txbuilder/utxo.ts (coin selection dry-run)
- For NEAR Intents sendMax BTC: estimate fee first, re-quote with net
  delivery amount so the deposit channel receives what was committed
- Block Pioneer's 'thorchain' swapper adoption for NEAR Intents swaps
  (Pioneer misidentifies memo-less BTC deposits as THORChain)
- Exclude NEAR Intents from Relay request-id backfill path (wrong protocol)
- Update Pioneer handoff with on-chain evidence of the stuck swap and
  confirmed PARTIAL_DEPOSIT root cause from NEAR Intents status API
…ty resume swap

P1: Pass isMax to getSwapQuote so the NEAR Intents sendMax re-quote
path actually fires. SwapDialog was omitting isMax from the quote RPC
call, making the backend guard a dead code path for sendMax BTC swaps.

P2: Queue vault commands when Dashboard is unmounted. CommandPalette
calls onJumpToVault() then dispatchVaultCommand() synchronously, but
React hasn't re-rendered yet so Dashboard's listener isn't subscribed.
commandBus now stashes one pending command and drains it on the next
subscribeVaultCommand() call (Dashboard mount).

P2: ActivityPage can now resume pending swaps. Dashboard was rendering
ActivityPage without onResumeSwap, so clicking a pending swap fell
through to a static detail view. Added activityResumeSwap state and
wired it to LazySwapDialog alongside the ActivityPage render.
…elds

- Dashboard: don't close ActivityPage when opening resume dialog — both
  live in the same showActivityPage branch so the dialog stays mounted.
  onResumeSwap now only sets activityResumeSwap; onClose clears it.
- types: add isMax? and feeLevel? to SwapQuoteParams so the SwapDialog
  quote call and index.ts backend guard have matching type contracts.
…ix (#183)

* feat(ui): vault dashboard redesign + dogecoin easter egg

- Sidebar chain list replaces the cards grid; main area centers a
  flex-1 hero region with stable sun/donut y-position.
- Chain-detail orbital (chain icon as the sun, clean tokens orbiting)
  with per-token glow extracted from each icon's dominant color.
- Always-on Receive/Send/Swap action row in drilled mode; Swap opens
  the SwapDialog directly via a lazy import (no AssetPage detour).
- DonutChart enlarged and restyled to match the rest of the app;
  legend pill uses ink palette + bigger USD amount.
- View toggle (orbital/donut) moved to the TopNav, shared with
  Dashboard through a tiny context.
- Mono is the app-wide default font; serif aliases collapse to mono
  and decorative italic headers are dropped.
- Full-screen ambient radial glow, removed per-card chrome around
  the orbital so the glow reads as the page background.
- Refresh + Reports moved to top-right of the main canvas.
- Icon preloader keeps chain/token logos warm so they don't
  re-fetch when switching chains in the sidebar.
- Dogecoin easter egg: bottom-right Shiba that slides in, bobs,
  barks (synthesized), spouts random doge speak, and auto-dismisses
  after 8s (timer resets on click).

* feat(ui): ⌘K command palette + token-direct nav + tinted hover cards

- ⌘K / Ctrl+K command palette (CommandPalette.tsx) for fuzzy-jumping
  to any chain or token. Substring matching across name/symbol/CAIP
  with priority (exact symbol > startsWith > contains). Arrow-key
  navigation, Enter to select, Esc/backdrop to close.
- Tiny commandBus.ts pub/sub bridges the palette (lives at the App
  level) to Dashboard's drilldown state and serves Dashboard's
  balances Map for token search without lifting state.
- AssetPage accepts an `initialToken` prop. Clicking a token in the
  orbital (satellite or token list row) now lands directly on the
  token detail view — no more select-again after entering the chain.
- Token hover card in the chain-detail orbital uses the icon's
  extracted dominant color: tinted inset/outer shadow, colored
  symbol text. Falls back to white when extraction returned the
  fallback color.
- Doge easter egg picks a random phrase on initial appearance so
  every remount feels fresh instead of always saying "wow such doge".

* feat(ui): heatmap view + command palette polish

- Add a third "heatmap" portfolio view next to orbital/donut in the
  TopNav pill. Renders a squarified treemap (Bruls et al, 2000) where
  each chain (or each token in chain-detail mode) is a tile with area
  proportional to its USD value, tile color from chain.color (or a
  rotating token palette). Click a tile to drill in / open the token.
- DashboardView type extended to 'orbital' | 'donut' | 'heatmap',
  persistence respects the new value.
- Tighten CommandPalette styles: kill the browser default focus ring on
  the search input, restyle the esc badge as a small ink-3 pill,
  smaller magnifying-glass icon in muted text-3 for a calmer look.

* feat(ui): eye-icon view picker + horizontal stack view

- Replace the three-button orbital/donut/heatmap pill in the TopNav with
  a single eye-icon button that drops a 260px menu (ViewPickerMenu.tsx).
  Each menu row carries a small SVG "thumbnail" of the view so the
  selection is visually recognizable, plus a one-line description and
  a gold check on the currently active view.
- Add a fourth view "stack": a horizontal stacked bar (StackedBarView.tsx)
  where each chain (or each token, when drilled) is a colored segment
  proportional to its USD share. Segments above 5% get inline labels
  (name + percentage + value); smaller items collapse into a chip
  legend below the bar. Optional 24h delta slot is wired but not fed
  data yet — pass `deltaUsd`/`deltaPct` once the engine surfaces it.
- DashboardView type extended to 'orbital' | 'donut' | 'heatmap' | 'stack'.

* feat(heatmap): fill the full available canvas

- HeatmapView measures its parent via ResizeObserver and lays out
  squarified tiles to the actual rendered dimensions, instead of the
  prior hardcoded 520×420. Explicit width/height props still supported
  for embedded usages.
- Dashboard wraps the heatmap in a flex-stretch container when the
  user is on the heatmap view, and collapses the bottom slot's reserved
  height when no per-chain content needs to render — gives the heatmap
  the entire area to the right of the sidebar.

* fix(ui): heatmap viewport clamp + boost small tiles + hover-only sidebar scrollbar

- Heatmap squarified layout now applies a value^0.65 compression curve
  so tiny-balance chains still get a usable tile next to five-figure
  positions. Pure proportional sizing made $5 chains effectively
  invisible against $5k chains; this trades exact area accuracy for
  glanceability.
- Heatmap container clamps to `calc(100vh - 90px)` (h + maxH) so the
  layout area equals the visible canvas — small chains no longer fall
  below the viewport fold.
- Outer hero region drops its 60–70vh minH when on the heatmap view
  so the column doesn't push beyond the viewport.
- Sidebar scrollbar is hidden by default and only fades in on hover
  (kk-sidebar-scroll class + scrollbar-width + webkit-scrollbar CSS).

* fix(heatmap): viewport-fit canvas via dynamic measurement

The previous calc(100vh - 90px) guess was wrong because the actual
available height varies with the Dashboard's top-right utility row,
banners, and the App's nav padding. Introduce HeatmapHost which:

- on mount + window resize, reads the container's top via
  getBoundingClientRect and subtracts from window.innerHeight with a
  12 px breathing margin, then sets that as the explicit px height
- re-measures one frame later so it picks up flex-layout-settled rects

Net result: smaller tiles no longer fall below the fold; the heatmap
fills exactly the area between its top edge and the window bottom.

* feat: activity page with PDF reports, all-network rescan, NEAR Intents alias fix

- ActivityPage: replace homemade CSV export with ReportDialog (PDF + CSV)
- ActivityPage: chain dropdown now shows all supported networks, not just
  networks that happen to appear in current activity
- ActivityPage: Rescan now works without selecting a network — iterates all
  chains when no filter is active; removes the "select a network to scan" gate
- swap-tracker: map nearIntents → shapeshift in PIONEER_INTEGRATION_ALIAS so
  NEAR Intents BTC→EVM swaps register with Pioneer (validator rejects nearIntents)
- ActivityTracker/ActivityPanel: moved bubble and slide-up panel to right side
  (avoids overlapping Vlad's left sidebar)
- docs/handoffs: pioneer-near-intents-tracking.md documenting Pioneer's two bugs
  (integration enum validator, protocol misidentification as thorchain)
- Additional handoff docs from PR #178 diagnostic work

NEAR Intents BTC broadcast confirmed (txid 292ddbb5, 7 confirmations as of today).
Pioneer registration was failing silently on every NEAR Intents swap — now fixed.

* fix: NEAR Intents sendMax BTC always refunds due to PARTIAL_DEPOSIT

Root cause: getSwapQuote(balance) commits NEAR Intents to receiving the
full balance, but buildUtxoTx(isMax=true) only delivers balance-miner_fee.
NEAR Intents hard-fails with PARTIAL_DEPOSIT on any shortfall — slippage
tolerance only applies to the ETH output, not the BTC input amount.

Fixes:
- Add estimateUtxoFee() to txbuilder/utxo.ts (coin selection dry-run)
- For NEAR Intents sendMax BTC: estimate fee first, re-quote with net
  delivery amount so the deposit channel receives what was committed
- Block Pioneer's 'thorchain' swapper adoption for NEAR Intents swaps
  (Pioneer misidentifies memo-less BTC deposits as THORChain)
- Exclude NEAR Intents from Relay request-id backfill path (wrong protocol)
- Update Pioneer handoff with on-chain evidence of the stuck swap and
  confirmed PARTIAL_DEPOSIT root cause from NEAR Intents status API

* fix: three review findings — isMax in quote, command bus race, activity resume swap

P1: Pass isMax to getSwapQuote so the NEAR Intents sendMax re-quote
path actually fires. SwapDialog was omitting isMax from the quote RPC
call, making the backend guard a dead code path for sendMax BTC swaps.

P2: Queue vault commands when Dashboard is unmounted. CommandPalette
calls onJumpToVault() then dispatchVaultCommand() synchronously, but
React hasn't re-rendered yet so Dashboard's listener isn't subscribed.
commandBus now stashes one pending command and drains it on the next
subscribeVaultCommand() call (Dashboard mount).

P2: ActivityPage can now resume pending swaps. Dashboard was rendering
ActivityPage without onResumeSwap, so clicking a pending swap fell
through to a static detail view. Added activityResumeSwap state and
wired it to LazySwapDialog alongside the ActivityPage render.

* fix: activity resume dialog unmount race + SwapQuoteParams missing fields

- Dashboard: don't close ActivityPage when opening resume dialog — both
  live in the same showActivityPage branch so the dialog stays mounted.
  onResumeSwap now only sets activityResumeSwap; onClose clears it.
- types: add isMax? and feeLevel? to SwapQuoteParams so the SwapDialog
  quote call and index.ts backend guard have matching type contracts.

---------

Co-authored-by: xvlad <116202536+sktbrd@users.noreply.github.com>
NEAR Intents BTC→EVM had a confirmed fund loss (75k sat refunded to keepkey.near
which KeepKey does not control). The vault was accumulating workarounds for Pioneer's
misidentification of NEAR as THORChain and status never advancing past "pending".
Removing the integration entirely until Pioneer has proper server-side support.

- swap-tracker.ts: drop nearIntentsData forwarding, isNearIntentsSwap(), 1Click polling
- swap.ts: drop BTC→EVM quote minimum/expiry guards
- swap-parsing.ts: remove 'NEAR Intents' from DEPOSIT_CHANNEL_SWAPPERS, genericise guards
- docs: remove 5 NEAR Intents handoff docs
- swap-parsing.ts: drop NEAR Intents quotes before selecting best so a
  supported fallback route (e.g. Chainflip) is used when Pioneer returns
  NEAR first. Falls back to full list only if all quotes are filtered.
- swap-parsing.test.ts: two stale NEAR Intents tests now assert throws;
  added fallback-route test covering the NEAR-first scenario.

Fixes P1 findings from PR #184 review.
latestBalances in commandBus.ts was module-level and never reset, so
stale balances from a previous wallet session persisted in the ⌘K palette
after disconnect or wallet switch.

- commandBus.ts: add clearBalances() that zeroes the cache and notifies listeners
- Dashboard.tsx: call clearBalances() on unmount via a mount-only useEffect

Fixes P2 from PR #184 review.
When Pioneer returns only NEAR Intents for a pair (e.g. BTC→ETH), the vault
now surfaces "No supported routes — Pioneer returned only 'NEAR Intents'" with
full diagnostic dump instead of the generic memo/calldata message. Tests updated
to match. Pioneer handoff added: BTC→ETH should surface THORChain/Chainflip routes.
Reverts the removal from 4d748b8. Pioneer now sets refundTo to the
user's BTC address so the previous fund-loss path is closed. Restored:
- isMemolessTransfer in swap-parsing (UTXO deposit to BTC address, no memo)
- NEAR Intents in DEPOSIT_CHANNEL_SWAPPERS (ETH→BTC deposit channel)
- isNearIntentsSwap() + anti-misidentification guard in swap-tracker
- minimum-amount guard in swap.ts (prevents sub-$50 refundable swaps)
- Removed UNSUPPORTED_SWAPPER filter added in fc42025
Two bugs caused the account breakdown to show a stale balance (e.g. 0.000246 BTC)
while the portfolio header correctly showed 0:

1. saveCachedPubkey had a no-walk-backwards guard that prevented genuine zeros
   from Pioneer from clearing the cached_pubkeys table row. Added force=true
   param; getBalances passes it so confirmed-zero responses overwrite stale rows.

2. getBtcAccounts re-read the stale DB row and called updateXpubBalance with the
   old value, clobbering what getBalances had correctly written to in-memory.
   Fixed by adding pioneerFetched flag to BtcAccountManager; getBtcAccounts skips
   DB hydration once Pioneer has responded at least once.
Race condition: the initial relayRequestId backfill runs while the tx has
0 confirmations (not yet indexed by api.relay.link). Pioneer then marks
the swap completed on the same refreshSwap tick, pushes an update with
relayRequestId=undefined, and polling stops. The tracker button never
appears even after the swap settles.

Fix: after Pioneer marks the swap terminal, do one more backfill attempt
for Relay swaps that still have no requestId. This covers the 10-20s
window between broadcast and relay.link indexing the request.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Safe/Closer reserve toggle appears when user hits MAX on a native EVM
asset. Safe keeps the full static reserve; Closer uses 35% of it (with
chain-specific floor) so high-balance users can send more. Warning label
shown in Closer mode. Also shows exact reserve amount + USD value inline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- pioneer-socket.ts: persistent WS client that subscribes to Pioneer
  push events; reconnects on drop; started on device-ready, stopped
  on disconnect
- pioneer.ts: export QUERY_KEY so pioneer-socket can share the same key
- swap-tracker.ts: poll 1Click /v0/status to backfill nearTxHash for
  NEAR Intents swaps; include nearTxHash in swap-update push
- trackers.ts: NEAR Intents tracker URL → nearblocks.io/txns/<hash>
- rpc-schema.ts: add tx-push-received message for frontend notification
- types.ts: nearTxHash on PendingSwap + SwapStatusUpdate
- Dashboard.tsx: listen for tx-push-received → forceRefresh balances
- index.ts: wire PioneerSocket on state-change ready/disconnected

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- pioneer.ts: persistent queryKey (DB-backed via 'pioneer_query_key'
  setting); registers key with Pioneer after init so SSE auth works
- event-stream.ts: SSE client for POST /api/v1/events/subscribe;
  auto-reconnects on drop; watches tx:incoming + tx:confirmed events;
  only individual addresses subscribed (no xpubs — UTXO handled server-side)
- index.ts: start stream after getBalances derives all addresses;
  stop on device disconnect; fires tx-push-received RPC event to
  frontend on tx:incoming and tx:confirmed

Additive: existing GetPortfolioBalances polling continues unchanged if
the SSE connection fails or the server doesn't support the endpoint.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BitHighlander and others added 28 commits May 26, 2026 22:27
…ck status endpoint

The previous fail-fast queried 1click.chaindefuser.com/v0/status at sign-time to
get the expected deposit amount. That was unreliable: 5s timeout, amount may be in
a different unit, endpoint may return no data yet.

params.amount already IS the quoted amount — compare depositOut.value against it
locally (converted to sat via fromChain.decimals). Throw before device signs if the
UTXO tx delivers less than what 1Click was quoted, with > 1 sat tolerance for float
rounding. Zero network calls needed.
NEAR Intents (1Click) returns legacy P2PKH format BCH deposit addresses
(starting with '1'). The KeepKey firmware requires cashaddr format —
hdwallet-keepkey blindly prepends 'bitcoincash:' producing an invalid
address that the firmware rejects with 'Failed to compile output'.

- Add normalizeBchAddress() in utxo.ts: decodes legacy P2PKH/P2SH via
  bs58check, re-encodes as cashaddr using @shapeshiftoss/bitcoinjs-lib
- Apply normalization to 'to' param in buildUtxoTx for BCH chains
- Fix NEAR Intents fail-fast address comparison in swap.ts (normalized)
- Fix refundTo safety validation: strip bitcoincash: prefix before
  comparing Pioneer/1Click refund address to wallet address (BCH)
- Remove .slice(0,20) truncation from output address log line
…e for actual buildTx

Root cause of all NEAR Intents UTXO sendMax INCOMPLETE_DEPOSIT failures:

  getSwapQuote re-quotes with netFromAmount (balance - estimatedFee) and the
  deposit address is committed to receive exactly that. But executeSwap always
  passed params.amount=fullBalance + isMax=true to buildTx, which did
  coinSelectSplit(isMax=true) → deposit = balance - actualFee. If
  actualFee ≠ estimatedFee the deposit was short and 1Click returned
  INCOMPLETE_DEPOSIT.

Fix: look up the cached quote BEFORE calling executeSwap (was after), then
if NEAR Intents + isMax + bip122 + netFromAmount cached, override execParams
with amount=netFromAmount and isMax=false. coinSelectSplit(isMax=false) outputs
exactly netFromAmount to the deposit address (fee added on top from remaining
UTXOs) regardless of the exact fee rate.

The fail-fast in swap.ts now correctly compares depositOut.value against
netFromAmount — passes when re-quote worked, throws when it didn't.
estimateUtxoFee was returning the raw coinSelectSplit fee (~960 sat at
5 sat/byte for ZEC), while buildUtxoTx enforces the ZIP-317 minimum
(5000 × max(2, logical_actions) = 10,000 sat for a 1-in/1-out tx).

This divergence broke every ZEC sendMax NEAR Intents swap:
  netFromAmount = balance - 960
  buildTx(isMax=false, netFromAmount) needs balance >= netFromAmount + 10,000
  → always "Insufficient ZEC", PARTIAL_DEPOSIT/INCOMPLETE_DEPOSIT refund

Fix: apply the same ZIP-317 floor inside estimateUtxoFee so
  netFromAmount = balance - 10,000 (or higher for multi-input txs)
which is exactly what buildUtxoTx will produce.
- hdwallet → master (e95d048c): zcash deshield fixes, Hive adapter, CI
- device-protocol → af3b9b5b: NEAR proto definitions (MessageType 1600-1603)
- docs: add handoff notes for hive-vault, pioneer-cacao-decimals,
  swaps-polish-2026-05-24, zcash-pczt-tdd-retro
…er noise (#195)

* fix(balance): stale closure, CAIP mismatch, dismiss persistence, ledger noise

Audit findings addressed (bugs 1-9):

Bug 1+2 (CRITICAL): refreshBalances used a stale closure over `balances` and
preserved Pioneer-confirmed zeros forever. Switched to a functional updater —
confirmed chains always win (zero is valid), absent chains are preserved.
Also removed `loadingBalances` from useCallback deps since it's no longer read.

Bug 3 (HIGH): No balance refresh after SendForm broadcast. Added
rpcFire('getBalance') after successful broadcastTx so Dashboard stays current.

Bug 4 (HIGH): SSE tx-push-received events in AssetPage never matched EVM chains
because payload.chain is CAIP-19 ("eip155:1/slip44:60") while the guard only
checked chain.id and chain.symbol. Added chain.caip and networkId-prefix checks.

Bug 5 (HIGH): Dashboard had no tx-push-received handler — incoming SSE events
never triggered a refresh on the overview. Added handler that finds the chain
by CAIP and fires a targeted getBalance.

Bug 6 (MEDIUM): Dismissed swap banners reappeared on next swap-update poll.
Added dismissedSwapTxids ref; dismiss records the txid and swap-update filters
it before re-adding to activeSwaps.

Bug 7 (MEDIUM): ActivityPage swaps stat counted terminal states (completed/
failed/refunded). Hoisted the active-filter and reused it for the stat.

Bug 8 (MEDIUM): refreshBalances silently dropped when loadingBalances=true,
causing swap-complete refresh to be discarded if a concurrent refresh was
in-flight. Removed the loadingBalances guard; concurrent calls are safe
(functional updater + last-writer-wins via React state).

Bug 9 (LOW): rectifyWallet was called on every getBalances/getBalance fetch,
flooding the journal with Reconcile entries. Added 5-minute rate-limit per
deviceId so reconcile entries are only written on the first call after the
interval.

* fix(caip-throttle): address PR #195 review findings

- CAIP startsWith ambiguity: use networkId+"/" suffix so eip155:10 (Optimism)
  no longer matches eip155:1 (Ethereum) in both Dashboard and AssetPage handlers
- swapOutputChainId CAIP mismatch: resolve the output ChainDef from CHAINS and
  compare against its caip/networkId instead of doing string-includes against
  the vault internal chain id which never matches CAIP payloads
- ledger throttle granularity: key lastRectifyMs by deviceId:chainId so a
  single-chain getBalance call does not suppress reconcile for all other chains
  for 5 minutes

* fix(refresh-race): generation counter + backend CAIP normalization

Generation counter (Dashboard.tsx): parallel refreshBalances calls now discard
stale responses — each call stamps the current generation and only applies its
result if no newer call has completed first. Prevents an older pre-swap response
from overwriting the fresher post-swap result.

Backend CAIP normalization (index.ts): PioneerSocket forwarded d.symbol raw
when d.chain was absent, producing symbol strings like "ETH" that the CAIP-only
UI matchers silently dropped. Now resolves d.symbol → chain definition → caip
before emitting tx-push-received, and drops events that can't be resolved rather
than forwarding an unmatchable raw symbol.

* fix(ordering): per-chain freshness, load guard, CAIP norm, ledger stamp, output-chain

#2+#3: Per-chain freshness guard. refreshBalances captures refreshStartedAt at
call entry; when merging the full result it skips any chain whose chainLastUpdated
(set by balance-updated events) is newer than that start time. This prevents an
older bulk response from overwriting a fresher single-chain update that arrived
while the bulk request was in-flight. Concurrent single-chain ordering (no
per-request IDs on balance-updated) is noted as a known remaining gap.

#7: Non-forced refresh loading guard. loadingBalancesRef (a ref, not state) lets
non-forced calls skip when any refresh is in progress while still letting forced
refreshes always proceed and supersede the in-flight non-forced call via the
generation counter.

#4+#5: Backend CAIP normalization. Symbol fallback removed entirely — d.symbol
is ambiguous (ETH = Ethereum, Arbitrum, Optimism, Base). d.chain is now
normalized: CAIP-19 (contains "/") passes through; CAIP-2 or internal id
resolves via chains lookup; anything else is dropped.

#9: Debounce key narrowed to networkId (CAIP-2 prefix). Multiple token-level
CAIP-19 pushes on the same EVM network (eip155:1/erc20:0x...) now collapse into
a single debounced refresh rather than each getting its own 2s timer.

#8: Ledger stamp moved after diff check. lastRectifyMs is only updated when a
real reconcile entry is actually written, so a no-op refresh (diff ≈ 0) no
longer consumes the 5-minute throttle window.

#10: AssetPage output-chain match now fires rpcFire('getBalance') for the swap
output chain id, not handleRefresh() which would call getBalance for chain.id
(the page's current chain).

* fix(swap): live balance sync in dialog + stale-cache deposit-channel trim

- SwapDialog subscribes to balance-updated so sendMax math uses current
  balance, not the snapshot from dialog-open time
- Deposit-channel value trim now also fires when isMax=false but live
  balance can't cover relayValue + gas (stale quote against changed balance)
- Improved insufficient-balance error message hints user to re-enter MAX

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a teal Recheck button to the submitted-phase swap view alongside
the Explorer and tracker buttons. Fires a single on-demand refreshSwap
poll — spins while in-flight, reverts to idle on settle. Lets users
unstick a swap that stopped updating without closing the dialog.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ty pill clip

- Move view-picker out of the top-center flex row and absolutely position
  it at the top-right of the chart canvas (zIndex above the donut).
- Shrink DonutChart from 380 to 300 and tighten the hero region's
  minH (60vh/70vh -> 44vh/50vh) so the chart sits closer to the legend.
- Tighten the below-sun row: minH 200->160, pt/pb 3->1/2, gap 3->2,
  so the legend + Receive/Send/Swap + token rows fit without scrolling
  in the drilled-chain view.
- Activity pill: offset 20->28px and pin transform-origin to bottom-right
  so the bounce/hover scale doesn't push it past the viewport edge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- AssetPage: show combined balance indicator when EVM balance spans multiple
  addresses (⬡⬡ total · N addrs)
- SwapDialog: add "From address" switcher pill UI (scoped to dialog via local
  evmAddressIndexOverride — doesn't mutate global selectedIndex in AssetPage)
- Fix signing path: buildRelaySwapTx + buildEvmSwapTx now use fromEvmAddressIndex
  to select the correct HD key (was always hardcoded to index 0)
- Fix fromAddress/fromBalance/fromEvmAddressIndex desync — all now derive from
  effectiveEvmIndex; EVM address check is prioritized over prop address in useMemo
- Invalidate stale quotes when address changes mid-review/quoting phase
- Raise Polygon MAX gas reserve: 0.05 → 0.5 MATIC (covers 3000+ gwei spikes)
- Revert calldata relay trim (was silently sending wrong amounts on stale quotes);
  replace with loud "Quote was built for a different address" error on large mismatch
Two pieces:

1. Fix EVM derivation path. Vault was deriving m/44'/60'/0'/0/{N}, which
   varies the receive-address index inside account 0. MetaMask, ShapeShift
   and keepkey-client all derive m/44'/60'/{N}'/0/0 — varying the hardened
   account index. Result: autoDiscover scanned indices 1-9 against the wrong
   addresses and always found 0 balance. Now derives the MetaMask-compatible
   path. Bumped SETTINGS_VERSION (1 → 2) so any persisted indices > 0 from
   the old scheme are dropped and re-discovered. Account 0 is identical
   under both schemes, so no migration is needed for users who only ever
   used account 0.

2. Per-account drilled view. Added getEffectiveBalance(chainId) helper that,
   for EVM chains with a selected non-default address, returns that
   account's per-chain balance + tokens instead of the chain-wide aggregate.
   Routed through this helper:
     - drilledChainTokensChartData (center donut)
     - ChainDetailOrbital
     - Heatmap + Stack drilled views
     - DonutChart token-click handler
     - Receive/Send/Swap action row + token list

   Sidebar chain row still shows the aggregate. Drop-down only renders when
   the chain is drilled AND has 2+ funded accounts (single funded account
   is already represented by the chain row itself). Drop-down rows are
   clickable to switch the active account; selected row gets a colored
   left-bar indicator, address snippet, and bold balance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pioneer's Relay SOL→EVM bridge returns txParams.serializedTx (base64
wire tx) instead of EVM calldata. The parser only recognized hasRealCalldata
and isDepositChannel, so Solana quotes fell through to the "missing memo"
error. Now hasSolanaPrebuiltTx triggers the prebuilt path and populates
relayTx.serializedTx, which swap.ts already routes to the Solana signer.

Also makes RelayTxParams.gasLimit optional (the EVM builder already emits
undefined for it when Pioneer omits the field).
…n-chain list is empty

When a user searches a symbol and no assets match on the selected chain,
fire Pioneer's SearchAssets endpoint and show results from all networks
with a "Other networks — will cross-chain swap" header. Results use the
existing unknown/TRY-QUOTE availability status so they're visually
distinct but still selectable. Eliminates "No assets found" dead-ends
for tokens not in GetAvailableAssets (e.g. VVV on Base).
…kens, heatmap polish

Layout consistency:
- Chart slot is now a fixed 380px when a chain is drilled, 62vh in the
  All Chains view. Action row + token list sit at the same Y position
  across all view modes (orbital, donut, heatmap, stack) when drilled.
- Orbital and ChainDetailOrbital capped at 640/380 to fit the slot.
- Heatmap maxW switches between 1100 (all-chains) and 640 (drilled) for
  visual consistency with the other view modes.

Tokens area:
- Token list is now scrollable inside its own region (maxH 38vh,
  overflowY auto) so the chart + Receive/Send/Swap stay pinned while
  the user scrolls a long token list. Reuses the kk-tokens-scroll CSS
  class that matches the existing sidebar scrollbar styling.
- Tightened padding/gaps on the below-sun row so tokens sit higher.

Heatmap polish:
- valueExponent compression 0.65 → 0.45 so small tiles get usable area
  next to a five-figure position.
- Skip tiles whose share < 3% of portfolio (drop into "Others"); skip
  the Others tile itself if its cumulative share is < 3%. Eliminates
  the 1-pixel slivers that the squarified treemap produced for tiny
  chains in a wide-distribution portfolio.
- overflow:hidden on the heatmap wrapper so anything past bounds gets
  clipped cleanly instead of bleeding tooltips below the canvas.
- Cap to top N tiles to keep the layout legible.
- For drilled chain detail: native + top tokens + Others fold, same rule.

Single-asset fallback:
- When heatmap/stack would render <2 tiles, fall back to orbital
  (ChainDetailOrbital for drilled, OrbitalView for all-chains) instead
  of donut. Single-slice donut is meaningless; orbital still reads.
- View picker hides Heatmap + Stack options when there are <2 assets
  to compare.

Watch-only:
- Keep btcAccounts + evmAddresses in memory on device disconnect so the
  watch-only / cache UI keeps rendering the last-known per-account
  data. Seed-change handler still resets them on true seed swap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Added pt="6" (24px) to the chart container so the top orbital satellite
clears the canvas top edge in the All Chains view. Other view modes
(donut, heatmap, stack) are unaffected since they don't sit at the very
top of the slot.

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

swap-support-matrix: adds a `has()` indirection layer that checks Pioneer-loaded
dynamic chain sets first, falling back to static sets when Pioneer is unavailable.
loadSupportedChains() fetches /api/v1/swappers/supported-chains at startup and
caches for the session. Adds Solana to SHAPESHIFT_CHAINS (LiFi covers thousands
of SPL tokens, confirmed 2026-05).

App.tsx: calls loadSupportedChains(pioneerApiBase) once settings load so dynamic
coverage is available before the first swap dialog opens.

Dashboard.tsx: persists dismissed swap txids to localStorage so dismissals survive
reload. Force-refreshes balances (bypassing Pioneer cache) on swap-completed event
with retries at 30 s and 90 s to catch indexer lag.

SwapDialog.tsx: shows a spinner for per-address chain balances that haven't loaded
yet; falls back to global balances for the currently selected address so the UI
never shows a misleading zero.

useEvmAddresses.ts: re-fetches EVM address set on every balance-updated event so
per-address chainBalances stay current after a refresh cycle.

Tests: new Solana static + dynamic override coverage in swap-support-matrix.test.ts.
…ap/stack + ⌘K)

# Conflicts:
#	projects/keepkey-vault/src/mainview/components/AssetPage.tsx
#	projects/keepkey-vault/src/mainview/components/Dashboard.tsx
…r, gated minFirmware 7.16+)

# Conflicts:
#	modules/device-protocol
#	modules/keepkey-firmware
… stale Pioneer URL cache, localStorage type safety

useEvmAddresses: remove balance-updated listener — redundant with the existing
evm-addresses-update push in index.ts:2676 which already fires per-EVM-chain
on every balance fetch. The old listener triggered ~30 concurrent getEvmAddresses
RPCs on each full refresh including non-EVM chains.

swap-support-matrix: track loaded Pioneer base URL; re-fetch when loadSupportedChains
is called with a different base (e.g. user changes Pioneer host in settings).
Previously the `if (dynamicChains) return` guard would keep stale coverage for
the whole session after a URL change. _resetDynamicChains now also clears the
base URL so test isolation is preserved.

Dashboard: validate localStorage content before populating dismissedSwapTxids;
filter to string-only entries so a corrupted or future-format stored value
cannot cause all dismissed swaps to reappear on reload.
Both setPioneerApiBase and setActivePioneerServer already reset Pioneer,
clear the chain catalog, and flush the swap cache. Added
loadSupportedChains(getPioneerApiBase()) to each so the Bun process
also re-fetches dynamic chain coverage from the new server.
Without this the Bun module's dynamicChains stayed bound to the
original startup server for the whole session.
Reverts the view-picker from absolute top-right back to the original centered flex row above the hero area.
DonutChart now renders a muted empty ring with $0.00 center text when
total === 0, instead of returning null. Drilled-chain placeholder value
changed from 1 to 0 so the chart always appears but accurately reflects
the real balance.
…r v7.14.1 hashes

- Pin modules/device-protocol to keepkey/device-protocol upstream master (f2c3c005)
  dropping fork-only Zcash clear-signing + NEAR proto fields not yet in firmware
- Register firmware v7.14.1 hash (e0c05185) in firmware/manifest.json,
  firmware/releases.json, firmware-bundle/releases.json, and firmware-versions.ts
- Update firmware-bundle v7.14.1 binary to match new hash
- Handle Pioneer 404 on Relay-id registration (race condition, retry on next refresh)
@BitHighlander BitHighlander requested a review from pastaghost as a code owner May 27, 2026 23:59
@BitHighlander BitHighlander merged commit ee7c23e into master May 28, 2026
7 checks 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.

2 participants