Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb91568d99 |
@@ -882,9 +882,18 @@ On those screens, when the truncated string would contain no digit from 1 to 9
|
|||||||
and the value does, the amount is extended to its first significant digit
|
and the value does, the amount is extended to its first significant digit
|
||||||
instead: `0.000000000000000001 DAI`, not `0.0000 DAI`. The test is on the whole
|
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. A
|
the exception only fires where the entire displayed figure would read as zero.
|
||||||
genuine zero still renders `0.0000`, and truncation stays truncation: `0.99999`
|
Truncation stays truncation: `0.99999` shows as `0.9999`, never rounded up. A
|
||||||
shows as `0.9999`, never rounded up.
|
genuine zero reaching this rule renders `0.0000`. The ERC-20
|
||||||
|
`approve`/`transfer` amount does exactly that, and so does the swap's `Amount`
|
||||||
|
line for a literal-zero `amountIn` on a V2 or V3 exact-in swap, or a zero
|
||||||
|
`WRAP_ETH` (`0.0000 ETH`). Only two zeros are stated in words before the floor:
|
||||||
|
the swap's `Min. received` line reads `None (no minimum guaranteed)` for any
|
||||||
|
zero minimum, and its `Amount` line reads `All available (V4 open delta)` for a
|
||||||
|
V4 exact-in `amountIn` of zero, which V4 treats as the whole open credit rather
|
||||||
|
than a quantity. So the guarantee that a zero is never shown as `0.0000` covers
|
||||||
|
the `Min. received` line and the V4 exact-in `Amount`; a V2/V3 or `WRAP_ETH`
|
||||||
|
`Amount` still renders it (see the list of amount-slot strings below).
|
||||||
|
|
||||||
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
|
||||||
@@ -936,6 +945,38 @@ and compare against. Reading the stored field directly instead answers `null`
|
|||||||
for a bundled or tracked token the explorer merely omitted, which is not a
|
for a bundled or tracked token the explorer merely omitted, which is not a
|
||||||
refusal the wallet has any reason to make.
|
refusal the wallet has any reason to make.
|
||||||
|
|
||||||
|
**Every string an amount slot can show:** taken together, the exceptions above
|
||||||
|
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
|
||||||
|
always a number:
|
||||||
|
|
||||||
|
- A formatted quantity, e.g. `17.1900 USDT`: the token's scale is known and the
|
||||||
|
figure is at or above the floor, or below it and extended to its first
|
||||||
|
significant digit. This is `truncateAmountNeverZero()`
|
||||||
|
(`src/shared/amountDisplay.js`).
|
||||||
|
- `Unlimited`: an unbounded allowance or permit, which needs no scale to
|
||||||
|
describe — a `uint256`-max ERC-20 `approve` (`src/popup/views/approval.js`) or
|
||||||
|
a Permit2 amount at the `uint160` max on a swap's `Amount`
|
||||||
|
(`src/shared/uniswap.js`).
|
||||||
|
- `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
|
||||||
|
the calldata states no quantity at all. Swap `Amount` line only
|
||||||
|
(`src/shared/uniswap.js`).
|
||||||
|
- `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
|
||||||
|
reaches a `BALANCE_CHECK_ERC20` step: a zero `minBalance`, which once rendered
|
||||||
|
`0.0000` beside the token symbol, now reads this. Swap `Min. received` line
|
||||||
|
(`src/shared/uniswap.js`).
|
||||||
|
- `<amount> base units (decimals unknown)`: the token's scale could not be
|
||||||
|
resolved, so the base-unit integer is shown with that caveat rather than
|
||||||
|
formatted (see Unknown token scale above). Reaches both the ERC-20 amount line
|
||||||
|
and the swap's `Amount` and `Min. received` (`unknownDecimalsAmount()` in
|
||||||
|
`src/shared/approvalAmount.js`).
|
||||||
|
- `Unknown (not named in the calldata)`: not an amount but the currency itself —
|
||||||
|
the `Token In` or `Token Out` line when nothing in the calldata established
|
||||||
|
which token, shown beside the amount and, like the strings above, a sentence
|
||||||
|
rather than a value (`src/shared/uniswap.js`).
|
||||||
|
|
||||||
#### 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
|
||||||
|
|||||||
@@ -132,28 +132,20 @@ but the review is broader than any of them.
|
|||||||
constant rather than `isDebug()`, so it survives only in a debug build; a
|
constant rather than `isDebug()`, so it survives only in a debug build; a
|
||||||
testnet or the runtime debug toggle still raises the banner but without the
|
testnet or the runtime debug toggle still raises the banner but without the
|
||||||
view id.
|
view id.
|
||||||
|
- 2026-09-21: `README.md` now documents the approval screen's amount-slot
|
||||||
- 2026-09-21: The Confirm Delete button on the delete-wallet screen no longer
|
vocabulary and no longer contradicts itself
|
||||||
stays dead after a successful delete
|
([#369](https://git.eeqj.de/sneak/AutistMask/issues/369)). The stale claim
|
||||||
([#335](https://git.eeqj.de/sneak/AutistMask/issues/335)). The password route
|
that a genuine zero still renders `0.0000` is corrected: it holds for the
|
||||||
disabled the button before the decrypt and never re-enabled it, so a second
|
ERC-20 amount, and also for the swap's `Amount` line on a literal-zero
|
||||||
delete in the same popup session needed a reopen; the lost-password route
|
`amountIn` in a V2/V3 exact-in swap or a zero `WRAP_ETH`. Only two zeros are
|
||||||
re-enabled its own button in its leave hook, so the two screens behaved
|
stated in words upstream — the swap's `Min. received` (any zero minimum) and
|
||||||
differently. Both now reset through the shared `finishDelete()`, the one path
|
its V4 exact-in `Amount` (an `amountIn` of zero, V4's open delta). The
|
||||||
both routes take, so they behave the same and the button is live for the next
|
amount-display section now names every string a slot can show — a formatted
|
||||||
delete.
|
quantity, `Unlimited`, `All available (V4 open delta)`,
|
||||||
|
`None (no minimum guaranteed)`, base units with decimals unknown, and
|
||||||
- 2026-09-21: The EIP-6963 provider UUID is generated fresh on each page load
|
`Unknown (not named in the calldata)` — and records that a zero `minBalance`
|
||||||
and never persisted ([#398](https://git.eeqj.de/sneak/AutistMask/issues/398)).
|
on a `BALANCE_CHECK_ERC20` step now reads `None (no minimum guaranteed)` where
|
||||||
It was created once and stored, then announced verbatim to every page on every
|
it once read `0.0000`. Docs only; each claim checked against the tree.
|
||||||
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-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
|
||||||
wallet list was the reported case: the address shared one row with the
|
wallet list was the reported case: the address shared one row with the
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ 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
|
||||||
@@ -19,6 +21,30 @@ 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;
|
||||||
|
|||||||
+11
-8
@@ -204,14 +204,7 @@
|
|||||||
"</svg>",
|
"</svg>",
|
||||||
);
|
);
|
||||||
|
|
||||||
// EIP-6963 wants a fresh UUIDv4 per page load — it identifies one
|
let providerUuid = crypto.randomUUID(); // fallback until real UUID arrives
|
||||||
// announcement, so a provider can be told apart from another instance of
|
|
||||||
// itself in the same page. 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 {
|
||||||
@@ -233,6 +226,16 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
|
||||||
|
|||||||
@@ -51,12 +51,16 @@ function clear() {
|
|||||||
// The lost-password screen holds no secret — a wallet name is not one —
|
// The lost-password screen holds no secret — a wallet name is not one —
|
||||||
// but it is wiped on leave for the neighbouring reason: a typed
|
// but it is wiped on leave for the neighbouring reason: a typed
|
||||||
// confirmation left standing in a hidden view is one click away from
|
// confirmation left standing in a hidden view is one click away from
|
||||||
// destroying a wallet the user has since navigated off.
|
// destroying a wallet the user has since navigated off. The button is
|
||||||
|
// re-enabled here too, so a screen left mid-delete is usable on re-entry.
|
||||||
function clearLostPassword() {
|
function clearLostPassword() {
|
||||||
lostPasswordIndex = null;
|
lostPasswordIndex = null;
|
||||||
$("delete-wallet-lost-name-input").value = "";
|
$("delete-wallet-lost-name-input").value = "";
|
||||||
$("delete-wallet-lost-flash").textContent = "";
|
$("delete-wallet-lost-flash").textContent = "";
|
||||||
$("delete-wallet-lost-flash").style.visibility = "hidden";
|
$("delete-wallet-lost-flash").style.visibility = "hidden";
|
||||||
|
const btn = $("btn-delete-wallet-lost-confirm");
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.classList.remove("text-muted");
|
||||||
}
|
}
|
||||||
|
|
||||||
function show(walletIdx) {
|
function show(walletIdx) {
|
||||||
@@ -94,17 +98,6 @@ function showLostPassword() {
|
|||||||
// cleanup and the accountsChanged broadcast cannot drift apart between
|
// cleanup and the accountsChanged broadcast cannot drift apart between
|
||||||
// them.
|
// them.
|
||||||
async function finishDelete(walletIdx) {
|
async function finishDelete(walletIdx) {
|
||||||
// Each route's confirm button was disabled by its own click handler
|
|
||||||
// before the delete ran. Re-enable both here, on the one path they
|
|
||||||
// share, so the two routes reset the same way and a second delete in
|
|
||||||
// the same popup session finds a live button instead of a dead one.
|
|
||||||
const passwordBtn = $("btn-delete-wallet-confirm");
|
|
||||||
passwordBtn.disabled = false;
|
|
||||||
passwordBtn.classList.remove("text-muted");
|
|
||||||
const lostPasswordBtn = $("btn-delete-wallet-lost-confirm");
|
|
||||||
lostPasswordBtn.disabled = false;
|
|
||||||
lostPasswordBtn.classList.remove("text-muted");
|
|
||||||
|
|
||||||
const { activeAddressChanged } = removeWalletFromState(state, walletIdx);
|
const { activeAddressChanged } = removeWalletFromState(state, walletIdx);
|
||||||
|
|
||||||
deleteWalletIndex = null;
|
deleteWalletIndex = null;
|
||||||
@@ -194,8 +187,8 @@ function init(_ctx) {
|
|||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.classList.add("text-muted");
|
btn.classList.add("text-muted");
|
||||||
|
|
||||||
// finishDelete() re-enables the button; navigating away then runs
|
// finishDelete() navigates, and the leave hook re-enables the
|
||||||
// the leave hook that wipes the typed name.
|
// button and wipes the typed name on the way out.
|
||||||
await finishDelete(lostPasswordIndex);
|
await finishDelete(lostPasswordIndex);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -172,15 +172,6 @@ async function openLostPassword(deleteWallet, walletIdx) {
|
|||||||
await click("btn-delete-wallet-lost-password");
|
await click("btn-delete-wallet-lost-password");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete a wallet through the password route: open its confirm screen,
|
|
||||||
// enter the password, and confirm. The vault is mocked, so the password
|
|
||||||
// text itself is irrelevant — decryptWithPassword decides pass or fail.
|
|
||||||
async function deleteWithPassword(deleteWallet, walletIdx) {
|
|
||||||
deleteWallet.show(walletIdx);
|
|
||||||
node("delete-wallet-password").value = "any password";
|
|
||||||
await click("btn-delete-wallet-confirm");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------ tests
|
// ------------------------------------------------------------ tests
|
||||||
|
|
||||||
// The stub is what every persistence assertion below rests on, so its one
|
// The stub is what every persistence assertion below rests on, so its one
|
||||||
@@ -465,21 +456,15 @@ describe("what the screen leaves behind", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Both routes now re-enable through finishDelete(), not their leave
|
// Left mid-delete, the screen has to come back usable.
|
||||||
// hooks, so the button comes back live once a delete completes.
|
test("the confirm button is re-enabled on the way out", async () => {
|
||||||
test("the confirm button is re-enabled after a delete", async () => {
|
const { helpers, deleteWallet } = load();
|
||||||
const { deleteWallet } = load();
|
|
||||||
await openLostPassword(deleteWallet, 1);
|
await openLostPassword(deleteWallet, 1);
|
||||||
|
|
||||||
node("delete-wallet-lost-name-input").value = "Wallet 2";
|
node("btn-delete-wallet-lost-confirm").disabled = true;
|
||||||
await click("btn-delete-wallet-lost-confirm");
|
helpers.showView("settings");
|
||||||
|
|
||||||
expect(node("btn-delete-wallet-lost-confirm").disabled).toBe(false);
|
expect(node("btn-delete-wallet-lost-confirm").disabled).toBe(false);
|
||||||
expect(
|
|
||||||
node("btn-delete-wallet-lost-confirm").classList.contains(
|
|
||||||
"text-muted",
|
|
||||||
),
|
|
||||||
).toBe(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// A wallet name is not a secret, so the screen is excluded for the
|
// A wallet name is not a secret, so the screen is excluded for the
|
||||||
@@ -490,48 +475,3 @@ describe("what the screen leaves behind", () => {
|
|||||||
expect(RESTORABLE_VIEWS.has("delete-wallet-confirm")).toBe(false);
|
expect(RESTORABLE_VIEWS.has("delete-wallet-confirm")).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// The password route is the pre-existing bug this file's fix addresses:
|
|
||||||
// its Confirm Delete button was disabled before the decrypt and never
|
|
||||||
// re-enabled on success, so a second delete in the same popup session
|
|
||||||
// found a dead button. Now both routes re-enable through finishDelete().
|
|
||||||
//
|
|
||||||
// Against head these tests fail: with the re-enable absent, the button
|
|
||||||
// stays disabled after the first delete, so the disabled assertions read
|
|
||||||
// true where they expect false.
|
|
||||||
describe("the password route's confirm button", () => {
|
|
||||||
test("is re-enabled after a successful delete", async () => {
|
|
||||||
const { deleteWallet, vault } = load();
|
|
||||||
vault.decryptWithPassword.mockResolvedValue();
|
|
||||||
|
|
||||||
await deleteWithPassword(deleteWallet, 1);
|
|
||||||
|
|
||||||
expect(node("btn-delete-wallet-confirm").disabled).toBe(false);
|
|
||||||
expect(
|
|
||||||
node("btn-delete-wallet-confirm").classList.contains("text-muted"),
|
|
||||||
).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
// The reported symptom: delete one wallet, then open Delete Wallet for
|
|
||||||
// a second one without reopening the popup. The button must be live on
|
|
||||||
// that second visit, and the second delete must actually persist.
|
|
||||||
test("a second delete works in the same popup session", async () => {
|
|
||||||
const { deleteWallet, vault, storage } = load();
|
|
||||||
vault.decryptWithPassword.mockResolvedValue();
|
|
||||||
|
|
||||||
await deleteWithPassword(deleteWallet, 1);
|
|
||||||
|
|
||||||
// Wallet 2 is gone; the list is now [Wallet 1, Wallet 3]. Opening
|
|
||||||
// the confirm screen for the wallet now at index 1 (Wallet 3) must
|
|
||||||
// find its button live, not the dead one the first delete left.
|
|
||||||
deleteWallet.show(1);
|
|
||||||
expect(node("btn-delete-wallet-confirm").disabled).toBe(false);
|
|
||||||
|
|
||||||
node("delete-wallet-password").value = "any password";
|
|
||||||
await click("btn-delete-wallet-confirm");
|
|
||||||
|
|
||||||
expect((await persistedWallets(storage)).map((w) => w.name)).toEqual([
|
|
||||||
"Wallet 1",
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -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 a
|
// EIP-6963, asked of the provider itself. The announcement carries the
|
||||||
// UUIDv4 inpage.js generates fresh for this page load (nothing persists
|
// uuid src/content/index.js reads out of extension storage — call site 1
|
||||||
// it — see issue #398) and has to name this extension and hand back the
|
// in the issue — and it has to name this extension and hand back the very
|
||||||
// very object on window.ethereum.
|
// 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 provider uuid: " +
|
"the announcement carries no stored provider uuid: " +
|
||||||
JSON.stringify(announced.uuid),
|
JSON.stringify(announced.uuid),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
// 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 wants a fresh UUIDv4 per announcement instead. The fix generates
|
|
||||||
// it per page load and stores nothing.
|
|
||||||
//
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Evaluate inpage.js once against a fresh stub window and return every UUID it
|
|
||||||
// announced. A `requestProvider` event is dispatched too, so a re-announcement
|
|
||||||
// within one load is observed as well as the announcement at load.
|
|
||||||
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(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, unprompted, with nothing delivered", () => {
|
|
||||||
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", () => {
|
|
||||||
const first = announcedUuids()[0];
|
|
||||||
const second = announcedUuids()[0];
|
|
||||||
expect(first).toMatch(UUID_V4);
|
|
||||||
expect(second).toMatch(UUID_V4);
|
|
||||||
expect(second).not.toBe(first);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user