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
|
(`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
|
`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
|
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
|
value included. Balances are stored exactly (`src/shared/balances.js`), whatever
|
||||||
balance below the floor reaches these screens as it is; the one cut is that a
|
decimals a token declares, so a balance below the floor reaches these screens as
|
||||||
token declaring more than 18 decimals is stored to 18, the most the confirmation
|
it is. The history list (`src/shared/transactions.js`) uses the unfloored one:
|
||||||
screen's balance check reads. The history list (`src/shared/transactions.js`)
|
the transaction detail view is the authoritative record and already shows exact
|
||||||
uses the unfloored one: the transaction detail view is the authoritative record
|
precision. The balance lists use neither: they round to four places with
|
||||||
and already shows exact precision. The balance lists use neither: they round to
|
`toFixed(4)` (`balanceLine()` in `src/popup/views/helpers.js`). The 4-decimal
|
||||||
four places with `toFixed(4)` (`balanceLine()` in `src/popup/views/helpers.js`).
|
rule is unchanged everywhere else, including for amounts at or above the floor
|
||||||
The 4-decimal rule is unchanged everywhere else, including for amounts at or
|
on the approval screens.
|
||||||
above the floor on the approval screens.
|
|
||||||
|
|
||||||
The floor applies only where the token's scale is known. Where it is not, the
|
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
|
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
|
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
|
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
|
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
|
whatever its size, also while "Show tracked tokens with zero balance" is off,
|
||||||
other contract address is always dropped, and so is any token claiming a symbol
|
and one too small for the list's four decimal places reads `0.0000`. A token
|
||||||
that belongs to the native asset and therefore has no legitimate contract at all
|
claiming a symbol from the bundled list from any other contract address is
|
||||||
(`"ETH"`). That filter is unconditional — the "Hide tokens with fewer than 1,000
|
always dropped, and so is any token claiming a symbol that belongs to the native
|
||||||
holders" setting governs the transaction history and the send-screen token
|
asset and therefore has no legitimate contract at all (`"ETH"`). That filter is
|
||||||
selector, not this list. Tracked tokens with a zero balance are listed as well
|
unconditional — the "Hide tokens with fewer than 1,000 holders" setting governs
|
||||||
while "Show tracked tokens with zero balance" is on.
|
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
|
#### 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
|
([#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
|
(`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,
|
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
|
whatever decimals a token declares; the balance check reads a token balance to
|
||||||
the balance check reads. A token holding below 0.000001 is still not listed,
|
its first 18 places, the most an amount can have. A token holding below
|
||||||
but only for a token the user does not track, so a tracked token's holding
|
0.000001 is still not listed, but only for a token the user does not track, so
|
||||||
reaches the Send screen and the send is checked against it. The Send screen's
|
a tracked token's holding reaches the Send screen and the send is checked
|
||||||
`Current balance`, and the confirmation screen's balance, fee, reserve and
|
against it. The Send screen's `Current balance`, and the confirmation screen's
|
||||||
insufficient-balance messages, go through `truncateAmountNeverZero()` in
|
balance, fee, reserve and insufficient-balance messages, go through
|
||||||
`src/shared/amountDisplay.js`, the helper the approval screen already used.
|
`truncateAmountNeverZero()` in `src/shared/amountDisplay.js`, the helper the
|
||||||
The confirmation and approval screens both render the fee through
|
approval screen already used. The confirmation and approval screens both
|
||||||
`formatFee()` in `src/popup/views/helpers.js`, which prices the exact fee in
|
render the fee through `formatFee()` in `src/popup/views/helpers.js`, which
|
||||||
USD, so the same fee reads the same on both, USD value included.
|
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
|
- 2026-10-04: Settings lists the sites connected without "Remember", and
|
||||||
removing a site there disconnects it
|
removing a site there disconnects it
|
||||||
([#406](https://git.eeqj.de/sneak/AutistMask/issues/406)). Such a connection
|
([#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.
|
// flight; a stale fee must not reach the screen or the balance check.
|
||||||
if (pendingTx !== txInfo) return;
|
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.
|
// does, so the same fee reads the same on both.
|
||||||
if (estimateWei !== null && estimateWei < gasCostWei) {
|
if (estimateWei !== null && estimateWei < gasCostWei) {
|
||||||
$("confirm-fee-amount").textContent = "~" + formatFee(estimateWei);
|
$("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:
|
// 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
|
// 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
|
// 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) {
|
function formatFee(wei) {
|
||||||
const eth = formatEther(wei);
|
const eth = formatEther(wei);
|
||||||
const ethPrice = getPrice("ETH");
|
const ethPrice = getPrice("ETH");
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ const { LOW_HOLDER_THRESHOLD, parseHoldersCount } = require("./holders");
|
|||||||
const { isSpoofedSymbol } = require("./symbolSpoof");
|
const { isSpoofedSymbol } = require("./symbolSpoof");
|
||||||
const { toDecimals } = require("./transferAmount");
|
const { toDecimals } = require("./transferAmount");
|
||||||
const { resolveTokenDecimals } = require("./approvalAmount");
|
const { resolveTokenDecimals } = require("./approvalAmount");
|
||||||
const { SCALE_DECIMALS } = require("./txValidation");
|
|
||||||
|
|
||||||
// Use a static network to skip auto-detection (which can fail and cause
|
// Use a static network to skip auto-detection (which can fail and cause
|
||||||
// "could not coalesce error" on some RPC endpoints like Cloudflare).
|
// "could not coalesce error" on some RPC endpoints like Cloudflare).
|
||||||
@@ -53,14 +52,13 @@ function requireNetworkId(networkId) {
|
|||||||
return net;
|
return net;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A token balance as a decimal string, exact except for a token that declares
|
// A token balance as an exact decimal string, never cut: a cut stores a small
|
||||||
// more than 18 decimals: that one is cut to 18 places, the most the balance
|
// nonzero holding as zero.
|
||||||
// check on the confirmation screen reads (SCALE_DECIMALS in txValidation.js).
|
|
||||||
function formatTokenBalance(raw, decimals) {
|
function formatTokenBalance(raw, decimals) {
|
||||||
const val = formatUnits(raw, decimals);
|
const val = formatUnits(raw, decimals);
|
||||||
const parts = val.split(".");
|
const parts = val.split(".");
|
||||||
if (parts.length === 1) return val + ".0";
|
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;
|
return parts[0] + "." + dec;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -139,7 +139,15 @@ function validateTransfer({
|
|||||||
const feeFp = known ? feeWei : null;
|
const feeFp = known ? feeWei : null;
|
||||||
|
|
||||||
if (isErc20) {
|
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 (amountFp > tokenFp) codes.push(CODES.INSUFFICIENT_TOKEN);
|
||||||
if (feeFp !== null && feeFp > ethFp) {
|
if (feeFp !== null && feeFp > ethFp) {
|
||||||
codes.push(CODES.INSUFFICIENT_ETH_FOR_FEE);
|
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 send = require("../src/popup/views/send");
|
||||||
const confirmTx = require("../src/popup/views/confirmTx");
|
const confirmTx = require("../src/popup/views/confirmTx");
|
||||||
const approval = require("../src/popup/views/approval");
|
const approval = require("../src/popup/views/approval");
|
||||||
|
const {
|
||||||
|
addressHoldsFunds,
|
||||||
|
balanceLinesForAddress,
|
||||||
|
} = require("../src/popup/views/helpers");
|
||||||
|
|
||||||
const HOLDER = "0x" + "a".repeat(40);
|
const HOLDER = "0x" + "a".repeat(40);
|
||||||
const RECIPIENT = "0xC0FfEE0000000000000000000000000000c0fFEe";
|
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,
|
// The stored balance keeps all 24 places, and the balance check reads the
|
||||||
// and refuses a balance string longer than that as no balance at all.
|
// 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 () => {
|
test("with more than 18 decimals, the send is checked against 18 of them", async () => {
|
||||||
state.trackedTokens[0].decimals = 24;
|
state.trackedTokens[0].decimals = 24;
|
||||||
// 1.5 plus one base unit.
|
// 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("");
|
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 () => {
|
test("while an untracked holding below 0.000001 is still not listed", async () => {
|
||||||
state.trackedTokens = [];
|
state.trackedTokens = [];
|
||||||
await refreshWith(10n ** 18n, [
|
await refreshWith(10n ** 18n, [
|
||||||
|
|||||||
Reference in New Issue
Block a user