Skip to content

release: land twelve v0.10.6 audit fixes in one merge (#582–#632) - #636

Merged
derek-flossman merged 58 commits into
release/v0.10.6from
batch/v0.10.6-audit-2026-10-12b
Oct 12, 2026
Merged

derek-flossman merged 58 commits into
release/v0.10.6from
batch/v0.10.6-audit-2026-10-12b

Conversation

@derek-flossman

Copy link
Copy Markdown
Collaborator

Lands twelve draft PRs in one merge. release/v0.10.6 requires 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-ff onto release/v0.10.6 at a8ec8e59 (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.

PR Head What it fixes
#582 47aea945 a 5xx on a relay broadcast is treated as a lost answer, not a refusal; the wallet's relay deadline is now 20s (Sherlock 159)
#586 cde40a25 long nested SNIP-12 key paths can no longer hide signed values; no Sign button past 259-character keys (Sherlock 257, part of 187)
#588 4d20d7db relay fee-token fixes; a batch that deposits the fee token is refused before review (Sherlock 189, 232, 233)
#596 b0a7084b multisig relay sync no longer wedges, floods or re-imports removed intents (Sherlock 129, 141, 168, 178, 179, 237)
#600 7bfad481 history scan and late-failure notice fixes (Sherlock 201, 219, 221, 223, 239, 249)
#602 2604631a locks down the dev gateway, dev proxy and private-swap API (Sherlock 127, 195, 199, 244)
#604 37da3ddf dapp handler guards, review shapes and Wallet API conformance (Sherlock 165, 166, 241, 251, 252, 254). The outside review was light: please give the Sherlock 166 lock re-checks a careful pass.
#608 c3518c19 ties the Ledger sign prompt to its queue and releases idle device claims (Sherlock 227, 228, 259)
#618 67361e27 token suggestions, price and chart caches, phishing refresh, and watched-token and add-network checks (Sherlock 125, 130, 182, 185, 207, 210)
#621 501435bb keeps the privacy submission marker until acceptance, and fixes the Send unit toggle (#474)
#629 6c775934 one main landmark per page, and an amber toast for an unknown outcome (#526, #382, #432, #298)
#632 605a54eb records the fee each wallet transaction paid (part of #463)

The 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>.

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 to release/v0.10.6.

Checks run locally on this exact tree (Linux, Bun 1.3.3)

Check Result
bun run check pass
bun run check:coverage pass: functions 95.08%, lines 95.62%
bun run check:audit pass (run before 8782418b, which changes no dependencies)
bun run build pass

🤖 Generated with Claude Code

derek-flossman and others added 30 commits October 11, 2026 20:30
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>
derek-flossman and others added 6 commits October 12, 2026 13:20
…to batch/v0.10.6-audit-2026-10-12b

# Conflicts:
#	apps/extension/src/background/provider/watch-asset.ts
…tch/v0.10.6-audit-2026-10-12b

# Conflicts:
#	packages/presentation/wallet-ui/src/sidepanel/shared/dom.ts
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
derek-flossman merged commit 7eb9028 into release/v0.10.6 Oct 12, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants