release: v1.4.0#198
Merged
Merged
Conversation
- 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>
…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)
… THORChain, Ripple)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Release
Pre-release: https://github.com/keepkey/keepkey-vault/releases/tag/v1.4.0
Develop merge-back: 5bb2de9
Test plan