Repository navigation
release: land twelve v0.10.6 audit fixes in one merge (#582–#632) - #636
Merged
derek-flossman merged 58 commits intoOct 12, 2026
Merged
Conversation
The paymaster client treated every non-2xx status as a refusal the relay stated for itself. The wallet's HTTP peer is usually the STRK20 gateway, not the relay, and the gateway answered 502 when its own 25s upstream deadline expired, five seconds inside the wallet's 30s one. A relay that accepted and forwarded paymaster_executeTransaction but replied slowly therefore came back as a determinate failure, the maturity scope released its submitting guard, and the next private action proved and broadcast again (Sherlock 159). - paymaster-rpc: a 5xx on the broadcast method is a lost answer and throws IndeterminateRpcWriteError, so the guard holds. A 4xx stays a refusal, and quote failures stay determinate. - The wallet's deadline is now 20s, below the gateway's 25s, so its own deadline normally fires first. A conformance test pins the order against the gateway's constant, now named in _lib/security.ts. - Gateway: an upstream timeout answers 504 upstream_timeout instead of 502, and a paymaster upstream 4xx stays a 4xx (upstream_rejected) rather than becoming a 502 the wallet would now read as lost. The 503 the submission suite pinned as a stated refusal is now a lost answer: through the gateway a 5xx says nothing about the relay. The counterweight is pinned with a 400 and a 429 instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With a non-STRK paymaster fee token configured in Privacy services, a batch that spends only STRK is quoted in STRK, because the quote follows the spent token. A STRK fee is the exact route and carries no ceiling, and the submission guard refused any ceiling-less plan whose fee token was not the configured one. Every STRK-only private send and unshield therefore failed with "strk20 private submission unavailable" inside the broadcast step, after review and after proving (Sherlock 233). The guard now also admits STRK without a ceiling: its fee is matched exactly against the pool's on-chain charge at quote time. A plan in any third token still needs the ceiling it was admitted under. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
spentTokens counted an OPEN transfer as a spend. An OPEN transfer is a note the batch fills for the user during execution (a vault redeem, a position withdrawal, a sub-account exit), so it names a token the user receives. The fee preference followed it, the relay was asked to quote the fee in the received token, and a user holding none of that token privately, the normal case for proceeds, was refused with "insufficient private balance" after approving, on every retry, even with ample private STRK (Sherlock 232). spentTokens now skips OPEN transfers, matching the balance guard beside it, so such a batch keeps the configured fee token. The balance read in assertActionBatch no longer asks for a token the batch only receives, which the balance guard never looked at anyway. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The relay's fee is a public withdrawal added to the batch after the caller's actions pass assertNoPrivacyLeak, so the deposit/withdrawal pairing that check refuses was never looked for on the combined set. The fee-token preference tried to avoid it by returning the configured fee token when the spent token was also deposited, but on the shipped mainnet routing that token is STRK, so a batch depositing and spending STRK paid its fee in STRK: a public STRK deposit and a public STRK withdrawal in one transaction (Sherlock 189). The same happened when the relay vetoed the spent token and the fallback was deposited, or when the batch spent more than one token. quoteStrk20Plan and submitStrk20Actions now run the leak check on the caller's actions plus the plan's fee action. A batch with no fee token that avoids the pairing is refused before review with the existing privacy-leak error rather than linking the deposit to the private spend. Deposit-only batches are unaffected: they pay from the public account and carry no fee withdrawal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A sign card row key composes one SNIP-12 name per nesting level, and each name may be 64 characters, so a nested message produced dotted keys wider than the card. The key was laid out at full width with no break opportunity (flex: none), and the value beside it shrank to nothing: an ordinary four-level order showed its recipient, amount and nonce at zero visible pixels while the card offered Sign (Sherlock 257). The key now shrinks and wraps anywhere, up to half the row; the value keeps the rest, and the block's single column is held to the card's width. A payload whose key is longer than four maximum-length names (259 characters) is reported unclassified, so the card withholds Sign and shows its existing warning instead of a key that buries its value. The approval summary cuts the domain name, and the primary type, at 64 characters. A revision-1 domain name is a ByteArray with no length bound, and the panel drops an approval whose summary is over 300 characters, which left the site's request waiting with nothing on screen (Sherlock 187). UI Lab gains a nested typed-data checkpoint whose story checks, in a real browser, that no row overflows the card and every value keeps a visible width. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
importGiftWraps recorded every wrap as seen the moment it decrypted, before importing it and whatever the result. recordSync persisted that set, so a wrap whose import failed for a passing reason (an RPC error during the import review, another active network) was skipped by every later sync and its intent never arrived over relay sync (Sherlock 129). The loop also awaited each import with no boundary. The call-safety validators throw a WalletError for a multi-call containing an account self-call and for unsupported self-calls, which passed every earlier check, so one such package from a co-signer escaped the sync, skipped recordSync, and was fetched and thrown again on every later sync, stalling the account's co-signer channel for good (Sherlock 141). Each wrap now reports whether the sync is finished with it: imported, a no-op (untrusted sender, nothing new), or refused for a reason that depends only on the package and settled history (a stale nonce or configuration, a conflicting or invalid package, a call set the safety checks throw on). Only those get a receipt. Anything else is opened again while the wrap stays inside the three-day incremental lookback, and a throw from one wrap no longer stops the rest of the batch. The coordination test network moves to tests/support so the new suite reuses it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
importIntent recomputes an intent's summary from the importer's own token list and address book, and the merge took the incoming summary. canReplaceArgentMultisigSigningIntent requires the summary unchanged, so once a signer added the token an intent moves (or approved a wallet_watchAsset for it), every later package for that intent was refused as "Argent multisig intent conflict": co-signer signatures never reached that signer's copy, and over relay sync the refusal was silent (Sherlock 178). The merge now keeps the stored summary. It is local display text; the frozen prepared transaction and its hash already pin what is signed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Relay sync deduplicated on the gift wrap's relay-supplied event id, held in a 200-entry list, and removing an intent left no record. Krusty opens a wrap's content but returns no event id, so the id is never authenticated: a relay (or anyone flooding the list past 200 entries) could serve a removed intent's package again under a fresh id, and sharedIntentHasUpdate treated the missing intent as new and imported it again (Sherlock 168, 237). Removing an intent now records its id, per multisig account, in a bounded store beside the sync checkpoints (newest 100 per account), and relay sync refuses to import a recorded intent that is not stored. The record is written before the removal, so a failed removal leaves only a harmless record. A package the user imports by hand is still accepted, and once it is stored again relay updates merge as before. The event-id list stays as a work-saving cache. The comment in nostr-relay.ts that called relay ids "untrusted until Krusty authenticates each wrap" now says they are never authenticated. Both coordination keys are added to docs/WALLET_STORE.md, which listed neither. Not done: an authenticated per-sender nonce inside the package, which would also stop replay of an intent that was never removed. That changes the shared package format, which older co-signer wallets would refuse, so it is left as a decision. Replaying a stored intent is already a no-op, and a removed one is now refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The store held at most 100 intents across every multisig and could evict only settled ones. Relay sync imports every proposal a trusted co-signer publishes, with zero signatures and no quota, so one co-signer of one multisig could publish 100 proposals and fill the store: the victim then could not prepare on any multisig, could not import honest co-signers' proposals, and could not remove the flooded multisig while it had open intents (Sherlock 179). A multisig now holds at most 25 intents, inside the same wallet-wide 100, and a multisig at its own bound only ever evicts one of its own settled intents. A flood stays inside the multisig whose co-signer sent it. The placement rule moves to intent-capacity.ts with isTerminalIntent, which also keeps repository.ts under the size cap. A proposal refused as full is a passing refusal, so relay sync retries it once room is made rather than dropping it. Not done, and left as product decisions: removing a multisig together with its open intents in one confirmed step, a bulk "clear proposals from this co-signer" action, and expiring open intents whose nonce the chain has passed. Until then the flooded multisig's open intents are removed one at a time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each relay answers with its newest 50 gift wraps, and the union across relays was cut to 50 before the wallet skipped wraps it had already finished with. Anyone holding the recipient's key could publish 50 newer wraps to each relay and push an honest, older wrap out of every window, so it was never surfaced (Sherlock 129). The sync now passes its receipted event ids to the transport, which skips them before applying the 50-wrap bound. While a relay's answer comes back full, it is asked again with `until` below the oldest wrap it returned, up to four pages per relay per sync; a full page from a single second steps one second lower rather than repeating. This raises the cost of pushing a wrap out of the window to 200 newer wraps per relay per sync; it cannot remove it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An unshield to another address was recorded as "Unshield 5 STRK" with no counterparty, so Activity showed a public payment to someone else as a move between the owner's own balances: the format layer reads a privacy row as outgoing only when it names another party (Sherlock 201). A shield to someone else already named them. The activity summary now names the destination whenever it is not the account itself, for shields and unshields alike, and the row records that address as its counterparty, so the payee joins the paid-recipient set like any other wallet-submitted payment. A private transfer records its recipient as the counterparty too; its summary is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Late-failure notices reached the panel through showToast, which maps every error message through friendlyError. friendlyError rewrites a message by the words in it, and a notice quotes the entry's summary, which a dapp can choose: a revert warning for a summary containing 429 was shown as rate-limit copy, hiding that the transaction reverted (Sherlock 219). showNoticeToast shows a wallet-authored sentence as written, still as an error toast with its copy control, and the alert drain uses it. showToast keeps mapping the error messages it is given. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wallet.transactionAlerts drained notices only for the active vault account, but multisig transactions and deployments are journaled under the multisig's own id. Reconciliation still marked their late failures as owed, and nothing ever delivered them: a reverted multisig transfer or deployment never raised its alert (Sherlock 221). The drain now covers the active account and every stored multisig on the active network. A scope whose drain fails is skipped so it cannot hide the others. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wallet.getChainId, wallet.supportedSpecs and wallet.supportedWalletApi were the only dapp-reachable messages that skipped both the sender-origin corroboration and the blocklist. An origin the user had blocked could still poll the active network and the wallet-API version (Sherlock 165). The data is public, but a blocked origin should learn nothing from this wallet, and a content script should not answer for an origin it is not on. The three handlers now corroborate the claimed origin and refuse a blocked one with the same "origin blocked" error every other surface returns. One existing test sent a mismatched origin and is corrected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hydrateView painted the cached view and awaited it before creating any live refresh job, with the paint unguarded. Cached data that made the paint throw, such as an activity timestamp outside the Date range, aborted hydration before the live jobs that would replace it were created, so the bad data blocked its own repair; the refresh scheduler swallowed the rejection, so every tick repeated it (Sherlock 239). The post-flush paint had the same shape before the deferred jobs. Both paints now run through paintIfCurrent, which keeps the previous screen when a paint throws and lets hydration continue. The navigation and first-render entry points in start.ts catch a failed status read or render instead of leaving an unhandled rejection; the refresh lifecycle renders again on its next tick. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The history scan took every tracked token as configured, including
tokens the wallet itself flags as likely spam, and wrote each token's
stored symbol into the activity summary. A dapp-added contract calling
itself USDC therefore appeared in Activity as "Received 5000 USDC",
priced and shown with a logo by that symbol, although the home screen
hides it (Sherlock 249).
accountHistory now gives a likely-spam token the identity Activity
already uses for a token it does not track ("TOKEN", "Untracked
token"), so its rows read "Received 5000 TOKEN" and fall under the
untracked-token planted-row rules. Tokens that are not flagged keep
their own identity. The wallet-activity harness gains options to read
custom preferences and observe the scan request.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The RPC history scan asked the node only for Transfer events with the account at key 1 or key 2. A legacy (Cairo 0) ERC20 indexes only the selector and carries from, to and the amount in data, so those filters can never match its events, and every transfer of such a token that was not already journaled was missing from Activity and the CSV export whenever the RPC lane served history (Sherlock 223). The decoder already handled the layout; nothing asked for it. The scan now reads one Transfer event per token to learn which layout it emits. A legacy token is read with the selector alone, under the same page budget and recent-stride fallback, and decodeTransferEvent keeps only the transfers whose data names the account; an indexed token keeps the account-key filters. Probing instead of listing legacy tokens avoids a list that goes stale as contracts are upgraded. It costs one extra one-event getEvents per token on indexed tokens and saves one on legacy ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Retrying a declaration fee estimate first withdraws the reviewed fee, so nothing can be approved against a fee that is being replaced. It did so by writing an "estimating" shape (no fee, no failure) that the panel's validator rejects, so readPendingApprovals dropped the card the user had just clicked. Three early returns (a lock, a network change, a revoked grant) then left that shape in place, and the card stayed gone (Sherlock 241). The withdrawn shape is now a failed estimate, the one fee-less shape the panel accepts. The card stays on screen with its retry control, and the background still refuses to approve it until a fresh estimate lands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wallet.signTypedData, wallet.switchChain and wallet.addDeclareTransaction read the session once and never re-checked it, while the executors in the same files capture the authorization and re-check it after every await. A lock that landed during the grant read or the fee estimate still let the handler finish its vault reads and queue a card under the unlock the user had just revoked (Sherlock 166). Signing still failed at the signer seam, so this was never a signing bypass, but locking is meant to end the session there and then. Each handler now captures the authorization before its first read and re-checks it after the grant read and again before queueing. A lock answers the way a locked wallet does: "wallet locked" for sign and declare, "origin not connected" for a network switch. The typed-data perimeter moves into admitSignTypedData to keep the handler within the complexity budget. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An approved network switch answers { chainId, ok, pending }. The plain
{ ok, pending } reply lost its whole-envelope JSON budget in the
MCW-V09-106 fix, but this branch kept one, so a queue of individually
valid prompts behind the switch exceeded it and the panel reported
"approval failed" for a switch the background had already applied
(Sherlock 251). Each queued prompt keeps its own budget through
readPendingApprovals, as on the plain reply.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Vite dev server proxies /proxy/extended, /proxy/avnu-markets and /proxy/starkscan by slicing off the prefix and resolving the rest with new URL(rest, target). A rest starting with "//" is protocol-relative, so /proxy/starkscan//attacker.example/x was fetched from attacker.example: a blind SSRF from the developer's machine while the dev server runs (Sherlock 127). devProxyUpstream now refuses a remainder that does not start a single "/" path, and checks the resolved URL is still on the upstream's origin. Both are needed: URL parsing drops tabs and newlines, so "/\t/host" passes the first test and still names another host. The proxy also fetches with redirect: "error", like the production gateway. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The local STRK20 dev gateway holds a developer's paymaster key and vouches for its callers to the deployed gateway. - Sherlock 199: the paymaster leg matched "/api/strk20/paymaster" with a bare startsWith and concatenated the remainder onto an unterminated relay URL, so /api/strk20/paymaster.evil.example/x became https://starknet.paymaster.avnu.fi.evil.example/x, with the key already attached. The leg now matches only on a path boundary, joins segments that pass the production gateway's grammar below a fixed origin, checks the result is on that origin, and attaches the key only after that. Both upstreams must be bare https origins or the server does not start. - Sherlock 244: CORS echoed any Origin, preflights were answered before the path gate, and every forwarded request was stamped with the privileged extension Fetch-Metadata shape, so any page a developer visited could use the deployed gateway through it and read the answer. Only an extension origin, or an origin-less request carrying the privileged extension shape, is now admitted; anything else gets a 403 with no CORS grant, and preflights are answered only on gateway paths. The request policy lives in src/protocols/privacy/dev-gateway-policy.ts so it is type-checked and tested, and the script exports its handler and opens the listener only when run directly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/api/private-swap refused only `plan` while PRIVATE_SWAP_EXECUTOR is unreviewed (null), but `fee` and `submit` also attach the operator's AVNU_PAYMASTER_API_KEY, and their only gate is an Origin header any HTTP client can set. With swaps off the terminal UI cannot reach either action, so they could only be used to spend the operator's sponsorship from outside it (Sherlock 195). All three actions are now refused before any upstream call while the executor is unreviewed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two Wallet API calls a spec-following dapp makes were refused with no
prompt (Sherlock 252, 254):
- wallet_watchAsset's Asset may carry options.image. The parser now
accepts it when it is a string and drops it; the wallet never fetches
a site's image. An empty or over-64-character on-chain token name no
longer refuses the token: a usable on-chain name is cut to 64
characters as wallet.addToken stores it, else the site's name is
used, else the symbol. A name the site sends is still refused when it
is not a valid token name.
- request({ method, params: undefined }) now reads the same as leaving
params out, in both the method and the type call forms. Unknown keys
are still refused, even when their value is undefined.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The device-sign prompt is a repaint-surviving full-panel overlay, and only a completed sign or a cancel ever took it down. A lock rejects every parked request, but the locked branch paints the gate and returns before poll() runs, so the opaque prompt stayed over the unlock screen still showing the old hash; after the queue's own timeout it could show one hash while another was live (Sherlock 228). poll() now dismisses a presented request the background no longer holds, the locked branch dismisses it directly, and a device operation still running for a dismissed request neither reports nor repaints. poll() also ran in the toolbar popup. Connecting there opens the grant tab, which closes the popup and orphans the session with the device still claimed (Sherlock 227). The popup now leaves a parked request for the side panel, the same gate the Ledger onboarding entry already has. The sign and onboarding takeovers shared z-index 40, so the onboarding overlay, mounted later, covered the hash and the Cancel control when a sign request parked during Ledger onboarding (Sherlock 259). The two layers are now one exported constant with the sign prompt on top. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The grant tab serves device commands until the panel sends close. A panel that died without sending it, such as a toolbar popup closed by the grant tab taking focus, left the tab holding the device for good, and every later attempt reported device-busy with copy blaming Ledger Live (Sherlock 227). serveLedgerBroker documented an idle expiry that did not exist. The broker now closes the session through the same onClose path, which the grant page turns into releasing the transport and closing the tab, after five minutes with no command. The timer is disarmed while a command runs, so a user reading the device screen is never cut off, and re-armed after each one. Panel sessions open per operation and close when it ends, and five minutes outlasts the panel's 120s wait for the grant tab. The bound is injectable for tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ttles Review of the previous commit found a gap in the hand-over from a dismissed request. poll() dismisses a request the background no longer holds and presents the next one, but present() ignores a request while the dismissed one's device operation is still closing its transport, and nothing re-polled once it had: the new request stayed parked with no prompt until some unrelated wallet event repainted the panel. The controller now exposes whenIdle(), which resolves once the device operation has finished, and poll() arms a single re-poll when the request it offered was skipped. The re-poll fetches the queue afresh, so a request settled in the meantime is never shown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The side panel accepted wallet.detectTokens only if every suggestion passed the token store's rules. The curated mainnet registry lists bridged USDC as "USDC.e", which the shared symbol rule refuses, so for any account holding it "Scan for tokens" reported "token detection failed" and suggested none of the account's other tokens either (Sherlock 207). The panel now checks the envelope and drops each row the token store would refuse, keeping one row per id. The registry ticker itself is left as it is: making it pass needs either a new ticker or a wider symbol rule, which is a product call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The token detail chart lived in one cache slot with no token identity, and the detail view takes the last chart point as the token's price. Opening USDC after ETH painted USDC with ETH's sparkline and price, so 1,000 USDC read as millions of dollars until the new chart loaded, and for good if that load failed (Sherlock 210). Hydration now records which token the chart was read for, and the scene draws the chart only on that token's screen. Any other token falls back to its own price from the price map. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The price and chart caches are only ever filled on mainnet, the one network with a price feed, and are keyed by contract address alone. ETH and STRK use the same addresses on mainnet, Sepolia and Devnet, and the no-feed branch returned the cache anyway, marked fresh. After viewing mainnet, Sepolia test ETH and STRK showed live mainnet USD values, with no "saved prices" note (Sherlock 185). A network with no feed now gets no prices, changes or chart points, which is what the feed module's own contract says: no fiat by design. The cache is read only on the branch that can use it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…batch/v0.10.6-audit-2026-10-12b
…to batch/v0.10.6-audit-2026-10-12b # Conflicts: # apps/extension/src/background/provider/watch-asset.ts
…batch/v0.10.6-audit-2026-10-12b
…tch/v0.10.6-audit-2026-10-12b # Conflicts: # packages/presentation/wallet-ui/src/sidepanel/shared/dom.ts
…batch/v0.10.6-audit-2026-10-12b
Merging these PRs together pushed two files past the file-size cap and function coverage just under 95%: - chain-ops.ts (508 lines): the SNIP-12 approval summary moves to typed-data-summary.ts, unchanged. - runtime.ts (501 lines): detectTokens returns early on one line. - The Sherlock 159 deadline-order test imported a constant from the terminal gateway's security module, loading that whole module into the wallet's coverage run. The two upstream deadlines move to upstream-deadlines.ts, which the gateway and the test both import. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
derek-flossman
marked this pull request as ready for review
October 12, 2026 14:37
derek-flossman
requested review from
Ryun1,
adrienlacombe,
l-henri,
omarespejel,
robertkodra and
teddyjfpender
as code owners
October 12, 2026 14:37
l-henri
approved these changes
Oct 12, 2026
This was referenced Oct 12, 2026
Audit: long nested SNIP-12 key paths hide the signed values on the approval card (Sherlock 257)
#585
Closed
Closed
Closed
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.
Lands twelve draft PRs in one merge.
release/v0.10.6requires a branch to be up to date with its base, and the last push to be approved, so merging them one at a time would need a fresh approval after every merge. Same approach as #635.Fixes #581, #585, #587, #595, #599, #601, #603, #607, #617, #474, #526, #382, #432, #298. Refs #463, #483.
What this contains
Each PR head is merged with
--no-ffontorelease/v0.10.6ata8ec8e59(after #635), in this order. None of these PRs has been approved on its own. Review each one's diff on its own PR page, then approve here.47aea945cde40a254d20d7dbb0a7084b7bfad4812604631a37da3ddfc3518c1967361e27501435bb6c775934605a54ebThe behaviour changes above were accepted by @derek-flossman on 2026-10-12. None of these PRs adds user-facing copy that is still waiting on approval.
Code that is only in this PR
Three merge commits resolve conflicts, and one commit keeps the combined tree within the repository gates. Review them with
git show <sha>.cbde004c(activity: history scan and late-failure notice fixes (Sherlock 201, 219, 221, 223, 239, 249) #600 meets the merged activity: retention, merge provenance and validation fixes (Sherlock 150, 158, 180, 196, 203, 204, 238) #598):activity-ops.tskeeps activity: retention, merge provenance and validation fixes (Sherlock 150, 158, 180, 196, 203, 204, 238) #598'sNO_HISTORYand history-lane result, and adds activity: history scan and late-failure notice fixes (Sherlock 201, 219, 221, 223, 239, 249) #600's mapping of likely-spam tokens to the untracked identity (Sherlock 249). The test harness takes both activity: retention, merge provenance and validation fixes (Sherlock 150, 158, 180, 196, 203, 204, 238) #598'shistoryLaneand activity: history scan and late-failure notice fixes (Sherlock 201, 219, 221, 223, 239, 249) #600'sonHistoryScan.efe00020(data: token suggestions, price and chart caches, phishing refresh, watched-token and add-network checks (Sherlock 125, 130, 182, 185, 207, 210) #618 meets provider: dapp handler guards, review shapes and Wallet API conformance (Sherlock 165, 166, 241, 251, 252, 254) #604) inwatch-asset.ts: decimals and symbol come only from the on-chain read (data: token suggestions, price and chart caches, phishing refresh, watched-token and add-network checks (Sherlock 125, 130, 182, 185, 207, 210) #618, Sherlock 125). The name keeps provider: dapp handler guards, review shapes and Wallet API conformance (Sherlock 165, 166, 241, 251, 252, 254) #604's rule (Sherlock 252): use the on-chain name when it is valid (cut to 64 characters), otherwise the site's name, otherwise the symbol.19b2fe1a(ui: one main landmark per page, and an amber toast for an unknown outcome (fixes #526, #382, #432, #298) #629 meets activity: history scan and late-failure notice fixes (Sherlock 201, 219, 221, 223, 239, 249) #600) indom.ts:renderToastkeeps activity: history scan and late-failure notice fixes (Sherlock 201, 219, 221, 223, 239, 249) #600'sshowNoticeToastpath (Sherlock 219). ui: one main landmark per page, and an amber toast for an unknown outcome (fixes #526, #382, #432, #298) #629's amber tone is treated like an error: it gets the friendly error text and a copy button.8782418b:chain-ops.ts(508 lines) totyped-data-summary.ts.runtime.ts(501 lines) loses two lines.apps/terminal/api/_lib/upstream-deadlines.ts. privacy: a 5xx on a relay broadcast is a lost answer, not a refusal (Sherlock 159) #582's deadline-order test imported them fromsecurity.ts, which loaded that whole untested module into the wallet's coverage run and put function coverage at 94.93%.Once this merges, the twelve PRs' heads are on
release/v0.10.6, so GitHub should show each one as merged. #612 (stacked on #604) and #614 (stacked on #586) then move torelease/v0.10.6.Checks run locally on this exact tree (Linux, Bun 1.3.3)
bun run checkbun run check:coveragebun run check:audit8782418b, which changes no dependencies)bun run build🤖 Generated with Claude Code