test: the entire address-poisoning defense in transactions.js is untested #160
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
src/shared/transactions.js(268 lines) has zero test coverage, includingfilterTransactions(transactions.js:214) andisSpoofedSymbol(
transactions.js:203).These two functions are the anti-scam feature set that
README.md:730-814and
docs/README.mddescribe at length: known-symbol spoof detection,low-holder filtering, the fraud contract blocklist, and dust filtering. The
README devotes ~85 lines to explaining this as a core security property of the
wallet, with specific real-world attack transactions cited. None of it is
verified by a test.
A silent regression here does not crash — it just stops filtering, and the
user starts seeing poisoned addresses in their history again. That is exactly
the failure mode the feature exists to prevent, and it is the kind that ships
unnoticed.
Implementation requirements
the tests are grounded in real cases rather than invented ones:
0xD05339f9Ea5ab9d9F03B9d57F671d2abD1F55c82with zero holders, spoofingthe ETH symbol (
README.md:735-750).0x2708ebddfb9b5fa3f7a89d3ea398ef9fd8771b83ed861ecb7c21cd55d18edc74from the look-alike address
0xC3c6B3b4402bD78A9582aB6b00E747769344F37E(
README.md:800-808).Fixtures must be static local objects — the tests must not perform network
requests.
isSpoofedSymbol: a transfer claiming a symbol fromKNOWN_SYMBOLSbutoriginating from a contract that is not that symbol's known address is
spoofed; the genuine contract for the same symbol is not; an unknown symbol
from an unknown contract is not flagged by this particular check.
filterTransactions: cover each filter independently and prove each one canbe turned off, since
README.md:810-814promises every filter isindividually user-configurable and defaults to on:
default)
filtered out by any of the four rules — false positives here lose real user
history and are as bad as false negatives.
transactions.jsowns it, since Home merges across addresses.Definition of done
tests/transactions.test.jsexists and covers every bullet above.fixtures and are proven to be filtered.
when off.
asserted explicitly.
positive.
make teststill completes within the 30-second timeout.TODO.mdupdated in the same commit.make checkpasses.Manager note — dispatching this now.
Rationale for taking it ahead of most of the milestone:
README.md:730-814spends roughly 85 lines promising this exact feature set as a core security
property of the wallet, citing specific real-world attack transactions, and it
currently has zero tests. Unlike most of the remaining 1.0.0 items, this
one has no browser-verification gap — it is pure unit-test work against
exported functions, so it can be completed and genuinely proven in this
environment rather than reaching
merge-readywith an unverifiable claim.The failure mode also deserves emphasis, because it shapes how the tests should
be written: a regression here does not crash. The filters just quietly stop
filtering, and the user starts seeing poisoned look-alike addresses in their
transaction history again. That is precisely the attack the feature exists to
prevent, and it is the kind of regression that ships unnoticed for months.
Notes for the implementer beyond the issue body:
1. This branches cleanly from
main. It addstests/transactions.test.jsand touches no source files, so it does not collide with PR #169, PR #171, or
the #170 work currently in flight. The only shared file is
TODO.md— keepthat edit surgical, one line if possible, and do not refresh the stale
Status/Next Step blocks.
2. Do not modify
src/shared/transactions.js. If you find a genuine bugwhile writing the tests — and given zero coverage on 268 lines, that is
plausible — do not fix it here. Write a test that documents the actual
current behaviour, call the bug out clearly in the PR body, and I will file it
separately. Mixing a behaviour fix into the first test coverage for a security
filter makes both unreviewable.
3. The false-positive case is not optional. The issue asks for at least one
legitimate transaction that survives all four filters. Take it seriously and
make it realistic — a plain ETH transfer of a normal amount from a normal
address, and a transfer of a genuine high-holder token like USDC from its real
contract. Over-filtering silently deletes real user history, which is as
damaging as under-filtering and much easier to ship by accident.
4. Boundaries matter more than midpoints. The 1,000-holder rule and the
100,000 gwei dust threshold both need tests exactly at, just below, and just
above the boundary. Off-by-one here is the most likely real defect.
5. No network. Fixtures must be static local objects. If the code path
under test wants to fetch holder counts, stub it at the module boundary rather
than reaching out.
Environment hygiene note being passed around the roster: if you write any
scratch or intermediate file, put it under a per-session path rather than a
predictable shared one such as a bare
/tmp/<fixed-name>. A peer session had asubagent's scratch file overwritten mid-task by an unrelated process, with
content from a different repository. Re-read anything you wrote before relying
on it.
Context:
make checkis green onmainat23aeae4(5 suites, 62 tests).script/lintis still onlyprettier --check(#152) and cannot catchundefined identifiers.
Implementation plan — branching from
mainat23aeae4astest/issue-160-transactions-filters.Read of the module first, because it changes two things about the plan:
isSpoofedSymbol(transactions.js:203) is not exported —module.exportsattransactions.js:268is only{ fetchRecentTransactions, filterTransactions }. Per the "test the exported surface only" rule and the "do not modifysrc/shared/transactions.js" constraint, I will not add an export. EveryisSpoofedSymbolrequirement in the issue will be asserted throughfilterTransactions, which calls it unconditionally attransactions.js:223and reports the detected contract vianewFraudContracts— that return value makes spoof detection directly observable without reaching into internals. Each of the three required cases (known symbol from wrong contract, genuine contract for that symbol, unknown symbol from unknown contract) gets its own assertion pair (filtered/not-filtered plusnewFraudContractscontents).The cross-address merge/dedup the issue mentions lives in
home.js:178-186(loadHomeTxs), not intransactions.js. Whattransactions.jsdoes own is the within-address merge of normal transactions with ERC-20 transfers by hash, including swap consolidation (transactions.js:148-195), insidefetchRecentTransactions. I will cover that by mocking../src/shared/log(the module boundary that ownsdebugFetch) with static fixture responses, per manager note 5. Nofetchis reachable from the tests.Test plan for
tests/transactions.test.js:0xd05339f9ea5ab9d9f03b9d57f671d2abd1f55c82withholders: 0(README.md:735-750), carrying the real transfer hash0x85215772...; and the 1 gwei native dust transfer0x2708ebdd...from the look-alike0xC3c6B3b4402bD78A9582aB6b00E747769344F37E(README.md:800-808). Both proven filtered under defaults.valueGwei99999 / 100000 / 100001 against the documented 100000 gwei default.state.js:24-27matches whatREADME.md:810-814and the Settings view claim (all three toggles true, threshold 100000), and thatfilterTransactionsapplies 100000 whendustThresholdGweiis omitted.0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48with a realistic holder count, both surviving all four filters with defaults on.Where behaviour diverges from the README I will assert the actual behaviour, mark the test as documenting current behaviour, and write the divergence up in the PR body for separate filing rather than fixing it here. I already see at least one candidate around the "individually disableable" claim in
README.md:810-814; details in the PR.Verification:
make fmt, thenmake check(script/lint+script/test), confirming the suite stays inside the 30-secondtimeoutinscript/test.TODO.mdgets one added line under Completed Steps, with Status and Next Step left untouched.