From d909567ab92ca0475e9c1509ebc3d3d214e279e7 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Wed, 11 Nov 2026 04:46:32 +0100 Subject: [PATCH] fix: every balance is cut to four decimals (closes #757) The balance lists and Home's ETH line rounded with toFixed(4), so 0.99996 ETH read 1.0000 there and 0.9999 on Send. Send's current balance and ConfirmTx's balance line kept the nonzero floor, so 0.00004 USDC read 0.0000 in the lists and 0.00004 on Send. All four now cut the stored decimal string with truncateAmount(), as the history rows do. balanceLine() takes the stored balance string instead of a parsed number, and escapes the quantity cell. The amounts, fees and allowances a screen asks the user to approve keep the floor of truncateAmountNeverZero(). Disclosure: for a holding below 0.0001, ConfirmTx shows the amount sent beside a balance that reads 0.0000. Model: opus-5-5 --- README.md | 16 ++++-- src/popup/views/addressToken.js | 11 ++-- src/popup/views/confirmTx.js | 11 ++-- src/popup/views/deleteAddress.js | 4 +- src/popup/views/helpers.js | 29 +++++----- src/popup/views/home.js | 3 +- src/popup/views/send.js | 13 ++--- tests/approvalDisplayFloor.test.js | 15 ----- tests/balanceLineEscaping.test.js | 10 ++-- tests/balanceTruncation.test.js | 91 ++++++++++++++++++++++++++++++ tests/deleteAddress.test.js | 10 ++-- tests/e2e/run.js | 18 +++--- tests/sendDisplayFloor.test.js | 37 ++++++------ tests/transactions.test.js | 25 ++++++++ tests/unknownScaleDisplay.test.js | 4 +- 15 files changed, 208 insertions(+), 89 deletions(-) create mode 100644 tests/balanceTruncation.test.js diff --git a/README.md b/README.md index f2d4b03..9f38fe9 100644 --- a/README.md +++ b/README.md @@ -1191,15 +1191,23 @@ These helpers write one kind of value on every screen that shows it: Most screens show token and ETH amounts with four decimal places. History rows cut amounts to four decimals and never round up (`truncateAmount()` in -`src/shared/amountDisplay.js`). The balance lists and Home's ETH balance round -to four places. The screens that state what is being authorized cut as the -history does, and extend an amount below 0.0001 to its first nonzero digit +`src/shared/amountDisplay.js`). These balances are cut the same way: + +- the balance lists; +- Home's ETH balance; +- Send's current balance; +- ConfirmTx's balance line. + +So a balance below 0.0001 reads `0.0000` there. + +The screens that state what is being authorized cut as the history does, and +extend an amount below 0.0001 to its first nonzero digit (`truncateAmountNeverZero()`). So a nonzero amount never reads as zero there. Those screens are: - TxApproval; - SignApproval's Permit warning; -- ConfirmTx, and Send's current balance; +- ConfirmTx's amount and network fee; - WaitTx, SuccessTx and ErrorTx. TransactionDetail shows the exact amount and its base units. diff --git a/src/popup/views/addressToken.js b/src/popup/views/addressToken.js index ec8e3d5..9bfec5e 100644 --- a/src/popup/views/addressToken.js +++ b/src/popup/views/addressToken.js @@ -159,10 +159,10 @@ function renderBalance() { const addr = state.wallets[state.selectedWallet].addresses[state.selectedAddress]; const tokenId = state.selectedToken; - let amount, price; + let balance, price; if (tokenId === "ETH") { // null when no refresh has read the balance. - amount = unknownableAmount(addr.balance); + balance = addr.balance; price = getPrice("ETH"); } else { const id = tokenId.toLowerCase(); @@ -175,12 +175,13 @@ function renderBalance() { // null when the scale is unknown, or when no refresh has read the // token's balance: no quantity to show, and none to price. // balanceLine() states that rather than printing 0.0000. - if (tb) amount = unknownableAmount(tb.balance); - else amount = tracked && tracked.balanceNotFetched ? null : 0; + if (tb) balance = tb.balance; + else balance = tracked && tracked.balanceNotFetched ? null : "0"; price = getPrice(tokenId); } // USD total for this token only + const amount = unknownableAmount(balance); const usdVal = price && amount !== null ? amount * price : null; const usdStr = formatUsd(usdVal); $("address-token-usd-total").innerHTML = escapeHtml(usdStr) || " "; @@ -188,7 +189,7 @@ function renderBalance() { // Single token balance line (no tokenId — not clickable here) $("address-token-balance").innerHTML = balanceLine( currentSymbol, - amount, + balance, price, ); } diff --git a/src/popup/views/confirmTx.js b/src/popup/views/confirmTx.js index 2c7c0a9..4ed2322 100644 --- a/src/popup/views/confirmTx.js +++ b/src/popup/views/confirmTx.js @@ -43,9 +43,12 @@ const { transferAmountUnits, } = require("../../shared/transferAmount"); const { assertWithinCeilings } = require("../../shared/approvalVerify"); -// The amount, the balance line and the fee reserve go through it, as the -// approval screen's amounts do. -const { truncateAmountNeverZero } = require("../../shared/amountDisplay"); +// The amount and the fee reserve keep the nonzero floor, as the approval +// screen's amounts do. The balance line is cut as the balance lists cut it. +const { + truncateAmount, + truncateAmountNeverZero, +} = require("../../shared/amountDisplay"); const { CODES, FEE_PENDING, @@ -160,7 +163,7 @@ function renderBalance(txInfo) { const price = getPrice(txInfo.token, txInfo.chainId); const balUsd = price ? parseFloat(bal) * price : null; $("confirm-balance").textContent = valueWithUsd( - truncateAmountNeverZero(bal) + " " + symbol, + truncateAmount(bal) + " " + symbol, balUsd, ); } diff --git a/src/popup/views/deleteAddress.js b/src/popup/views/deleteAddress.js index 5614957..e3507d1 100644 --- a/src/popup/views/deleteAddress.js +++ b/src/popup/views/deleteAddress.js @@ -79,8 +79,8 @@ function recoveryPathText(wallet) { // "Holds" means ETH or any ERC-20 the wallet knows about — an address with no // ETH and a five-figure stablecoin position must not get the blank line on // the one screen whose job is to warn. The sentence names no figure of its -// own: the rendered lines round to four decimals, so a sentence built from a -// rounded number would report "0.0000 ETH" for an address holding real money. +// own: the rendered lines cut to four decimals, so a sentence built from their +// figure would report "0.0000 ETH" for an address holding real money. // The lines below it carry the amounts, in the same format as Home and // AddressDetail, followed by the USD total when there is one to give — no // total line at all on testnet, before the first price fetch or while no diff --git a/src/popup/views/helpers.js b/src/popup/views/helpers.js index af027c0..04f8490 100644 --- a/src/popup/views/helpers.js +++ b/src/popup/views/helpers.js @@ -18,6 +18,7 @@ const { formatEther } = require("ethers"); const makeBlockie = require("ethereum-blockies-base64"); const { + truncateAmount, truncateAmountNeverZero, isBelowOneMillionth, } = require("../../shared/amountDisplay"); @@ -373,13 +374,15 @@ function formatFee(wei, symbol, chainId) { // the row that issue #307 was reported against: every screen that lists a // holding renders through here. // -// `amount` is null for a holding whose scale nothing knows +// `balance` is the stored balance, a decimal string. It is null for a holding +// whose scale nothing knows // (https://git.eeqj.de/sneak/AutistMask/issues/349), and for a balance no // refresh has read. There is no quantity to print for it and no fiat value to -// derive from one, and printing 0.0000 for a real holding is the failure this -// whole rule exists to prevent, so the row says so instead. -function balanceLine(symbol, amount, price, tokenId) { - const qty = amount === null ? "quantity unknown" : amount.toFixed(4); +// derive from one. A holding whose amount is unknown is shown as +// `quantity unknown` rather than as a zero. +function balanceLine(symbol, balance, price, tokenId) { + const amount = unknownableAmount(balance); + const qty = amount === null ? "quantity unknown" : truncateAmount(balance); const usd = price && amount !== null ? escapeHtml(formatUsd(amount * price)) || " " @@ -394,7 +397,7 @@ function balanceLine(symbol, amount, price, tokenId) { `