fix: honour a dust threshold of 0 and compare addresses case-insensitively (closes #179) #228
Reference in New Issue
Block a user
Delete Branch "fix/issue-179-dust-threshold-and-spoof-case"
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?
Closes #179.
Two defects in
src/shared/transactions.js, both silent over-filtering: thewallet hid transactions the user had asked to see.
1. A dust threshold of
0filters.dustThresholdGwei || 100000swallowed the one value a user wouldpick to mean "show everything". It is now
??, computed once before thefilter loop, so
0is a real threshold: no transaction has a value below0 gwei, so nothing is hidden. That makes it exactly equivalent to clearing the
hide-dust checkbox, and neither control can override the other.
README.mdnow says so in the dust-filtering bullet.
src/shared/state.jsalready distinguished unset from zero on load(
saved.dustThresholdGwei !== undefined), so a stored0survives a reload.Settings input (
src/popup/views/settings.js:305)Was
parseInt(value, 10)accepted when!isNaN(val) && val >= 0. What eachinput does now, and what changed:
0-5)-5abc)abc100 gwei)parseInt1.5)parseIntRejected input never mutates state, and the field is put back to the stored
value so it cannot display a threshold the wallet is not using.
2.
isSpoofedSymbolwas case-sensitive on the contract addressEIP-55 mixed case is a checksum over the address, not part of its identity, so
tx.contractAddress !== legitclassified a genuine token arriving checksummedas a spoof — hiding a real transfer and recording the real contract on the
fraud blocklist. Every address comparison in the module now goes through one
normalizeAddress()helper (parseTxdirection and token lookup,parseTokenTransferdirection and contract,fetchRecentTransactions,isSpoofedSymbol, the fraud set and its lookups). The contract recorded innewFraudContractsis normalised too, so the persisted blocklist matcheslater transfers in any casing.
mergeTransactions()and its tests from#177 are untouched.
Failing first
Six new tests, run against the current
src/shared/transactions.jswith onlythe test file changed:
The checksummed-genuine and threshold-0 cases fail the same way — the
transaction the user should see is missing:
And the fraud contract was recorded in whatever casing it arrived in:
All six pass with the fix. Both
current behaviour:tests named in the issue(the checksummed spoof and the threshold-0 fallback) are inverted into
regression guards; the other two
current behaviour:tests, which documentspoof filtering not being disableable, are left alone — that is
#176's territory, and no
settings toggle is added here.
Mutation survivor from the issue comment
tx.holders !== nullat the low-holder rule had no fixture reaching it. Addedone pairing a real
contractAddresswithholders: null, asserting it is notfiltered. Deleting the guard now fails it:
Guard restored afterwards; the rest of the suite was unaffected by the
mutation, so that test is what kills it.
holders: undefinedis not reachable from the Blockscout response shape,so no fixture was added for it:
parseTokenTransfercomputesparseInt(tt.token?.holders_count || "0", 10), andparseTxsetsholders: nulloutright. Checked, not assumed.||-as-default audit ofsrc/shared/Only one numeric-default instance was in
src/shared/, and it is the onefixed here. Two adjacent cases found and not fixed (out of scope, worth a
decision):
src/shared/transactions.js:112—parseInt(tt.token?.holders_count || "0", 10).A token whose holder count the explorer omits (rate limit, self-hosted
instance) is parsed as 0 holders and then hidden by the low-holder rule.
This is the same over-filtering harm the
holders !== nullguard exists toprevent, one layer earlier: the guard can never fire for a token transfer,
because the parser never produces
nullfor one. Fixing it means emittingnullwhen the field is absent.src/popup/views/send.js:135—(t.holders || 0) < 1000hides a token withan unknown holder count from the send selector, same shape, outside
src/shared/.Benign (0 is the intended default):
src/shared/state.js:97andsrc/background/index.js:599, bothlastBalanceRefresh || 0.Verification
make checkon the rebased branch: 10 suites, 251 passed, 1 skipped, 252total; prettier clean. The skip is the pre-existing
test.skipattests/wallet.test.js:314, from#159, not from this
branch.
docker build --no-cache .(via the Dockerfile'smake checkandmake build) executed uncached and green:Tests: 179 passed, 179 total,All matched files use Prettier code style!, andverify-buildconfirmed 4bundles with
autistmask-build-debug=off. Run before the rebase ontof455b0a; the hostmake checkabove is post-rebase.nextatf455b0aimmediately before pushing. The onlyconflict was the
TODO.mdCompleted Steps list; all sides' entries kept.PASS — independent review of #228 against #179: all DoD items met, every normalized site verified behaviour-neutral except the two intended ones, no fraud-set migration break, tests have teeth,
make checkandscript/cibuildgreen onea1fcd4, mergeable onnext, policy clean.Flags (none blocking):
src/popup/views/settings.js:307-317— rejected input reverts the field silently, but this same file already usesshowFlash()with full-sentence messages for invalid RPC and Blockscout URLs (:198,:243), which is the idiom README "Language & Labeling" describes. A user typing100 gweior1.5(both accepted before this PR) now sees the box snap back to the stored number with no explanation. Not a documented-rule break, so not a fail — but ashowFlash("Please enter a whole number of gwei, or 0 to hide nothing.")alongside the resync would be strictly better and is a one-line change.src/popup/views/settings.js:308—Number(raw)accepts non-decimal notation the oldparseInt(raw, 10)did not:0x10stores 16 and1e3stores 1000. Harmless and no worse than before (parseInt("0x10", 10)stored 0), just wider than the PR table states.src/shared/transactions.js:305— a stored non-numeric threshold makestx.valueGwei < dustThresholdGweiNaN-false, i.e. it fails open and hides nothing rather than defaulting. Unreachable through the new Settings validation and identical to the pre-PR behaviour under||; noted only because it is silent.Disclosure: mutation testing was done in a scratch clone working tree and reverted;
git statusverified clean before thescript/cibuildrun whose result is cited (layer#11 make checkexecuted, notCACHED: 251 passed, 1 skipped, prettier clean).