fix: every plain ERC-20 transfer produces two rows in transaction history #177
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
Found while writing the first test coverage for the transaction filters
(#160 / PR #175), and deliberately not fixed there.
A plain ERC-20 transfer shows up twice in the transaction history: once as
the real token transfer row, and once as a spurious zero-value native row.
Mechanism:
src/shared/transactions.js:35setsmethod === "transfer"for the nativeside of an ERC-20 transfer.
src/shared/transactions.js:163-190deduplicates a nativerow against its token row by testing
existing.direction === "contract".Because
methodis"transfer"rather than a contract call, that testfails, so the native row is never absorbed and survives alongside the token
row.
transactions.js:253-260skips dust filtering when
isContractCallis true, so a zero-value row thatwould otherwise be filtered as dust is displayed.
This is user-visible on the Home, AddressDetail and AddressToken transaction
lists, and it affects the single most common operation in the wallet after
sending ETH. It also undermines the anti-poisoning work: a history padded with
spurious zero-value rows is harder to scan, and scannability is the whole
reason the README allows truncation in list views.
Implementation requirements
into its token row. Prefer matching on the transaction hash pairing rather
than widening the
direction/methodstring comparison — string-matching onmethodis what produced this bug, and adding another special case invitesthe next one.
narrowly tailored to
transfer: ERC-20approve, a swap through theUniversal Router, a contract creation, and a self-send. Each should produce
the row count a user would expect. Say in the PR which you checked and what
each produces before and after.
native transaction is a real thing and should still be visible.
transactions.js:253-260once thededup is fixed. If the spurious row is gone, the
isContractCallexemptionmay no longer be doing what it was written for; do not change it without
saying why.
filterTransactionsbehaviour otherwise identical — the fouranti-poisoning filters are now covered by tests and must stay green.
Test coverage already waiting
tests/transactions.test.js(from PR #175) contains a test named with acurrent behaviour:prefix pinning the duplicate-row output. Invert it ratherthan deleting it, so it becomes the regression guard. The existing merge/dedup
tests must continue to pass unmodified.
Definition of done
approve, a Universal Router swap, a contract creation, and a self-sendeach produce the expected row count; the PR states what each was before
and after.
current behaviour:test is inverted into a regression guard.tests/transactions.test.jsstill pass unmodified.src/shared/transactions.jsis the only source file changed, or the PRexplains why not.
TODO.mdupdated in the same commit.make checkpasses.Additional requirement, from the mutation testing done during the review of
PR #175.
The reviewer applied 15 mutations to
src/shared/transactions.jsand 13 werekilled by the new test suite. One of the two survivors is in exactly the code
this issue changes:
src/shared/transactions.js:173— narrowingisReceived || needsAmounttoisReceivedsurvives undetected. No fixture covers a swap whose token legsare all
sent, so theneedsAmountfallback that supplies the display amountin that case is currently unpinned. Someone could delete that condition and
every test would still pass.
That fallback lives in the same merge loop (
transactions.js:163-190) whosededup condition this issue fixes, so please close the gap while you are in
there:
sent, and assert themerged entry gets its display amount from the
needsAmountpath.isReceivedalone, confirm at least one test fails, then revert. State the result in the
PR, the same way PR #175 did.
This matters for the fix itself, not just for coverage. This issue asks you to
change how the merge loop decides to absorb a native row into its token row,
and an unpinned amount-selection branch a few lines away is precisely the thing
a dedup change can break silently. Better to have it under test before you
touch the loop than after.