release: 0.0.28 — Tron + TON, side-panel approval merge, hot-swap fixes#51
release: 0.0.28 — Tron + TON, side-panel approval merge, hot-swap fixes#51BitHighlander wants to merge 29 commits intomasterfrom
Conversation
When an MV3 service worker wakes up and calls set() with an updater (e.g. requestStorage.addEvent spreading prev into a new array), the in-memory cache may still be null because _getDataFromStorage() is async. The updater then runs with prev = null and [...null] throws "TypeError: a is not iterable", aborting addEvent and leaving the approval flow in a bad state (popup opens for a request that was never persisted). Load the cache synchronously-ish (await from chrome.storage) in set() if it's still null, so every updater receives a real value. Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
* fix: harden popup lifecycle against hangs and duplicates
Addresses three symptoms: popup hanging open after completion, multiple
popup windows opening per request, and the approval promise never
resolving when the user X's the popup.
methods.ts
- Replace in-memory isPopupOpen flag with chrome.windows.getAll lookup
that survives service worker restarts. Focuses an existing popup
(matched by URL) instead of opening a second one.
- Register chrome.windows.onRemoved exactly once at module load and
fan out to per-request subscribers, instead of adding a new listener
on every openPopup() call (previously leaked forever).
- requireApproval now resolves {success:false} when the popup closes
without an eth_sign_response, so chain handlers no longer hang and
the dapp's RPC call terminates.
chain handlers
- Every transaction_complete / signature_complete / transaction_error
message now carries eventId so the popup can match it to the right
in-flight request. ethereum threads the id through signMessage /
signTypedData; solana's buildEvent mutates requestInfo.id so the
match works end-to-end.
Transaction.tsx
- Ignores completion/error messages whose eventId doesn't match the
current event. Previously any signature_complete would close the
popup, slamming the window on unrelated queued requests.
ethereumHandler.ts
- Removes dead duplicate openPopup / requireUnlock (never called,
would have bypassed dedup if it had been).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* fix: tag outer handleWalletRequest error with eventId
The outer catch in handleWalletRequest sends transaction_error to the
popup but was the one remaining site missing the eventId scope tag.
Combined with Transaction.tsx's backward-compat rule that accepts
unscoped messages, a thrown chain handler would surface its error on
the wrong event if two events were queued — exactly the
cross-contamination this PR is fixing everywhere else.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
* fix: harden popup empty state UX and make it self-healing Addresses the "popup open but says 'no events' with no recovery" failure mode, plus a latent crash if requestStorage returns null. Events.tsx - Null-guard requestStorage.getEvents() — previously a null return would throw on `for...of null` and white-screen the popup. - Wrap the fetch in try/catch and render a dedicated error state instead of crashing. - Subscribe to requestStorage changes so a second request arriving while the popup is open becomes visible immediately (no need to resolve the current request first). - Auto-close the window 3s after landing in an empty state. Covers the case where a request was cancelled upstream, where cleanup ran before the window closed itself, or where the popup was opened with no pending request. - Better copy in the empty / loading / error states so the user knows what is happening and that the window will close itself. - Keep currentIndex in bounds if the event list shrinks beneath it. Popup.tsx - Replace the placeholder "Error Occur" / "Loading ..." fallbacks with a proper Chakra-styled error panel that includes a Close button, so a rendering crash doesn't leave the user with no way out other than clicking the OS window close button. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix: clamp Events currentIndex inline to avoid one-render crash The useEffect-based bounds check snapped currentIndex back only AFTER the render that caused it to go out of bounds. When requestStorage's subscribe fired and the event list shrank beneath currentIndex, the first render after the shrink still had the stale index — passing events[currentIndex] = undefined into <Transaction />, which reads event.id immediately and error-boundary crashes. Compute a clamped safeIndex during render instead. currentIndex as state is preserved for user-driven navigation; only the array access is guarded. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix: auto-close popup on fetch error, not just empty state The error-state UI copy already said the window would close itself, but the auto-close effect bailed on fetchError, leaving the user stuck on a dead-end error screen — the exact failure mode this PR is trying to remove. Collapse the two conditions into a single shouldAutoClose predicate so any non-actionable state (empty OR fetch failure) triggers the timer. Cleanup still runs correctly on recovery (error clears → new events arrive → previous cleanup cancels the timer, new effect short-circuits). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
* fix: replace broken remote logo refs with local assets
The client referenced api.keepkey.info/coins/{keepkey,pioneerMan,ethereum}.png
shortnames that never existed on the CDN (all 403). The CDN only serves
CAIP-base64-encoded filenames, which the dynamic asset-icon code already
generates correctly. This change fixes every hardcoded shortname usage:
- KeepKey brand marks across popup + side-panel switch to bundled /kk-logo.png
(and chrome.runtime.getURL for the EIP-6963 announce icon and manifest
web_accessible_resources so dapps can load it).
- AddDappModal default icon now uses caipToIcon('eip155:1/slip44:60'), which
the CDN does serve.
- NetworkDropdown hides the avatar entirely when no network is selected
instead of rendering Chakra's default silhouette from a 403 fallback.
- headerUtils.getIconUrl last-resort fallback no longer builds
btoa(chainSymbol) URLs (always 403); returns '' so Chakra Avatar falls
back to an initial letter.
- wallet.ts serviceImageUrl points at pioneers.dev (chrome-extension:// URLs
can't load in the vault desktop UI).
Third-party image hosts also replaced with bundled assets:
- Animated kk.gif (5.6MB from i.ibb.co) converted to kk.webp (286KB, 240px,
15fps) — 95% smaller, same animation.
- MetaMask fox and Keplr logos downloaded from their official repos.
- Xfi src dropped (Coming Soon overlay covers the avatar; letter fallback).
Net: zero remote image hosts remain outside the working CAIP CDN pattern.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* fix: move new branded assets to chrome-extension/public root
Absolute src="/kk.webp" and /brand/... paths resolve to the extension
origin root (chrome-extension://<id>/kk.webp). Vite's base:'' does not
rewrite these, so emitting them under pages/side-panel/public/ (→
dist/side-panel/) was a 404. Moving to chrome-extension/public/ lands
them at the dist root and matches the already-working /kk-logo.png
convention.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
SidePanel.tsx default-state welcome screen uses <img src="/logo_vertical.svg"> as an opacity-0.25 watermark. The file was in pages/side-panel/public/, so Vite emitted it to dist/side-panel/logo_vertical.svg while the absolute path requested chrome-extension://<id>/logo_vertical.svg — 404. Same class of bug that #41 fixed for kk.webp and brand icons. Moving both vertical logos to chrome-extension/public/ lands them at the dist root and matches the working /kk-logo.png / /kk.webp convention. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Previous encode (q=50, 15fps) produced visible "tracing" — residual inter-frame prediction errors that looked like motion trails. Bumping quality to 90 and frame rate to 20fps eliminates the smear while still landing at 560KB (90% smaller than the original 5.6MB GIF). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The ffmpeg-produced animated WebP had broken frame disposal — every frame composited on top of the previous, leaving "tracing" ghosts. Falling back to a properly-optimized GIF (240px, 192 colors, coalesced + layered optimize) lands at 690KB and animates correctly. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
- Prefetch Solana pubkey during onStart so the network shows up in the dropdown without waiting for a dapp-initiated request. Previously the Solana pubkey was only derived lazily on first dapp interaction, so the network list silently skipped it on fresh sessions. - Hide the total balance + Send/Receive action row during the initial balance fetch. Showing $0.00 + disabled buttons while loading caused a jittery shift once balances arrived. - Center the Balances loading spinner full-height with a caption and smoother spin. Replaces the top-anchored inline spinner that looked cramped above the card list. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat: spice up balance loading screen with hero spinner and skeleton rows Replace the plain centered spinner + "Loading balances…" text with a multi-layer animated hero spinner (triple counter-rotating rings, pulsing glow, breathing center dot) above skeleton cards that mirror the real asset row layout. Adds a subtle kk-logo watermark behind everything and a teal shimmer sweep that travels across each skeleton card staggered by 150ms. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix: show Solana balance and SPL tokens in side panel Three layered bugs were each sufficient to zero out Solana balance display; all three are fixed together because fixing any one alone leaves the chain still broken: 1. Wrong CAIP in shortListSymbolToCaip['SOL'] / shortListNameToCaip.solana. Previously pointed at wrapped-SOL SPL token (solana:.../solana:so111…, all lowercase). Now points at native SOL (solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501), matching pioneer-caip's ChainToCaip and vault-v11's config. 2. Wrong Pioneer endpoint for Solana. /charts/portfolio returns empty {balances:[], tokens:[]} for any Solana pubkey (verified via direct curl). Vault-v11 uses /portfolio via pioneer.GetPortfolioBalances, which returns natives + SPL tokens in one flat array. Route Solana pubkeys to a third batch hitting /portfolio with the required key:public-* Authorization header; EVM/UTXO still go through /charts/portfolio for its richer Zapper/Unchained token data. 3. Response case mismatch. Pioneer echoes CAIP/networkId back in lowercase regardless of request casing. The side-panel asset list uses canonical mixed-case network IDs from ChainToNetworkId, so strict b.networkId === asset.networkId comparisons in Balances.tsx silently dropped every Solana entry. Rewrite Solana entries to canonical casing before they enter the merged balances array. Also eliminates a first-run race: the initial fetchBalancesFromPioneer() fired before prefetchSolanaPubkey() persisted the Solana pubkey, so run 1 never included Solana at all. Chain a forced refetch on prefetch resolution so the Solana entry lands in cachedBalances before the UI mounts. Verified against the live Pioneer API: for the exact address the client derives from the device at m/44'/501'/0'/0', /portfolio returns {native SOL + 3 SPL tokens}. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(review): prevent stale-fetch cache clobber and push balance updates to UI Addresses two PR review findings: 1. HIGH: stale earlier fetch could clobber a later fetch's result. The first cold-start fetchBalancesFromPioneer() starts before the Solana pubkey is persisted, and the chained forceRefresh fetch runs in parallel. If the forced fetch finishes first (correctly populating cachedBalances with SOL + SPL tokens) and the original slower fetch finishes later, the original unconditionally overwrote cachedBalances back to the pre-Solana snapshot. Fix: tag each fetch with a monotonic latestFetchId bumped only when real work starts (not for dedup-return paths), and only commit to cachedBalances if this fetch is still the most recent (myFetchId === latestFetchId). Superseded fetches log their discard and return their result to direct callers without touching the cache. 2. MEDIUM: UI never observed late cache updates. Balances.tsx and SidePanel.tsx each fetched GET_APP_BALANCES once and then stopped listening, so the cold-start forced refetch that lands Solana after the panel mounts was invisible to users. Fix: background now emits BALANCES_UPDATED via chrome.runtime.sendMessage every time cachedBalances is successfully written. Balances.tsx, SidePanel.tsx, and Tokens.tsx subscribe to that message and re-fetch so the UI reflects the latest cache without the user having to manually refresh. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
…back networkIdToIcon() was mapping Solana to the wrapped-SOL SPL CAIP (solana:.../solana:So111…), which returns 403 on keepkey.info/coins/<base64-caip>.png. The Avatar component fell back to rendering the first letter of the network name — a green "S" badge where the logo should be. Native SOL's slip44:501 CAIP has a real icon (200), matching pioneer-caip's ChainToCaip convention used elsewhere in the codebase after PR #42. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Adds thin Makefile over pnpm scripts so the stack-wide "use make for everything" convention applies here too. Also commits the current minified build of the Solana/EVM injected script. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Port the approval UI into the side panel and remove the popup entirely.
Side panel is now the sole surface for both portfolio and dApp approvals.
Three-phase merge plus iterative review fixes across five rounds:
Phase A: port popup approval components to side-panel/src/approval and
subscribe SidePanel to requestStorage.
Phase B: background.methods.ts opens the side panel via
chrome.sidePanel.open; setPanelBehavior({openPanelOnActionClick:true})
+ action badge as user-gesture fallback; 10-minute approval timeout
replaces the old popup-closed escape hatch.
Phase C: delete pages/popup, its e2e specs, and the OPEN_SIDEBAR
background handler. Chrome-only; Firefox build hard-gated until a
replacement UI ships (task #5).
Iterative fixes touched: approval routing / id mismatch, PersonalSignTx
wire-up, bridge same-origin + falsy RPC handling, provider coexistence
(stop stomping window.ethereum), GET_PUBKEY_CONTEXT asset scoping,
Receive dedup by CAIP, ETH account removal runtime state, custom
network mirror into provider stores, GET_CHARTS networkId filter,
Clear-all covers every user-written storage, approval window routing,
accountIndex preservation end-to-end, full asset-context pass-through,
native-preference for default global send/receive, scalar balance
fallback in Transfer. New side-panel approval e2e smoke covers the
reject path.
Lint check is red on develop (470 errors pre-existing); this PR's
lint delta is ~0 after ignoring the ported approval subtree.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Bump every workspace to 0.0.27 via ./update_version.sh for the release cut. Also restore chrome-extension/public/injected.js to its esbuild-minified form — prettier had un-minified it on a prior lint-staged run, bloating the shipped bundle and making every build produce a noisy diff. Add the file to .prettierignore so this stays fixed. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The "KeepKey Vault Required" card rendered without a background on top of the side panel's dark theme, so the title (no color) and the \`gray.500\` / \`gray.400\` helper text were effectively black on near- black. Users couldn't read the "vault must be running" instruction. Give the card an explicit dark-but-distinct background (\`bg="gray.800"\` over the \`gray.900\` panel) with a faint border for separation, and switch every Text to the white/whiteAlpha scale the rest of the sidebar uses. Link color bumped to \`teal.300\` so it's visible against gray.800. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
… fetch
TON
- New tonHandler.ts wraps the vault's three-call flow:
/ton/build-transfer (vault owns BOC + seqno + deploy detection)
/ton/sign-transaction (device signs the body hash)
/ton/finalize-transfer (vault assembles + broadcasts via TonCenter).
- BIP-44 path is m/44'/607'/0' (3 levels — NOT 5). Five-level paths
make the firmware return {"error":"Failed to derive private key"}.
- Address guard (isPlausibleTonAddress) rejects anything that isn't
UQ/EQ/kQ/0Q + 46 base64url chars or 0:/−1: raw hex, so a stale
cache or misrouted call can't silently poison the TON pubkey slot
with an ETH address.
- Signature normalizer: /ton/sign-transaction returns whatever shape
hdwallet produces (unlike /tron/sign-transaction which hex-encodes
server-side). Coerce string / Buffer / number[] / Uint8Array-like
into 64-byte hex before handing to /ton/finalize-transfer, which
strictly requires a hex string.
- Balance unit fix: Pioneer's /api/v1/ton/accountInfo returns decimal
TON already (e.g. "15.701798194"), not nanoTON — the prior /1e9
divide turned a real 15.7 TON balance into 1.5701798194e-8 and
made the Asset page / Send page show 0.0000 TON.
Tron
- Transaction.tsx adds a 'tron' router case so approvals render via
OtherTransaction (same generic cards Solana/TON/Ripple use) — was
falling through to 'unknown' → "Unknown Transaction Type".
- tronHandler already did the build / sign / TronGrid broadcast and
the vault's /tron/sign-transaction already hex-encodes, so no
signature-shape normalization needed on this side.
Balance fetch stabilization
- fetchBalancesFromPioneer now rejects commits from fetches whose
pubkey snapshot was taken before a concurrent addPubkey landed.
Without this, three parallel prefetches (Solana / Tron / TON) could
race such that the fetch with the highest id (the "latest" winner
of the pre-existing latestFetchId check) had a stale snapshot
missing the dynamic pubkey. Symptom: dashboard briefly showed
correct TON / Tron balance then "overwrote" it to 0 when a stale
but id-latest fetch committed. Guard: compare wallet.getPubkeys()
length at commit vs at snapshot — if it grew, supersede ourselves.
- Added a #N committed log with the native-row summary for triage.
Asset context
- assetContextStorage.updateContext now REPLACES instead of merging.
The previous {...prev, ...newContext} let stale fields from an
earlier asset (decimals / contractAddress / token:true) leak into
the next asset — switching from an ERC-20 to TON would carry the
token flag forward and mis-route the Send page.
Other
- SDK bump keepkey-vault-sdk ^2.0.1 → ^3.0.1 (required by the new
vault feat/ton-build-transfer branch).
- shared/chainConfig adds Ton + Tron entries (Chain enum,
ChainToNetworkId, COIN_MAP_LONG).
- chrome-extension/chainConfig mirrors shortListSymbolToCaip +
shortListNameToCaip for the same two.
- background/index.ts: fetchTonBatch / fetchTronBatch + prefetch +
resetState on disconnect. Valid until we land real price feeds
for TON / TRX — both hardcode valueUsd/priceUsd=0 today.
- Extension gets TON / Tron debug logs in key hot paths; cheap and
worth keeping for the next round of triage.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Pioneer's /charts/portfolio silently omits TON and Tron rows when called without a queryKey. Our previous workaround used per-address /accountInfo calls that returned raw balance but no price — dashboard USD stuck at \$0.00 for both chains even when the vault's own UI showed correct values.
Append ?key=key:public-\${Date.now()} so the portfolio call returns TON and Tron with full priceUsd + valueUsd (same auth scheme /portfolio already used for Solana). Keep /accountInfo as a targeted patch for partial-response edge cases — priceless row beats missing row. Also dedupe + stale-state-aware retry on chrome.action.setIcon to fix cold-start icon flicker.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The add-chain flow had five UI gaps that together rendered as
"Unknown Method / Verify before proceeding" above an infinite spinner:
- RequestMethodCard had no case for wallet_addEthereumChain → default.
- RequestDetailsCard routed it through LegacyTx, which reads fields
off `unsignedTx` (null for this flow).
- The `!unsignedTx` spinner guard hid any content until a tx built —
which never happens for add-chain.
- The fees tab tried to render RequestFeeCard for an event with no tx.
- On Approve, Transaction.tsx set `awaitingDeviceApproval`, flashing
"Please approve on your KeepKey" — but this flow never touches the
device, the background just writes to blockchainStorage and fires
signature_complete.
Fixes:
- New AddEthereumChainTx component — dedicated view with:
- warning banner ("the site chooses the RPC"), plus a red banner if
the RPC URL is plain http://,
- chain name + chainId (hex and decimal),
- native currency (symbol / name / decimals),
- full, untruncated, monospaced RPC URL block (trust surface),
- explorer link(s).
- Route wallet_addEthereumChain through the new component in
RequestDetailsCard. Rename the spinner-skip set from MESSAGE_SIGN_METHODS
to NO_UNSIGNED_TX_METHODS and add the type to it.
- RequestMethodCard gets a yellow "Add Network" header with an
explainer that reminds the user to verify the RPC.
- Fees tab shows a "No transaction fees — this flow only stores the
RPC configuration locally." placeholder.
- Transaction.tsx: add NO_DEVICE_STEP_TYPES. For those methods, Accept
shows a minimal "Saving network configuration…" spinner state (the
form and the device overlay both hidden) until the background emits
signature_complete and onDismiss fires.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* fix: detect KeepKey hot-swap + drop stale cache on device change
The extension treated any reachable vault as a reachable device, with no
validation that the device currently paired with the vault is the same
one whose pubkeys we cached. That made three failure modes silent:
1. Hot-swap on same vault — checkKeepKey kept state.deviceConnected=true
and never re-probed, so addresses/balances pinned to the old device
indefinitely.
2. Extension reload with a new device — init() loaded the old pubkey
cache without comparing deviceId against the freshly-probed one, so
cached addresses survived even a full BEX reload.
3. Vault auth rejection (stale apiKey after re-pair) — fetchPubkeys
caught the error and silently fell back to cached pubkeys from the
previous device, masking the re-pair requirement.
Four narrow changes:
- wallet.init() now compares cached deviceInfo.deviceId to the freshly-
probed deviceId when a device is reachable. Mismatch → clear storage
before fetchPubkeys runs. (Covers case 2.)
- wallet.handleDeviceSwitch() primitive clears in-memory pubkeys/paths,
resets the deviceConnected flag, and wipes pubkeyStorage so the next
fetch rebuilds from the new device. Keeps SDK alive — only the
device-specific state is dropped.
- background/index.ts checkKeepKey now runs a periodic (30s, throttled)
getFeatures re-probe while deviceConnected. Compares before/after
deviceId; on mismatch calls handleDeviceSwitch which clears balance
cache, per-chain address caches (Solana/Tron/TON), and triggers a
fresh pubkey+balance refresh. (Covers case 1.)
- fetchPubkeys no longer silently falls back to cache on auth errors.
Detects 401 / "unauthorized" / "not paired" shapes, clears the stored
apiKey, and throws a user-facing message for the sidebar to surface.
Non-auth failures (network blip, device busy) still fall back to cache
as view-only — that path is correct. (Covers case 3.)
No user-visible UX change on the happy path. Recovery from device swap
is now automatic within ~30s on steady state, immediate on extension
reload.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* fix: rebuild default paths on device switch so refreshPubkeys has a batch
handleDeviceSwitch cleared state.paths to [], but the caller
(background checkKeepKey) then calls wallet.refreshPubkeys() which
only probes + fetches — it does not repopulate paths. fetchPubkeys
maps state.paths into the batch sent to the device, so an empty
paths array meant an empty batch, meaning the hot-swap recovery
finished with zero pubkeys and zero balances instead of the new
device's view.
Reset to getDefaultPaths() on switch so refreshPubkeys has something
to query against.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* fix: re-prefetch Solana/Tron/TON pubkeys on device switch
refreshPubkeys only re-runs the default-path batch via
wallet.getPublicKeys — that batch covers BTC / LTC / ETH / Cosmos /
etc., but *not* SOL / TRX / TON. Those three are derived dynamically
at onStart via prefetchSolanaPubkey / prefetchTronPubkey /
prefetchTonAddress (each calls its chain's `*GetAddress` method and
stashes the result in a per-chain cache + adds a pubkey to state).
After a hot-swap:
- handleDeviceSwitch clears the per-chain caches (resetXState)
- refreshPubkeys repopulates the default batch
- but nothing re-runs the three prefetches
→ SOL/TRX/TON vanish from the network dropdown / asset list / balance
fetch until the user reloads the extension or manually navigates to
those chains, which racy-triggers the lazy getAddress in each handler.
Fan out all three prefetches in parallel via Promise.allSettled after
refreshPubkeys. Non-throwing by design, so if one chain's derivation
fails (minFirmware mismatch, etc.) the other two still recover.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
…etwork-ids (#47) Two related fixes that together make TRC-20 (USDT-TRON) appear on the dashboard for the first time. 1. One endpoint instead of two --------------------------------- Before: the Pioneer call was split across /charts/portfolio (for EVM/UTXO/Cosmos/TON) and /portfolio (for Solana only), with a separate fallback for TON/TRON partial responses. /charts/portfolio is a fast cached path backed by pre-computed charts + Zapper-provided EVM tokens; it does NOT run Pioneer's SPL / TRC-20 / ERC-20 auto-discovery blocks that live in balance.controller.ts. Only /portfolio (GetPortfolioBalances) hits those — which is why the vault, which uses /portfolio exclusively via pioneer-client, saw USDT while the BEX did not. Consolidated every pubkey (Solana + Tron + EVM + UTXO + Cosmos + TON + Ripple + THORChain) onto /portfolio in one call, matching vault's GetPortfolioBalances flow exactly. Classification (native vs token) uses the same rule as vault: caip path not in {slip44:, native:} OR type === 'token' OR (isNative === false && contract). Net: -118 lines vs. the three-batch split. Slower per call (no pre-warm charts cache) but correct, and the user explicitly accepted the latency trade. 2. Tron has two network-ids; alias them ------------------------------------------ Pioneer emits the native TRX balance under `tron:27Lqcw` (CAIP-2 genesis-hash convention), but TRC-20 tokens under `tron:0x2b6653dc` (hex chain-id convention). Both refer to Tron mainnet. Pioneer's own ChainToNetworkId declares `tron:0x2b6653dc` as canonical (vault consumes that and works); our BEX has hardcoded `tron:27Lqcw` throughout `chainConfig.ts` and `tronHandler.ts`. Rather than rewire every pubkey cache entry in users' storage, alias Pioneer's response ids at the normalizer: NETWORK_ID_ALIASES = { 'tron:27lqcw' -> 'tron:27Lqcw' // casing 'tron:0x2b6653dc' -> 'tron:27Lqcw' // cross-convention 'solana:5eykt4...' -> 'solana:5eykt4UsFv8...' // existing Solana casing } Rewrites both `networkId` and the network prefix of `caip`. The side-panel's strict `networkId === 'tron:27Lqcw'` filter now matches both native rows and TRC-20 token rows, so USDT-TRON appears on the Tron asset page. Known ceiling: the canonical-id mismatch is tech debt. Switching our BEX to `tron:0x2b6653dc` would remove the alias entry but requires a storage migration (existing pubkey caches have `networks: ['tron:27Lqcw']`). Deferred — alias is localized and cheap. Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
* fix(tron): send the right asset when user picks a TRC-20 token
The side-panel Send flow builds a `transfer` request with the asset's
caip in the payload, but the Tron handler was ignoring it — every send
called `buildTronTransfer` (TronGrid's /createtransaction endpoint,
which only builds native TRX transfers). So clicking USDT-TRON and
hitting send silently sent TRX instead.
Branch the handler on the caip:
tron:*/token:T... or tron:*/trc20:T... → TRC-20 transfer
tron:*/slip44:195 → native TRX transfer
For the TRC-20 path:
- Parse the contract address out of the caip (accepts both of
Pioneer's Tron network-id conventions — 27Lqcw and 0x2b6653dc).
- Read decimals from assetContextStorage (populated by the side-panel
when the user clicks the asset); default to 6 if missing so the
common USDT/USDC case still works on cold state.
- Convert the human amount → base units via padded-string math (same
approach as trxToSun, but parametrised by decimals). BigInt throughout
so 18-decimal tokens don't overflow Number.
- ABI-encode `transfer(address,uint256)`:
a9059cbb | recipient_20bytes_left_padded_to_32 | amount_uint256
The 0x41 Tron prefix is stripped from the address before padding.
- POST /wallet/triggersmartcontract to TronGrid, get raw_data_hex back
in the same shape as /createtransaction so signing + broadcast reuse
the native code path.
signTronViaRest now accepts `amountRaw: string | number` — passing
Number for large TRC-20 values would silently truncate at ~2^53 base
units. Vault's endpoint stringifies internally; we just hand it through.
The approval event carries eventKind='trc20-transfer' + contractAddress
for the OtherTransaction approval UI; firmware still decodes raw_data
itself so there's no device-side regression.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* fix(tron): approval UI renders TRC-20 sends correctly
The approval-details renderer in `pages/side-panel/src/approval/other`
has two hardcoded assumptions:
1. RequestMethodCard only recognizes type='transfer' — anything else
shows "Unknown Method".
2. RequestDetailsCard reads `unsignedTx.payment.{destination,amount}`
and divides amount by 1,000,000 (XRP drops / TRX sun).
Previous commit emitted type='trc20-transfer' and no `payment` block,
so clicking USDT and tapping Send surfaced "Unknown Method" / N/A.
Fix it at both ends:
Handler (tronHandler.ts):
- Emit type='transfer' for both native TRX and TRC-20 — no semantic
win from a sub-kind at the UI layer; `unsignedTx.contractAddress`
+ `caip` distinguish when downstream code actually needs to branch.
- Populate `unsignedTx.payment = { destination, amount, decimals, symbol }`
so the renderer has what it expects. `amount` stays as a raw-base-units
decimal string (not a Number) to preserve precision for high-decimal
TRC-20 tokens.
- Carry `decimals` on the event (sourced from assetContextStorage for
TRC-20, hardcoded 6 for native) so the UI can format without a
separate asset lookup.
UI (RequestDetailsCard.tsx):
- New `formatAmount()` helper. Integer-string inputs route through a
BigInt-safe path (slice to whole/frac, trim trailing zeros). Other
inputs fall back to Number/Math.pow to preserve the existing Ripple
display behavior for any legacy rows that still write a plain number.
- Decimals cascade: `payment.decimals` (event hint) → `assetContext.assets.decimals`
→ 6 (matches XRP drops and TRX sun — the two chains this renderer has
historically served).
- Symbol pulled from `payment.symbol` or `assetContext.assets.symbol` and
appended to the amount — tells the user at a glance what they're sending.
- Suppress the destinationTag row entirely when it's undefined (every
non-Ripple chain previously rendered a misleading "none").
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* fix(ui): formatAmount handles zero-decimal assets
With decimals === 0, str.slice(0, -0) returns '' (slice treats -0 as 0
and the negative second arg short-circuits to empty) and str.slice(-0)
returns the entire string. Without guarding, "123" at 0 decimals
rendered as ".123".
Fast-path `decimals <= 0` returns the integer string as-is, which is
the correct formatting for any zero-decimal token.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* fix(ui): approval renderer branches on unsignedTx.kind for Tron
Previous pass unified Tron dApp events on type='transfer' so they'd
land on this renderer at all. Side-effect: contract calls (swaps,
stakes, approvals, anything other than native TRX or TRC-20 transfer)
displayed as "basic transfer" — actively misleading to the user
clicking Approve on-device.
Teach the renderer to branch on `unsignedTx.kind` (the chain-specific
refinement the Tron handler attaches):
RequestMethodCard:
- kind='contract-call' → "Smart Contract Call" (orange warning icon,
copy asks user to verify before approving)
- kind='trc20-transfer' → "Token Transfer"
- kind='trx-transfer' / missing → existing basic-transfer label
RequestDetailsCard:
- Contract-call path renders Contract + Function selector instead of
recipient/amount — raw call_value in TRX shown only when non-zero
(most TRC-20 flows have call_value=0; swaps spending native TRX
have a real value worth surfacing to the user).
- Native + TRC-20 transfer paths unchanged.
This keeps the dApp integration unblocked (SunSwap-style contract
calls still sign) while giving the user accurate UX at approval time.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* fix(tron): carry kind on side-panel events + gate asset-context fallback
Two fixes that go together:
1. Side-panel transfer case now sets unsignedTx.kind
('trc20-transfer' | 'trx-transfer'). Previously clicking USDT in
the asset list and hitting Send produced an event without `kind`,
so the approval UI's RequestMethodCard fell through to the default
'transfer' label — "basic transfer" rather than "Token Transfer"
even though we were actually signing a TriggerSmartContract.
2. RequestDetailsCard's asset-context fallback is now gated on
ctx.caip === event.caip. Without the guard, dApp-originated sign
events (which carry their own caip — e.g. tron:*/token:TR7NHq...)
could inherit symbol/icon/decimals from whatever the user last
clicked in the side-panel asset list. Concrete failure mode a
reviewer flagged: a dApp USDT-TRON approval rendering with the ETH
icon + "ETH" symbol because ETH was the last selected asset.
Side-panel sends keep the context fallback because the user just
clicked the asset — caips match, fallback fires normally. Only
the non-matching case is suppressed.
Also gates the Avatar `src` on the same ctxMatches check so a
wildly-off icon can't leak through to the dApp approval pane.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
* feat(dapp): Tron injection + functional MetaMask masking
Two related dApp-injection improvements shipped together:
Tron dApp support
-----------------
Mount window.tronLink + window.tronWeb shims that mirror TronLink's API
surface. dApps connect via the standard "Connect TronLink" button; our
shim intercepts (isTronLink: true), opens the KeepKey approval side
panel, signs via the vault, and returns the signed tx for the dApp to
broadcast.
- New injected provider in chrome-extension/src/injected/tron-provider.ts
* tronWeb.trx.{sign, sendRawTransaction, getBalance, getAccount}
* tronWeb.transactionBuilder.{sendTrx, triggerSmartContract}
* tronWeb.utils.{isAddress, toSun, fromSun, toHex}
* Direct TronGrid for reads/builds/broadcast, extension pipeline for signing
- Background handler extended with tron_requestAccounts, tron_sign,
tron_signMessage (throws until vault adds the endpoint)
- tron_sign decoder handles TransferContract (native TRX), TriggerSmartContract
with transfer(address,uint256) (TRC20/USDT), and passes any other
TriggerSmartContract through for firmware to validate
- Verified on SunSwap: detection + connect + address populate. Signing
paths need on-device verification per firmware capability.
MetaMask masking pipeline
-------------------------
The Settings → Enable MetaMask Masking toggle was cosmetic — isMetaMask
and window.ethereum mounts were hardcoded on. Made it actually work
end-to-end, default off.
- Content script reads 'masking-settings' from chrome.storage before
injection, stamps data-masking="{...}" on the <script> tag
- Injected script parses dataset.masking at startup and branches:
* isMetaMask reflects enableMetaMaskMasking
* window.ethereum mount gated on the flag (otherwise EIP-6963 only)
* window.xfi mount gated on enableXfiMasking
- When MetaMask masking is ON, also announce via EIP-6963 with
rdns: 'io.metamask' so SDKs that key off the canonical MetaMask rdns
(MetaMask SDK, Dynamic.xyz's MetaMaskConnector, RainbowKit) find us.
Same trick Rabby uses.
- Known ceiling: MetaMask SDK v0.28+ does anti-impersonation probes
beyond EIP-6963 that we can't realistically beat. Works on simpler
legacy sites; SDK-hardened dApps need WalletConnect (future work).
- Removed dead, wrong-key-reading getMaskingSettings handler from
background/index.ts.
- Single diagnostic log per page load: "[KeepKey] masking: metamask=on/off ..."
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* fix(tron): keep TRC-20 amounts as strings through dApp sign path
decodeTronTx was holding the decoded TRC-20 amount as BigInt, then
casting to Number before stashing it on DecodedTronTx.sunAmount and
passing it to signTronViaRest + the approval event. Any 18-decimal
token (most non-stablecoin TRC-20s) exceeds Number.MAX_SAFE_INTEGER
in base units, so the value silently rounds before the vault sees it
— firmware displays the wrong amount on-device and the signed tx
spends the wrong amount on-chain.
Changes:
- DecodedTronTx.sunAmount (number) → amountRaw (decimal string).
Renamed to signal "this is base units not native sun".
- TransferContract decode emits String(v.amount); TRC-20 decode emits
BigInt.toString(); contract-call emits String(callValue). All paths
preserve precision.
- signTronViaRest signature widened to `string | number | bigint`.
Internal stringification uses bigint.toString() when applicable so
nothing re-enters the Number casting path.
- Approval event now carries amountRaw (not sun) so downstream UIs
that display the raw-unit hint get the untruncated value.
Native TRX amounts never overflow Number (max 90B TRX in sun = 9e16,
fits in 2^53 comfortably), but the change makes that path uniform
and the code easier to audit.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* fix(tron): dApp sign events land on the shared approval UI
Previous pass changed side-panel Send to emit type='transfer' +
unsignedTx.payment, but the dApp sign path (tron_sign) was still
emitting type='trc20-transfer' / 'contract-call' and had no payment
block. The shared /approval/other renderer only cases 'transfer' and
reads unsignedTx.payment.*, so dApp TRC-20 + contract-call approvals
surfaced as "Unknown Method" / N/A.
Unify on the same contract:
- type='transfer' for all Tron paths, always.
- decoded.kind now travels on unsignedTx.kind for downstream branches
that actually care about native vs TRC-20 vs generic contract-call
(storage/history, txid explorer link selection, etc).
- Populate unsignedTx.payment = { destination, amount, decimals, symbol }
so RequestDetailsCard renders without a second asset-context
round-trip.
- amount stays as a raw base-units decimal string (preserved from the
previous amountRaw fix).
- decimals defaults:
trx-transfer → 6 (sun)
contract-call → 6 (call_value is TRX attached to the call)
trc20-transfer → 0, so the UI shows the raw integer we decoded.
We don't have the token's decimals from the
raw_data alone and the asset context isn't the
dApp's caller — showing a raw integer is less
misleading than applying the wrong divisor.
Future work: decodeTronTx can call the contract's
decimals() view or look up from assetData.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* fix(tron): set explicit TRX symbol on contract-call payment
Review follow-up: contract-call events left payment.symbol undefined,
which previously leaned on the UI's asset-context fallback for the
"TRX sent" row. Combined with the UI now gating that fallback on
ctx.caip === event.caip (#48), a dApp-initiated contract call whose
caip doesn't match the side-panel's selected asset would render the
call_value row with no symbol at all.
Two tightenings in one:
- contract-call & trx-transfer both emit symbol='TRX' explicitly —
the call_value on any TriggerSmartContract is always native TRX,
so stating it in the handler is accurate and removes the UI's
need to guess.
- trc20-transfer stays symbol=undefined — we don't know the token's
symbol without an on-chain symbol() call or an assetData lookup.
The UI's caip-match gate on asset context will decline to show a
wrong symbol when the dApp caip differs from whatever the side
panel had selected.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* fix(tron): scope trc20-transfer event caip to the token contract
Review follow-up to the caip-match gate in #48: dApp TRC-20 events
were still emitting caip: TRON_CAIP (tron:27Lqcw/slip44:195 — the
native-TRX caip). The UI's ctx.caip === event.caip guard matched
when the user had TRX selected in the side panel, leaking the TRX
symbol/icon onto a USDT approval — exactly the failure the gate
was supposed to block.
Emit a token-scoped caip for trc20-transfer:
tron:27Lqcw/token:${decoded.contractAddress}
Now the fallback only fires when the side-panel happened to have
this exact token selected. Any other selection (TRX, a different
TRC-20, or an unrelated chain) leaves symbol/icon empty, which
is the correct behavior — the handler still populates
payment.decimals=0 so amounts render as raw base units.
trx-transfer keeps TRON_CAIP (legitimate native TRX match desired).
contract-call keeps TRON_CAIP too — its renderer owns its own
labels (Contract:, Function:, "TRX sent:") with a hardcoded 'TRX'
fallback on the call_value row, so the caip match affects only
the avatar icon. Not worth the extra complexity.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
* feat(ui): dark theme + header + asset list refresh (Tier 1–3 of design handoff) Adopts the Claude Design handoff's visual language for the side panel — darker base (#0b0d10), Inter + JetBrains Mono typography, gold-gradient primary buttons, and tokenized surfaces/lines. Shield badge on the left doubles as both "go home" button and device-status indicator (gold pulsing when transient, green when paired, red when errored), replacing the separate status icon. Bug fixes along the way: - "Add blockchain" picker now hides the dashboard block and is resettable from the home button (state lifted out of Balances into SidePanel). - Asset-detail drawer no longer renders a redundant "Ethereum" title bar; a minimal floating back chevron replaces it. - Header row heights unified at 32px so the Network/Account pills and shield all sit on the same baseline. - TronLink badge next to "Tron" on the asset page (passive, no link — TronLink is an unaffiliated wallet; we just signal protocol use). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(side-panel): receive tab loads offline + auto token discovery Receive tab - Derive UTXO receive addresses locally from xpubs (BIP32 + script-type encoding) instead of round-tripping the device. Page now renders in view-only mode and survives hot-swaps. Adds @scure/bip32, @scure/base, @noble/hashes; new utxoDerive.ts covers BTC (legacy/segwit/native segwit), LTC, DOGE, DASH, BCH cashaddr. - Fix the account dropdown — it looked up pubkeys by .address|.master (both empty on UTXO) so clicks were silent no-ops. Keys by pubkey.note now and shows derived addresses, not truncated xpubs. - Labels read "Account 0 · Native Segwit" etc. so the three BTC entries that all share account index 0 are distinguishable. - Tighten GET_UTXO_ADDRESS match order so note wins over scriptType; multiple p2wpkh accounts can be disambiguated. - Center Receive + AssetDetail vertically; hide the Tokens tab on UTXO chains instead of rendering an empty state. Token auto-discovery - Replace the per-chain prefetch().then(fetch(true)) chains with a single Promise.allSettled([sol, tron, ton]).then(fetch(true)). The old shape raced three parallel fetches that the snapshot staleness guard then discarded — SPL/TRC-20 tokens silently never landed in cachedBalances, and users had to press "Discover Tokens" to see them. - Wire the Discover/Refresh button to REFRESH_ALL_BALANCES so it forces a Pioneer round-trip instead of re-reading the cache. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(receive): address review findings on UTXO derivation - script_type, not scriptType: raw pubkey objects use snake_case (matches chainConfig.ts and the SDK request shape in wallet.ts). Reading .scriptType was always undefined, so segwit / native-segwit accounts silently fell through to the p2pkh branch and rendered legacy "1..." addresses for "Account 0 · Native Segwit" entries. - Drop the chrome.storage.session derived-address cache. Local BIP32 + hash160 + encoding is microseconds; the cache only added a stale surface — it was keyed by (networkId, scriptType, note) without the xpub or device id, so a hot-swap in the same session could return the previous device's address for the same note. - Pick the UTXO Receive default address from pubkeyContext.note (the header's selected account) instead of ctxAsset.pubkeys[0]. After SET_ASSET_CONTEXT, the asset's pubkeys field carries every pubkey on the network, and idx===0 was the first configured chainConfig path (legacy BTC), not the user's selection. New effect resolves addressByNote[pubkeyContext.note] when both are present, falls back to pubkeys[0].note only if no context is set. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(receive): carry header-selected UTXO script_type into pubkey scope The high-finding from the second review pass: header-selected UTXO accounts weren't reliably carried into Receive. Header fired SET_ASSET_CONTEXT with the selected pubkey, but the background overwrote asset.pubkeys with all network pubkeys, and GET_PUBKEY_CONTEXT only restored by accountIndex — UTXO accounts share an accountIndex (BTC's Legacy / Segwit / Native Segwit all live at account 0), so it always fell back to scoped[0] and Receive opened on the first configured chainConfig path. - NetworkAccountHeader.setAssetContext now puts script_type on the asset alongside accountIndex (snake_case to match raw pubkey shape). - GET_PUBKEY_CONTEXT prefers script_type for the scoped lookup, then falls through to accountIndex (multi-account EVM) and finally scoped[0]. Receive's existing pubkeyContext.note → addressByNote resolver picks up the right entry without further changes. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(receive): disambiguate UTXO accounts that share a script_type Previous fix carried script_type through SET_ASSET_CONTEXT, but multiple BTC paths share each script_type (chainConfig has account 0 + account 1 both at p2wpkh, plus several legacy accounts). The header also keyed rows on script_type alone, so account-1 Native Segwit collapsed onto account-0 in the dropdown, and even if surfaced separately GET_PUBKEY_CONTEXT would have matched the first p2wpkh (account 0). - AccountItem gains an optional `note` field. headerUtils' BTC and UTXO builders key rows on `pk.note` (unique per chainConfig path) and append "· Account N" to the label only when the script_type repeats. Only the first p2wpkh row is flagged isDefault now. - NetworkAccountHeader.setAssetContext sends `note` alongside `script_type` and `accountIndex`. - GET_PUBKEY_CONTEXT match priority is now note → script_type → accountIndex → scoped[0]. Note is the only identifier that's unique across every chainConfig path. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(receive): make UTXO defaults agree across all entry points Header dropdown was correct, but two entry points still desynced: - Global Receive / dashboard / asset list: SidePanel.handleGlobalReceive picks a default balance row that has no note or script_type, so the background's GET_PUBKEY_CONTEXT fell through to scoped[0] (legacy BTC). The header could read "Native SegWit" while Receive surfaced a "1..." address. Background SET_ASSET_CONTEXT now defaults a UTXO asset that arrived without note/script_type to the network's first p2wpkh pubkey (else scoped[0]) before storing. - Header auto-default: the useEffect that picks the default account on mount/network-change set local selectedAccountKey only — it never fired SET_ASSET_CONTEXT. Stored context could be stale or absent while the dropdown showed Native SegWit. Effect now calls setAssetContext for the chosen account when a network is selected. Reordered the effect to sit after setAssetContext's useCallback so it isn't referenced in the temporal dead zone. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(receive): close five remaining gaps in UTXO selection plumbing 1. Header restore by note (was: accountIndex only). UTXO accounts share an accountIndex, so a stored "BTC Native Segwit account 0" round-trip would resolve to whatever chainConfig path landed first at accountIndex 0 — usually legacy. fetchPubkeys now seeds desiredNote / desiredScriptType / desiredAccountIndex from the stored asset context. 2. ASSET_CONTEXT_UPDATED resync. Listener feeds the same desired* state, so within-network context changes now retarget the dropdown instead of leaving selectedAccountKey pinned to a stale row. The auto-select effect compares before firing setAssetContext, so the broadcast back from our own SET_ASSET_CONTEXT can't loop. 3. Per-chain UTXO fallback in background SET_ASSET_CONTEXT. Previously defaulted every bip122:* asset to first p2wpkh, which silently shifted LTC from p2pkh-default (header's items.length===0) to native segwit. Now BTC keeps p2wpkh-first, other UTXO uses scoped[0] — both match the header builders. 4. Auto-default no longer opens AssetDetail. setAssetContext is now a pure data setter (sends SET_ASSET_CONTEXT, returns the asset). The click handlers (handleNetworkSelect / handleAccountSelect) call onSelectNetwork explicitly, so the auto-default effect can sync stored context without an unprompted drawer-open on cold start. 5. AssetDetail derives UTXO addresses. asset.pubkeys[0].address is empty on UTXO and Pioneer's /portfolio stuffs the xpub into b.address (background/index.ts:439), so the address bar / copy / explorer link were either blank or showing an xpub. New UTXO branch in the address-fetch effect calls GET_UTXO_ADDRESS with the asset's note + script_type. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(side-panel): don't auto-open AssetDetail on every context broadcast The ASSET_CONTEXT_UPDATED listener fires for every SET_ASSET_CONTEXT the background receives — including the header auto-default sync we just added (cold-start restore + ASSET_CONTEXT_UPDATED resync) and dApp-driven chain switches. With onAssetDetailOpen() unconditional in the listener, the drawer would still pop up unprompted even after we made setAssetContext a pure data setter. Drop the auto-open. setSelectedAsset still runs, so an already-open drawer reflects the new context. Explicit opens already go through handleAssetSelect (asset list click, header click), which sets selectedAsset and calls onAssetDetailOpen together. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Pre-release cleanup before promoting develop → master. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
🔴 HOLD — release blocker identified Uniswap swap regression — Permit2 typed-data signs but the swap flow fails downstream. Full retro and handoff notes: 📄 Do not merge until that retro's "Concrete next steps" section is closed out. |
Captures the diagnostic conversation, hypotheses tried/ruled out, test scaffolding built in keepkey-vault-v11/projects/keepkey-sdk, and concrete next-step ordering for whoever picks this up. Holds release of 0.0.28 (PR #51). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Per user feedback: handoff is cross-repo (keepkey-client → keepkey-vault-v11 SDK + firmware submodule + private memory), so every file pointer must be a full /Users/... path. Added a File Index section so the next reader doesn't have to grep for which clone holds which file. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
ethers v6 provider.call(tx) and provider.estimateGas(tx) both take a
single argument; passing positional (callParams, blockTag, stateOverride)
silently drops blockTag AND stateOverride. Uniswap's pre-quote flow uses
eth_call with stateOverride to simulate the swap as if the not-yet-
broadcasted Permit2 approval were in place — when we strip the override
the simulation runs against real state where the user has 0 allowance,
reverts, and Uniswap rejects the quote (/v1/swap → 404).
Switch both handlers to provider.send('eth_call', params) /
provider.send('eth_estimateGas', params) so the dApp's params arrive
byte-identical at the RPC.
Also extend [HANDOFF] instrumentation to log params alongside the
return value at every BEX → content-script boundary and at the
page-side resolve, so cross-chain audit of a swap flow is possible
via grep '[HANDOFF]'.
Drops the SET_ASSET_CONTEXT-driven EIP-1193 emit block per earlier
diagnosis (ruled out as the root cause but the timing was still wrong).
Refs: HANDOFF_uniswap_swap_v2.md, RETRO_uniswap_swap_release_blocker.md
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
End-to-end trace of EVM fee handling through BEX → vault REST → hdwallet-keepkey → firmware. Confirmed no layer mutates fees; what the dApp supplies is what gets signed and broadcast. Documents: - The two adjacent defects fixed this session (nonce 'latest'→ 'pending', JSON-RPC `gas` honored alongside `gasLimit`) - The eth_feeHistory gap (RPC dApps depend on for percentile fee estimates — currently throws "method not supported") - Recommended placement and shape of the user-visible fee-warning + bump policy when dApp suggests under-floor fees - Files left uncommitted in worktree for review Refs: HANDOFF_uniswap_swap_v2.md, RETRO_uniswap_swap_release_blocker.md Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
… logs Three independent defects in ethereumHandler.ts that surfaced during the Uniswap swap regression audit: 1. signTransaction + handleTransfer queried getTransactionCount with 'latest' block tag, counting only confirmed txs. Re-attempting a swap while a prior attempt was still pending would reuse the same nonce and trigger EIP-1559's replacement-underpriced rule (requires +10% on both fees). The replacement tx silently sat in mempool until eviction — the "pending forever, eth_getTransactionByHash returns null" symptom we tracked. Switched both sites to 'pending'. 2. signTransaction only consulted transaction.gasLimit (camelCase), so the dApp's `gas` field (per JSON-RPC spec) was silently dropped and we re-estimated against publicnode. Now honors transaction.gas as a fallback before estimation. 3. Added [HANDOFF] decode-friendly logs at three more boundaries: eth_sendRawTransaction (dApp-supplied raw tx), the full signed serialized output of eth_signTransaction, and the broadcast request/result. Greppable end-to-end with the existing [HANDOFF] instrumentation. Refs: HANDOFF_fee_pipeline_audit.md Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Three independent improvements to the EVM signing surface, motivated
by the Uniswap stuck-pending regression diagnosed in HANDOFF_*.md.
1. eth_feeHistory passthrough — modern dApps (incl. Uniswap's UI via
ethers v6 fee estimator) use this for percentile-based fee math.
Previously fell through to "method not supported" → dApp dropped to
stale eth_gasPrice fallback, supplying low fees.
2. Fee-warning + user override:
- New chrome-extension/src/background/chains/feeFloors.ts: per-chain
static minimums (1 gwei mainnet, 30 gwei polygon, 3 gwei BSC, etc.)
OR'd with currentBaseFee*1.1 (always-correct dynamic floor).
- handleSigningMethods now probes feeData + base fee, builds a
FeeWarning blob if dApp's maxFeePerGas falls below the floor, and
attaches it to the approval event.
- New side-panel/src/approval/evm/FeeWarningBanner.tsx renders an
inline banner above the tx-detail tabs with three choices: keep
dApp's, use suggested, or custom (two number inputs, gwei). Choice
is persisted to the event via requestStorage.updateEventById.
- Approve button is disabled until a choice is made.
- sendTransaction reads event.feeChoice from storage at sign time
and overrides transaction.maxFeePerGas / maxPriorityFeePerGas
before handing to signTransaction. Fee policy never silently
mutates — user always picks.
3. Nonce visibility (read-only this round):
- Same probe in handleSigningMethods also fetches latest + pending
transaction counts and computes willReplace if the dApp set a
nonce equal to a pending one.
- New side-panel/src/approval/evm/NonceInfoRow.tsx renders a small
row above the tabs: "Nonce N (next available)" green, "N
pending tx ahead" yellow, or "Replaces pending tx — needs +10%
on both fees" red. Etherscan deep-link for mainnet.
- Manual nonce override + cancel-tx flow deferred to follow-up.
Build green. The user must reload the unpacked extension at
chrome://extensions for the changes to take effect.
Refs: HANDOFF_fee_pipeline_audit.md, HANDOFF_uniswap_swap_v2.md
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Today's progress (commit
|
Summary
Promote
develop→masterfor v0.0.28. Cuts master from 0.0.26 → 0.0.28 (the in-between 0.0.27 bump was develop-only).What's in this release (vs master)
New chains / dApp surface
wallet_addEthereumChainArchitecture
/portfolio+ Pioneer Tron network-id alias (fix(balances): consolidate Pioneer calls on /portfolio + fix USDT-TRON #47)Fixes
Release hygiene (this commit)
[TON-DEBUG]console logs from production paths (background + side panel)Test plan
make buildproduces a clean Chrome bundle (verified locally)wallet_addEthereumChainshows the new approval UI🤖 Generated with Claude Code