fix: never render a nonzero approval amount as zero (closes #322)
An amount below the 4-decimal display floor now extends to its first significant digit on the approval and confirmation screens, instead of stating a real transfer, allowance or swap Min. received as 0.0000. The rule had been implemented three times; all three now share src/shared/amountDisplay.js, which holds the plain truncation and the floored variant side by side. History and balance lists keep the unfloored rule, pinned by test.
This commit was merged in pull request #339.
This commit is contained in:
@@ -25,6 +25,12 @@ const {
|
||||
resolveTokenDecimals,
|
||||
unknownDecimalsAmount,
|
||||
} = require("../../shared/approvalAmount");
|
||||
// Four decimals, with the nonzero floor these screens hold: every amount this
|
||||
// view renders — the ERC-20 line, the ETH value, the max fee — and every one
|
||||
// it carries forward to the wait/success/error screens goes through it.
|
||||
const {
|
||||
truncateAmountNeverZero: formatTxValue,
|
||||
} = require("../../shared/amountDisplay");
|
||||
const { decryptWithPassword } = require("../../shared/vault");
|
||||
const { getSignerForAddress } = require("../../shared/wallet");
|
||||
const { walletDefect } = require("../../shared/walletDefects");
|
||||
@@ -40,13 +46,6 @@ function approvalAddressHtml(address) {
|
||||
return renderAddressHtml(address, { title });
|
||||
}
|
||||
|
||||
function formatTxValue(val) {
|
||||
const parts = val.split(".");
|
||||
if (parts.length === 1) return val + ".0000";
|
||||
const dec = (parts[1] + "0000").slice(0, 4);
|
||||
return parts[0] + "." + dec;
|
||||
}
|
||||
|
||||
// The amount line for a decoded ERC-20 call. With a known scale it is the
|
||||
// token quantity; with `decimals` null it is the base-unit integer with the
|
||||
// unknown scale stated, because formatting it with an assumed scale is what
|
||||
|
||||
46
src/shared/amountDisplay.js
Normal file
46
src/shared/amountDisplay.js
Normal file
@@ -0,0 +1,46 @@
|
||||
// The 4-decimal amount rule from README.md's Display Consistency section, and
|
||||
// the one exception to it, in one place. Three call sites had grown their own
|
||||
// copy of the truncation — the history and balance lists
|
||||
// (`src/shared/transactions.js`), the approval screen's ERC-20 amount line
|
||||
// (`src/popup/views/approval.js`) and its Uniswap swap detail lines
|
||||
// (`src/shared/uniswap.js`) — and a fix applied to one of them left the other
|
||||
// two showing a different number for the same value.
|
||||
//
|
||||
// The two functions below are the two policies, not two implementations of
|
||||
// one: summary lists truncate, and the screens that state what is being
|
||||
// authorized truncate with a floor. Keeping them adjacent is the point, so a
|
||||
// change to the rule cannot reach one screen and miss another.
|
||||
|
||||
// Truncate to exactly four decimal places. Truncation, never rounding: an
|
||||
// amount must never be displayed as larger than it is, so 0.99999 stays
|
||||
// 0.9999.
|
||||
function truncateAmount(val) {
|
||||
const parts = val.split(".");
|
||||
if (parts.length === 1) return val + ".0000";
|
||||
return parts[0] + "." + (parts[1] + "0000").slice(0, 4);
|
||||
}
|
||||
|
||||
// The same rule, plus the invariant the approval and confirmation screens
|
||||
// hold: a nonzero amount never renders as zero. Truncating to four decimals
|
||||
// does exactly that to an amount below 0.0001 — one base unit of an 18-decimal
|
||||
// token, 500 of an 8-decimal one — and a real transfer or allowance then reads
|
||||
// as "nothing is being moved" on the screen whose whole job is to say what is
|
||||
// being authorized.
|
||||
//
|
||||
// When the truncated string carries no significant digit and the value does,
|
||||
// the amount is extended to its first significant digit instead. It stays in
|
||||
// token units, the same unit as the symbol printed beside it. A genuine zero
|
||||
// still renders 0.0000, and anything at or above the floor is untouched.
|
||||
function truncateAmountNeverZero(val) {
|
||||
const truncated = truncateAmount(val);
|
||||
// Tests the whole truncated string, integer part included: 1.00005 has a
|
||||
// significant digit already and stays 1.0000.
|
||||
if (/[1-9]/.test(truncated)) return truncated;
|
||||
const parts = val.split(".");
|
||||
if (parts.length === 1) return truncated;
|
||||
const sig = parts[1].search(/[1-9]/);
|
||||
if (sig === -1) return truncated;
|
||||
return parts[0] + "." + parts[1].slice(0, sig + 1);
|
||||
}
|
||||
|
||||
module.exports = { truncateAmount, truncateAmountNeverZero };
|
||||
@@ -11,6 +11,10 @@ const { log, debugFetch } = require("./log");
|
||||
const { TOKEN_BY_ADDRESS } = require("./tokenList");
|
||||
const { parseHoldersCount, isLowHolderCount } = require("./holders");
|
||||
const { isSpoofedSymbol } = require("./symbolSpoof");
|
||||
// The plain 4-decimal rule. The history and balance lists deliberately keep
|
||||
// truncation without the approval screens' nonzero floor: the transaction
|
||||
// detail view is the authoritative record and already shows exact precision.
|
||||
const { truncateAmount: formatTxValue } = require("./amountDisplay");
|
||||
|
||||
// Ethereum addresses are case-insensitive: EIP-55 mixed case is a checksum
|
||||
// over the address, not part of its identity. Every address comparison in
|
||||
@@ -20,13 +24,6 @@ function normalizeAddress(addr) {
|
||||
return (addr || "").toLowerCase();
|
||||
}
|
||||
|
||||
function formatTxValue(val) {
|
||||
const parts = val.split(".");
|
||||
if (parts.length === 1) return val + ".0000";
|
||||
const dec = (parts[1] + "0000").slice(0, 4);
|
||||
return parts[0] + "." + dec;
|
||||
}
|
||||
|
||||
function parseTx(tx, addrLower) {
|
||||
const from = tx.from?.hash || "";
|
||||
const to = tx.to?.hash || "";
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
const { Interface, AbiCoder, getBytes, formatUnits } = require("ethers");
|
||||
const { TOKEN_BY_ADDRESS } = require("./tokenList");
|
||||
const { truncateAmountNeverZero } = require("./amountDisplay");
|
||||
|
||||
const coder = AbiCoder.defaultAbiCoder();
|
||||
|
||||
@@ -34,11 +35,13 @@ const COMMAND_NAMES = {
|
||||
0x21: "Execute Sub-Plan",
|
||||
};
|
||||
|
||||
// The swap's Amount and Min. received lines land on the same approval screen,
|
||||
// and Amount is carried to the wait/success/error screens as the ERC-20 line
|
||||
// is, so they take the same nonzero floor: a swap of an amount below 0.0001 is
|
||||
// not "0.0000", and a slippage floor of one base unit does not read as "you may
|
||||
// receive nothing".
|
||||
function formatAmount(raw, decimals) {
|
||||
const parts = formatUnits(raw, decimals).split(".");
|
||||
if (parts.length === 1) return parts[0] + ".0000";
|
||||
const dec = (parts[1] + "0000").slice(0, 4);
|
||||
return parts[0] + "." + dec;
|
||||
return truncateAmountNeverZero(formatUnits(raw, decimals));
|
||||
}
|
||||
|
||||
function tokenInfo(address) {
|
||||
|
||||
Reference in New Issue
Block a user