Skip to content

Batch: v0.10.6 feature PRs (16) - #642

Merged
derek-flossman merged 67 commits into
release/v0.10.6from
batch/v0.10.6-features-2026-10-13
Oct 13, 2026
Merged

derek-flossman merged 67 commits into
release/v0.10.6from
batch/v0.10.6-features-2026-10-13

Conversation

@derek-flossman

@derek-flossman derek-flossman commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

Lands sixteen feature PRs into release/v0.10.6 in one merge, the same way as #635–#641. Merging them one at a time would need a fresh approval after each one.

Left out: #554 (privacy SDK rc.8) is held until the deployed contracts are confirmed (see its PR comments). #568 (vendor dataset tooling) will land separately: it excludes its folder from the repository gates and derives keys outside Krusty. #450 is not part of this batch.

What this contains

Each PR head is merged with --no-ff onto release/v0.10.6 at c1aab546. Fifteen of the sixteen are Moss's fork PRs, which have never run CI; this branch is their first CI run.

PR Issue What it does
#520 #464 UI Lab: inline the NFT fixture so the Gallery story stops timing out
#519 #442 Activity: pin the title bar so only the feed scrolls
#527 #524 UI Lab: an accessibility readout of the embedded wallet document
#523 #482 UI Lab: guard every main screen at the 320px side-panel minimum
#525 #486 Home: explain what the 24h change figures mean
#560 #468 Connect card names its action ("Connect account" / "Cancel"); the send review shows the network and full addresses
#573 #485 Activity CSV: new columns and a per-transfer summary
#521 #477 Send: quote the estimated fee on the Amount screen
#562 #563 Recovery discovers accounts funded only in native USDC
#570 #569 Recover and deploy a Braavos account owned by a Ledger key
#518 #441 Send: state the minimum a private send needs
#510 #271 Terminal: bound HTTP and RPC requests with one shared network boundary
#528 #434 Send: resolve Starknet names (name.stark) as recipients
#532 #465 Multisig deploy: quote the fee and address beside the approval controls
#533 #468 The remaining "Don't Make Me Think" review fixes
#541 #481 Activity: filter, sort and search, with a per-counterparty summary

Code that is only in this PR

Conflict resolutions (in merge commits; each keeps release's audit behaviour):

Fixes from review. Each has a test that fails without it.

Combination commits:

  • 69af3ef4, 515b8375, b163d186: splits to stay under the 500-line cap.
  • 77b4daaa: error copy for name resolution.
  • 4d774bf4: test fixture.
  • 28b4e4d0: knip.
  • c9eadb8a: UI Lab send fixture split.

Fixes from the second review round (Codex gpt-6-astra, high effort, on the fix commits):

Fixes from the independent audit (three reviewers split by area, plus Codex on the core diff):

Checks run locally (Linux, Bun 1.3.3)

Check Result
bun run check pass (4038e6f0)
bun run check:coverage pass: functions 95.34%, lines 95.77% (33c0eb7d)
bun run check:audit pass
bun run build pass
bun run test:e2e 21 of 21 (33c0eb7d)
UI Lab storybook tests 258 of 258 (4038e6f0)

Independent reviews: three first-round reviewers (one per group of PRs), and a second-round review of the six conflict resolutions, which found nothing actionable, and a Codex review of the fix commits (above). All copy is approved on the copy review page.

🤖 Generated with Claude Code

mossbot55 and others added 30 commits October 5, 2026 16:19
The Send amount step let the user compose a transfer without saying what it
would cost; the fee only appeared once the review was built, after the amount
had been committed and a preparation had run.

The amount screen now quotes the same figure the review will bind. A plain
public transfer shows the network estimate from the review's own estimator,
deployment-inclusive total included; a pool route states the pool's per-batch
charge in the same sentence the review's Cost box uses and invents no network
fee, because a deposit's calldata does not exist before its preparation. The
preview is debounced, token-guarded against a superseded estimate, and clears
when the step or the route moves on.

The audit #248 recipient-Enter guard encoded "no request from this step"; it now
encodes what that request must carry, which is the address on screen and never
the previous choice.

The UI Lab gains an amount-and-fee checkpoint and a shield pool-fee checkpoint,
with stories that assert the rendered line. No existing checkpoint showed the
amount screen with an amount entered, so the state could not be rendered or
guarded.
The amount step refused an unaffordable private send with "Not enough
private STRK for this amount plus the pool fee" and named no figure, so a
user could not tell what to change. The shared send-route resolver now
appends the minimum the amount needs.

- send-routes: shortfall() takes the needed amount and appends
  "You need at least <amount> <symbol>."; the pool-fee-only refusal names
  the fee minimum and its token.
- wallet-model networks: export STRK_TOKEN_DECIMALS and reuse it for the
  STRK entries instead of a literal 18.
- transfer-route: pass the asset's decimals into the resolver.
- ui-lab: a send checkpoint renders the refused unshield from a one-STRK
  private balance, and a story asserts both refusal sites carry the
  minimum.
A typed Starknet ID name (for example altaga.braavos.stark) is resolved to
the account address it points at instead of failing recipient validation.

- new pure @bramble/starknet/stark-name module: normalizeStarkName plus
  resolveStarkName over the library's Starknet ID naming contract, with a
  discriminated result (resolved / not-found / unsupported-network /
  unreachable) and transport failover; an unregistered name is the naming
  contract's own zero answer, so it never resolves to 0x0.
- background wallet.resolveStarkName (privileged by default), readStarkNameInput
  parser, response type and guard, sidepanel client method.
- Send recipient step: a valid name enables Continue and is resolved before it
  is committed; only the resolved address is reviewed and the typed name stays
  as its label, with resolving/not-found/unreachable notes under the field.
- focused tests for the resolver, the background handler and the UI path, plus
  UI Lab checkpoints for the resolved and not-found states.
Address the review findings: the note must not let an unbreakable name widen
the panel (overflow-wrap: anywhere), the detached/edit-during-flight commit
guard gains tests, and the multi-label calldata is pinned.

- .send-recipient-note wraps with overflow-wrap: anywhere; a 40+40 character
  name previously set the stage's min-content width and pushed the panel to
  710px on a 400px surface, now it wraps and the panel stays inside its width.
- transfer-compose-ui: drop a resolution whose field was edited since the read
  started, not only one whose form detached; both guarded branches now have
  tests.
- resolver test pins the four-felt calldata for altaga.braavos.stark.
Address derek-flossman's review of #528.

A bare word is no longer a name: normalizeStarkName requires the .stark suffix
instead of adding it, so an ordinary word can never become a lookup. A typed
text that names a listed contact or account commits that row and is never
resolved.

The resolution is bound to a network: the wallet.resolveStarkName request and
response both carry the networkId the read was made on, the panel names the
network it shows, and a reply read on another network is dropped rather than
committed.

The recipient is labelled with the canonical name the wallet resolved, never the
typed spelling. The review shows the full address with "Resolved from Starknet
ID", and warns when the resolved recipient matches no row on the screen.

Row lookups move to recipient-rows.ts so the compose binding stays within the
file-length cap.

New tests cover the bare word, contact shadowing, the canonical label, the
network carried on the request and the response, and a reply read on another
network. The UI Lab gains a review checkpoint for a name-resolved recipient that
no row lists.
…view

Review asked for four changes on the composed-fee preview:

- Every path back to the amount step from the review (header Back, preparation
  failure, review failure) now goes through goTo("details") so the fee lines are
  re-quoted, rather than left blank or stale until the next input event.
- The preview stops estimating against a detached form: paint(), the target read
  and the debounced callback all bail when the form is no longer connected.
- The amount gate shares send-model's finitePositive, so a comma decimal such as
  1,5 is refused at the amount step the same way the submit path refuses it.
- The recipients Enter guard distinguishes the amount step's debounced preview
  from a submit-driven review by holding the injected schedule: nothing is
  reviewed until the debounce runs, and the one request then names the address
  on screen.

Test fixtures move to tests/support/compose-fee-fixtures.ts to keep the suite
under the file-length cap.
The submit path keeps its own inline strictly-positive-finite check, so the
comment no longer claims one shared gate across every Send surface.
The first-run recovery scan decided an undeployed account was worth
importing by probing ETH and STRK balances alone. An account holding only
native USDC — which a counterfactual address can receive before deployment
— read as empty and was skipped, so a real account was invisible to import
(the D3 "USDC-only" gap in the vendor-import dataset).

Add a discovery-only RECOVERY_PROBE_TOKENS set (the display defaults plus
native mainnet USDC) and probe it in the scan via recoveryProbeTokens().
The user's displayed token set is unchanged — USDC is an activity signal,
never shown by default — so detection widens without touching the portfolio.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Braavos lets a Ledger stark key own a Braavos account, deriving it at
Ready's `argentx` EIP-2645 path (confirmed from Braavos's shipped client
bundle and a live mainnet account). This wallet already scans that path for
the "Restore a Ready Ledger account" route, so discovery is a small
extension: also derive a Braavos candidate at each device key, at the
Braavos deploy class with salt = the owner public key. The same key yields
both a Ready Argent and a Braavos address; the on-chain probe reveals which
is live.

- Admit Braavos as a Ledger family: isLedgerAccountContractType (all three
  families) replaces the creatable guard at the Ledger message boundary.
  The live account validator already accepts the record, so no schema
  change; create, preview, and raw-key import still refuse Braavos.
- Default the Braavos deploy class on the Ledger derivation path only, since
  a device scan probes the vendor's current convention rather than naming a
  class the way an import does.
- Signing composes unchanged: compatibility is family-dispatched (the
  Braavos signer check reads chain) and the signer is keyless-dispatched
  (the Ledger blind-signs the stark hash).

A ground-truth test pins a real mainnet Braavos+Ledger account's address.
Deploying a funded-but-undeployed Braavos Ledger account follows next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The explicit Braavos deployment is key-source-agnostic: it routes the
passed signHash through the family's extended-deploy assembly (aux/params
first, deploy hash last, captured as the one broadcastable), so a keyless
device signer composes with no change. This pins it — a Ledger-owned
Braavos account deploys through the identical extended signature, the
deploy hash signed last and accepted by the node, with no keyless guard
rejecting the one family a device can own here but not create.

Braavos validates its deploy signature even on query versions, so unlike
OpenZeppelin and Argent (which estimate against a mock) a Ledger Braavos
deploy signs for real and prompts the device during estimate and submit
alike.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The terminal's HTTP and RPC clients enforced neither a deadline nor a
response-size limit. The shared request cache also stripped the caller's
signal before the deduplicated fetch, so a request that never settled
stayed in the in-flight map for the lifetime of the page, and a caller
that detached could not release it.

Route every request through one reusable boundary:

- Add protocols/support/bounded-network.ts: boundedFetch composes the
  caller's signal with an internal deadline (so a caller that detaches
  no longer strands the request), rejects an oversized response before
  it is parsed or stored, and passes a caller-cancellation through as an
  AbortError rather than mislabelling it as a timeout.
- data/core/client.ts: fetchJson and the HttpError body now use the
  boundary; error bodies are read up to a fixed prefix.
- data/core/cache.ts: give a shared request its own total budget that is
  independent of any one consumer, and derive each attempt's deadline
  from the remaining budget so retries cannot outlive it.
- protocols/support/provider.ts: the default RpcProvider is built on the
  bounded fetch with a wider RPC deadline and byte cap.
- Migrate the remaining direct fetch/RpcProvider call sites (avnu swap,
  private-swap client, vesu, privacy pool, NFT punk runtime).

Tests: tests/network-boundary.test.ts covers the internal deadline
firing on caller detach, the byte cap before parse and before error
storage, the total request budget, a null body, and a source guard that
every production fetch/RpcProvider goes through the boundary. Existing
fetch stubs now return real Response objects, as production does.

Refs #271
The Activity screen scrolled as one block, so the heading and its export and
refresh controls left the panel with the list; neither the screen's identity
nor its actions stayed reachable while reading history.

The title bar is the screen's first grid item, so it now sticks to the top of
the panel while the rows pass beneath it. A pseudo element bleeds the panel
fill past the text column and over the shell's top inset, so a row never shows
through the bar and the resting layout is unchanged: the bleed and its negative
inset cancel.

The UI Lab gains a long-feed checkpoint and a story that asserts the bar rests
below the panel edge and pins at zero on scroll. The existing fixture never
overflowed the surface, so there was no state in which the behavior could be
seen or guarded.

The account bar above the screen is shared shell chrome on every dock screen
and is left as it was; this change is scoped to the Activity screen the issue
names.
The Gallery checkpoint's `data-lab-ready` signal is set only after the wallet
frame's `load` event, and that frame's initial HTML loads one eager fixture
image. A slow response for that image pushed readiness past the 5s budget in
`assertCheckpointReady`, which is the intermittent timeout reported for the
story.

Inline the fixture artwork as a data URI (Vite `?inline`) so the checkpoint's
readiness path stays local and deterministic, and drop the now-unused public
asset.

Refs #464
Add a 320 x 760 surface preset to the review bench and a set of journey
stories that render each main screen at that width through the bench's own
surface control.

Each story fails if the panel scrolls horizontally or a named amount or
address is clipped, so the layout cannot regress below the width the project
documents (320px, and 256px under zoom).

Refs #482
The narrow-width guard checked only the first element matching a selector, so
a selector aimed at a list (`.token-amount`) would pass while a clipped later
row went unnoticed. Both helpers now walk every match with `querySelectorAll`
and report the offending index; an empty match is still an error. A negative
control confirms the old helper passed a clipped fifth row that the new helper
rejects.
…lations

The review frame is script-free (sandbox without allow-scripts), so its realm
never fires setTimeout and axe-core's run never settles there; the lab recorded
that as a manual limitation instead of a violations readout.

New runtime/a11y-scan.ts renders the live wallet document once more in an
ephemeral, offscreen, same-origin twin frame that is allowed to run the engine,
scans that twin's realm, and removes the twin in a finally. scanWalletFrame
serializes the frame's current DOM (checkpoint prepare hooks mutate it after
load) and sizes the twin to the frame's rendered box so contrast and layout
agree; scanWalletDocument is the underlying helper. axe-core 4.13.0 becomes a
ui-lab devDependency, imported with Vite's ?raw suffix.

The wallet frame document gains a title element, so the lab no longer raises the
serious document-title finding against its own document.

New Wallet UI Lab/Accessibility scan story drives the helper: a positive control
(image-alt on a crafted document), the real readout resolving, every flagged node
resolving inside the reviewed document, and no leftover twin with the review
frame's sandbox unchanged.
Split out of #533 so the approval and review copy can go through the
reviewed-group pass on its own (AGENTS.md, docs/COPY_STYLE.md). No
behaviour change beyond the copy and the review surface it describes.

- Connect card: the consent action reads "Connect account" and its escape
  hatch "Cancel", and the shared account's familiar display name leads,
  stacked over the address it shares.
- Send review: the confirm face names the reviewed network between the
  parties and Amount, and each party's complete address is one native
  <details> disclosure away, closed by default and keyboard reachable.
  The network label is read from the form's own data-send-network-label,
  so it stays correct after edits and repaint.
The deploy flow no longer hides the threshold and signer behind a separate
check step. The approval stage quotes the deployment itself, debounced, and
renders the predicted address and fee next to the fields it was computed from.
One button, always labelled "Deploy multisig", submits the reviewed
deployment.
While a deployment quote was in flight or had failed, the single action
still read "Deploy multisig" although pressing it only re-quoted. The
action now reads "Check fee and address" until a matching review is held
and switches to "Deploy multisig" only then. It is marked busy for the
whole quote so a fast second press cannot deploy a review the reader has
not seen, and the failure toast is reserved for an explicit submit rather
than every debounced auto-quote settle.
…or failed

The revision changes the deploy action's label and busy state, so both
affected states need to be directly inspectable. The multisig scenario
gains a checkpoint whose fixture quote never resolves and one whose quote
rejects, so the action's busy label and its failure label can be reviewed
on the real surface. Recount the lab's checkpoint total.
The approval and review copy (the connect card labels and the send review
summary) is split out into its own PR so it can go through the reviewed
copy group on its own. This PR keeps the remaining review fixes.

- New-wallet setup: present the sole derived account as the account being
  created (a static read-out with its check mark, no click-to-select)
  behind an outcome-named "Finish setup", gated on walletReady &&
  previewMatchesInput.
- Dock: each root shows its icon plus a visible caption (Home, Activity,
  Settings) in a labelled capsule, replacing the icon-only disc and the
  hover-only title. Accessible names, selected state, keyboard behaviour
  and badge placement are unchanged.
- Receive: one name, "Wallet card", for the card object and its copy
  action.
- UI Lab: give the activation review a realistic fee spelled the way
  production spells it, and represent a fee-estimate failure through the
  real preparation failure path as the activation-fee-failed checkpoint.
The Activity dock listed every record the account held with no way to narrow
it. Add a toolbar above the feed: a search box that matches a full or partial
address or a saved name, a type, status and token filter offering only the
values the account's own records carry, and a date/amount sort. Searching a
single counterparty adds a summary of what was sent and received there.

Filtering happens over the rendered rows, so no extra transport or storage is
involved; the view state is ephemeral, panel-local, and cleared when the
account or network scope changes or the vault locks. Sorting re-renders through
the status read, because the amount order drops the day headings.

Two UI Lab checkpoints render the toolbar and a collapsed search, since the lab
had no way to show this screen.
… token

Address the review requests on the Activity feed controls:

- Reconcile the stored filter against the options the current feed offers
  before the toolbar renders, so an axis whose value has left the feed (a
  pending transaction settling between paints) resets to "all" instead of
  emptying the feed behind a control that reads "All".
- Group the amount sort by token so a raw amount is never compared across
  tokens (1 ETH is not below 2 USDC); each token's rows order by amount.
- Repaint the sort locally from the data already on screen instead of reading
  the wallet status, so a failed status read can no longer leave the pressed
  control and the stored sort disagreeing.
The hero percentage and each token row's change are 24h price moves at the user's current balances, so sends and receives do not count and the row figures do not sum to the hero. One muted caption under the hero says this once, rather than repeating a label on every row, which the copy style discourages (say the subject once per screen). The caption renders only when a priced baseline exists.
Each token row now carries a quiet window tag beside its percentage, so
the row figure says it is a price move rather than restating the caption.
The hero line and its one caption move into a shared heroChange module
that both the account Home hero and the watched multisig hero render, so
the two surfaces cannot name the same figure two ways; the caption still
renders only when a priced baseline exists.

The caption gate test now drives a priced-but-unmeasured baseline (prices
present, no balance priced), so removing the gate fails it; the old empty
fixture also passed on base, so it did not guard the gate.
Replace accountId with the plain accountAddress (the network is already its
own column), rename hash to transactionHash, add an explorer URL, and render
the timestamp as 'YYYY-MM-DD HH:MM:SS' UTC with the zone named in the header
and one precision on every row. Rebuild a transfer row's summary from the
structured fields so it names the counterparty in full and reads with one
verb, keeping the recorded summary for every other row and for rows missing a
detail. Formula-injection escaping is unchanged.

The fee and fiat columns depend on issue 463 and are deferred; the
fee-as-transfer rows travel with them.
derek-flossman and others added 3 commits October 13, 2026 12:49
DOMParser never enters a template's content, so the sanitiser left an
`on*` handler inside one untouched. The twin's srcdoc parser does attach
a declarative shadow root from `<template shadowrootmode>`, so that
handler ran in the script-enabled, same-origin twin. Templates are now
removed with the other active elements. The embedded-code story gains a
shadow-root control case; it fails against the previous sanitiser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#521 routed the two async failure paths (review and private preparation)
through `goTo("details")`, which is bound to the form mounted when the
submit started. A re-render while a preparation ran (it can take about a
minute) replaced that form, so the failure repainted the detached copy
and the live screen stayed on "reviewing" with its button locked and no
reason shown. The failures now repaint the active binding and re-quote
its Amount-screen fee, keeping #521's intent. Both paths have a
regression test that fails against the previous code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The per-counterparty summary added every visible in or out row to its
totals whatever its status, so a reverted or rejected send read as
"Sent". One accepted 1 STRK beside a reverted 5 and a rejected 7 showed
"Sent 13 STRK". The summary row now carries the status, and only an
accepted or pre-confirmed transfer adds to the totals; every visible row
is still counted. No user-facing wording changes. The new test fails
against the previous code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A non-STRK pool fee is bounded at twice an independent price for the STRK
charge, and the relay's quote, known only at review, may be lower. The
Amount screen now says "Pool fee: up to X" for it. STRK's exact fee is
unchanged. Copy approved by Derek, 2026-10-13.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@derek-flossman
derek-flossman merged commit fecb613 into release/v0.10.6 Oct 13, 2026
9 checks passed
@derek-flossman
derek-flossman deleted the batch/v0.10.6-features-2026-10-13 branch October 13, 2026 14:02
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.

3 participants