Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
504a25dead |
@@ -899,15 +899,14 @@ on the confirmation screen for the wallet's own send
|
||||
(`src/popup/views/confirmTx.js`). Both screens render a network fee through
|
||||
`formatFee()` in `src/popup/views/helpers.js`, which prices the exact fee in USD
|
||||
rather than its truncated figure, so the same fee reads the same on both, USD
|
||||
value included. Balances are stored exactly (`src/shared/balances.js`), so a
|
||||
balance below the floor reaches these screens as it is; the one cut is that a
|
||||
token declaring more than 18 decimals is stored to 18, the most the confirmation
|
||||
screen's balance check reads. The history list (`src/shared/transactions.js`)
|
||||
uses the unfloored one: the transaction detail view is the authoritative record
|
||||
and already shows exact precision. The balance lists use neither: they round to
|
||||
four places with `toFixed(4)` (`balanceLine()` in `src/popup/views/helpers.js`).
|
||||
The 4-decimal rule is unchanged everywhere else, including for amounts at or
|
||||
above the floor on the approval screens.
|
||||
value included. Balances are stored exactly (`src/shared/balances.js`), whatever
|
||||
decimals a token declares, so a balance below the floor reaches these screens as
|
||||
it is. The history list (`src/shared/transactions.js`) uses the unfloored one:
|
||||
the transaction detail view is the authoritative record and already shows exact
|
||||
precision. The balance lists use neither: they round to four places with
|
||||
`toFixed(4)` (`balanceLine()` in `src/popup/views/helpers.js`). The 4-decimal
|
||||
rule is unchanged everywhere else, including for amounts at or above the floor
|
||||
on the approval screens.
|
||||
|
||||
The floor applies only where the token's scale is known. Where it is not, the
|
||||
approval screen states base units instead of a quantity — see Unknown token
|
||||
@@ -1062,13 +1061,15 @@ tokens do appear without the user adding them. An ERC-20 is shown when its
|
||||
balance is nonzero and it is in the bundled known-token list, is tracked by the
|
||||
user, or has 1,000 or more holders. A holding below 0.000001 of a token the user
|
||||
does not track is dust and is not shown; a tracked token's holding is shown
|
||||
whatever its size. A token claiming a symbol from the bundled list from any
|
||||
other contract address is always dropped, and so is any token claiming a symbol
|
||||
that belongs to the native asset and therefore has no legitimate contract at all
|
||||
(`"ETH"`). That filter is unconditional — the "Hide tokens with fewer than 1,000
|
||||
holders" setting governs the transaction history and the send-screen token
|
||||
selector, not this list. Tracked tokens with a zero balance are listed as well
|
||||
while "Show tracked tokens with zero balance" is on.
|
||||
whatever its size, also while "Show tracked tokens with zero balance" is off,
|
||||
and one too small for the list's four decimal places reads `0.0000`. A token
|
||||
claiming a symbol from the bundled list from any other contract address is
|
||||
always dropped, and so is any token claiming a symbol that belongs to the native
|
||||
asset and therefore has no legitimate contract at all (`"ETH"`). That filter is
|
||||
unconditional — the "Hide tokens with fewer than 1,000 holders" setting governs
|
||||
the transaction history and the send-screen token selector, not this list.
|
||||
Tracked tokens with a zero balance are listed as well while "Show tracked tokens
|
||||
with zero balance" is on.
|
||||
|
||||
#### Stored state and its version
|
||||
|
||||
|
||||
@@ -50,16 +50,17 @@ but the review is broader than any of them.
|
||||
([#343](https://git.eeqj.de/sneak/AutistMask/issues/343)). The stored balances
|
||||
(`src/shared/balances.js`) and the confirmation screen's fee were each cut to
|
||||
six decimal places by a rule of their own. Balances are now stored exactly,
|
||||
except that a token declaring more than 18 decimals is stored to 18, the most
|
||||
the balance check reads. A token holding below 0.000001 is still not listed,
|
||||
but only for a token the user does not track, so a tracked token's holding
|
||||
reaches the Send screen and the send is checked against it. The Send screen's
|
||||
`Current balance`, and the confirmation screen's balance, fee, reserve and
|
||||
insufficient-balance messages, go through `truncateAmountNeverZero()` in
|
||||
`src/shared/amountDisplay.js`, the helper the approval screen already used.
|
||||
The confirmation and approval screens both render the fee through
|
||||
`formatFee()` in `src/popup/views/helpers.js`, which prices the exact fee in
|
||||
USD, so the same fee reads the same on both, USD value included.
|
||||
whatever decimals a token declares; the balance check reads a token balance to
|
||||
its first 18 places, the most an amount can have. A token holding below
|
||||
0.000001 is still not listed, but only for a token the user does not track, so
|
||||
a tracked token's holding reaches the Send screen and the send is checked
|
||||
against it. The Send screen's `Current balance`, and the confirmation screen's
|
||||
balance, fee, reserve and insufficient-balance messages, go through
|
||||
`truncateAmountNeverZero()` in `src/shared/amountDisplay.js`, the helper the
|
||||
approval screen already used. The confirmation and approval screens both
|
||||
render the fee through `formatFee()` in `src/popup/views/helpers.js`, which
|
||||
prices the exact fee in USD, so the same fee reads the same on both, USD value
|
||||
included.
|
||||
- 2026-10-04: Settings lists the sites connected without "Remember", and
|
||||
removing a site there disconnects it
|
||||
([#406](https://git.eeqj.de/sneak/AutistMask/issues/406)). Such a connection
|
||||
|
||||
@@ -361,7 +361,7 @@ async function estimateGas(txInfo) {
|
||||
// flight; a stale fee must not reach the screen or the balance check.
|
||||
if (pendingTx !== txInfo) return;
|
||||
|
||||
// The fee lines go through formatFee(), as the approval screen's
|
||||
// The fee line goes through formatFee(), as the approval screen's
|
||||
// does, so the same fee reads the same on both.
|
||||
if (estimateWei !== null && estimateWei < gasCostWei) {
|
||||
$("confirm-fee-amount").textContent = "~" + formatFee(estimateWei);
|
||||
|
||||
@@ -248,7 +248,7 @@ function unknownableAmount(balance) {
|
||||
// A network fee in wei as the confirmation and approval screens both show it:
|
||||
// the ETH figure through truncateAmountNeverZero(), then its USD value when the
|
||||
// ETH price is known. The USD value is of the exact fee, not of the truncated
|
||||
// figure, so it never understates what the fee costs.
|
||||
// figure.
|
||||
function formatFee(wei) {
|
||||
const eth = formatEther(wei);
|
||||
const ethPrice = getPrice("ETH");
|
||||
|
||||
@@ -17,7 +17,6 @@ const { LOW_HOLDER_THRESHOLD, parseHoldersCount } = require("./holders");
|
||||
const { isSpoofedSymbol } = require("./symbolSpoof");
|
||||
const { toDecimals } = require("./transferAmount");
|
||||
const { resolveTokenDecimals } = require("./approvalAmount");
|
||||
const { SCALE_DECIMALS } = require("./txValidation");
|
||||
|
||||
// Use a static network to skip auto-detection (which can fail and cause
|
||||
// "could not coalesce error" on some RPC endpoints like Cloudflare).
|
||||
@@ -53,14 +52,13 @@ function requireNetworkId(networkId) {
|
||||
return net;
|
||||
}
|
||||
|
||||
// A token balance as a decimal string, exact except for a token that declares
|
||||
// more than 18 decimals: that one is cut to 18 places, the most the balance
|
||||
// check on the confirmation screen reads (SCALE_DECIMALS in txValidation.js).
|
||||
// A token balance as an exact decimal string, never cut: a cut stores a small
|
||||
// nonzero holding as zero.
|
||||
function formatTokenBalance(raw, decimals) {
|
||||
const val = formatUnits(raw, decimals);
|
||||
const parts = val.split(".");
|
||||
if (parts.length === 1) return val + ".0";
|
||||
const dec = parts[1].slice(0, SCALE_DECIMALS).replace(/0+$/, "") || "0";
|
||||
const dec = parts[1].replace(/0+$/, "") || "0";
|
||||
return parts[0] + "." + dec;
|
||||
}
|
||||
|
||||
|
||||
@@ -139,7 +139,15 @@ function validateTransfer({
|
||||
const feeFp = known ? feeWei : null;
|
||||
|
||||
if (isErc20) {
|
||||
const tokenFp = toFixedPoint(tokenBalance) ?? 0n;
|
||||
// A token can declare more than 18 decimals, and its balance is
|
||||
// stored with all of them. Only the first 18 places (SCALE_DECIMALS)
|
||||
// are read: an amount with more was refused above, so the places
|
||||
// after them cannot decide whether the amount fits.
|
||||
const tokenText =
|
||||
typeof tokenBalance === "string"
|
||||
? tokenBalance.replace(/(\.\d{18})\d+$/, "$1")
|
||||
: tokenBalance;
|
||||
const tokenFp = toFixedPoint(tokenText) ?? 0n;
|
||||
if (amountFp > tokenFp) codes.push(CODES.INSUFFICIENT_TOKEN);
|
||||
if (feeFp !== null && feeFp > ethFp) {
|
||||
codes.push(CODES.INSUFFICIENT_ETH_FOR_FEE);
|
||||
|
||||
@@ -147,6 +147,10 @@ const { prices, clearPrices } = require("../src/shared/prices");
|
||||
const send = require("../src/popup/views/send");
|
||||
const confirmTx = require("../src/popup/views/confirmTx");
|
||||
const approval = require("../src/popup/views/approval");
|
||||
const {
|
||||
addressHoldsFunds,
|
||||
balanceLinesForAddress,
|
||||
} = require("../src/popup/views/helpers");
|
||||
|
||||
const HOLDER = "0x" + "a".repeat(40);
|
||||
const RECIPIENT = "0xC0FfEE0000000000000000000000000000c0fFEe";
|
||||
@@ -293,8 +297,8 @@ describe("a tracked token holding below 0.000001 never renders as zero", () => {
|
||||
);
|
||||
});
|
||||
|
||||
// Past 18 places the stored balance is cut: the balance check reads 18,
|
||||
// and refuses a balance string longer than that as no balance at all.
|
||||
// The stored balance keeps all 24 places, and the balance check reads the
|
||||
// first 18 of them rather than refusing it as no balance at all.
|
||||
test("with more than 18 decimals, the send is checked against 18 of them", async () => {
|
||||
state.trackedTokens[0].decimals = 24;
|
||||
// 1.5 plus one base unit.
|
||||
@@ -305,6 +309,30 @@ describe("a tracked token holding below 0.000001 never renders as zero", () => {
|
||||
expect(errors()).toBe("");
|
||||
});
|
||||
|
||||
test("with more than 18 decimals and a holding below 10^-18", async () => {
|
||||
state.trackedTokens[0].decimals = 24;
|
||||
// One base unit, 0.000000000000000000000001 TOK, and no ETH, so the
|
||||
// token is all the address holds.
|
||||
await refreshWith(0n, [tokenRow(1n, { decimals: "24" })]);
|
||||
state.selectedToken = TOKEN;
|
||||
send.updateSendBalance();
|
||||
expect(text("send-balance")).toBe(
|
||||
"Current balance: 0.000000000000000000000001 TOK",
|
||||
);
|
||||
await confirmSend("0.000000000000000001", TOKEN);
|
||||
expect(text("confirm-balance")).toBe("0.000000000000000000000001 TOK");
|
||||
expect(errors()).toContain(
|
||||
"You have 0.000000000000000000000001 TOK but are trying to send" +
|
||||
" 0.000000000000000001 TOK.",
|
||||
);
|
||||
// Listed with zero balances hidden, and counted as funds.
|
||||
const addr = state.wallets[0].addresses[0];
|
||||
expect(
|
||||
balanceLinesForAddress(addr, state.trackedTokens, false),
|
||||
).toContain(`data-token="${TOKEN}"`);
|
||||
expect(addressHoldsFunds(addr)).toBe(true);
|
||||
});
|
||||
|
||||
test("while an untracked holding below 0.000001 is still not listed", async () => {
|
||||
state.trackedTokens = [];
|
||||
await refreshWith(10n ** 18n, [
|
||||
|
||||
Reference in New Issue
Block a user