Compare commits
1 Commits
main
...
3fb6a53f77
| Author | SHA1 | Date | |
|---|---|---|---|
| 3fb6a53f77 |
12
README.md
12
README.md
@@ -689,6 +689,18 @@ Both are click-copyable. Truncating to 4 decimals in summary views is acceptable
|
|||||||
for scannability, but the detail view must never discard precision — it is the
|
for scannability, but the detail view must never discard precision — it is the
|
||||||
one place the user can always use to verify exact details.
|
one place the user can always use to verify exact details.
|
||||||
|
|
||||||
|
**Specific Exception — nonzero floor on the approval screens:** A nonzero amount
|
||||||
|
must never render as zero. Truncating to 4 decimals does exactly that to an
|
||||||
|
amount below 0.0001 — 1 base unit of an 18-decimal token, 500 base units of an
|
||||||
|
8-decimal one — and on the dApp approval screen and the wait/success/error
|
||||||
|
screens that carry its amount forward, a real transfer or allowance then reads
|
||||||
|
as "nothing is being moved". On those screens (`formatTxValue()` in
|
||||||
|
`src/popup/views/approval.js`), when the first 4 decimals would all be zero and
|
||||||
|
the value is not, the amount is extended to its first significant digit instead:
|
||||||
|
`0.000000000000000001 DAI`, not `0.0000 DAI`. It stays in token units, the same
|
||||||
|
unit as the symbol beside it. The 4-decimal rule is unchanged everywhere else,
|
||||||
|
including for amounts at or above the floor on these same screens.
|
||||||
|
|
||||||
#### Partial USD totals
|
#### Partial USD totals
|
||||||
|
|
||||||
Prices are fetched for the top 25 tokens only, so an address can hold assets the
|
Prices are fetched for the top 25 tokens only, so an address can hold assets the
|
||||||
|
|||||||
14
TODO.md
14
TODO.md
@@ -44,6 +44,20 @@ but the review is broader than any of them.
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 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, `formatTxValue()` still truncated a small amount to
|
||||||
|
`0.0000` — 1 base unit of an 18-decimal token, 500 of an 8-decimal one — so a
|
||||||
|
real transfer or allowance was stated as nothing on the one screen whose job
|
||||||
|
is to say what is being authorized. Its copy in `src/popup/views/approval.js`
|
||||||
|
now extends to the first significant digit when the first 4 decimals would all
|
||||||
|
be zero and the value is not, keeping the amount in token units rather than
|
||||||
|
switching to base units mid-line. The 4-decimal rule is otherwise unchanged,
|
||||||
|
the copy in `src/shared/transactions.js` that serves the history list is
|
||||||
|
untouched, and `README.md`'s Display Consistency section records the
|
||||||
|
exception.
|
||||||
|
|
||||||
- 2026-08-20: A second extension page can no longer silently delete a wallet
|
- 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
|
([#304](https://git.eeqj.de/sneak/AutistMask/issues/304)). `saveState()` wrote
|
||||||
the entire state blob, and every extension page — the toolbar popup, a dApp
|
the entire state blob, and every extension page — the toolbar popup, a dApp
|
||||||
|
|||||||
@@ -40,11 +40,22 @@ function approvalAddressHtml(address) {
|
|||||||
return renderAddressHtml(address, { title });
|
return renderAddressHtml(address, { title });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Four decimal places, per the display rule in README.md, with one exception:
|
||||||
|
// a nonzero amount must never render as zero. Truncation is a scannability
|
||||||
|
// decision for summary views, and on the screens that state what is being
|
||||||
|
// authorized it cannot be allowed to produce a figure reading as "nothing is
|
||||||
|
// being moved" — 1 base unit of an 18-decimal token is not 0.0000. When the
|
||||||
|
// first four decimals are all zero and the value is not, the string is
|
||||||
|
// extended to the first significant digit instead: still token units, so the
|
||||||
|
// number stays in the same unit as the symbol beside it.
|
||||||
function formatTxValue(val) {
|
function formatTxValue(val) {
|
||||||
const parts = val.split(".");
|
const parts = val.split(".");
|
||||||
if (parts.length === 1) return val + ".0000";
|
if (parts.length === 1) return val + ".0000";
|
||||||
const dec = (parts[1] + "0000").slice(0, 4);
|
const truncated = parts[0] + "." + (parts[1] + "0000").slice(0, 4);
|
||||||
return parts[0] + "." + dec;
|
if (/[1-9]/.test(truncated)) return truncated;
|
||||||
|
const sig = parts[1].search(/[1-9]/);
|
||||||
|
if (sig === -1) return truncated;
|
||||||
|
return parts[0] + "." + parts[1].slice(0, sig + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The amount line for a decoded ERC-20 call. With a known scale it is the
|
// The amount line for a decoded ERC-20 call. With a known scale it is the
|
||||||
|
|||||||
112
tests/approvalDisplayFloor.test.js
Normal file
112
tests/approvalDisplayFloor.test.js
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
// The floor of the approval screen's amount line.
|
||||||
|
//
|
||||||
|
// Amounts are truncated to four decimal places for scannability (README.md,
|
||||||
|
// Display Consistency). With the token's true scale resolved, that truncation
|
||||||
|
// can still take a real amount below the floor and print it as `0.0000`: one
|
||||||
|
// base unit of an 18-decimal token, or a few hundred of an 8-decimal one. On
|
||||||
|
// the one screen whose job is to state what is being authorized, a nonzero
|
||||||
|
// transfer or allowance then reads as nothing.
|
||||||
|
//
|
||||||
|
// The invariant asserted here is narrow: a nonzero amount never renders as
|
||||||
|
// zero. The four-decimal rule itself is unchanged, and the string the
|
||||||
|
// confirmation screens carry as `txInfo.amount` is the same one, so it is
|
||||||
|
// asserted on `rawValue` alongside the displayed line.
|
||||||
|
|
||||||
|
globalThis.chrome = {
|
||||||
|
storage: { local: { get: async () => ({}), set: async () => {} } },
|
||||||
|
};
|
||||||
|
|
||||||
|
const { Interface } = require("ethers");
|
||||||
|
const { ERC20_ABI } = require("../src/shared/constants");
|
||||||
|
const { state } = require("../src/shared/state");
|
||||||
|
const { decodeCalldata } = require("../src/popup/views/approval");
|
||||||
|
|
||||||
|
const iface = new Interface(ERC20_ABI);
|
||||||
|
|
||||||
|
// Bundled tokens, so the scale and the symbol both come from the list.
|
||||||
|
const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; // 6 decimals
|
||||||
|
const WBT = "0x925206b8a707096Ed26ae47C84747fE0bb734F59"; // 8 decimals
|
||||||
|
const DAI = "0x6B175474E89094C44Da98b954EedeAC495271d0F"; // 18 decimals
|
||||||
|
// Outside the list, so the scale comes from what the user tracks and the line
|
||||||
|
// carries no symbol.
|
||||||
|
const NOVEL = "0xE2E0000000000000000000000000000000000E2e";
|
||||||
|
|
||||||
|
const RECIPIENT = "0xC0FfEE0000000000000000000000000000c0fFEe";
|
||||||
|
const SPENDER = "0x1111111111111111111111111111111111111111";
|
||||||
|
|
||||||
|
function transferData(amount) {
|
||||||
|
return iface.encodeFunctionData("transfer", [RECIPIENT, amount]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function approveData(amount) {
|
||||||
|
return iface.encodeFunctionData("approve", [SPENDER, amount]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Amount detail as the approval screen renders it: `value` is the line on
|
||||||
|
// the screen, `rawValue` is what is carried to the wait/success/error screens.
|
||||||
|
function amount(data, token) {
|
||||||
|
const decoded = decodeCalldata(data, token);
|
||||||
|
return decoded.details.find((d) => d.label === "Amount");
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
state.trackedTokens = [];
|
||||||
|
state.wallets = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("a nonzero amount never renders as zero", () => {
|
||||||
|
test("500 base units of a 6-decimal token", () => {
|
||||||
|
const detail = amount(transferData(500n), USDC);
|
||||||
|
expect(detail.value).toBe("0.0005 USDC");
|
||||||
|
expect(detail.rawValue).toBe("0.0005");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("1 base unit of an 18-decimal token", () => {
|
||||||
|
const detail = amount(transferData(1n), DAI);
|
||||||
|
expect(detail.value).toBe("0.000000000000000001 DAI");
|
||||||
|
expect(detail.rawValue).toBe("0.000000000000000001");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("500 base units of an 8-decimal token", () => {
|
||||||
|
expect(amount(transferData(500n), WBT).rawValue).toBe("0.000005");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("an allowance below the floor is not rendered as zero either", () => {
|
||||||
|
expect(amount(approveData(1n), DAI).value).toBe(
|
||||||
|
"0.000000000000000001 DAI",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// The floor holds at any scale, not only the three above: for every
|
||||||
|
// decimals a token can declare, one base unit has to show a digit.
|
||||||
|
test("one base unit shows a significant digit at every scale", () => {
|
||||||
|
for (let decimals = 0; decimals <= 30; decimals++) {
|
||||||
|
state.trackedTokens = [{ address: NOVEL, decimals }];
|
||||||
|
expect(amount(transferData(1n), NOVEL).rawValue).toMatch(/[1-9]/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("the four-decimal rule is otherwise unchanged", () => {
|
||||||
|
test("a whole amount keeps exactly four decimals", () => {
|
||||||
|
expect(amount(transferData(5000000000n), USDC).rawValue).toBe(
|
||||||
|
"5000.0000",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("precision beyond four decimals is still truncated", () => {
|
||||||
|
expect(amount(transferData(1234567890123456789n), DAI).rawValue).toBe(
|
||||||
|
"1.2345",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("an amount at the floor is not extended", () => {
|
||||||
|
expect(amount(transferData(100000000000000n), DAI).rawValue).toBe(
|
||||||
|
"0.0001",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a genuine zero still renders as zero", () => {
|
||||||
|
expect(amount(transferData(0n), DAI).rawValue).toBe("0.0000");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user