chore(release): 0.0.34#90
Merged
Merged
Conversation
Wire up a unit-test harness for the background service worker (vitest 2.1.x, aligned to the repo's Vite 5). Adds a `test` turbo task and root `pnpm test`. First coverage targets are the pure, highest-stakes modules — the code that decides what gets signed, broadcast, derived, or hidden: - feeFloors.ts (23): floor/tip logic incl. a regression guard for the PR #55 "tip headroom" hole — the wallet's own suggested fees must never re-trip the warning. Plus the EIP-1559 effective-tip cap. - spamFilter.ts (17): scam/phishing token detection tiers, native-always-kept, possible-vs-confirmed, user-override precedence. - utxoDerive.ts (7): receive-address derivation pinned to the canonical BIP84 spec vector, plus script-type prefixes and error handling. - lastResortRpcs.ts (5): broadcast-fallback table contract. - utils.ts (7): user-facing error formatting. 59 tests, all green. Modules that import workspace packages (@extension/*) are deferred to a follow-up that sets up vitest workspace resolution. No production behavior changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Alias @extension/storage and @extension/shared to their TS source in the vitest config so modules importing workspace packages are testable. Modules whose real source touches browser globals are additionally vi.mock-ed. - rpcFailover.ts (10): export the transient-vs-definitive classifier and test it; cover withRpcFailoverByNetworkId — first-URL success, fail-over on transient errors, immediate rethrow on definitive errors, exhaustion, the no-candidates case, and custom→pioneer→last-resort dedup ordering. - chainConfig.ts (9): bip32ToAddressNList hardening/parse rules and getDefaultPaths BIP44/49/84 purpose-index mapping. 78 tests total, all green. The only production change is adding `export` to isTransientRpcError (additive; no behavior change). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fetchUtils is the shared network layer for background HTTP (Pioneer, local services). Its timeout + transient-retry logic is reliability-critical and fully isolated, so it's an easy, high-value unit to lock down. fetchWithTimeout / fetchJsonWithTimeout (9 tests), with a mocked fetch and fake timers for deterministic backoff: - first-success no-retry; 4xx returned without retry; 5xx retried to budget; recovery after a transient 5xx; retry on thrown network/timeout error; throw after exhaustion; custom retryOn predicate. - fetchJsonWithTimeout: parses JSON on success; throws with status+url on non-OK. Tests only; no production changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a Unit Tests workflow that runs `pnpm test` (the vitest suite added in #69). Mirrors lint.yml: pnpm + setup-node from .nvmrc + frozen-lockfile install. The `pull_request` trigger has no branch filter, so it gates every PR — including PRs into develop, which is where active work lands. This is the first real CI gate on test correctness. A type-check gate is a separate follow-up: `pnpm type-check` currently surfaces real type errors in chrome-extension (background/index.ts, wallet.ts) that need fixing first. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…pe errors Type-check had been fully red on develop (228 tsc errors) but never caught — it isn't in CI, and the missing `target` masked it: tsc bailed at the private-field syntax errors in injected/solana-* before checking the rest. - packages/tsconfig: set `target: ES2022` (aligns with module: esnext, lib: ESNext). Clears 18 TS18028 (private fields) + 47 TS2737 (BigInt literals) immediately. - packages/storage: type web3ProviderStorage as a Web3Provider object (was wrongly `string`); broaden BlockchainData inner record to the heterogeneous shape it actually stores; add 'broadcasted' to Event.status. These were the root cause of ~30 "property does not exist on string" and the addBlockchainData/addEvent @ts-expect-error hacks (now removed). - chrome-extension background: null-guard every requestStorage.getEventById across the 11 chain handlers + ethereum sign/broadcast paths; annotate implicit-any handler params; narrow unknown catch errors before .message; guard nullable currentProvider/savedChains; fix thisPromise self-reference. chrome-extension and all workspace packages except side-panel now type-check clean (0 errors). Tests (59) and build (9 tasks) green. side-panel (80 pre-existing errors, incl. genuine broken-code refs) tracked separately. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Brings the side-panel package to 0 tsc errors so the whole repo type-checks clean. Mix of real-bug fixes and mechanical strictness. Real bugs (would throw / misbehave at runtime): - AssetSelect called storage methods that never existed (addBlockchains, removeBlockchains, removeBlockchainData) and an undefined loadEnabledChains. Added the three batch/remove methods to @extension/storage; pointed the reload at the component's onStart. - HarpyDetailsCard referenced an undefined `harpieLogoUrl` (Avatar src) — the def was commented out. Switched to <Avatar name="Harpie">. - <TabList defaultIndex> is a no-op; defaultIndex belongs on <Tabs>. Moved it in the other/tendermint/utxo approval tabs. - Chakra <Avatar> doesn't accept `alt`; removed the invalid prop (4 sites). Dead code removed (orphaned by the popup→side-panel merge, never importable): - approval/evm/ThreatPrompt.tsx — imported a RequestModalContainer that has never existed in git history; nothing imports ThreatPrompt. - approval/utxo/utils.ts — unused; its estimateTxSize is duplicated inline in ProjectFeeCard, and it imported a non-existent ./types. Mechanical: annotate implicit-any params/bindings; useState<any[]>/<any>() where state was inferred never/null; narrow unknown catch errors before .message; add `qrcode` ambient module typing (no @types/qrcode installed); add missing useEffect cleanup return. Repo-wide type-check now 15/15 packages green. Tests (59) and build green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Now that the whole repo type-checks clean (15/15 packages), run `pnpm type-check` in the same workflow as the unit tests so regressions are caught on every PR/push. type-check runs before tests (fails fast on type errors). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `wallet-standard:app-ready` handler was inverted: the spec passes the
app's `{ register, on }` API object as `event.detail`, and the wallet must
call `register(wallet)` on it. Our code checked `typeof detail === 'function'`
— always false for spec-compliant apps — so the listener never fired.
Consequence: KeepKey only registered with dApps that loaded BEFORE the
extension (they catch our initial `register-wallet` dispatch via their
persistent listener). Heavy SPAs that initialize the Wallet Standard AFTER
injection — Uniswap — announce via `app-ready`, which we ignored, so they
showed "No Solana wallet detected".
Fix:
- app-ready now calls `callback(detail)` per the canonical registerWallet
(detail.register(wallet)), with a tolerant fallback for non-standard shapes.
- Re-announce `register-wallet` at +100ms/+1s (mirrors the EVM EIP-6963
re-announce) for SPA timing; idempotent since apps dedupe by wallet ref.
EVM (EIP-6963) path was already correct and is unchanged. Rebuilt injected.js.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Scroll: - Global styles never set a document height, so the side panel's flex/scroll chain had no bounded height to scroll within. Add height:100% + margin:0 + overscroll-behavior:none to html/body/#app-container. - Settings Modal used Chakra's default scrollBehavior="outside"; in a fixed-height side panel the wheel had nothing to scroll. Set scrollBehavior="inside" + maxH="85vh" so its content scrolls internally. Header dropdowns (Network + Account): - Both wrapped their absolutely-positioned panel in Chakra <Collapse>, which measures child height to animate — an absolute child reports 0, so Collapse clamped overflow and broke the panel's own scroll. Render the panel conditionally instead; cap maxH to the viewport so options never strand below the fold; overscroll-behavior:contain so list scroll doesn't bleed. - Add useOutsideClick to both so clicking away closes the dropdown. Header declutter: - Network trigger now shows the compact chain symbol (BNB, ETH…) instead of the long, mid-word-truncated network name; the dropdown list keeps full names for clarity. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Aligns develop with the release branch (release/0.0.33). Drop this commit if versioning should stay release-branch-owned. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The type-check job (newly added to CI) and lint both failed in CI though they passed locally — local resolution masked two real gaps: - @types/uuid was resolving from a globally-hoisted ~/node_modules, so uuid (which ships no types) type-checked locally but TS7016'd in CI's isolated --frozen-lockfile install. Declared @types/uuid as a chrome-extension devDependency so it's in the lockfile. - prefer-const error on the fetchBalances `let thisPromise`. Reworked the in-flight guard to compare myFetchId === latestFetchId instead of the promise identity, removing the self-reference so it's a plain `const` (also resolves the original TS2454 without a definite-assignment hack). - consistent-type-imports auto-fix: type-only ethers/utils imports → import type. Verified: type-check 15/15, lint 0 errors, test 59/59, build 9/9. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Transaction.tsx imported from `@extension/storage/dist/lib`, which only exists after the storage package is built. CI type-checks without a build step, so the path was absent (TS2307); it resolved locally only because a stale dist/ was present. Use the package root `@extension/storage` (its `types` points at source), matching every other consumer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
develop hardening: type-check green + CI gate, Solana/Uniswap detection, side-panel UX, 0.0.33
Add account 0/1/2… for non-Bitcoin UTXO (LTC/DOGE/DASH/BCH), Cosmos-family (ATOM/OSMO/RUNE/CACAO), and Solana — across create, display, receive, in-app send, and dApp. EVM keeps ethAccountsStorage; Bitcoin stays static. - accountsByNetworkStorage (networkId→idx[]) + ADD/REMOVE/GET_ACCOUNT handlers - buildAccountPaths clones account-0 templates and bumps the BIP44 account segment (addressNList[2]); Solana derives off-batch at m/44'/501'/N'/0' - onStart re-derives persisted accounts; header gains add/remove for these families - in-app send threads the selected account; getSendPubkeys scopes UTXO/Cosmos buildTx to it (also fixes Bitcoin's latent cross-account spend) - Solana wallet-standard enumerates accounts and signs with the selected one; Cosmos request_accounts returns all accounts Closes the receive-on-N / sign-from-0 hazard: a non-zero account that can't resolve returns empty so the send fails loudly rather than signing from account 0. Known limitation: the generic cosmos dApp `transfer` has no account selector, so it signs from the primary account (approval UI shows it); in-app cosmos is scoped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… accounts - buildUtxoAccounts: non-Bitcoin UTXO account-0 rows now carry accountIndex 0 (via parseAccountIndex) instead of undefined, so in-app sends scope to the selected account's pubkeys instead of falling back to the full network set. - resolveSolanaAccountIndex: an explicit dApp account address that maps to no known pubkey now throws "Unknown Solana account" rather than silently signing with account 0. An omitted account (legacy window.solana) still defaults to 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(accounts): multi-account for non-EVM chains (UTXO/Cosmos/Solana)
In-extension swap composed entirely in the side panel — a faithful port of the KeepKey BEX design's swap flow — wired to vault's headless swap REST. No dApp dependency; the device signs. - pages/side-panel/src/swap/: ported design components (SwapScreen, SwapProgress, SwapEmblem, SwapTimeline, TokenButton, AssetPicker, theme/icons/ui) + a Swap orchestrator (assets -> debounced quote -> execute -> submitted-status polling). - chrome-extension/src/background/swapHandler.ts: authenticated bridge to vault headless swap REST (GET /api/v2/swap/assets, POST /quote, POST /execute, GET /api/v1/swaps/:txid); Bearer off the paired SDK; 5-min execute timeout; graceful on 404/503/401. - SidePanel home Swap button + Drawer; SWAP_REQUEST router case in background. - /execute is stateless: passes the full quote (relayTx, netFromAmount, fees, estimatedTime, minimumOutput, nearIntentsDepositAddress). /quote omits addresses so vault derives them from the device (authoritative). Depends on vault headless endpoints (docs/HANDOFF_bex_swap_headless_endpoints.md), incl. /quote deriving fromAddress/toAddress when omitted.
In-flight work carried on this branch: swap screen tweaks (AssetPicker/ SwapScreen/SwapProgress/Swap), swap API/types, new SwapHistory/SwapReview screens, and client-side activity intake (activityReport.ts) + swap SSE (swapEventStream.ts) wired at the EVM broadcast chokepoint. The activity intake + unified-history endpoints are vault-gated (see HANDOFF docs). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two theme systems had diverged: the swap UI shipped a standalone lime accent (swap/theme.ts) while the rest of the app used KeepKey gold (#d29929). Add styles/tokens.ts as the single source of truth for surfaces/text/status/accent; both the Chakra theme (styles/theme/index.ts) and the swap inline theme (swap/theme.ts) now derive from it so they can't drift. The swap accent flips lime->gold, making the swap PrimaryBtn gradient byte-identical to the Chakra solid button. Stray swap Spinner color=green.300 were retargeted to the gold accent token in the accompanying swap commit. Also adds EPICS_ui_polish.md — the 8-epic UI-polish plan this is the first of. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…C-2, EPIC-3) EPIC-2 — wire the per-token hide override that was dead code: - fetchBalancesFromPioneer now loads getTokenVisibilityMap() and passes it to filterSpamTokens at the single chokepoint, so a 'hidden' override drops a token and 'visible' force-keeps one. - New SET_TOKEN_VISIBILITY handler persists the override, optimistically drops a hidden token from the cache, and refetches. - Tokens.tsx gains a per-row hover Hide action — a permanent kill switch for scam tokens like 'Mortal' that carry a fabricated >=$1 value and pass every heuristic. Adds a spamFilter test for that exact case. EPIC-3 — fix empty-token-on-first-load: - GET_APP_BALANCES on an empty cache now force-refreshes (discovers ERC-20/SPL/ TRC-20) instead of a natives-only fetch (the worker-restart case). - A non-empty natives-only cache kicks a one-time backstop discovery (guarded by tokenDiscoveryDone + in-flight) and reports `discovering` so Tokens.tsx shows a distinct 'Discovering tokens…' state instead of a premature 'No Tokens Found'. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
No more gray-circle token icons. Add a single <AssetIcon> that tries the asset's icon URL and degrades to a deterministic colored monogram on empty/invalid/404 (generalizes the swap TokenGlyph). Move colorForSymbol to the shared design layer (styles/assetColor.ts); swap/theme.ts re-exports it for swap callers. Migrate every bare Chakra <Avatar> asset/token/network icon onto it: Tokens (replacing the bespoke IconWithFallback), AssetDetail, Balances, Receive, AssetSelect, and the header NetworkDropdown. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ana Devnet (#82) Adds a Settings toggle that registers/removes three testnets with default public RPCs (multiple per chain so rpcFailover has fallbacks): - Ethereum Sepolia (eip155:11155111) - Base Sepolia (eip155:84532) - Solana Devnet (solana:EtWTRABZaYq6iMfeYKA7Kzv3HTKFbW7R) EVM testnets register exactly like a user-added custom network (custom + blockchainData + blockchain storages), so the header dropdown, balance enrichment, and failover all pick them up. Balance enrichment now goes through withRpcFailoverByNetworkId so every URL in providers[] is tried, not just providerUrl. Solana devnet: registered in chain storages; solanaHandler routes sign/broadcast to the devnet cluster when the devnet network is the active asset context, and devnet native balance is fetched directly (Pioneer only indexes mainnet). RPCs verified reachable + correct chainId on 2026-06-22. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ens (EPIC-7 pt1) Replace raw Chakra colors with kk.* semantic tokens and the gold solid button: - Connect: gold buttons, kk.surface card, kk.* text; fix the white full-card spinner overlay that flashed light (now dark). - Transfer (Send): kk.* surfaces/text, gold primary + confirm buttons, accent focus ring and Max/50% buttons; asset Avatar -> AssetIcon; remove the dead useColorModeValue light-mode branch + unused bgColor/headingColor. - DonutChart: slices use colorForSymbol (same palette as AssetIcon) instead of a hardcoded blue/green/orange palette. - Balances: gold loader ring instead of teal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ns to gold tokens (EPIC-7 pt2) Color/token-only migration onto kk.* semantic tokens + gold buttons; logic untouched: - Settings: gold primary buttons, kk.bad destructive actions, kk.* text. - History: kk.* surfaces; status colors -> kk.good/kk.warn/kk.bad; gold/ghost action buttons; removed the dead commented-out network <Select> block. - AssetDetail: Send gold + Receive/Swap ghost; kk.* hero/address/tabs; semantic sent/received arrows -> kk.bad/kk.good; activity rows + status badges on tokens. - Tokens: row surfaces, loaders -> kk.accent, USD value -> kk.good, empty-state buttons gold/ghost. - Network/Account dropdowns: kk.* surfaces/text, neutral chips, kk.bad destructive, kk.accent links. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…cate, a11y, icon fallback) 1. GET_APP_BALANCES: report discovering:true whenever a discovery is warranted (no token rows + none committed) REGARDLESS of an in-flight fetch — only gate STARTING a new force-refresh on !balancesFetchInProgress. Fixes the premature 'No Tokens Found' when the asset page opens during the cold-start/post-prefetch force refresh. 2. Swap balanceByCaip: native predicate now uses the CAIP path (/slip44:, /native:) instead of 'no contractAddress', so a token asset lacking a contract field no longer inherits its chain's native balance and appears swappable. Normalize CAIP keys (lowercase) on build + lookup so checksummed/lowercased contracts match. 3. a11y: AssetPicker search focuses via a ref instead of autoFocus; SwapScreen 50%/Max chips and the route expander get role=button + tabIndex + onKeyDown. 4. Icon fallback: AssetIcon and swap TokenGlyph reset the broken-image flag when the src changes (reused instances no longer stay stuck on the monogram); TokenGlyph now has an onError monogram fallback (was a bare <img>). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(side-panel): native swap UI + UI-polish epics (design unify, scam-token hide, token discovery, AssetIcon, page migration)
Port vault's SpinningDevice (a pure CSS-3D rotating KeepKey — 6-face box, no
Three.js/Lottie/deps) into the side panel as a reusable <SpinningDevice>,
parameterized by `scale` so it fits the narrower panel. The front OLED face is a
slot, so each use shows contextual text ('FETCHING', 'CONFIRM', 'APPROVE',
'SYNCING') for an immersive device feel.
Replaces the hand-built multi-ring spinner in these device-interaction moments:
- Balances 'Fetching balances' (the screenshotted one)
- Swap 'Confirm on your KeepKey' (signing) + 'Checking your KeepKey balances'
- dApp approval 'Please approve on your KeepKey' (was a static SVG; also tidied
that card onto the dark kk.* theme + gold/ghost button)
Drops the now-unused ring keyframes (and their residual teal).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…atalog (EPIC-5) The dashboard listed every chain in the static catalog, so empty chains rendered as $0.00 rows. Filter to networks with a positive balance (native or token) — keyed off the balance AMOUNT, not USD, so a held asset stays visible even when its price is missing/0. The full catalog stays one tap away via '+ Add blockchain' (now also shown in the empty state). The donut/total are computed separately in SidePanel, so they still reflect everything. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(EPIC-5) MV3 evicts the service worker constantly, so cachedBalances started empty and the dashboard flashed $0 on every reopen until a refetch landed. Persist the committed balances to chrome.storage.local and hydrate them at worker start, then revalidate. Scoped to a fingerprint of the active wallet's pubkeys: GET_APP_BALANCES drops a hydrated cache whose fingerprint doesn't match the connected wallet, so a different device — or a different passphrase wallet on the same device — never sees the previous wallet's balances. Display-only; getSendPubkeys/signing untouched. Also returns updatedAt for a future 'stale since' indicator. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e Hidden (EPIC-6)
Add a 'suppressed' tier: a non-allowlisted, non-custom token reporting a fabricated
>=$1 value (the 'Mortal' class) is hidden BY DEFAULT but recoverably. Make tiers 4
(fake stablecoin) and 6 (<$1) price-aware so a not-yet-priced legit token isn't
flagged. partitionSpamTokens() returns {visible, hidden}: confirmed phishing is
hard-dropped, 'suppressed' + user-'hidden' go to a recoverable Hidden bucket (tagged
_hidden). filterSpamTokens stays as a visible-only wrapper. +3 tests (21 pass).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…on, Hidden bucket (EPIC-5/6) - Price backfill: held natives Pioneer returned at $0 borrow a price already in the same response (caip match / mainnet-ETH for L2 gas); else flag priceUnavailable. No new calls, display-only. - Error state: module lastFetchError (set on real fetch failure, cleared on success/ genuine-empty); GET_APP_BALANCES surfaces it additively so the UI can show retry. - EVM reconciliation: GET_EVM_BALANCE writes the live balance back into the cached native row keyed by (networkId, ADDRESS) — safe because rows are per-account — and pushes BALANCES_UPDATED, so dashboard + detail show one number. - Spam chokepoint now partitions: cachedBalances stays visible-only (totals never include scam value); hidden rows kept separately, exposed via GET_HIDDEN_TOKENS; user-added custom tokens exempt. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…able Hidden tokens (EPIC-5/6) - AssetDetail renders from the cached aggregate (matches dashboard); GET_EVM_BALANCE fires only to refresh the cache (background write-back + BALANCES_UPDATED). - Balances shows a distinct 'Couldn't load — Retry' state instead of 'No assets'. - Tokens gains a collapsible 'Hidden (N)' section (GET_HIDDEN_TOKENS) with one-click un-hide (persists a 'visible' override). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…coverability) From a 4-dimension review of the branch diff: - HIGH: persist the Hidden bucket alongside the portfolio cache + restore on hydrate (was lost across MV3 worker eviction → 'recoverable' guarantee silently lapsed). - HIGH: never borrow the mainnet ETH price for TESTNET ETH (was inflating the total with worthless faucet ETH) — guard the backfill + the GET_EVM_BALANCE fallback. - MED: assign hiddenBalances only on the winning fetch commit (staleness-guarded). - MED: handleDeviceSwitch now resets hidden/error/fingerprint/discovery state (no cross-device leakage). - MED: raise SUSPICIOUS_VALUE_FLOOR $1 → $1000 so tier-5.5 only auto-hides egregious fabricated-value lures, not legit mid-cap holdings (which were understating the total). Bumped the Mortal test values accordingly. - LOW: walletFingerprint = direct address-set string (no hash collisions); drop a hydrated cache with a missing/mismatched fingerprint; id-guard lastFetchError in the outer catch; fire GET_EVM_BALANCE for async-resolved EVM addresses. tsc green (both packages); 21 spamFilter tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…CACHE (review) P1 — visibility now durable: SET_TOKEN_VISIBILITY applies the override to the in-memory set deterministically via reconcileVisibility() (re-partition + persist + push, no network), so a hide/un-hide survives a failed refetch or MV3 worker restart instead of relying on a later refetch. P1 — removed the value-floor auto-suppression (tier 5.5). A high USD value is NOT evidence of fabrication and there's no server-side trust signal to distinguish a scam from a legit unlisted token, so auto-hiding non-allowlisted >=$N tokens underreported real holdings. The 'Mortal' class is handled by the per-token Hide + recoverable Hidden section (one-click, durable) instead. Nothing auto-vanishes from the dashboard or the total. Tests updated accordingly. P2 — CLEAR_CACHE now clears hiddenBalances/lastFetchError/cacheFromHydrate/ hydratedFingerprint/tokenDiscoveryDone AND removes the persisted PORTFOLIO_CACHE_KEY, so a worker restart can't re-hydrate the just-cleared portfolio. tsc green (both packages); 21 spamFilter tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat: portfolio data layer + durable scam suppression (EPIC-5 + EPIC-6)
feat(side-panel): spinning-KeepKey loader for device-interaction moments
The background service worker re-paired with the vault far too often,
spamming "A pairing request is already pending" and forcing the user to
re-approve a new key on the device. Root causes (all client-side here;
vault-side causes handed off separately):
- wallet.init() had no single-flight. The boot timer, the 5s health-poll
retry, and ON_START (Connect + refresh) all call onStart()→init() and
overlap, so each POSTed /auth/pair and the second collided with the
first's pending request → 429 "already pending" → errored icon → user
mashes refresh → more pair attempts.
- The SDK's verifyAuth() has no retry and pairs on ANY failure, so a
transient vault blip at cold start escalated to a device prompt.
- isAuthError() matched the bare substring 'auth', and a false positive
WIPED the saved key (saveApiKey('')) — a self-inflicted re-pair.
Changes (chrome-extension/src/background/wallet.ts only):
- Single-flight init(): concurrent callers share one in-flight promise.
- probeVaultAuth(): retry-aware health + key check BEFORE the SDK can
auto-pair. Distinguishes valid / explicitly-rejected / unreachable, and
only pairs when the key is genuinely absent or vault-rejected. A
transient/unreachable vault → view-only from cache + retry, never a
prompt and never a key wipe.
- hydrateViewOnly(): keep showing last-good cached data when the vault is
briefly down, instead of an errored icon, and auto-recover on the poll.
- isAuthError(): key off the structured HTTP status (401/403) and drop the
over-broad 'auth' substring so transient errors no longer wipe the key.
Vault-side defects (proven, not client-fixable) are documented for the
vault team in HANDOFF_vault_pairing_persistence.md (no dedup → duplicate
keys, FIFO eviction, TTL-from-creation, 60s/600s pending-timeout mismatch)
and HANDOFF_vault_connectivity_api.md (idempotent pairing + concurrent-pair
coalescing REST contract).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(connection): harden vault pairing/init for always-on connectivity
…s-asset labeling + dust-precision balances
Completes the asset-detail-hero page:
- Spinning KeepKey hero carries the asset icon + balance on its OLED,
replacing the static icon block. Branded SpinningDevice loaders replace
the kk.gif on the Loading, Connect (offline), and welcome screens.
- EVM gas-asset + network labeling so same-symbol assets are
distinguishable: native rows read "Ethereum / ETH on Base"; swap rows
(AssetPicker/SwapReview/SwapProgress/TokenButton) show the network under
the symbol. New EVM_NATIVE_GAS map + getNetworkName()/networkLabelFor().
- Balances now show 8-decimal precision with the trailing 4 digits
("dust") rendered smaller and dimmer (DustAmount), replacing toFixed(4),
so sub-0.0001 precision is visible without crowding the headline.
- Fix: ARB/OP/MATIC governance ERC-20s are no longer mislabeled as the
chain's native gas asset — the symbol-match fallback that relabels a
native row now excludes token rows (gated on !asset.token).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tokens did not load on first view of an asset page — the user had to press "Discover Tokens" to surface them. The background's `discovering` flag is global (any cached token on any chain, or a completed forced discovery, clears it), so a network opened for the first time could hold zero cached token rows yet report discovering=false, stranding the page on "No Tokens Found". Tokens.tsx now auto-kicks one forced portfolio discovery per network when the cache has no tokens for it and the background isn't already discovering. BALANCES_UPDATED repaints the list when the discovery commits. The dedup set is module-scoped (not a per-component ref) so it survives the asset Drawer's unmount/remount and won't re-fire a heavy /portfolio refresh on every reopen of a token-less chain. The "Discovering…" spinner self-clears on the background's no-broadcast and error paths (no pubkeys yet, wallet not initialized, Pioneer 5xx) instead of hanging. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(side-panel): asset-detail hero — auto-loading tokens + 8-decimal dust balances
…tart A dApp's eth_requestAccounts was rejected with "Wallet not initialized" whenever it arrived during the service-worker cold-start window. Root cause: MV3 evicts the background service worker after ~30s idle, wiping all in-memory wallet state. A dApp's first RPC wakes the worker, but boot init runs on a 5s setTimeout and PR #85's retry-aware probeVaultAuth adds several seconds more before wallet.init() resolves. The WALLET_REQUEST handler threw immediately for any request landing in that window — it never triggered init. PR #85 lengthening the init window turned this from occasional into "totally broke." Fix: WALLET_REQUEST now awaits init on demand (via a single-flight ensureStarted() wrapper around onStart) instead of rejecting the dApp, and only throws if the wallet is genuinely uninitialized (no device and no cached pubkeys) after init completes. The boot timer, ON_START, and the health-poll retry route through the same wrapper so a burst of connect-time RPCs shares one init rather than stacking redundant runs. Awaiting the full onStart (not just wallet.init) is deliberate: the module-level ADDRESS returned by eth_requestAccounts is set in onStart's body, so a bare init() would return an empty address. onStart catches internally and never re-throws, so ensureStarted always settles; the await is skipped entirely in steady state and view-only mode, adding zero latency to warm read RPCs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ld-start fix(background): init wallet on demand so dApp RPCs survive SW cold start
test: rpcFailover + chainConfig coverage (vitest workspace resolution)
test: cover fetchUtils timeout + retry/backoff
TxidPage shows a bare hash with no "View on Explorer" link when the stored web3 provider lacks explorerTxLink. Three provider-build paths dropped it even though getChainInfo() already supplies it: - onStart default ETH provider (index.ts) — the common mainnet send path - SET_ASSET_CONTEXT Pioneer fallback (index.ts) - wallet_switchEthereumChain Pioneer-switch (ethereumHandler.ts) Carry explorerTxLink (+ networkId) from ChainInfo at each site. The two transaction_complete senders read explorerTxLink straight from the stored provider, so fixing the source closes the gap and also restores the address-explorer links in the asset/switch UIs. Re-implements #53 (its EIP155_CHAINS fallback is obsolete — that static table was replaced by the Pioneer registry). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(eth): populate explorerTxLink on all EVM provider-build paths
…+ RELEASE SOP The build-zip/lint/prettier/test/e2e workflows triggered `push` on `main` — a branch that doesn't exist here (the branch is `master`) — so those push gates never fired on releases. Repoint them to `master` (test keeps `develop`). E2E drives the extension against a physically connected KeepKey and can't pass on GitHub's headless runners, so convert it to `workflow_dispatch` (manual) instead of leaving a dead `main` trigger. Add the missing `make test` target — the Makefile wraps every other pnpm script but had no unit-test target, so `make test` errored. Add RELEASE.md documenting the develop → release/x.y.z → master → tag → build → sync-back flow, including why the sync-back step matters (the develop/master divergence it prevents). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
chore(ci,docs): point CI at master, make E2E manual, add make test + RELEASE SOP
# Conflicts: # chrome-extension/package.json # package.json # packages/dev-utils/package.json # packages/hmr/package.json # packages/i18n/package.json # packages/shared/package.json # packages/storage/package.json # packages/tailwind-config/package.json # packages/tsconfig/package.json # packages/ui/package.json # packages/vite-config/package.json # packages/zipper/package.json # pages/content-runtime/package.json # pages/content-ui/package.json # pages/content/package.json # pages/devtools-panel/package.json # pages/devtools/package.json # pages/options/package.json # pages/side-panel/package.json # tests/e2e/package.json
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.
Release 0.0.34
Ships everything merged to
developsince the 0.0.32 release and reconciles thedevelop/masterdivergence. Cut per the new RELEASE.md SOP.What's in it
WALLET_REQUESTawaits single-flight init so dApp RPCs survive SW eviction.explorerTxLinknow populated on all EVM provider-build paths; TxidPage deep-links the txid instead of showing a bare hash.maintomaster; E2E made manual (workflow_dispatch, needs a device);make testtarget; RELEASE SOP.Reconciliation note
Merging
masterin applied #64 (dropchrome.alarmspermission →setTimeoutdrop-check).developwas stale and still carried the old alarms code; this release converges on master's deliberate decision (avoids a Chrome Web Store re-consent wall). Manifest permissions are back to['storage', 'tabs', 'commands'], and no code referenceschrome.alarms. The only merge conflicts were the 20package.jsonversion strings (resolved to 0.0.34).Verification (local, on the merged branch)
make type-check15/15make test91/91make buildall tasks passpnpm install --frozen-lockfileclean (auto-merged lockfile consistent)After merge: tag
v0.0.34, build Chrome/Firefox zips for the GitHub Release, then syncmasterback intodevelop.🤖 Generated with Claude Code