Skip to content

polish: signing dialog, unpriced balances, Hive button contrast - #382

Merged
BitHighlander merged 6 commits into
keepkey:developfrom
sktbrd:develop-polish
Jul 31, 2026
Merged

polish: signing dialog, unpriced balances, Hive button contrast#382
BitHighlander merged 6 commits into
keepkey:developfrom
sktbrd:develop-polish

Conversation

@sktbrd

@sktbrd sktbrd commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Three independent UI/correctness fixes found while running the latest develop locally. Each is its own commit and can be split if you'd rather review separately.

1. Hive panel outline buttons unreadable

The PeakD / Hive.blog / Ecency profile links and both Retry buttons used a bare Chakra variant="outline". The app mounts ChakraProvider with no color-mode provider, so that variant resolves against the gray palette's light-mode pair — near-black text inside a bright border — which inverts against the dark glass card.

Every other outline button in the app already passes explicit colors (SweepDialog, AddChainDialog, StakingPanel, …); this panel was the only one relying on the default, which is why it was the only place the inversion was visible.

Worth a follow-up: the light-color-mode setup means the next bare variant="outline" hits this again. Either force dark mode or add _dark conditions to the semantic tokens in theme.ts.

2. Signing dialog polish

  • Glow — the card ran an infinite 2s pulse peaking at a 24px/0.7-alpha gold shadow plus a 48px halo. Reduced to a faint 10px/0.18 breathe over 4s; border 2px solid gold → 1px at 45% alpha.
  • Actions always visible — the whole card was maxH="90vh" overflowY="auto", so a large calldata blob pushed Approve/Reject below the fold. Now: pinned header, scrolling content region, pinned action row.
  • Decoded calldata — new Etherscan-style Hex/Decoded toggle for payloads we can't clear-sign. No ABI means no parameter names, but selector + indexed 32-byte words (annotated where a word looks like an address or fits a safe integer) is what makes a blind-signing payload auditable. Malformed non-word-aligned trailing bytes are flagged.

New strings added to en only — other locales already run behind and fallbackLng: "en" covers them.

3. Unpriced balances rendered as "no balance"

const hasAggregate = (agg?.balanceUsd ?? 0) > 0

The drilled chain view gated its empty state on USD alone. A chain with a missing/failing price feed reports balanceUsd: 0 while the user still holds funds, so the pane rendered "No Arbitrum balance yet" over a real 0.000891 ETH balance — the sidebar showed the amount, the detail pane claimed the chain was empty. The fundedAddrs filter above it had the same bug.

Both now also check the token amount, via a shared hasNonZeroBalance() in lib/formatting.

Out of scope: the All-Chains total still excludes unpriced chains — that figure is USD and the arithmetic is correct. That Arbitrum native ETH resolves no price at all is a separate upstream pricing bug worth its own issue.

Testing

tsc --noEmit clean (aside from the pre-existing TS2688 minimatch implicit-type-library error on develop), vite build passes.

🤖 Generated with Claude Code

sktbrd and others added 3 commits July 30, 2026 16:22
The profile links (PeakD / Hive.blog / Ecency) and both Retry buttons used
a bare Chakra `variant="outline"`. The app mounts ChakraProvider without a
color-mode provider, so that variant resolves against the gray palette's
light-mode pair — near-black text inside a bright border — which inverts
against the dark glass card and renders the labels almost unreadable.

Every other outline button in the app already passes explicit colors
(SweepDialog, AddChainDialog, StakingPanel, ...); this panel was the only
one relying on the default. Add a shared `outlineBtn` style using the v3
tokens the panel already speaks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three fixes to the signing approval dialog, all visible on any blind-sign
request with a large payload:

- Glow: the card ran an infinite 2s pulse peaking at a 24px/0.7-alpha gold
  shadow plus a 48px halo, which washed out the card edge and made the hex
  payload hard to scan. Reduce to a faint 10px/0.18 breathe over 4s and drop
  the border from 2px solid gold to 1px at 45% alpha.

- Pinned actions: the whole card was `maxH=90vh overflowY=auto`, so a large
  calldata blob pushed Approve/Reject below the fold — the user had to
  scroll past dozens of lines of hex to reach either button. Split into a
  pinned header, a `flex=1 minH=0 overflowY=auto` scroll region, and a
  pinned action row.

- Decoded calldata: add an Etherscan-style Hex/Decoded toggle for payloads
  we can't clear-sign. Without an ABI we can't name parameters, but the
  4-byte selector plus indexed 32-byte words — annotated where a word looks
  like an address or fits a safe integer — is what makes a blind-signing
  payload auditable. Non-word-aligned trailing bytes are flagged, since
  that indicates malformed calldata.

New strings are added to en only; other locales already run behind and
i18next falls back to English via fallbackLng.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The drilled chain view gated its empty state on USD value alone:

    const hasAggregate = (agg?.balanceUsd ?? 0) > 0

A chain whose price feed is missing or failing reports balanceUsd 0 while
the user still holds funds, so the pane rendered "No <chain> balance yet"
over a real balance. Reproduced with Arbitrum holding 0.000891 ETH: the
sidebar showed the amount, the detail pane claimed the chain was empty.
For a wallet, "your funds are gone" is the worst possible false message.

The `fundedAddrs` filter directly above had the same bug, so the
"held on N other accounts" hint was likewise blind to unpriced accounts.

Both now also check the token amount, via a shared `hasNonZeroBalance()`
in lib/formatting so the next balance gate has something correct to reach
for. USD stays presentation-only.

Note: the All-Chains total is unchanged and still excludes unpriced chains
— that figure is USD and the arithmetic is right. That Arbitrum native ETH
resolves no price at all is a separate upstream pricing bug.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every EVM broadcast path handed the device's serialized bytes straight to
eth_sendRawTransaction without checking that the signature recovers to the
account we asked to sign. That is exactly the failure the browser extension
already guards against, and the vault had no equivalent seatbelt: a
WalletConnect dApp signing through the vault would silently broadcast a
doomed transaction.

The failure is not a compromised device. KeepKey firmware 7.x.0 through
7.14.0 hashes the EIP-1559 empty access-list byte (0xC0) too early, so any
tx whose calldata exceeds the 1024-byte single-chunk transport limit gets a
non-canonical pre-image. The signature is mathematically valid, so RPC nodes
ACCEPT the broadcast — and then the tx is dropped from the mempool forever,
because the address it recovers to has no balance and the wrong nonce. Fixed
in firmware 7.14.1, but the stable channel still ships an affected build, so
the check has to live in the vault.

Add verifyEvmSigner() and make `expectedFrom` a required parameter of
broadcastEvmTx(). Required rather than optional on purpose: a silently
mis-signed tx is unrecoverable once it leaves, so a future broadcast path
must not be able to forget the check. All five call sites are updated (three
in swap.ts, the custom-chain handler in index.ts, WalletConnect).

WalletConnect verifies even when it is not the broadcaster — the dApp may
broadcast the bytes we hand back, so the check belongs before they leave.

Failing loudly turns a silently stuck transaction — and, on a swap, a spent
approval with no swap — into an actionable error naming the firmware fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sktbrd

sktbrd commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

Added a fourth commit (ab68484e) — the funds-safety gap found while diagnosing the wrong-signer report.

Verify the device signer before broadcasting

Every EVM broadcast path handed the device's serialized bytes straight to eth_sendRawTransaction with no check that the signature recovers to the account we asked to sign. The browser extension already guards this; the vault had no equivalent, so a WalletConnect dApp signing through the vault would silently broadcast a doomed transaction.

This isn't about a compromised device. Firmware 7.x.0–7.14.0 hashes the EIP-1559 empty access-list byte (0xC0) too early, so any tx whose calldata exceeds the 1024-byte single-chunk transport limit gets a non-canonical pre-image. The signature is mathematically valid, so RPC nodes accept the broadcast — then it's dropped from the mempool forever, because the recovered address has no balance and the wrong nonce. Fixed in firmware 7.14.1, but stable still ships an affected build, so the check belongs in the vault.

Design note: expectedFrom is a required parameter of broadcastEvmTx() rather than optional. A silently mis-signed tx is unrecoverable once it leaves, so a future broadcast path must not be able to forget the check. All five call sites updated — three in swap.ts, the custom-chain handler in index.ts, and WalletConnect.

WalletConnect verifies even when it isn't the broadcaster, since the dApp may broadcast the bytes we hand back.

Tests

New __tests__/evm-signer-verify.test.ts, wired into make test-unit — 4 passing: valid signature accepted, checksum-case tolerated, mismatch rejected (the firmware bug in miniature), unparseable bytes rejected rather than passed through.

Full suite: 325 pass / 2 fail. Those 2 failures are pre-existing — I confirmed by stashing this branch's changes and re-running develop, which gives the identical 2 fail / 2 errors (the errors are electrobun's Updater.ts looking for ../Resources/version.json outside an app bundle). Not introduced here.

@BitHighlander BitHighlander left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the full 11-file diff. I found two fail-open paths in the EVM signer guard and pushed commit 33495f65 to the PR branch:

  • signer-verification failures are now typed and rethrown before the swap code can fall back to Pioneer, so rejected bytes cannot route around the guard
  • custom EVM broadcast now verifies against the request's fromAddress, not mutable current account selection between build/sign/broadcast

Validation after the correction:

  • bun test __tests__/evm-signer-verify.test.ts — 4 passed
  • full renderer/backend/Electrobun bun run build passed
  • git diff --check passed

I found no further code-review blockers in the Hive styling, unpriced-balance handling, signing-dialog/calldata inspector, WalletConnect verification, or EVM paths. The remaining gate is canonical GitHub Actions; develop PR builds are being enabled in #381, after which this branch should be synchronized/re-run before merge.

@BitHighlander BitHighlander left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved after full review and correction of the EVM signer-verification paths.

The branch was synchronized with current develop after #381 merged. Combined head 33a33e00 passes canonical GitHub Actions on Linux and macOS, including module builds, the Ironwood Zcash sidecar, the full Vault app build, architecture verification, packaging, and artifact upload.

The earlier fail-open findings remain fixed:

  • signer-verification errors cannot fall through to Pioneer broadcast
  • custom EVM broadcast verifies against the signed request's fromAddress, not mutable account selection

No unresolved review threads or remaining merge blockers.

@BitHighlander
BitHighlander merged commit 76af0f2 into keepkey:develop Jul 31, 2026
4 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