Commit Graph

11 Commits

Author SHA1 Message Date
34c1b00710 refactor: one shared extension-API module, and drive the dApp flows on Firefox (closes #153)
All checks were successful
check / check (push) Successful in 28s
Every call site that touched `browser.*` or `chrome.*` now goes through
`src/shared/browserApi.js`, the only file in the tree that names either.
It exposes lazily-resolved namespace handles for events and synchronous
methods, and promise-returning wrappers for everything that is
callback-shaped on Chrome. Callers await; `runtime.lastError` is gone,
folded into the rejection the wrapper produces on the Chrome path.

The Firefox suite gains the four dApp round trips the issue's definition
of done asks for — `eth_requestAccounts`, `personal_sign`,
`eth_sendTransaction`, and a closed approval window rejecting with
EIP-1193 4001 — driven through the real content script, background page
and approval windows. `--network none` was thought to rule that out
because it leaves no `http://` origin to inject into; loopback survives
it, so the page and a JSON-RPC node are served from 127.0.0.1 inside the
container and the run still reaches nothing but itself.

That harness refutes the premise it was built to verify. On Firefox
153.0.3, `browser.*` honours a trailing Chrome-style callback and does
populate `runtime.lastError`, both measured directly, and all four flows
pass against the unconverted code. So this is a uniformity and coverage
change, not a repair of a broken target; the PR records the measurement
in full.

One real defect is fixed on the way past: the window id written back
into a pending approval after `windows.create()` was unguarded, so an
approval settled during the open — an address switch will do it —
dereferenced a deleted entry.
2026-08-12 11:55:53 +00:00
9dcd875dd4 fix: carry EIP-1193 error codes through to the page (closes #274)
All checks were successful
check / check (push) Successful in 28s
The provider rebuilt every rejection as a bare Error carrying only a message,
so a dApp checking err.code === 4001 saw undefined and could not tell a user's
deliberate refusal from a failure. Well-behaved sites therefore showed an error
or retried instead of accepting the refusal. The code was produced correctly
and did cross the extension boundary; it was lost in the last hop.

Rejections now reach the page as a ProviderRpcError carrying code, and data
where present. The code is passed through verbatim rather than matched against
a whitelist, so a code added upstream later needs no change here. An error that
genuinely has no code stays a plain Error with no code property at all, rather
than advertising code: undefined -- 'code' in err is what a careful dApp asks.

Messages are unchanged for every path, verified byte-for-byte against the
previous provider across every background error shape.

The end-to-end assertion that printed the observed code now requires it.
2026-08-12 13:47:33 +02:00
d5595c0151 test: drive the EIP-1193 dApp approval round trips in the browser (closes #183)
All checks were successful
check / check (push) Successful in 29s
The dApp signing path was the largest unverified surface in the milestone: the
only place where the content script, the inpage provider, the background worker
and the popup all have to work together, with unit tests covering each side in
isolation and none covering the seam.

A page served by the harness speaks EIP-1193 to the real provider -- asserted by
EIP-6963 object identity, not by shape -- and eth_requestAccounts, personal_sign,
eth_signTypedData_v4 and eth_sendTransaction are each driven through to approval
and to rejection.

Every signature is recovered and compared to the approved address; the broadcast
transaction is parsed from the bytes captured at eth_sendRawTransaction and
checked for signer, recipient, value, calldata and chain. A signature that
merely came back would pass against a wrong key, a wrong message or a wrong
chain, so each assertion was demonstrated failing against a variant that is
wrong in exactly one of those ways.

The password is asserted absent from every message crossing the extension
boundary, which gives #157's fix a permanent floor rather than a one-time
review.

Two defects this surfaced are tracked separately: EIP-1193 error codes never
reach the page (#274), and approving a site connection races the popup teardown
(#275). Neither is asserted as correct here. A real dApp with real funds against
mainnet remains an uncovered human pass and is documented as such.
2026-08-12 13:23:57 +02:00
52c7c1b060 test: containerized Firefox end-to-end harness (closes #184)
All checks were successful
check / check (push) Successful in 34s
Drives the real popup in a real Firefox with dist/firefox/ installed as an
unpacked MV2 temporary add-on via geckodriver. make test-e2e-firefox, outside
make check like the Chrome suite. Zero npm dependencies: plain fetch and
child_process against geckodriver's HTTP API. Base image, Firefox tarball and
geckodriver are each pinned by digest and verified at build time.

Error capture reads the privileged console service through Marionette's chrome
context, not WebDriver BiDi. BiDi delivers nothing at all for extension pages,
so a BiDi-based harness would observe zero events and report success -- the
vacuous-check shape this repo has shipped twice. Both the driver and the README
say so where someone would be tempted to simplify.

Demonstrated to discriminate: a background page that throws at the top of the
file, a missing import, and an async throw where every UI assertion still
passes each fail the run.

Three limits are measured and documented rather than papered over: capture is
poll-based so an error is attributed to a step, not a moment; the console ring
buffer holds 250 messages and evicts the oldest, measured against a clean-run
peak of 4; and the drained window ends roughly 1.5s after the last step, with
observed jitter rather than a hard boundary. Content-script capture is marked
unverified because --network none leaves no page to inject into, and that same
choice inverts coverage of network-dependent code.
2026-08-12 12:20:14 +02:00
18b47cd579 test: close the empty-batch hole in the e2e unstubbed-request guard (closes #187)
Some checks failed
check / check (push) Has been cancelled
The guard that reports unrecognised POST bodies used batch.every(), which is
vacuously true on an empty array, so a POST with body [] was answered 200 []
and escaped the one mechanism whose job is to make unrecognised outbound
traffic fail the suite rather than pass silently. Unreachable in practice
today, which is exactly the qualifier that stops being true later.

The comment explaining the guard also described a mechanism that does not
exist: playwright-core decodes a binary body lossily rather than returning
null, so such a body reaches the JSON parse as mojibake and is reported by the
catch, while only an absent or empty body decodes to null and is reported by
the type guard. Both are reported; the comment now describes the two real
routes.
2026-08-12 11:50:38 +02:00
5af89a1b63 test: drive ConfirmTx in the e2e suite, gate assertion included (closes #238)
All checks were successful
check / check (push) Successful in 38s
ConfirmTx -- the screen that decides what gets signed -- had no automated
coverage of its own behaviour. The arithmetic underneath was well tested; the
wiring was not, so a mutant making the spend gate read the displayed fee
estimate instead of the reserve would have reintroduced the #154 overspend with
the suite still green.

Nine end-to-end tests now drive it for both the native and ERC-20 paths,
covering the pending, funded, over-balance and estimate-failed states, and
asserting that the gate reads the reserve rather than the estimate. Swapping the
two makes the suite fail. The view height is asserted constant across every
state transition rather than merely printed.

Reaching the screen needs a funded balance and a gas estimate, so the route
interception gains fixtures for both. Testing the estimate-failed state means
provoking the console error the code is supposed to emit, which the harness
otherwise fails a run on; an expectation mechanism consumes exactly one matching
record, is scoped to the declaring test, and fails that test if nothing matched,
so it cannot mask an unrelated error.
2026-08-12 11:35:20 +02:00
c6a1f97247 fix: explain a rejected dust threshold instead of silently snapping back (closes #233)
All checks were successful
check / check (push) Successful in 33s
The dust-threshold field was the only validated input in Settings that rejected
without saying anything: the value silently changed back to the stored one with
no explanation. It now flashes "Please enter a whole number of gwei, zero or
greater." alongside the existing resync, matching the idiom the RPC URL field
already uses.

The parse moves to its own module and accepts plain decimal digits only, zero
or greater. Hex and exponent notation are refused rather than accepted: Number()
reads "0x10" as 16 and "1e3" as 1000, neither of which the previous parseInt
produced, and storing a number the user did not type is the same silent
substitution this change exists to remove.

The message must fit one line of the reserved flash area -- a wrapped message
pushes the settings view down, which the No Layout Shift policy forbids. That is
pinned by an end-to-end test measuring the rendered line height and the position
of the elements below it, in a single round trip because the flash clears after
two seconds.
2026-08-12 11:29:09 +02:00
937f699fb1 feat: remove an address from an HD wallet, behind a confirmation (closes #162)
All checks were successful
check / check (push) Successful in 36s
Address rows on Home gain an [x] control, on wallets that derive addresses from
an extended key and hold more than one, opening a DeleteAddress confirmation
screen.

Removal cannot destroy anything: the key material stays. Derivation indices are
not renumbered, so the next "+" derives the next unused index rather than
resurrecting the removed one. The confirmation states the real route back --
delete the whole wallet in Settings, which asks for the password and destroys
the stored recovery phrase, then import it again -- and notes that the scan
which follows only finds addresses with on-chain activity. The copy varies by
wallet type, since an xprv wallet has no recovery phrase.

Removing an address that holds a balance is allowed, with a warning naming no
figure; the funds are at the address on-chain and stay there either way.
Selection and active address move only when the removed address was the one
selected, and site permissions are dropped for it alone.

The state transition shares its address comparison, permission cleanup and
active-changed broadcast with the wallet-level removal.
2026-08-12 11:16:29 +02:00
bf1dbec87c fix: run libsodium on WebAssembly under the extension CSP (closes #182)
All checks were successful
check / check (push) Successful in 26s
2026-08-12 10:30:15 +02:00
3e5d6323ce feat: password-gated recovery phrase display for HD wallets (closes #161)
Some checks failed
check / check (push) Has been cancelled
2026-08-11 15:25:17 +02:00
e8ad8325c8 test: containerized Chrome end-to-end harness that drives the real popup (closes #181)
Some checks failed
check / check (push) Has been cancelled
Runs the real popup in a pinned containerized Chrome and fails on any uncaught
page error or console.error. Also fixes the two defects it caught: the missing
showView import in addToken.js and the missing addressDotHtml import in
transactionDetail.js.

closes #150
closes #151
2026-08-10 15:49:32 +02:00