Commit Graph
121 Commits
Author SHA1 Message Date
clawbot 90a9d5597f test: the e2e suite waits for each save before it closes the popup (closes #446)
check / check (push) Failing after 2s
e2e / e2e-chrome (push) Failing after 2s
e2e / e2e-firefox (push) Failing after 2s
The Settings round trip switched the theme and the network and closed
the popup at once. A close before the change handler's save lands loses
the switch, and the suite then ran on Sepolia.

tests/e2e/run.js now has one helper that polls a field of the stored
record until it holds the expected value, in place of the wait that
only read viewStack. Each Settings switch and spam-filter toggle waits
for its save, the recovery-phrase reopen waits for its saved view, and
reopenPopup() waits until the view it expects to reopen on is the saved
one. README.md no longer lists #446 among the open reports of the
Chrome suite failing under load.

Model: opus-5-5
2026-10-05 05:09:04 +02:00
clawbot 6a86b726d2 fix: a change made while an earlier save is running is stored (closes #448)
check / check (push) Failing after 3s
e2e / e2e-chrome (push) Failing after 2s
e2e / e2e-firefox (push) Failing after 3s
saveStateOnce() took its baseline from the page's state after the write, so
a change made while the save waited on storage counted as already stored and
the save queued after it wrote nothing. A setting changed during the read was
lost, and so was a wallet added, a site revoked or an endpoint changed during
the write. The save now copies the page's fields when it starts, writes from
that copy, and keeps the copy as the baseline, so anything changed after the
copy is still a difference for the next save.

Model: opus-5-5
2026-10-05 04:43:06 +02:00
clawbot 18bdafd130 fix: open no approval window for a site-connection prompt already answered (closes #287)
check / check (push) Failing after 3s
e2e / e2e-chrome (push) Failing after 3s
e2e / e2e-firefox (push) Failing after 3s
When a site-connection prompt was decided before the toolbar popup raised
for it had loaded, that popup was torn down, chrome.action.openPopup()
rejected, and the background opened its fallback window for the answered
approval and only then removed it. In the Chrome end-to-end suite the next
test could take that window for its own prompt and lose it under its wait.
openApprovalWindow() now returns before creating a window when the approval
is no longer pending.

The blocklist test clicked its self-closing Reject with a plain click; it
now clicks it as the other site Reject does, with the click witnessed.
README.md and the e2e workflow comment no longer name this issue as what
keeps e2e-chrome from being a required check.

Model: opus-5-5
2026-10-05 04:09:07 +02:00
clawbot 8c8caafe33 harden: lost-password confirmation refuses empty input and ignores invisible characters (closes #336)
check / check (push) Failing after 3s
e2e / e2e-chrome (push) Failing after 2s
e2e / e2e-firefox (push) Failing after 2s
A wallet named only with spaces compared equal to an empty field, so
typing nothing would have deleted it, and a zero-width space in a name
made the name impossible to type back.

An empty typed confirmation is now refused whatever the name is. The
characters src/shared/symbolSpoof.js already defines as painting nothing
are removed from both sides before comparing. A name that shows nothing
at all is shown on the delete screens as "Wallet N", so it can still be
typed back.

Model: opus-5-5
2026-10-05 03:26:05 +02:00
clawbot 6c885a0c05 harden: a holders_count that is not plain digits is unknown, not read in part (closes #251)
check / check (push) Failing after 2s
e2e / e2e-chrome (push) Failing after 2s
e2e / e2e-firefox (push) Failing after 2s
parseHoldersCount used parseInt, which reads "1,000" as 1, "0x10" as 0 and
"1e3" as 1: a reported low count, which hides the token in the transaction
history and the send-screen token selector. It now accepts only a whole
number of zero or more, or a string of digits alone, no larger than
Number.MAX_SAFE_INTEGER, and returns null for anything else. The balance
list's holders !== null check did nothing, since null >= 1000 is already
false, and is dropped. README.md and docs/README.md say how each filter
treats an unknown count and that the token screen then leaves out its
Holders row; README.md lists src/shared/holders.js.

Model: opus-5-5
2026-10-05 02:59:15 +02:00
clawbot f86740ce69 test: the popup boot's stand-in for filterTransactions returns the real shape (closes #429)
check / check (push) Failing after 2s
e2e / e2e-chrome (push) Failing after 2s
e2e / e2e-firefox (push) Failing after 2s
The stand-in in tests/support/popupBoot.js returned a bare list, while the
real filterTransactions returns { transactions, newFraudContracts }. Home,
AddressDetail and AddressToken read both fields, so every test boot onto
one of them threw inside its transaction loading, logged loadHomeTxs failed
or loadTransactions failed, and never ran the rest of that code. The
stand-in now returns the real shape, and tests/persistedFieldContract.test.js
boots onto each of the three views and asserts neither message is logged.

Model: opus-5-5
2026-10-05 01:59:16 +02:00
clawbot e3790c5da4 chore: the native token's label follows the network (closes #372)
check / check (push) Failing after 2s
e2e / e2e-chrome (push) Failing after 1s
e2e / e2e-firefox (push) Failing after 1s
networks.js gives each network a nativeCurrency (ETH, SepoliaETH) and nothing
read it: every screen wrote ETH. The wallet's balances and the Send and
confirmation screens now use the active network's. A transaction's figures use
the network its chain id names, through nativeCurrencyByChainId(): the
approval value and fee, the wait, success and error screens, history entries,
the detail screen and the fee-limit refusal, so a site switching networks
cannot make one read as another network's coin. The "ETH" that selectedToken
and txInfo.token hold is the native token's id and is unchanged. A token
reporting any network's nativeCurrency is a spoof, and the detail screen calls
an entry a token transfer when it has a token contract.

Model: opus-5-5
2026-10-05 01:26:04 +02:00
clawbot 2b97aae04a fix: an open popup moves to the recovery screen when its profile becomes unreadable (closes #373)
check / check (push) Failing after 2s
e2e / e2e-chrome (push) Failing after 3s
e2e / e2e-firefox (push) Failing after 2s
A popup already open when the stored profile became unreadable stayed on the
last good profile until reopened. Every save already runs the check loadState()
runs at open; a save refused by it now stops the ten-second refresh, runs the
leave cleanup of the current screen, and raises the recovery screen. From then
on showView() shows nothing else in that popup, so a transaction wait or a later
save cannot take the user off it or clear an export or a typed confirmation.
That is held in memory, never as the saved current view, so a popup opened
after the record is erased elsewhere opens normally. Any other failed save
keeps the "NOT SAVED" banner. The popup test harness now honours
clearInterval().

Model: opus-5-5
2026-10-05 00:09:06 +02:00
clawbot 6127fd9432 fix: a swap deadline later than a date can hold is stated in words (closes #437)
check / check (push) Failing after 2s
e2e / e2e-chrome (push) Failing after 2s
e2e / e2e-firefox (push) Failing after 2s
decode() rendered the Deadline line with toISOString(), which throws on a
date past 275760-09-13, the last a JavaScript date can hold. A later
deadline, such as the uint256 maximum, therefore left the whole swap
undecoded, with nothing saying why. That line now reads
"After 275760-09-13 00:00:00 (no deadline in practice)".

Model: opus-5-5
2026-10-04 23:43:06 +02:00
clawbot f4a51e1679 fix: the swap decoder reads a V2 already-paid zero and a zero balance check as the router does (closes #415)
check / check (push) Failing after 2s
e2e / e2e-chrome (push) Failing after 1s
e2e / e2e-firefox (push) Failing after 1s
A V2 exact-in amountIn of zero is the router's ALREADY_PAID marker: an
earlier step sent the tokens to the pair and the swap spends all of them.
Amount showed 0.0000 for it; it now reads "Whatever an earlier step sent
to the pair (V2 already paid)", in the style of the V4 open delta line.

A BALANCE_CHECK_ERC20 passes whenever the balance is at least minBalance,
so a zero one guarantees nothing. It now sets the output side only when
that side holds no minimum at the point the check is reached; a nonzero
one sets the output side as before.

README's Display Consistency text and TODO.md are updated to match.

Model: opus-5-5
2026-10-04 23:09:05 +02:00
clawbot 375998beaf harden: show a personal message's hex and its text in byte order, hidden characters marked (closes #403)
check / check (push) Failing after 2s
e2e / e2e-chrome (push) Failing after 2s
e2e / e2e-firefox (push) Failing after 2s
The signature screen showed only the text a personal message decodes
to, with bidirectional, right-to-left and zero-width characters acting
on it, so a site could make the message read differently from the
bytes that are signed, and a message that was not hex was decoded into
NUL characters. The screen now shows the hex as "Raw data" alongside
the text, lays the text out left to right in byte order, and shows each
control character, line and paragraph separator, and character that
paints nothing (the set src/shared/symbolSpoof.js already strips) as a
U+XXXX mark. A message is hex when getBytes, which signing uses, reads
it; one that is not cannot be signed, so it is shown as plain text with
"Sign" disabled.

Model: opus-5-5
2026-10-04 22:09:07 +02:00
clawbot 3b713809c8 harden: a token scale above 80 decimal places is refused as unknown (closes #350)
check / check (push) Failing after 2s
e2e / e2e-chrome (push) Failing after 3s
e2e / e2e-firefox (push) Failing after 2s
toDecimals() accepted any uint8 scale, but formatUnits() and parseUnits()
refuse more than 80 decimal places. A token reporting 81 to 255 made the
formatter throw, and the catch in the swap decoder and in the ERC-20 decoder
turned that into an undecoded approval screen with nothing saying why.

MAX_DECIMALS is now 80, the formatter's own limit, so such a scale is
treated exactly like an unknown one: both approval paths show the base-unit
amount with the scale stated as unknown. The balance list, the history list
and the Send screen use the same check.

Model: opus-5-5
2026-10-04 21:43:11 +02:00
clawbot 8ac2c87c2c harden: debug mode logs only a request's origin and JSON-RPC method (closes #410)
check / check (push) Failing after 2s
e2e / e2e-chrome (push) Failing after 1s
e2e / e2e-firefox (push) Failing after 2s
With debug mode on, debugFetch logged every request's full URL and body,
so an RPC endpoint with an API key in its path or query string printed
that key to the console on every request. It now logs the HTTP method,
the URL's origin and, for a JSON-RPC body, the method name. The balance
refresh and token lookup log the RPC endpoint by its origin too. Failed
RPC calls print ethers' short message, since its full message for an
HTTP error carries the request URL. A failed endpoint check in settings
prints the endpoint's origin, since fetch's error for a URL with a user
name and password carries the whole URL. The README's DEBUG Mode Policy
says what debug mode logs.

Model: opus-5-5
2026-10-04 21:09:04 +02:00
clawbot d1751beb32 harden: one connection and one signature prompt per site at a time (closes #405)
check / check (push) Failing after 1s
e2e / e2e-chrome (push) Failing after 2s
e2e / e2e-firefox (push) Failing after 2s
Each eth_requestAccounts or personal_sign call opened another approval
window, so a page calling in a loop could cover the screen with identical
prompts. While a site's connection or signature prompt is unanswered, a
further request of that kind from the same site is now refused with
EIP-1193 -32002 and opens no window; all signing methods count as one
kind. A connection prompt whose toolbar popup closed before it connected,
and which the toolbar popup no longer opens, is shown again by the site's
next request instead of refusing the site until the address changes.

Model: opus-5-5
2026-10-04 19:43:11 +02:00
clawbot de3f7a9a11 harden: ignore a nonce the page supplies with eth_sendTransaction (closes #404)
check / check (push) Failing after 3s
e2e / e2e-chrome (push) Failing after 2s
e2e / e2e-firefox (push) Failing after 2s
A site could fix the nonce of the transaction the user was asked to
sign: the same nonce as a pending transaction, at a higher fee,
replaces it, and a nonce above the account's next one leaves the new
transaction stuck behind a gap. `nonce` is no longer one of the fields
taken from the request, so the transaction always gets the account's
next nonce from the network, and that is the nonce the approval screen
shows and the popup signs.

Model: opus-5-5
2026-10-04 18:43:04 +02:00
clawbot 1144fdb71b harden: key remembered site permissions by full origin (closes #402)
check / check (push) Failing after 2s
e2e / e2e-chrome (push) Failing after 2s
e2e / e2e-firefox (push) Failing after 1s
allowedSites and deniedSites held the bare hostname, so a grant to
https://dapp.example also authorised http://dapp.example and every port
on that host, and the connection, transaction and signature prompts
named only the hostname. Both lists now store and match the full origin
(scheme://host[:port]), the key the connections approved without
Remember already used. The prompts, the Settings site lists and
AUTISTMASK_REMOVE_SITE use the origin too. Entries saved by hostname
are not migrated (pre-1.0): they match no site.

Model: opus-5-5
2026-10-04 18:09:04 +02:00
clawbot f24b5bca19 harden: take a request's origin from the frame that sent it (closes #407)
check / check (push) Failing after 2s
e2e / e2e-chrome (push) Failing after 3s
e2e / e2e-firefox (push) Failing after 2s
Where the browser gives no sender.origin (Firefox before 126), the
background credited a page's request to the tab's page, so a frame from
another site counted as the site embedding it, and with no tab it used
an origin the page wrote into the message. It now uses the origin of
sender.url, the frame that sent the message, and refuses the request
with code 4100 when the browser gives neither. The content script no
longer writes an origin into the message.

Model: opus-5-5
2026-10-04 17:26:05 +02:00
clawbot 9f0e88e963 test: load the popup's libraries once per test file, not on every boot (closes #428)
check / check (push) Failing after 2s
e2e / e2e-chrome (push) Failing after 2s
e2e / e2e-firefox (push) Failing after 2s
Every popup boot in the tests resets jest's module registry so that src/
loads fresh, and that also reloaded ethers, libsodium-wrappers-sumo, qrcode
and ethereum-blockies-base64 each time. tests/support/popupBoot.js now loads
those four once per test file and registers them once with jest.doMock(),
which jest.resetModules() keeps, so every boot gets the same copies. No test
or assertion changed. make test takes 8-13s on the shared build host, down
from 17-25s, measured in alternating runs before and after the change.

Model: opus-5-5
2026-10-04 16:59:12 +02:00
clawbot 45f11ee920 fix: say an unknown-scale balance the same way on Send and on confirm (closes #377)
check / check (push) Failing after 2s
e2e / e2e-chrome (push) Failing after 2s
e2e / e2e-firefox (push) Failing after 1s
When two addresses' explorer reports disagree on a token's decimals, the
Send screen showed the stored figure while the confirmation screen said
the balance was unknown. One function in send.js now gives both the
balance and scale, so both read `unknown (SYMBOL)`.

The confirmation screen's fee-unknown message names its cause: for an
unknown scale it says the wallet does not know the token's decimal
places and the transaction cannot be sent, instead of asking for a
retry that cannot help. Other causes keep the old sentence.

Model: opus-5-5
2026-10-04 15:59:15 +02:00
clawbot a68f30c480 fix: decode Uniswap V2 exact-out swaps, input amount shown as a maximum (closes #283)
check / check (push) Failing after 2s
e2e / e2e-chrome (push) Failing after 2s
e2e / e2e-firefox (push) Failing after 2s
decode() had no arm for Universal Router command 0x09, so the approval
screen showed no token or amount for it. It now takes the path's first
token and amountInMax as the input side, the last token and amountOut as
the output side. With such a step, the Amount figure reads "Up to
<amount>" whichever step set it, on the approval, wait, success and error
screens, unless it reads "Unlimited", as an unbounded PERMIT2_PERMIT does,
or "All available (V4 open delta)". In every swap, UNWRAP_WETH makes
Token Out ETH only when the output side is WETH, on mainnet or Sepolia, or
no step set it; otherwise the output keeps its own token and figure.
decodeV2SwapExactOut() loses its eslint-disable comment.

Model: opus-5-5
2026-10-04 15:09:09 +02:00
clawbot 43c236451f chore: run jest in three worker processes (closes #426)
check / check (push) Failing after 1s
e2e / e2e-chrome (push) Failing after 1s
e2e / e2e-firefox (push) Failing after 1s
The test and test:verbose scripts ran jest with one worker per CPU core,
about 47 processes and 7-8 GiB per run on the shared 48-core build host.
They now pass --maxWorkers=3. On that host the suite takes 23-29s, inside
the unchanged 30-second cap in script/test but not by much: one test
file, tests/persistedFieldContract.test.js, takes most of it. One or two
workers went past the cap, so this departs from the issue's two-process
limit. make check, the pre-commit hook and script/cibuild all reach jest
through these scripts; the timings in the script/test and Dockerfile
comments are updated to match.

Model: opus-5-5
2026-10-04 14:26:03 +02:00
clawbot 1247c24c4d fix: keep the dApp approval error containers to their reserved height (closes #297)
check / check (push) Failing after 1s
e2e / e2e-chrome (push) Failing after 1s
e2e / e2e-firefox (push) Failing after 2s
#approve-tx-error and #approve-sign-error reserved 20px, but their border
and padding took 10px of it, so a one-line error grew them to 26px and
pushed the Confirm/Sign buttons down 6px. They now reserve 30px, the same
4px to spare over one line that the other password error containers have.

A new end-to-end test shows each of the six password error containers on
its own screen at the popup viewport, empty and then with an error, and
fails if one changes height or the element below it moves.

Model: opus-5-5
2026-10-04 12:58:26 +02:00
clawbot bec20aa2bb fix: say a contract creation has no recipient instead of a blank line (closes #250)
check / check (push) Failing after 2s
e2e / e2e-chrome (push) Failing after 2s
e2e / e2e-firefox (push) Failing after 2s
A transaction with no `to` showed a blank address, with a colour dot whose
colour was `undefined`, on the wait, success and error screens, the
transaction detail view and the history rows on Home, AddressDetail and
AddressToken. The approval screen showed "(contract creation)".

All of them now say "This transaction creates a new contract. It has no
recipient." The three history lists draw a row's counterparty lines through
one helper in helpers.js. A transaction with a real `to` is unchanged. The
new test drives each screen and list both ways.

Model: opus-5-5
2026-10-04 12:24:39 +02:00
clawbot 467b849a13 fix: show balances and fees below 0.000001 as nonzero on the send screens (closes #343)
check / check (push) Failing after 2s
e2e / e2e-chrome (push) Failing after 1m46s
e2e / e2e-firefox (push) Successful in 37s
The stored ETH and token balances and the send-confirm screen's fee were each
cut to six decimal places, and a token holding cut to zero was dropped, so a
value below 0.000001 read as zero. Balances are now stored exactly, whatever
decimals a token declares, and every nonzero token holding is kept; the balance
check reads a token balance to its first 18 places. The balance lists, the
send-screen token selector, the address total and the remove-address warning
leave out a holding below 0.000001 themselves, through isBelowOneMillionth().
The send and send-confirm screens' balances, reserve and insufficient-balance
messages go through truncateAmountNeverZero(). The send-confirm and approval
screens both render the fee through formatFee(), which prices the exact fee in
USD.

Model: opus-5-5
2026-10-04 11:07:37 +02:00
clawbot 5bf8b5ff1f fix: keep the flash line to its one line at any message length (closes #252)
check / check (push) Failing after 2s
e2e / e2e-chrome (push) Failing after 2s
e2e / e2e-firefox (push) Failing after 2s
The flash line reserves one line, so a message that wrapped pushed the
screen below it down. #flash-msg no longer wraps: text too long for it is
cut with an ellipsis, and showFlash() puts the whole message in its title.
Every message is also reworded to at most 50 characters so none is cut,
and the add-token screens flash a fixed line for any error other than the
two lookup messages, logging the detail.

A new end-to-end test writes a message several lines long into the line
and fails if the line or the screen below it moves.

Model: opus-5-5
2026-10-04 10:22:51 +02:00
clawbot 4b62e31e80 fix: refuse an unsupported method with EIP-1193 code 4200 (closes #279)
check / check (push) Failing after 2s
e2e / e2e-chrome (push) Failing after 2s
e2e / e2e-firefox (push) Failing after 2s
handleRpc() answered a method it does not implement with
"Unsupported method: <method>" and no code, so a site probing for an
optional method could not tell "not implemented" from "the call failed"
and fall back. It now carries code 4200, which EIP-1193 defines for this
case; the message is unchanged. The provider already passes any code
through to the page.

The new test hands the real background's reply to the provider and
checks the page sees 4200.

Model: opus-5-5
2026-10-04 09:24:41 +02:00
clawbot 49a7da87e8 harden: list and end site connections made without Remember in Settings (closes #406)
check / check (push) Successful in 2m25s
e2e / e2e-chrome (push) Successful in 3m15s
e2e / e2e-firefox (push) Successful in 2m21s
A site allowed without "Remember" lives only in the background's
in-memory connectedSites map. Settings never listed it, and
AUTISTMASK_REMOVE_SITE, sent on every remove, did nothing, so the user
could not end such a connection.

Settings now asks the background for those sites and lists them under
Connected Sites. Removing a site from Allowed Sites or Connected Sites
drops its remembered entry under every address and sends
AUTISTMASK_REMOVE_SITE with the hostname; the background deletes every
matching connectedSites entry and sends accountsChanged with an empty
list to the site's tabs. Only the extension's own pages may send either
message.

Model: opus-5-5
2026-10-04 06:41:39 +02:00
clawbot 5f54fcbb24 harden: end a site's unremembered connection when its address or wallet is removed (closes #245)
check / check (push) Successful in 1m34s
e2e / e2e-chrome (push) Successful in 2m1s
e2e / e2e-firefox (push) Successful in 36s
A site connected without "Remember" lives only in the background's
in-memory connectedSites map. Removing an address or deleting a wallet
dropped the remembered permissions but never told the background; the
entry went only as a side effect of the accountsChanged broadcast, which
empties the whole map when the active address changes.

dropSitePermissions(), shared by both removal paths, now sends
AUTISTMASK_ADDRESSES_REMOVED with the removed addresses, and the
background deletes their entries. Only the extension's own pages may
send it.

Model: opus-5-5
2026-10-04 04:58:38 +02:00
clawbot 00d6193ee7 docs: fix stale zero claim, list what decoded approval amount lines read (closes #369)
check / check (push) Successful in 2m26s
e2e / e2e-chrome (push) Successful in 3m40s
e2e / e2e-firefox (push) Successful in 3m8s
The README said a genuine zero always renders `0.0000`, beside a
`Min. received` example, though a zero minimum there now reads
`None (no minimum guaranteed)`. It now says the amount rule renders a
zero as `0.0000` and that two swap lines say a zero in words instead:
`Min. received` for a zero minimum, and `Amount` when it shows a V4
exact-in `amountIn` of zero.

A new list says what the decoded ERC-20 and swap amount lines on the
transaction approval screen can read, including that a zero
`BALANCE_CHECK_ERC20` `minBalance` now reads the no-minimum wording.
The token permission warning on the signature screen is left to the
SignApproval section.

Model: opus-5-5
2026-10-04 04:41:38 +02:00
clawbot 6c70a82de8 harden: warn for token-permission typed data and show the primary type ethers signs (closes #400)
check / check (push) Successful in 3m10s
e2e / e2e-chrome (push) Successful in 4m3s
e2e / e2e-firefox (push) Successful in 3m28s
The typed-data screen listed a Permit or Permit2 signature as plain key/value lines, like a sign-in message. It now shows a red warning naming the spender and each token and amount, read only from the fields the signed type declares (a Permit's token is the domain's verifyingContract); anything those fields do not give reads Unknown.

The screen printed the page's primaryType, but ethers signs the type it derives from types. It now shows that type and refuses typed data whose stated type is missing or differs: Sign disabled, checked again where signing starts.

Deviation: the warning names no deadline or expiry; see the issue.
Judgement call: DAI's older permit and Permit2's batch and witness transfer types are recognised too.

Model: opus-5-5
2026-10-04 02:24:50 +02:00
clawbot add11e57de security: announce a fresh EIP-6963 provider UUID per page load (closes #398)
check / check (push) Successful in 1m4s
e2e / e2e-chrome (push) Successful in 1m47s
e2e / e2e-firefox (push) Successful in 33s
The provider UUID was generated once, kept in extension storage and
announced to every page on every load, so any site could read it as a
stable identifier for the install across sites and browser restarts.

inpage.js now generates one UUID per page load, shared by every
announcement in that load, and stores nothing. The eip6963Uuid storage
key and the AUTISTMASK_PROVIDER_UUID content-script message are removed.
The key was never part of the versioned autistmask profile, so the state
schema is untouched. Two inpage.js message listeners lose the leftover
name onUuid.

The test posts each load the same stored UUID the way the old content
script did, and asserts that no load announces it and that two loads
announce different UUIDs.

Model: opus-5-5
2026-10-03 16:26:39 +02:00
clawbot 598de3ff1a fix: re-enable Confirm Delete after a delete, so a second one needs no reopen (closes #335)
check / check (push) Successful in 3m12s
e2e / e2e-chrome (push) Successful in 4m40s
e2e / e2e-firefox (push) Successful in 3m27s
The password route disabled its Confirm Delete button before the decrypt
and never re-enabled it on success, so a second delete in the same popup
session found a dead button until the popup was closed and reopened. The
lost-password route re-enabled its own button in its leave hook, so the
two screens on the one screen behaved differently.

Both routes now reset the button through the shared finishDelete(), the
one path they both take, and the lost-password leave hook no longer
handles it separately. Tests drive a password-route delete and a second
delete in the same session; they fail against the prior head, where the
button stays disabled after the first delete.

Model: opus-4-8
2026-09-22 01:28:02 +02:00
clawbot ae61792aee chore: keep the internal view id out of the release banner (closes #375)
e2e / e2e-chrome (push) Failing after 1s
e2e / e2e-firefox (push) Failing after 1s
check / check (push) Successful in 1m10s
The debug/testnet banner appended the active view's internal id, so the
user saw text like "[TESTNET] (approve-tx)" — developer vocabulary, and on
the approval screen it sat directly above the carefully worded line stating
what is being authorized. The view id is now gated on the compile-time
DEBUG constant instead of isDebug(), so it survives only in a debug build.
A testnet or the runtime debug toggle still raises the banner, but without
the view id, which is what a release build shows.

Model: opus-4-8
2026-09-22 00:45:06 +02:00
clawbot a1f082d686 docs: a release procedure from a green main to tagged, packaged artifacts (closes #387)
check / check (push) Failing after 1s
e2e / e2e-chrome (push) Failing after 1s
e2e / e2e-firefox (push) Failing after 1s
Add docs/RELEASE.md, linked from README.md's Release Artifacts section, giving
the release procedure as a numbered list: confirm main is green in CI, confirm
the one version in package.json and the two manifests matches the intended tag,
make package from a clean checkout, verify SHA256SUMS, create the annotated tag
vX.Y.Z, then distribute per browser. Each step names who performs it, marks the
owner-only ones, and states the check that it worked. Every repo command cited
(make setup, make check, make package) exists on next; tagging and verification
use standard git and coreutils, and the CRX pack line is README's own.

The per-browser distribution step is written as pending the owner's choice on
issue 386, with the Firefox and Chrome options named but none presented as
settled. Docs only: no code or test changes.

Model: opus-4-8
2026-09-21 22:00:18 +02:00
clawbot 33fa25adca harden: bound the total network fee by gasLimit × fee, on both send paths (closes #399)
check / check (push) Failing after 1s
e2e / e2e-chrome (push) Failing after 1s
e2e / e2e-firefox (push) Failing after 1s
The two per-field ceilings in approvalVerify.js were checked independently,
but the fee a validator is paid is gasLimit × fee per gas: a gas limit and a
fee each under their own ceiling still multiply to thousands of ETH, which a
gas-consuming contract really collects. assertWithinCeilings now also bounds
that product against MAX_TOTAL_FEE (1 ETH), so both callers — populating the
dApp transaction and verifying the signed artifact — refuse it with a full
sentence naming the fee and the limit.

The wallet's own send in confirmTx.js pinned no fee fields, so ethers filled
them from the node with no bound; it now populates the transaction and runs the
same check before signing, showing the same error in the confirmation screen's
reserved errors box so nothing on screen moves.

Model: opus-4-8
2026-09-21 21:45:34 +02:00
clawbot 2fe6447625 fix: name a tracked or explorer-known token instead of "Unknown token" (closes #323)
check / check (push) Failing after 0s
e2e / e2e-firefox (push) Failing after 0s
e2e / e2e-chrome (push) Failing after 1m27s
The approval and transaction-status screens read a token's scale from the
bundled list, the tokens the user tracks, then the block explorer, but read
its symbol from the bundled list alone. A token the user added by hand was
scaled correctly yet labelled "Unknown token", and a non-bundled ERC-20 was
carried onto the wait screen as ETH.

resolveTokenSymbol() now draws the symbol through the same sources and
precedence as the scale, and the ERC-20 and Uniswap swap lines both use it. A
tracked or explorer-reported name stays subject to the spoof rule, so it
cannot claim a bundled or native ticker.

Folds in #354.

Model: opus-4-8
Co-authored-by: clawbot <clawbot@noreply.example.org>
2026-09-21 21:28:06 +02:00
clawbot 2da790fbe9 fix: say a second wallet's password is separate when one is chosen (closes #374)
check / check (push) Failing after 1s
e2e / e2e-chrome (push) Failing after 1s
e2e / e2e-firefox (push) Failing after 1s
The add-wallet screen offered only "Choose a password" while each wallet
keeps its own encrypted secret, so a second wallet silently accepted a
password different from the first with nothing marking it as separate. A
note now appears on that screen when the profile already holds a wallet,
saying each wallet has its own password and this one need not match any
already in use. It is shown only then — the first wallet has no other
password to differ from — and is decided on screen entry, so it does not
move the password fields. It promises no recovery or reset, staying
consistent with the no-password-reset design.

Model: opus-4-8
2026-09-21 21:11:09 +02:00
clawbot 9ac7df0128 harden: keep the test recovery phrase out of release bundles, match committed keys by content (closes #351)
check / check (push) Failing after 1s
e2e / e2e-chrome (push) Failing after 1s
e2e / e2e-firefox (push) Failing after 1s
The 12-word BIP-39 test phrase survived in every release bundle as dead
text: module.exports keeps DEBUG_MNEMONIC live even though wallet.js's only
use of it folds away in a release build, so it could not be tree-shaken.
Putting the value itself behind the __BUILD_DEBUG__ define makes esbuild fold
it to null, so no distributed bundle carries it. script/verify-build now
fails a release build if the phrase appears in any emitted file, so the fold
cannot silently regress; test-verify-build covers both the release failure
and the debug allowance.

tests/extensionId.test.js now scans the content of every tracked file for a
PEM private-key header instead of matching filename extensions alone, so a
key committed under an unexpected name is caught.

Model: opus-4-8
2026-09-21 18:29:39 +02:00
clawbot 99292b9188 fix: honour a transaction response only for a transaction approval (closes #262)
e2e / e2e-chrome (push) Failing after 1s
e2e / e2e-firefox (push) Failing after 1s
check / check (push) Successful in 1m42s
The liveness fix this issue describes — settle 4001 on release when the window
a retry would use is gone — already landed with
#271. This completes the rest.

AUTISTMASK_TX_RESPONSE now refuses any approval that is not a transaction
approval, so a reject can no longer retire a sign or connection approval, and a
signed artifact never runs the broadcast path against one — which before only
failed closed by throwing deeper in. Tests pin the site-connection port's
approve, reject and disconnect paths against a transaction approval broadcasting
behind them: each is declined and the dApp still receives its broadcast result.

Model: opus-4-8
2026-09-21 09:54:40 +02:00
clawbot 1197d2171b fix: give every address a row of its own, so none wraps or is shortened (closes #380) (#381)
check / check (push) Successful in 56s
e2e / e2e-chrome (push) Successful in 1m51s
e2e / e2e-firefox (push) Successful in 40s
2026-08-30 05:25:00 +02:00
clawbot a098bb0c32 fix: floor malformed allowedSites, fraudContracts and selectedToken entries (closes #362)
check / check (push) Successful in 42s
e2e / e2e-chrome (push) Successful in 1m45s
e2e / e2e-firefox (push) Successful in 31s
A stored allowedSites whose value was not a list rendered a working popup and then made every subsequent save fail silently, so the user operated a wallet that persisted nothing -- worse than a blank popup, which is at least visibly broken. fraudContracts and selectedToken had the same shape: a container floored by truthiness or not at all, while its entries were dereferenced. Entries are now floored as well as containers, following the idiom #311 established, and a failed save raises a persistent banner instead of vanishing into a swallowed rejection.

The per-field justifications that used to live in a hand-written header are replaced by a contract test that drives each field's hostile and falsy values through a real popup boot, so a claim about a field answers to the code rather than to prose. Its guarantee is stated narrowly and deliberately: no structural dereference on the code paths a wholly-corrupted profile takes, which is not every path a stored record takes. The paths it does not drive are named where the claim is made, and are tracked in #379.
2026-08-23 23:06:17 +02:00
clawbot 45500e66cf fix: declare and ship toolbar icons, so neither browser renders a puzzle piece (closes #371)
check / check (push) Successful in 33s
e2e / e2e-chrome (push) Successful in 1m45s
e2e / e2e-firefox (push) Successful in 32s
Neither manifest declared any icons, so both browsers showed a generic puzzle-piece -- the first thing seen on every browser start, and how a user tells a real extension from a look-alike. Both manifests now declare 16/32/48/128, and the PNGs ship inside each browser archive rather than being left at dist/ root, which is the trap that made a naive zip incomplete before.

build.js reads which icons to copy from each manifest's own icons block, so the manifest is the single source of truth and a declared-but-absent size fails the build rather than shipping a dangling reference; the packager's reference-resolver covers them independently. Manifest values are constrained before being joined into a path. The artwork is original, generated from geometry rather than traced or fetched.
2026-08-23 21:26:14 +02:00
clawbot 1b52aa1723 fix: store an absent explorer decimals as unknown instead of fabricating 18 (closes #349)
check / check (push) Successful in 34s
e2e / e2e-chrome (push) Successful in 1m45s
e2e / e2e-firefox (push) Successful in 30s
parseInt(decimals || "18") ran before writing stored tokenBalances[].decimals, so an explorer reporting no decimals produced a fabricated 18 indistinguishable from a real one at read time. That defeated the resolve-or-refuse guarantees of #306 and #340: their refusal paths were intact but never fired, because the guess was laundered upstream of them.

An absent scale is now stored as unknown, and a holding whose scale nothing knows carries a null balance -- unknown, never zero -- with six reader sites saying so rather than printing 0.0000. The Send screen resolves the display scale rather than reading the stored one, so a bundled token whose explorer row omits decimals still sends; when the scale cannot be resolved the stored quantity is withdrawn too, so the user is told the balance is unknown rather than only that the fee failed.

Existing fabricated 18s cannot be told apart retroactively and are replaced wholesale on the next balance refresh. An explorer-sourced scale stays trusted -- only fabrication is removed; the reasoning is recorded on the issue.
2026-08-23 21:19:04 +02:00
clawbot 75a5fa9891 harden: gate swap amounts on presence, not truthiness, so a figure always matches its token (closes #359)
check / check (push) Successful in 32s
e2e / e2e-chrome (push) Successful in 1m45s
e2e / e2e-firefox (push) Successful in 29s
Token and amount were gated on truthiness and gated independently. An address is never falsy once set but 0n is, so a hop supplying a zero amount fixed the token permanently while leaving the amount open, and the next hop's figure was rendered against the first hop's token at that token's scale -- 0.5 WETH shown as 500000000000.0000 USDT. Nine defective gates are now presence checks, and fourteen address gates were converted defensively so a sixth instance of this class cannot grow.

Zero is not one thing. Established from v4-periphery: OPEN_DELTA is 0 and V4Router substitutes the full credit unconditionally, so a zero V4 exact-in amount means "swap the whole balance" and now reads "All available (V4 open delta)" rather than 0.0000, which would assert the exact inverse. amountOutMinimum gets no such mapping and universal-router special-cases only CONTRACT_BALANCE, so a zero minimum and a zero V2/V3 amount stay literal -- a zero floor reads "None (no minimum guaranteed)".

closes #364
2026-08-23 20:45:58 +02:00
clawbot c9ebac822a harden: state an undetermined swap input token as undetermined, not as ETH (closes #357)
check / check (push) Successful in 33s
e2e / e2e-chrome (push) Successful in 1m45s
e2e / e2e-firefox (push) Successful in 31s
tokenInfo(null) yielded "ETH (native)", so a swap whose input token the calldata never named was asserted to the user as ETH. The determination established for the output side in #353 applies unchanged: Currency is a value type over address, so native ETH arrives as the truthy zero-address string and WRAP_ETH sets it explicitly -- null can only mean undetermined.

The rule now lives in tokenInfo() itself rather than at each call site, so the redundant output-side guard added by #356 is removed; both sides read one UNNAMED_CURRENCY constant and cannot drift. Verified by execution that a genuine native-ETH input still renders as ETH, including via WRAP_ETH and a V4 zero-address PoolKey, and that the real mainnet output-side fixture is unchanged.
2026-08-23 20:23:05 +02:00
clawbot ad6aa7b20d fix: version stored state, validate its shape, and give a corrupt blob a way out (closes #311)
check / check (push) Successful in 33s
e2e / e2e-chrome (push) Successful in 1m46s
e2e / e2e-firefox (push) Successful in 34s
Stored state had no version and no structural validation, so a corrupt blob produced a completely blank popup with no message and no recovery control, and made every dApp RPC call from every page answer a generic -32603. There was no reset or wipe control anywhere in the UI.

saveState() now stamps a schema version and loadState() validates the shape. A version it does not understand, or a wallets array it cannot parse, lands on a recovery screen that names the problem, offers the stored record verbatim for export, and offers a destructive reset behind a typed confirmation. Unversioned but valid state -- which every existing install has -- migrates in place and keeps working; it is never shown a wipe prompt. A dApp call against unusable state answers -32007, which EIP-1474 leaves unassigned, rather than -32603. networkById() refuses an unknown id loudly instead of returning mainnet, and networkId is validated so a corrupt value cannot be used as an object key.

Fields the gate does not refuse are floored by type, container and entries both: a malformed trackedTokens or tokenBalances entry is dropped rather than dereferenced. Verified by an independent sweep of 1152 corrupt blobs producing no blank popup, with the same harness showing 9 blanks against the previous revision.
2026-08-23 20:04:00 +02:00
clawbot 28a527295a harden: state an undetermined V4 output token as undetermined, not as ETH (closes #353)
check / check (push) Successful in 34s
e2e / e2e-chrome (push) Successful in 1m47s
e2e / e2e-firefox (push) Successful in 31s
A V4 swap could set minOutput while leaving outputToken null, and tokenInfo(null) returned ETH at 18 decimals, so the approval screen named an asset the calldata never stated. Established from the V4 encoding that this is wrong rather than protocol semantics: Currency is a user-defined value type over address, so native ETH arrives as the truthy zero-address string and is already mapped to ETH, and an UNWRAP_WETH output is caught earlier — both genuine native-ETH routes are covered without null, so null can only mean no sub-action yielded a currency. A genuine native-ETH V4 swap still renders as ETH, pinned by test against the real mainnet fixture.

The same gate also let a later step overwrite minOutput while an earlier hop's outputToken stayed on screen, showing a figure against the wrong token. Fixed together as the same two lines and the same defect.
2026-08-23 18:13:20 +02:00
clawbot bd0a626e7b harden: stop the background reading the shared state singleton, and enforce it at build time (closes #324)
check / check (push) Successful in 33s
e2e / e2e-chrome (push) Successful in 1m45s
e2e / e2e-firefox (push) Successful in 31s
Five defects, one of which destroyed every wallet, came from src/background reading and writing the module-level state singleton the MV3 worker never populates, which silently served DEFAULT_STATE. Each point fix created the next defect. The background now has its own per-call getState() and a queued read-modify-write updateState(); the singleton is unreachable from it, and an unpopulated read throws instead of serving defaults.

The prohibition is enforced by the build, not by review: build.js asserts over esbuild's own metafile that no forbidden module is an input of a background bundle, so every specifier syntax esbuild resolves is covered, and both halves of the table are checked for rot -- a stale key, a stale module, an empty list, or an unlisted entry point under src/background/ all fail the build. The ESLint rule remains as fast local feedback and reads the same shared table. Known bounds are documented where the table lives.

Also closes #320: getProvider() now requires a validated network id, so a cold worker no longer prepares a non-mainnet dApp transaction for mainnet and gets refused by the wallet's own verifier. backgroundRefresh() no longer mutates address objects across a network round trip, the broadcast path takes its endpoint and chain id from one snapshot, and eight test storage stubs now structured-clone on get as the real chrome.storage.local does.

closes #320
2026-08-23 17:57:30 +02:00
clawbot 36bc6bee0e fix: always name a swap's output token, by address when no symbol is known (closes #346)
check / check (push) Successful in 30s
e2e / e2e-chrome (push) Successful in 1m45s
e2e / e2e-firefox (push) Successful in 29s
The Token Out line was pushed only when a symbol was known, so a swap to a token absent from the bundled list showed a Min. received figure with no statement of which token was being received. It now falls back to the address, mirroring the Token In precedent in the same file. Composes with the unknown-scale refusal from #340: the address names the token while the figure declares its scale unknown. Native ETH out, ETH out via UNWRAP_WETH and bundled tokens render exactly as before.
2026-08-23 16:31:02 +02:00
clawbot 43784cab3f harden: resolve or refuse the swap token scale instead of guessing 18 (closes #340)
check / check (push) Successful in 29s
e2e / e2e-chrome (push) Successful in 1m47s
e2e / e2e-firefox (push) Successful in 31s
tokenInfo() returned decimals 18 for any token absent from the bundled list, so the swap approval line rendered a real 1000.00 of a 6-decimal token as 0.000000000001. The scale is now resolved from what the wallet already holds (bundled list, tracked tokens, explorer-reported decimals) or refused outright, matching the rule set for the ERC-20 path in #306. A refusal reuses unknownDecimalsAmount(), so it reads as "base units (decimals unknown)" with no decimal point and no symbol, and the same string propagates to rawValue so no downstream screen can render a figure the approval screen refused. No new network call on the approval path. Verified green on all three CI contexts: check, e2e-chrome, e2e-firefox.
2026-08-23 16:20:22 +02:00