fix(hive): render op summaries in the approval card#131
Conversation
hiveHandler built a one-line summary per operation (opSummary) into
unsignedTx.operations and no component ever read it. Grepping
pages/side-panel/src/approval for `.operations` returned zero hits, so a
Hive batch fell through to the generic amount table, which has no
destination or amount to show for an op batch and rendered "N/A" and "0".
The summaries surfaced only inside RequestDataCard's collapsed raw-JSON
dump.
That matters most for the four ops the extension does not construct
itself — claim_reward_balance, account_update2, limit_order_create and
limit_order_cancel reach the device via requestBroadcast, so a dApp
composes them and the panel was the user's only look at them before the
device screen. The OLED is authoritative and was always correct, so this
was never a signing hole; but the clear-sign table exists so the user can
compare the screen against what the dApp asked for, and half of that
comparison was an unreadable blob.
RequestDetailsCard now renders the account plus one row per op, falling
back to the op name rather than blanking a row — an unsummarized op must
stay visible, not disappear.
SUPPORTED_OPS and opSummary move to hiveOps.ts, a leaf module.
hiveHandler.ts imports @extension/storage, which touches chrome.* at
import time and throws under vitest ("chrome is not defined"), so the
table was untestable where it lived. No logic changed in the move.
hiveOpSummary.test.ts pins the contract the card depends on: every op in
SUPPORTED_OPS must summarize to something other than its own bare name
(opSummary's default arm returns `name`, which is exactly the unreadable
render this fixes) and must carry a test payload. It also pins the
details a user has to check against the OLED — amounts with their
symbols, the counterparty account, the '0.000000 VESTS' sentinels that
mean stop/remove rather than "send zero", and comment_options
beneficiary names and percentages, since a payout redirect is the one
thing in the table an attacker would most want unreadable.
Differential-verified: deleting the limit_order_create case fails with
"limit_order_create falls through to the default arm — add a case to
opSummary()".
121 tests pass, up from 116. type-check clean across all 15 packages.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review of #131 found three ways the approval could differ from what the device signs. custom_json rendered String(p.json). The vault serializes `typeof json === 'string' ? json : JSON.stringify(json ?? {})` (hive-ops.ts:151), so a dApp passing an object got "[object Object]" in the panel while the object's real contents went to the device — the approval showed neither the value nor that it was hiding one. Mirror the vault's own expression instead. Truncation at 120 chars was unmarked, so two payloads sharing a prefix rendered as the same string. The cut now carries the dropped length. comment_options showed only the post and its beneficiaries, omitting max_accepted_payout, percent_hbd, allow_votes and allow_curation_rewards. A declined payout, an all-HIVE split and a votes-disabled post therefore had identical browser approvals. Every control that differs from the Hive default is now named; defaults stay quiet so the common case remains a one-liner. percent_steem_dollars is read as the alias the vault also accepts (hive-ops.ts:225) — ignoring it would have shown the default while a non-default value was signed. unsignedTx.operations kept only {op, summary}, so the Raw tab could not recover what the summary elides. It now carries `params` verbatim. RequestDataCard renders transaction.unsignedTx and nothing else — the event's own `request` is never displayed in any tab — so this is the only surface on which the operation body appears at all. The approval also opened on the Raw tab (defaultIndex={1}), whose data section is useState(false) and so renders a collapsed chevron with no transaction facts on it. A user could approve having seen nothing. Basic is now the default. This affects all five chains routed to OtherTransaction (ripple, solana, ton, tron, hive), and is an improvement or a wash for each: Hive, Ripple and Tron contract-calls gain a populated table, and the chains that render N/A in Basic were previously landing on an empty panel anyway, so the cost is one click to reach Raw and no fact became less visible. Differential-verified: restoring String(p.json) fails with "expected 'follow: [object Object]' not to contain '[object Object]'"; dropping the payout controls fails with "expected 'Payout options for @alice/a-post' to contain '0.000 HBD'". 125 tests pass, up from 121. type-check, prettier and build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
All three addressed in 1. custom_json approval vs signed payload — fixedMirrored the vault's own expression rather than approximating it: function serializedJson(json: any): string {
return typeof json === 'string' ? json : JSON.stringify(json ?? {});
}Byte-identical to Tests added for object payloads, string payloads, the truncation marker, differing suffixes, and the no-marker-when-short case. 2. comment_options payout controls — fixed, both halvesEvery control that differs from the Hive default is now named; defaults stay quiet so the common case remains a one-liner: Also caught while implementing: On recoverability — you're right that 3. Default tab — fixed, and the finding was stronger than stated
Since this changes the default for all five chains routed to
No chain regresses; cost is one click to Raw for the wash row. Verification125 tests (was 121); type-check, prettier, build clean. Differential-verified both P1 fixes — reverting each fails loudly: I also ran an adversarial audit over the fixes (5 lenses × per-finding refutation, 35 candidates). None survived, but two pre-existing defects it surfaced are worth separate issues, both out of scope here:
Not touched here: host-side constraint validation (max-4, single-tier, beneficiary sort, |
What was broken
hiveHandlerbuilds a one-line summary per operation and stashes it inunsignedTx.operations(hiveHandler.ts:641). No component ever read it — greppingpages/side-panel/src/approval/for.operationsreturns zero hits.So a Hive op batch fell through to the generic amount table, which has no destination and no amount to show for a batch:
N/A0@alicelimit_order_createSell 1.500 HIVE for 0.400 HBDlimit_order_cancelCancel order 1 (@alice)The summaries did exist in the UI — inside
RequestDataCard's collapsed raw-JSON dump.Why it matters
Four ops in
SUPPORTED_OPSare passthrough-only:claim_reward_balance,account_update2,limit_order_create,limit_order_cancel. Keychain has no dedicated request method for them, so a dApp composes them and sends them throughrequestBroadcast— the panel was the user's only look before the device screen.The OLED is authoritative and was always correct, so this was never a signing hole. But the clear-sign table exists so the user can compare the screen against what the dApp asked for, and half of that comparison was an unreadable blob.
The move to
hiveOps.tsSUPPORTED_OPSandopSummaryare now a leaf module.hiveHandler.tsimports@extension/storage, which toucheschrome.*at import time and throwschrome is not definedunder vitest — the table was untestable where it lived. Same pattern as thefeeFloors/lastResortRpcsleaf modules already inchains/. No logic changed in the move.Tests
hiveOpSummary.test.tspins the contract the card now depends on: every op inSUPPORTED_OPSmust summarize to something other than its own bare name (opSummary'sdefaultarm returnsname— precisely the unreadable render this fixes) and must carry a test payload, so adding an op without a summary fails the build.It also pins what a user has to check against the OLED: amounts with symbols, the counterparty, the
'0.000000 VESTS'sentinels that mean stop / remove rather than "send zero", andcomment_optionsbeneficiary names + percentages — a payout redirect being the one thing in the table an attacker would most want unreadable.Differential-verified — deleting the
limit_order_createcase fails with:121 pass, 11 files (up from 116).
make type-checkclean across all 15 packages;make buildclean.Not in scope
Host-side constraint validation (max-4 ops, single-tier, beneficiary sort,
fill_or_killboolean) stays absent client-side on purpose — the vault enforces all of it with a readable 400 before the device is touched, perdocs/handoff-bex-hive-full-operations.md§4. Duplicating it here is two places to drift.🤖 Generated with Claude Code