fix: never render a nonzero approval amount as zero (closes #322)
All checks were successful
check / check (push) Successful in 31s
e2e / e2e-chrome (push) Successful in 1m11s
e2e / e2e-firefox (push) Successful in 24s

Amounts are truncated to four decimal places per README.md's Display
Consistency rule. With the token's true scale resolved, an amount below 0.0001
still printed as 0.0000 — 1 base unit of an 18-decimal token, 500 base units of
an 8-decimal one. On the dApp approval screen, and on the wait/success/error
screens that carry the same string forward as txInfo.amount, a real transfer,
allowance or swap was therefore stated as nothing. A swap's "Min. received" is
the sharper case: a slippage floor shown as 0.0000 states that the swap may
return nothing.

That truncation existed in three separate copies — src/popup/views/approval.js
(the ERC-20 amount, the ETH value, the max fee), src/shared/uniswap.js (the
swap's Amount and Min. received lines, on that same screen) and
src/shared/transactions.js (history and balance lists). They now share
src/shared/amountDisplay.js, which holds the rule and its one exception side by
side: truncateAmount() truncates, truncateAmountNeverZero() truncates with the
nonzero floor.

approval.js and uniswap.js take the floored function, so every amount those
screens display or hand to the confirmation screens obeys the invariant. When
the truncated string would carry no digit from 1 to 9 and the value does, the
amount is extended to its first significant digit: 0.000000000000000001 DAI,
not 0.0000 DAI. It stays in token units, the same unit as the symbol beside it;
the base-unit rendering already on this screen means "the scale is unknown", and
reusing it for a known scale would blur the two. A genuine zero still renders
0.0000, an amount at or above the floor is untouched, and truncation stays
truncation — 0.99999 shows as 0.9999, never rounded up.

transactions.js takes the unfloored function, keeping its current behaviour
exactly: balance lists and history are out of scope by the issue's definition of
done, and the transaction detail view already shows exact precision there. The
code path is shared; the policy is not.

tests/approvalDisplayFloor.test.js drives decodeCalldata() and uniswap.decode()
and asserts both the displayed line and the rawValue the confirmation screens
carry. Against this tree with src/shared/uniswap.js reverted to its previous
formatAmount(), its two new swap cases fail: a 50-base-unit USDT input gives
"0.0000 USDT" where "0.00005 USDT" is expected, and a 1-wei amountOutMin gives
"0.0000 WETH" where "0.000000000000000001 WETH" is expected. A case pinning the
list rule as unfloored is included so the shared module cannot drift into one
policy.

make check green: 42 suites, 848 tests; verify-build 39 cases; check-censored
153 files; eslint and prettier clean in the pinned container.
This commit is contained in:
2026-08-23 13:18:20 +00:00
parent 12b0c4d1c6
commit 578a3bc862
7 changed files with 295 additions and 18 deletions

16
TODO.md
View File

@@ -68,6 +68,22 @@ but the review is broader than any of them.
unchanged — the README said "nothing under `dist/` that the build did not
write", which was broader than that. Documentation only; no executable line
changed.
- 2026-08-23: An amount below the 4-decimal display floor no longer reads as
zero on the approval screens
([#322](https://git.eeqj.de/sneak/AutistMask/issues/322)). With the token's
true scale resolved, the 4-decimal truncation still printed a small amount as
`0.0000` — 1 base unit of an 18-decimal token, 500 of an 8-decimal one — so a
real transfer, allowance or swap was stated as nothing on the one screen whose
job is to say what is being authorized, and a swap's `Min. received` claimed
the user might receive nothing. Three copies of that truncation existed; they
now share `src/shared/amountDisplay.js`. Everything the approval and
confirmation screens render (`src/popup/views/approval.js`,
`src/shared/uniswap.js`) extends to the first significant digit when the
truncated figure would otherwise read as zero, keeping the amount in token
units rather than switching to base units mid-line. The history and balance
lists (`src/shared/transactions.js`) keep the unfloored rule, which is out of
scope by the issue's definition of done. `README.md`'s Display Consistency
section records the exception.
- 2026-08-20: A second extension page can no longer silently delete a wallet
([#304](https://git.eeqj.de/sneak/AutistMask/issues/304)). `saveState()` wrote
the entire state blob, and every extension page — the toolbar popup, a dApp