Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95bdc19b9b | ||
|
|
add11e57de |
@@ -884,16 +884,16 @@ instead: `0.000000000000000001 DAI`, not `0.0000 DAI`. The test is on the whole
|
|||||||
truncated string, integer part included, so `1.00005` still shows as `1.0000` —
|
truncated string, integer part included, so `1.00005` still shows as `1.0000` —
|
||||||
the exception only fires where the entire displayed figure would read as zero.
|
the exception only fires where the entire displayed figure would read as zero.
|
||||||
Truncation stays truncation: `0.99999` shows as `0.9999`, never rounded up. A
|
Truncation stays truncation: `0.99999` shows as `0.9999`, never rounded up. A
|
||||||
genuine zero reaching this rule renders `0.0000`. The ERC-20
|
genuine zero reaching this rule renders `0.0000`. The swap screen states a zero
|
||||||
`approve`/`transfer` amount does exactly that, and so does the swap's `Amount`
|
in words before it gets here in only two places. Its `Min. received` line reads
|
||||||
line for a literal-zero `amountIn` on a V2 or V3 exact-in swap, or a zero
|
`None (no minimum guaranteed)` for any zero minimum. Its `Amount` line reads
|
||||||
`WRAP_ETH` (`0.0000 ETH`). Only two zeros are stated in words before the floor:
|
`All available (V4 open delta)` for a V4 exact-in `amountIn` of zero, which V4
|
||||||
the swap's `Min. received` line reads `None (no minimum guaranteed)` for any
|
treats as the whole open credit rather than a quantity — but only when that V4
|
||||||
zero minimum, and its `Amount` line reads `All available (V4 open delta)` for a
|
step is the one that sets the `Amount` line. A Permit2 permit sets it whenever
|
||||||
V4 exact-in `amountIn` of zero, which V4 treats as the whole open credit rather
|
one is among the steps, so a swap carrying a permit shows the permit's amount
|
||||||
than a quantity. So the guarantee that a zero is never shown as `0.0000` covers
|
there and never the open-delta wording. Every other zero amount with a known
|
||||||
the `Min. received` line and the V4 exact-in `Amount`; a V2/V3 or `WRAP_ETH`
|
scale renders `0.0000`, the swap's `Amount` line included; the list of
|
||||||
`Amount` still renders it (see the list of amount-slot strings below).
|
amount-slot strings below says where.
|
||||||
|
|
||||||
The rule and its exception live in `src/shared/amountDisplay.js` as
|
The rule and its exception live in `src/shared/amountDisplay.js` as
|
||||||
`truncateAmount()` and `truncateAmountNeverZero()`. Everything the approval and
|
`truncateAmount()` and `truncateAmountNeverZero()`. Everything the approval and
|
||||||
@@ -950,18 +950,29 @@ mean an amount line on the dApp approval screen (and the wait/success/error
|
|||||||
screens that carry a figure forward) shows one of a fixed set of strings, not
|
screens that carry a figure forward) shows one of a fixed set of strings, not
|
||||||
always a number:
|
always a number:
|
||||||
|
|
||||||
- A formatted quantity, e.g. `17.1900 USDT`: the token's scale is known and the
|
- A formatted quantity, e.g. `17.1900 USDT`: the token's scale is known. A
|
||||||
figure is at or above the floor, or below it and extended to its first
|
figure at or above the floor is truncated to four decimals, one below it is
|
||||||
significant digit. This is `truncateAmountNeverZero()`
|
extended to its first significant digit, and a genuine zero renders `0.0000`,
|
||||||
|
e.g. `0.0000 USDT`. That zero reaches the ERC-20 `approve`/`transfer` line for
|
||||||
|
an amount of zero, and the swap's `Amount` line when the step that sets it
|
||||||
|
states zero: a Permit2 permit, a V2 or V3 exact-in `amountIn`, or a `WRAP_ETH`
|
||||||
|
(`0.0000 ETH`). This is `truncateAmountNeverZero()`
|
||||||
(`src/shared/amountDisplay.js`).
|
(`src/shared/amountDisplay.js`).
|
||||||
- `Unlimited`: an unbounded allowance or permit, which needs no scale to
|
- `Unlimited`: named rather than formatted, since it needs no scale to describe.
|
||||||
describe — a `uint256`-max ERC-20 `approve` (`src/popup/views/approval.js`) or
|
On the ERC-20 line it is an `approve` of the `uint256` maximum, an unbounded
|
||||||
a Permit2 amount at the `uint160` max on a swap's `Amount`
|
allowance (`src/popup/views/approval.js`); `transfer` has no such case. On a
|
||||||
(`src/shared/uniswap.js`).
|
swap's `Amount` line it is any input amount at or above the `uint160` maximum,
|
||||||
|
whichever step set the line (`src/shared/uniswap.js`): a Permit2 permit at
|
||||||
|
that maximum, which is an unbounded permit, or a V2 or V3 exact-in `amountIn`
|
||||||
|
or `WRAP_ETH` amount that large. That includes the router's whole-balance
|
||||||
|
value (`2^255`), which tells the router to use all it holds of the token — a
|
||||||
|
quantity decided at execution, not an allowance.
|
||||||
- `All available (V4 open delta)`: a V4 exact-in swap whose `amountIn` is zero.
|
- `All available (V4 open delta)`: a V4 exact-in swap whose `amountIn` is zero.
|
||||||
V4 reads that zero as "use the whole open delta", not as a literal zero, so
|
V4 reads that zero as "use the whole open delta", not as a literal zero, so
|
||||||
the calldata states no quantity at all. Swap `Amount` line only
|
the calldata states no quantity at all. Swap `Amount` line only, and only when
|
||||||
(`src/shared/uniswap.js`).
|
that V4 step is the one that sets the line. A Permit2 permit sets it instead
|
||||||
|
whenever one is among the steps, and so does any earlier swap or `WRAP_ETH`
|
||||||
|
step; the line then shows what that step stated (`src/shared/uniswap.js`).
|
||||||
- `None (no minimum guaranteed)`: a zero minimum — the swap guarantees nothing
|
- `None (no minimum guaranteed)`: a zero minimum — the swap guarantees nothing
|
||||||
back. It is a literal zero slippage floor on a V2/V3/V4 swap, and it also
|
back. It is a literal zero slippage floor on a V2/V3/V4 swap, and it also
|
||||||
reaches a `BALANCE_CHECK_ERC20` step: a zero `minBalance`, which once rendered
|
reaches a `BALANCE_CHECK_ERC20` step: a zero `minBalance`, which once rendered
|
||||||
|
|||||||
@@ -143,20 +143,32 @@ but the review is broader than any of them.
|
|||||||
both routes take, so they behave the same and the button is live for the next
|
both routes take, so they behave the same and the button is live for the next
|
||||||
delete.
|
delete.
|
||||||
|
|
||||||
|
- 2026-09-21: The EIP-6963 provider UUID is generated fresh on each page load
|
||||||
|
and never persisted ([#398](https://git.eeqj.de/sneak/AutistMask/issues/398)).
|
||||||
|
It was created once and stored, then announced verbatim to every page on every
|
||||||
|
load and across restarts, so any site — connected or not — could read it as a
|
||||||
|
stable cross-site, cross-session identifier for the install, contradicting the
|
||||||
|
"no tracking" promise. inpage.js now announces a per-load
|
||||||
|
`crypto.randomUUID()` and the `eip6963Uuid` storage key and the
|
||||||
|
`AUTISTMASK_PROVIDER_UUID` content-script message are gone. That key was a
|
||||||
|
standalone storage entry, never part of the versioned `autistmask` profile, so
|
||||||
|
the state schema is untouched and no existing profile is affected.
|
||||||
|
|
||||||
- 2026-09-21: `README.md` now documents the approval screen's amount-slot
|
- 2026-09-21: `README.md` now documents the approval screen's amount-slot
|
||||||
vocabulary and no longer contradicts itself
|
vocabulary and no longer contradicts itself
|
||||||
([#369](https://git.eeqj.de/sneak/AutistMask/issues/369)). The stale claim
|
([#369](https://git.eeqj.de/sneak/AutistMask/issues/369)). The stale claim
|
||||||
that a genuine zero still renders `0.0000` is corrected: it holds for the
|
that a genuine zero always renders `0.0000` is corrected: a zero with a known
|
||||||
ERC-20 amount, and also for the swap's `Amount` line on a literal-zero
|
scale does, except where the swap screen states it in words first —
|
||||||
`amountIn` in a V2/V3 exact-in swap or a zero `WRAP_ETH`. Only two zeros are
|
`Min. received` always, and the `Amount` line only when a V4 exact-in of zero
|
||||||
stated in words upstream — the swap's `Min. received` (any zero minimum) and
|
is the step that sets it. A Permit2 permit sets that line whenever one is
|
||||||
its V4 exact-in `Amount` (an `amountIn` of zero, V4's open delta). The
|
present, so a zero permit, a zero V2/V3 exact-in `amountIn` or a zero
|
||||||
amount-display section now names every string a slot can show — a formatted
|
`WRAP_ETH` still reads `0.0000`. The amount-display section now names every
|
||||||
quantity, `Unlimited`, `All available (V4 open delta)`,
|
string a slot can show and when — a formatted quantity (a known-scale zero
|
||||||
`None (no minimum guaranteed)`, base units with decimals unknown, and
|
included), `Unlimited` (on a swap also the router's whole-balance value),
|
||||||
`Unknown (not named in the calldata)` — and records that a zero `minBalance`
|
`All available (V4 open delta)`, `None (no minimum guaranteed)`, base units
|
||||||
on a `BALANCE_CHECK_ERC20` step now reads `None (no minimum guaranteed)` where
|
with decimals unknown, and `Unknown (not named in the calldata)` — and records
|
||||||
it once read `0.0000`. Docs only; each claim checked against the tree.
|
that a zero `minBalance` on a `BALANCE_CHECK_ERC20` step now reads
|
||||||
|
`None (no minimum guaranteed)` where it once read `0.0000`. Docs only.
|
||||||
|
|
||||||
- 2026-08-30: An address no longer wraps, or is shortened to fit, in any of the
|
- 2026-08-30: An address no longer wraps, or is shortened to fit, in any of the
|
||||||
common views ([#380](https://git.eeqj.de/sneak/AutistMask/issues/380)). The
|
common views ([#380](https://git.eeqj.de/sneak/AutistMask/issues/380)). The
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ const {
|
|||||||
hasBrowserNamespace,
|
hasBrowserNamespace,
|
||||||
runtimeApi,
|
runtimeApi,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
storageGet,
|
|
||||||
storageSet,
|
|
||||||
} = require("../shared/browserApi");
|
} = require("../shared/browserApi");
|
||||||
|
|
||||||
// In Chrome (MV3), inpage.js runs as a MAIN-world content script declared
|
// In Chrome (MV3), inpage.js runs as a MAIN-world content script declared
|
||||||
@@ -21,30 +19,6 @@ if (hasBrowserNamespace()) {
|
|||||||
(document.head || document.documentElement).appendChild(script);
|
(document.head || document.documentElement).appendChild(script);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the persisted EIP-6963 provider UUID to the inpage script.
|
|
||||||
// Generated once at install time and stored in extension storage.
|
|
||||||
(async function sendProviderUuid() {
|
|
||||||
let uuid = null;
|
|
||||||
try {
|
|
||||||
const items = await storageGet("eip6963Uuid");
|
|
||||||
uuid = items?.eip6963Uuid;
|
|
||||||
if (!uuid) {
|
|
||||||
uuid = crypto.randomUUID();
|
|
||||||
await storageSet({ eip6963Uuid: uuid });
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Storage was unavailable or refused the write. The announcement
|
|
||||||
// still has to go out — a provider that never announces is invisible
|
|
||||||
// to every EIP-6963 dApp — so it goes under a fresh uuid that this
|
|
||||||
// page load will not outlive.
|
|
||||||
if (!uuid) uuid = crypto.randomUUID();
|
|
||||||
}
|
|
||||||
window.postMessage(
|
|
||||||
{ type: "AUTISTMASK_PROVIDER_UUID", uuid },
|
|
||||||
location.origin,
|
|
||||||
);
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Relay requests from the page to the background script
|
// Relay requests from the page to the background script
|
||||||
window.addEventListener("message", (event) => {
|
window.addEventListener("message", (event) => {
|
||||||
if (event.source !== window) return;
|
if (event.source !== window) return;
|
||||||
|
|||||||
+9
-13
@@ -45,7 +45,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Listen for responses from the content script
|
// Listen for responses from the content script
|
||||||
window.addEventListener("message", function onUuid(event) {
|
window.addEventListener("message", (event) => {
|
||||||
if (event.source !== window) return;
|
if (event.source !== window) return;
|
||||||
if (event.data?.type !== "AUTISTMASK_RESPONSE") return;
|
if (event.data?.type !== "AUTISTMASK_RESPONSE") return;
|
||||||
const { id, result, error } = event.data;
|
const { id, result, error } = event.data;
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Listen for events pushed from the extension
|
// Listen for events pushed from the extension
|
||||||
window.addEventListener("message", function onUuid(event) {
|
window.addEventListener("message", (event) => {
|
||||||
if (event.source !== window) return;
|
if (event.source !== window) return;
|
||||||
if (event.data?.type !== "AUTISTMASK_EVENT") return;
|
if (event.data?.type !== "AUTISTMASK_EVENT") return;
|
||||||
const { eventName, data } = event.data;
|
const { eventName, data } = event.data;
|
||||||
@@ -204,7 +204,13 @@
|
|||||||
"</svg>",
|
"</svg>",
|
||||||
);
|
);
|
||||||
|
|
||||||
let providerUuid = crypto.randomUUID(); // fallback until real UUID arrives
|
// EIP-6963 asks for one UUIDv4 per provider for the life of the page: one
|
||||||
|
// per page load, shared by every announcement in that load. It is
|
||||||
|
// generated here and never stored: announcing one persisted value to every
|
||||||
|
// site, on every load and across restarts, turned it into a stable
|
||||||
|
// cross-site, cross-session tracking identifier any page could read
|
||||||
|
// (https://git.eeqj.de/sneak/AutistMask/issues/398).
|
||||||
|
const providerUuid = crypto.randomUUID();
|
||||||
|
|
||||||
function buildProviderInfo() {
|
function buildProviderInfo() {
|
||||||
return {
|
return {
|
||||||
@@ -226,16 +232,6 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for the persisted UUID from the content script
|
|
||||||
function onProviderUuid(event) {
|
|
||||||
if (event.source !== window) return;
|
|
||||||
if (event.data?.type !== "AUTISTMASK_PROVIDER_UUID") return;
|
|
||||||
window.removeEventListener("message", onProviderUuid);
|
|
||||||
providerUuid = event.data.uuid;
|
|
||||||
announceProvider();
|
|
||||||
}
|
|
||||||
window.addEventListener("message", onProviderUuid);
|
|
||||||
|
|
||||||
window.addEventListener("eip6963:requestProvider", announceProvider);
|
window.addEventListener("eip6963:requestProvider", announceProvider);
|
||||||
announceProvider();
|
announceProvider();
|
||||||
|
|
||||||
|
|||||||
@@ -372,10 +372,10 @@ step("the loopback dApp page gets the real inpage provider", async (env) => {
|
|||||||
STEP_TIMEOUT_MS,
|
STEP_TIMEOUT_MS,
|
||||||
);
|
);
|
||||||
|
|
||||||
// EIP-6963, asked of the provider itself. The announcement carries the
|
// EIP-6963, asked of the provider itself. The announcement carries a
|
||||||
// uuid src/content/index.js reads out of extension storage — call site 1
|
// UUIDv4 inpage.js generates fresh for this page load (nothing persists
|
||||||
// in the issue — and it has to name this extension and hand back the very
|
// it — see issue #398) and has to name this extension and hand back the
|
||||||
// object on window.ethereum.
|
// very object on window.ethereum.
|
||||||
const announced = await d.executeAsync(
|
const announced = await d.executeAsync(
|
||||||
`const done = arguments[arguments.length - 1];
|
`const done = arguments[arguments.length - 1];
|
||||||
const onAnnounce = (e) => {
|
const onAnnounce = (e) => {
|
||||||
@@ -402,7 +402,7 @@ step("the loopback dApp page gets the real inpage provider", async (env) => {
|
|||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
typeof announced.uuid === "string" && announced.uuid.length === 36,
|
typeof announced.uuid === "string" && announced.uuid.length === 36,
|
||||||
"the announcement carries no stored provider uuid: " +
|
"the announcement carries no provider uuid: " +
|
||||||
JSON.stringify(announced.uuid),
|
JSON.stringify(announced.uuid),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
// The EIP-6963 provider UUID inpage.js announces (src/content/inpage.js).
|
||||||
|
//
|
||||||
|
// The bug this pins down (issue #398): the UUID used to be generated once,
|
||||||
|
// persisted in extension storage, and announced verbatim to every page on
|
||||||
|
// every load and across browser restarts, so any site — connected or not —
|
||||||
|
// could read a stable cross-site, cross-session identifier for the install.
|
||||||
|
// EIP-6963 asks for one UUIDv4 per page load, shared by every announcement in
|
||||||
|
// that load. The fix generates it per page load and stores nothing.
|
||||||
|
//
|
||||||
|
// The stored UUID reached inpage.js as an AUTISTMASK_PROVIDER_UUID page
|
||||||
|
// message from the content script, and inpage.js then announced it. Each load
|
||||||
|
// below is posted the same stored UUID that way, so on the old code both loads
|
||||||
|
// announce it and the last two tests fail.
|
||||||
|
//
|
||||||
|
// inpage.js is a bare IIFE injected into the page's JS context, not a module;
|
||||||
|
// see tests/inpageErrors.test.js for why it is evaluated against a stub window
|
||||||
|
// rather than imported. Here the stub captures the CustomEvent that carries
|
||||||
|
// the announcement, so the UUID this file reads is the one a real dApp's
|
||||||
|
// eip6963:announceProvider listener would see.
|
||||||
|
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const { webcrypto } = require("crypto");
|
||||||
|
|
||||||
|
const SOURCE = fs.readFileSync(
|
||||||
|
path.join(__dirname, "..", "src", "content", "inpage.js"),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
|
||||||
|
const loadInto = new Function(
|
||||||
|
"window",
|
||||||
|
"self",
|
||||||
|
"crypto",
|
||||||
|
"Event",
|
||||||
|
"CustomEvent",
|
||||||
|
SOURCE,
|
||||||
|
);
|
||||||
|
|
||||||
|
class StubEvent {
|
||||||
|
constructor(type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StubCustomEvent extends StubEvent {
|
||||||
|
constructor(type, init) {
|
||||||
|
super(type);
|
||||||
|
this.detail = init && init.detail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// What the old content script read out of extension storage and posted to
|
||||||
|
// every page load.
|
||||||
|
const STORED_UUID = "11111111-2222-4333-8444-555555555555";
|
||||||
|
|
||||||
|
// Evaluate inpage.js once against a fresh stub window, post it STORED_UUID the
|
||||||
|
// way the old content script did, then dispatch a `requestProvider` event so a
|
||||||
|
// re-announcement is observed as well as the announcement at load. Returns
|
||||||
|
// every UUID the load announced, in order.
|
||||||
|
function announcedUuids() {
|
||||||
|
const listeners = {};
|
||||||
|
const uuids = [];
|
||||||
|
|
||||||
|
const win = {
|
||||||
|
addEventListener(type, fn) {
|
||||||
|
(listeners[type] || (listeners[type] = [])).push(fn);
|
||||||
|
},
|
||||||
|
removeEventListener(type, fn) {
|
||||||
|
const fns = listeners[type];
|
||||||
|
if (!fns) return;
|
||||||
|
const i = fns.indexOf(fn);
|
||||||
|
if (i !== -1) fns.splice(i, 1);
|
||||||
|
},
|
||||||
|
postMessage() {},
|
||||||
|
dispatchEvent(event) {
|
||||||
|
if (event.type === "eip6963:announceProvider") {
|
||||||
|
uuids.push(event.detail.info.uuid);
|
||||||
|
}
|
||||||
|
for (const fn of (listeners[event.type] || []).slice()) fn(event);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
win.window = win;
|
||||||
|
|
||||||
|
loadInto(win, win, webcrypto, StubEvent, StubCustomEvent);
|
||||||
|
win.dispatchEvent({
|
||||||
|
type: "message",
|
||||||
|
source: win,
|
||||||
|
data: { type: "AUTISTMASK_PROVIDER_UUID", uuid: STORED_UUID },
|
||||||
|
});
|
||||||
|
win.dispatchEvent(new StubEvent("eip6963:requestProvider"));
|
||||||
|
return uuids;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UUID_V4 =
|
||||||
|
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
||||||
|
|
||||||
|
describe("the EIP-6963 provider UUID is fresh per page load", () => {
|
||||||
|
test("a load announces a UUIDv4", () => {
|
||||||
|
const uuids = announcedUuids();
|
||||||
|
expect(uuids.length).toBeGreaterThan(0);
|
||||||
|
expect(uuids[0]).toMatch(UUID_V4);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("every announcement within one load carries the same UUID", () => {
|
||||||
|
const uuids = announcedUuids();
|
||||||
|
expect(uuids.length).toBeGreaterThan(1);
|
||||||
|
expect(new Set(uuids).size).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("two page loads announce different UUIDs, neither the stored one", () => {
|
||||||
|
const first = announcedUuids();
|
||||||
|
const second = announcedUuids();
|
||||||
|
expect(first).not.toContain(STORED_UUID);
|
||||||
|
expect(second).not.toContain(STORED_UUID);
|
||||||
|
expect(second[0]).not.toBe(first[0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user