From 1d26f151817da4b2e01b669dfd3cff8c890d9236 Mon Sep 17 00:00:00 2001 From: xvlad <116202536+sktbrd@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:22:43 -0300 Subject: [PATCH 1/5] fix(hive): make Hive panel outline buttons readable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../mainview/components/HiveAccountPanel.tsx | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/projects/keepkey-vault/src/mainview/components/HiveAccountPanel.tsx b/projects/keepkey-vault/src/mainview/components/HiveAccountPanel.tsx index 1352f1fc..fb3f2a11 100644 --- a/projects/keepkey-vault/src/mainview/components/HiveAccountPanel.tsx +++ b/projects/keepkey-vault/src/mainview/components/HiveAccountPanel.tsx @@ -34,6 +34,21 @@ function Confetti() { type Avail = { success: boolean; available: boolean; reason?: string } type CreateResp = { status: number; success?: boolean; txid?: string; username?: string; error?: string; retryAfter?: number } +// Chakra's bare `variant="outline"` resolves to the gray palette's light-mode +// pair (dark text on a bright border), which inverts against the glass card. +// Every other outline button in the app passes colors explicitly; these match +// that idiom using the v3 tokens this panel already speaks. +const outlineBtn = { + color: "var(--text-1)", + borderColor: "var(--line-2)", + bg: "rgba(255, 255, 255, 0.03)", + _hover: { + color: "var(--text-0)", + borderColor: "rgba(255, 255, 255, 0.22)", + bg: "rgba(255, 255, 255, 0.07)", + }, +} as const + // Standalone copy-icon button with its own transient "copied" state. function CopyBtn({ value, label }: { value: string; label: string }) { const [copied, setCopied] = useState(false) @@ -93,7 +108,7 @@ export function HiveAccountPanel({ activeKey, color, loading, deriveError, onRet if (deriveError || !loading) return ( {deriveError || "Couldn't derive your Hive key from the device."} - {onRetryDerive && } + {onRetryDerive && } ) return @@ -104,7 +119,7 @@ export function HiveAccountPanel({ activeKey, color, loading, deriveError, onRet if (state === "error") return ( Couldn't reach the Hive account service. Try again shortly. - + ) @@ -128,9 +143,9 @@ export function HiveAccountPanel({ activeKey, color, loading, deriveError, onRet { label: "Hive.blog", url: `https://hive.blog/@${account.name}` }, { label: "Ecency", url: `https://ecency.com/@${account.name}` }, ].map(l => ( - ))} From 782b2fdcb8abdda3dcc05977b931fe4020c66448 Mon Sep 17 00:00:00 2001 From: xvlad <116202536+sktbrd@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:22:54 -0300 Subject: [PATCH 2/5] feat(signing): calmer glow, decoded calldata view, pinned actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../components/device/SigningApproval.tsx | 162 ++++++++++++++++-- .../src/mainview/i18n/locales/en/device.json | 7 + 2 files changed, 154 insertions(+), 15 deletions(-) diff --git a/projects/keepkey-vault/src/mainview/components/device/SigningApproval.tsx b/projects/keepkey-vault/src/mainview/components/device/SigningApproval.tsx index 864641c7..ba359b6d 100644 --- a/projects/keepkey-vault/src/mainview/components/device/SigningApproval.tsx +++ b/projects/keepkey-vault/src/mainview/components/device/SigningApproval.tsx @@ -41,9 +41,13 @@ const METHOD_LABEL_KEYS: Record = { } const SIGNING_ANIMATIONS = ` + /* Deliberately restrained: a signing prompt should read as serious, not + alarming. The old 24px/48px throb washed the card edge out and made the + hex payload hard to scan. This keeps a faint gold presence that breathes + instead of pulsing. */ @keyframes signingPulseGlow { - 0%, 100% { box-shadow: 0 0 8px 2px rgba(233,196,106,0.4); } - 50% { box-shadow: 0 0 24px 8px rgba(233,196,106,0.7), 0 0 48px 16px rgba(233,196,106,0.15); } + 0%, 100% { box-shadow: 0 0 0 1px rgba(233,196,106,0.10), 0 16px 44px -12px rgba(0,0,0,0.75); } + 50% { box-shadow: 0 0 10px 1px rgba(233,196,106,0.18), 0 16px 44px -12px rgba(0,0,0,0.75); } } @keyframes signingFlashBorder { 0%, 100% { border-color: rgba(233,196,106,0.5); } @@ -372,6 +376,127 @@ function CalldataSection({ decoded, t }: { decoded: CalldataDecodedInfo; t: (k: ) } +// ── Raw calldata inspector (Etherscan-style hex / decoded toggle) ───── + +type CalldataWord = { index: number; hex: string; asAddress?: string; asUint?: string } + +/** + * ABI-less calldata split, mirroring Etherscan's "Decode Input Data" default + * view. Without an ABI we cannot name parameters, but the 4-byte selector plus + * indexed 32-byte words is exactly what makes a blind-signing payload auditable: + * a reviewer can spot recipient addresses and amounts in the argument slots. + * + * Heuristics per word (both may be shown; neither is authoritative): + * - 12 leading zero bytes + 20 non-zero bytes -> likely an address + * - fits in a JS-safe integer -> show the decimal value + * + * Dynamic types (offsets, arrays, bytes) still appear as words, matching how + * Etherscan renders them when no ABI is available. + */ +function decodeRawCalldata(data: string): { selector: string; words: CalldataWord[]; trailing?: string } | null { + const hex = data.startsWith("0x") || data.startsWith("0X") ? data.slice(2) : data + if (!/^[0-9a-fA-F]*$/.test(hex) || hex.length < 8) return null + + const selector = "0x" + hex.slice(0, 8) + const body = hex.slice(8) + const words: CalldataWord[] = [] + const fullWords = Math.floor(body.length / 64) + + for (let i = 0; i < fullWords; i++) { + const word = body.slice(i * 64, i * 64 + 64) + const w: CalldataWord = { index: i, hex: "0x" + word } + if (/^0{24}/.test(word) && !/^0{64}$/.test(word)) { + w.asAddress = "0x" + word.slice(24) + } + // Only surface a decimal when it round-trips exactly — a truncated + // big number is worse than no number at all. + const asBig = BigInt("0x" + word) + if (asBig <= BigInt(Number.MAX_SAFE_INTEGER)) w.asUint = asBig.toString() + words.push(w) + } + + const rest = body.slice(fullWords * 64) + return { selector, words, trailing: rest.length ? "0x" + rest : undefined } +} + +function CalldataInspector({ data, t }: { data: string; t: (k: string, f?: string) => string }) { + const [view, setView] = useState<"hex" | "decoded">("hex") + const parsed = view === "decoded" ? decodeRawCalldata(data) : null + + return ( + + + + {t("signing.data", "Data")} + + + {(["hex", "decoded"] as const).map(mode => ( + + ))} + + + + + {view === "hex" || !parsed ? ( + + {view === "decoded" && !parsed + ? t("signing.dataNotDecodable", "Payload is not valid hex calldata — showing raw value.") + " " + data + : data} + + ) : ( + + + + {t("signing.selector", "Selector")} + + {parsed.selector} + + {parsed.words.map(w => ( + + [{w.index}] + + {w.hex} + {(w.asAddress || w.asUint) && ( + + {w.asAddress && ( + + {t("signing.asAddress", "addr")}: {w.asAddress} + + )} + {w.asUint && ( + + {t("signing.asUint", "uint")}: {w.asUint} + + )} + + )} + + + ))} + {parsed.trailing && ( + + + {t("signing.trailing", "extra")} + + {parsed.trailing} + + )} + + )} + + + ) +} + // ── Solana decoded section ──────────────────────────────────────────── function shortenPubkey(pk: string): string { @@ -740,9 +865,9 @@ export function SigningApproval({ request, phase, onApprove, onReject, onCancel > {/* ── Header row: badge + app + method + timer + trust ── */} - + + {/* Scroll region. Only the reviewable content scrolls — the header + above and the action buttons below stay pinned, so a large + calldata blob can never push Approve/Reject out of reach. + minH=0 is required for a flex child to shrink below its + content height and actually scroll. */} + {/* ── Method ── */} - {methodLabel} + {methodLabel} {/* ── AdvancedMode gate ── */} {advancedModeRequired && ( @@ -957,7 +1088,7 @@ export function SigningApproval({ request, phase, onApprove, onReject, onCancel {request.chainId !== undefined && } {request.data && (!decoded || decoded.source === 'none') && ( - + )} @@ -966,9 +1097,10 @@ export function SigningApproval({ request, phase, onApprove, onReject, onCancel {/* ── Full raw payload (collapsible) ── */} + - {/* ── Action buttons ── */} - + {/* ── Action buttons (pinned below the scroll region) ── */} +