Commit Graph
101 Commits
Author SHA1 Message Date
sneak 7635af0107 chore: run jest in three worker processes (closes #426)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
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 11:48:13 +00:00
clawbot 1247c24c4d fix: keep the dApp approval error containers to their reserved height (closes #297)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
#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) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
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) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
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) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
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) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
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
clawbot 769f6a5289 release: produce a versioned per-browser artifact and pin the Chrome extension id (closes #310)
check / check (push) Successful in 32s
e2e / e2e-chrome (push) Successful in 1m44s
e2e / e2e-firefox (push) Successful in 32s
manifest/chrome.json now carries a fixed public key, so the extension id and the chrome.storage.local partition holding the wallet stay stable across checkout moves and re-clones instead of being derived from the absolute path. A release entrypoint produces a self-contained versioned artifact per browser, including the files that sit at dist/ root outside both browser directories. One version source of truth, enforced: the build fails naming the culprit when the two manifests and package.json disagree, and BUILD_COMMIT now marks a dirty tree as dirty. Firefox ships an unsigned XPI; the README states that release Firefox and ESR refuse it, that Developer Edition or Unbranded is required, and that Remove is irreversible except from the recovery phrase, which is asserted by test.
2026-08-23 16:13:22 +02:00
clawbot 669c443bf9 fix: never render a nonzero approval amount as zero (closes #322)
check / check (push) Successful in 31s
e2e / e2e-chrome (push) Successful in 1m12s
e2e / e2e-firefox (push) Successful in 22s
An amount below the 4-decimal display floor now extends to its first significant digit on the approval and confirmation screens, instead of stating a real transfer, allowance or swap Min. received as 0.0000. The rule had been implemented three times; all three now share src/shared/amountDisplay.js, which holds the plain truncation and the floored variant side by side. History and balance lists keep the unfloored rule, pinned by test.
2026-08-23 15:43:04 +02:00
clawbot 12b0c4d1c6 build: remove dist/ when a release build fails (closes #333)
check / check (push) Successful in 30s
e2e / e2e-chrome (push) Has been cancelled
e2e / e2e-firefox (push) Has been cancelled
A failed release build no longer leaves a complete, loadable debug bundle in dist/ whose every wallet uses the publicly committed test recovery phrase. Each step of the release build runs through script/discard-dist-on-failure, which removes dist/ on failure, says on stderr that it did and why, and returns the step's own status. build-debug is deliberately unwrapped. script/verify-build is untouched.
2026-08-23 15:39:04 +02:00
clawbot c36d8b6ddf docs: state the enforced dist/ verification scope precisely (closes #331)
check / check (push) Successful in 29s
e2e / e2e-chrome (push) Successful in 1m11s
e2e / e2e-firefox (push) Successful in 22s
README, the script synopsis, its header paragraph and the check_dist_tree comment now all say the same thing: regular files and symlinks under dist/ are covered; fifos, sockets, device nodes and empty directories are not, and why. No behaviour change — the walk is untouched.
2026-08-23 15:33:58 +02:00
clawbot cef6aaab11 fix: merge state per field instead of overwriting the whole blob (closes #304)
check / check (push) Successful in 33s
e2e / e2e-chrome (push) Successful in 1m13s
e2e / e2e-firefox (push) Successful in 24s
saveState() is now a read-modify-write that merges only the fields this page
changed, diffed against a deep-cloned per-page baseline. wallets, allowedSites,
deniedSites and networkEndpoints merge structurally by identity, so membership
comes from fresh storage except for this page's own adds and deletes.

Fixes a second extension page silently deleting a wallet, the background balance
refresh clobbering a concurrent add or resurrecting a delete, and a stale page
resurrecting a revoked site permission.

Colliding wallet identities keep both records and log rather than silently
dropping an encryptedSecret. Concurrent writers of the same leaf remain
last-writer-wins by design.
2026-08-20 16:41:19 +02:00
clawbot 20e911059a fix: give a wallet whose password is lost a way out, and say the password cannot be reset (closes #312)
check / check (push) Successful in 31s
e2e / e2e-chrome (push) Successful in 1m10s
e2e / e2e-firefox (push) Successful in 23s
A user who forgot their password but held their recovery phrase was permanently
locked out: deletion was password-gated and re-importing the phrase was refused
as a duplicate. Their only escape was destroying extension storage through
browser internals, taking every other wallet with it.

DeleteWallet gains an "I have lost my password" route that destroys the stored
secret after the wallet's name is typed back. No password gate was added:
requiring one to discard a secret protects nothing, since an attacker who wants
destruction can uninstall the extension, and the only person it stops is the
legitimate user who lost it. The screen is excluded from RESTORABLE_VIEWS and
registers an onViewLeave cleanup.

Deletion was chosen over re-import because a key wallet is duplicate-checked by
address rather than xpub, so an xpub-only relaxation would leave that user
still wedged; because re-import makes the user retype their recovery phrase
into a live popup merely to change a password; and because it reaches no end
state that delete-then-import plus scanForAddresses() does not. The attacker
argument did not decide it — re-import clears the "no worse than the phrase
alone" bar.

All three AddWallet password hints now state the password cannot be recovered
or reset and name that mode's only backup, the xprv mode correctly claiming no
recovery phrase. deleteAddress.js no longer tells the user that deleting a
wallet asks for a password, which this change made false.

The typed confirmation collapses internal whitespace on both sides: a wallet
renamed with two spaces displays with one, so the string a user could see and
type could never match, making the confirmation untypable on the one screen
whose purpose is un-wedging a stuck user.

Measured, not reasoned, after review found the first reserve twice too large
and pushing the Import button below the fold: #btn-add-wallet-confirm bottom
628.13 -> 580.13 at 360x600, scrollHeight 636 -> 600, hint box 48px identical
across all three tabs and on re-entry. make check 40 suites / 828 tests,
test-e2e 55/55, test-e2e-firefox 8/8.
2026-08-20 15:12:28 +02:00
clawbot aea999db85 build: make verify-build take an explicit expectation and a build receipt (closes #309)
check / check (push) Successful in 30s
e2e / e2e-chrome (push) Successful in 1m10s
e2e / e2e-firefox (push) Successful in 22s
verify-build read its expectation from AUTISTMASK_DEBUG in its own environment
and the Makefile invoked it bare, so an operator with that variable exported
who ran the release target got an INSECURE debug build — every wallet it
creates uses the publicly committed test phrase — verified green, exit 0. It
also had no provenance: a 26-byte file containing the right marker string
passed, the content script and manifest.json were never inspected, and an
entire hand-written dist/ passed.

--expect release|debug and --receipt PATH are now both required, with no
defaults and nothing read from the environment. build.js records every file it
emits with its sha256 and writes the receipt; the Makefile mktemps it outside
the repo per invocation with a trap, and build.js refuses a receipt path inside
dist/. Verification runs three passes in a load-bearing order — receipt shape,
full dist/ walk, then per-file bytes — so an unwalkable subtree cannot make
files look absent. dist/constants-bundles.txt, which was an unsigned trust root
living inside the tree it vouched for, is gone.

What this proves is bounded and stated as such: dist/ is byte-for-byte the
output of the build.js run that just finished, within one make build
invocation. It proves nothing about the honesty of the source tree or build.js,
and nothing to anyone handed a dist/ from elsewhere — that is signing, #310.
The standalone make verify-build target is removed because its only input would
be dist/ itself, i.e. the artifact vouching for itself.

Verified: make check green, test-verify-build 39 cases (was 18), test-e2e 55/55
and test-e2e-firefox 8/8 with make build running uncached inside both images.
All four original bypasses now exit 1. Mutations: digests disabled fails
exactly 4 cases, dropping the dist/ walk fails exactly 8, restoring the ambient
fallback fails exactly 1.
2026-08-20 14:24:55 +02:00
clawbot c8c2af0c6b harden: escape every interpolation into popup innerHTML, and add default-src to both manifests (closes #307)
check / check (push) Successful in 28s
e2e / e2e-chrome (push) Successful in 1m10s
e2e / e2e-firefox (push) Successful in 22s
A hostile ERC-20's symbol() reached an innerHTML string unescaped, and neither
manifest declared default-src, so an attacker deploying a token with 1,000+
holders and airdropping one unit could render a full-viewport cross-origin
iframe over the wallet's own UI, on screens where the user types their
password.

escapeHtml is now a pure string replace over & < > " ' — the old version
round-tripped through textContent, which escapes neither quote, while already
being used inside data-copy="...". All 19 files in src/popup/views/ were
audited: beyond the reported symbol site, the explorer-supplied directionLabel
in all three transaction lists, wallet.name, addr.ensName, the blockie data:
URIs and two ad-hoc quote-only escapes were also unescaped. Explorer URLs now
go through one helper that percent-encodes the path segment.

Both manifests add default-src 'self', frame-src 'none', form-action 'none' and
base-uri 'none'. Three loosenings are pinned in tests/manifest.test.js and
justified in README.md: style-src 'unsafe-inline' (39 static style attributes;
Firefox implements neither style-src-attr nor 'unsafe-hashes'), img-src data:
(blockies), connect-src https: http: (user-configurable RPC).

Note frame-src 'none' blocks a frame loading, not the element existing, so the
zero-iframe assertion is a claim about the escaping alone; the test asserts the
element count and the literal rendered text separately, taking the count before
any click an overlay could intercept.

Verified: make check 39 suites / 811 tests, test-e2e 55/55 including the
WebAssembly-under-CSP assertion, test-e2e-firefox 8/8, zero CSP violations
asserted rather than merely unobserved. Reverting only balanceLine's
interpolation reproduces the attack as 2 iframes on the address screen.
2026-08-20 13:47:28 +02:00
clawbot 59f68b8859 fix: answer eth_chainId and net_version from loaded state (closes #317)
check / check (push) Successful in 29s
e2e / e2e-chrome (push) Successful in 1m9s
e2e / e2e-firefox (push) Successful in 21s
Both methods answered from the module-level state singleton, which the MV3
worker never populates, so a cold worker reported mainnet 0x1 to a page whose
user was on Sepolia.

They now answer from getState(), the per-call detached storage read the other
read handlers already use. An earlier revision of this fix used loadState()
instead and was rejected in review: it replaces the whole singleton, and these
methods are page-callable with no connection gate (inpage.js sends eth_chainId
on every page load), so a load landing inside backgroundRefresh()'s network
round trip detached the address objects being mutated in place — persisting
pre-refresh balances while still stamping lastBalanceRefresh, letting a polling
page suppress background refreshes indefinitely.

The test stub now structured-clones on get and set, as chrome.storage.local
does. The aliasing stub it replaces was independently measured to hide this
defect class entirely: with the aliasing get restored and the defective handler
in place, the suite passes 794/794.

Verified failing first three ways: a plain singleton read fails the three
cold-worker cases; the rejected loadState() revision fails only the new
mid-refresh case ("1.5" expected, "0" received); moving saveState() ahead of
refreshBalances() fails that case and only it.
2026-08-20 13:11:48 +02:00
clawbot 50078b3566 fix: resolve approval-screen token decimals, and refuse to format an unknown scale (closes #306)
check / check (push) Successful in 34s
e2e / e2e-chrome (push) Successful in 1m13s
e2e / e2e-firefox (push) Successful in 28s
decodeCalldata consulted only the 512-entry bundled list and defaulted to 18
decimals, so a transfer of 5,000 units of a 6-decimal token rendered
"Amount 0.0000" and the user confirmed a drain reading zero. The same
understatement applied to approve, where an unbounded allowance also rendered
0.0000.

Decimals now resolve from the bundled list, then trackedTokens, then the
address's explorer-reported entry, with uint8 validation and a refusal when
sources for one contract disagree. When no source knows the scale, no
formatUnits call is reached at all: the line renders raw base units with an
explicit "decimals unknown" warning, and the same string reaches
pendingTxDetails.amount so the status screens carry no formatted figure either.

Verified failing first two independent ways: restoring the old
`token ? token.decimals : 18` fails 6 of 15 new tests with the unknown case
reporting "0.0000"; making the resolver return 18 rather than null on the
unknown path fails a different 6, spanning resolver and render levels.
2026-08-20 12:57:38 +02:00
clawbot 6350aad591 fix: gate the chain switch and remember endpoints per network (closes #308)
check / check (push) Successful in 29s
e2e / e2e-chrome (push) Successful in 1m10s
e2e / e2e-firefox (push) Successful in 22s
wallet_switchEthereumChain was answered for any origin at all, with no
connection check and no prompt, so any page could move the active chain and
clear the [TESTNET] banner under a user who believed they were on Sepolia. It
now takes the same allowedSites check the signing methods take and returns 4100
for an unconnected origin.

The handler also awaits loadState() before it reads or moves the network. The
MV3 worker populates nothing at module scope, so a worker revived by the page's
own message held DEFAULT_STATE: the same-chain check compared against the wrong
network, and the save wrote empty wallets, empty allowedSites and default
endpoints over the user's stored profile, destroying every wallet in the
extension. Also fixes #316.

Endpoints are now remembered per network in a persisted networkEndpoints map,
so a user running a local or private node no longer loses that url permanently
to a public endpoint on every switch. A stored map must be an actual object; a
primitive previously survived the load and made every switch fall back to the
public default with no self-healing.

Verified failing first: dropping only the added loadState() fails exactly the
two cold-worker cases; reverting only the type guard fails exactly the string
and number cases. Reverting both source files to next gives 12 failed / 751
passed.
2026-08-20 12:42:01 +02:00
clawbot 2f80a9bdb4 fix: sign the ERC-20 amount the confirmation screen displayed (closes #305)
check / check (push) Successful in 27s
e2e / e2e-chrome (push) Successful in 1m10s
e2e / e2e-firefox (push) Successful in 22s
The send screen was built from the indexer's decimals while the transfer was
encoded from the contract's decimals() read at signing time, with nothing
comparing them. A token whose scales disagree moved 10^12 times the approved
amount.

The displayed scale is now carried on pendingTx from the same tokenBalances
entry the amount, balance and symbol were rendered from, and both encode sites
use it. transferAmount.js refuses rather than falling back when the two scales
disagree or either is unusable.

Adds the first end-to-end coverage of the popup's own Send -> ConfirmTx ->
Sign & Send path; #btn-confirm-send had never been clicked by any test.
2026-08-20 12:31:28 +02:00
clawbot ff3387d8cf feat: vendor and censor the phishing blocklist at build time (closes #219)
check / check (push) Successful in 27s
e2e / e2e-chrome (push) Successful in 48s
e2e / e2e-firefox (push) Successful in 21s
2026-08-17 10:05:56 +02:00
clawbot 8fcdd8a053 fix: settle a site approval on the port that carries its teardown (closes #275)
check / check (push) Successful in 38s
e2e / e2e-chrome (push) Successful in 48s
e2e / e2e-firefox (push) Successful in 23s
2026-08-17 09:34:07 +02:00
clawbot 7690fe6429 fix: answer the page when a background handler throws (closes #280)
check / check (push) Successful in 36s
e2e / e2e-chrome (push) Successful in 48s
e2e / e2e-firefox (push) Successful in 22s
2026-08-17 09:16:21 +02:00
clawbot a60c4a616a test: drive the Settings screen in a browser and guard every popup element id (closes #229)
check / check (push) Has been cancelled
e2e / e2e-chrome (push) Has been cancelled
e2e / e2e-firefox (push) Has been cancelled
2026-08-17 09:14:59 +02:00
clawbot 47bf38644d build: add ESLint to script/lint and containerize linting (closes #152)
check / check (push) Successful in 39s
e2e / e2e-chrome (push) Successful in 48s
e2e / e2e-firefox (push) Successful in 24s
2026-08-17 09:10:03 +02:00
clawbot 4b7a678a9b refactor: one shared extension-API module, and drive the dApp flows on Firefox (closes #153)
check / check (push) Successful in 27s
e2e / e2e-chrome (push) Successful in 45s
e2e / e2e-firefox (push) Successful in 23s
2026-08-17 09:06:13 +02:00
clawbot ab1c1846a7 fix: one wording for an empty password field on every screen (closes #265)
check / check (push) Successful in 29s
e2e / e2e-chrome (push) Successful in 48s
e2e / e2e-firefox (push) Successful in 18s
2026-08-17 08:59:59 +02:00
clawbot 743b1962a5 build: run the browser e2e suites in CI (closes #259)
check / check (push) Successful in 28s
e2e / e2e-chrome (push) Successful in 47s
e2e / e2e-firefox (push) Successful in 20s
2026-08-17 08:52:26 +02:00