Compare commits

...
Author SHA1 Message Date
clawbot b4059af827 fix: a listed site ending in a dot gets the phishing warning (closes #534)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
The blocklist check hashed the hostname as the page gave it, so
"myetheywallet.com." and every subdomain written that way hashed
differently from the listed "myetheywallet.com" and opened approval
prompts with no warning. The browser and DNS treat both spellings as
one host. hostnameVariants() now drops one trailing dot before
building the hostname and its parent domains, so the check sees the
name the list holds.

Model: opus-5-5
2026-10-14 10:48:41 +02:00
clawbot 8c08b3d467 docs: say what a closed window does to a tx or sign approval (closes #505)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
The approval port's disconnect handler leaves a transaction or signing
approval pending, and its comments said that was so the user could reopen
the toolbar popup. Those approvals open in their own window. When that
window closes, the windows.onRemoved listener rejects the approval, unless
an approved attempt is still running, in which case that attempt answers
the page. Both comments now say so. No code changed.

Model: opus-5-5
2026-10-14 09:48:41 +02:00
clawbot fd5921f62e fix: make fmt refuses to run without the repo's own prettier (closes #509)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
In a clone where script/bootstrap has not run, node_modules/ is empty and
yarn falls back to whatever prettier is on the PATH, which can format files
differently from the version yarn.lock pins. script/fmt and script/fmt-check
now stop with a message telling the user to run script/bootstrap when
node_modules/.bin/prettier is missing. After bootstrap they behave as
before. The README Entrypoints entries say so.

Model: opus-5-5
2026-10-14 09:31:47 +02:00
clawbot 83f3549d37 fix: typed data for another chain cannot be signed (closes #519)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
EIP-712 has a wallet refuse typed data whose domain chainId is not the
active chain, because the signature is valid on the chain it names. The
sign screen now shows such a request with the reason on its error line
and Sign disabled, the same way it refuses a mismatched primary type.
The background checks again when the signature comes back, against the
network active then, and answers the site with that refusal instead of
the signature. A chain id is read as ethers signs it, so 1, "1" and
"0x1" are one chain. Typed data with no chainId signs as before.

Model: opus-5-5
2026-10-14 08:31:44 +02:00
clawbot eb012b9eb3 fix: Chrome draws the popup's text at the 14px text-sm declares (closes #456)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
Chrome gives extension pages a stylesheet of its own that sets body to
75%, outside any cascade layer, so it beat Tailwind's text-sm and Chrome
drew the popup at 12px where Firefox draws 14px. body now carries
text-sm!, the important form, as it already carries font-mono!. Both
browser suites check that body computes to 14px.

At 14px the recovery phrase instruction on the add wallet screen takes a
third line, which pushed Import below the fold at 360x600. The die button
now shares the instruction's row instead of taking a row of its own.

Model: opus-5-5
2026-10-14 08:02:14 +02:00
13 changed files with 262 additions and 21 deletions
+15 -3
View File
@@ -239,8 +239,12 @@ provide:
`make check` stays non-mutating. Linting runs only in the container, because `make check` stays non-mutating. Linting runs only in the container, because
an ESLint result that depends on whichever ESLint the host happens to have is an ESLint result that depends on whichever ESLint the host happens to have is
not a result, so docker is required to lint. not a result, so docker is required to lint.
- `script/fmt` — format all files (writes), on the host - `script/fmt` — format all files (writes), on the host, with the `prettier`
- `script/fmt-check` — check formatting (read-only), on the host that `script/bootstrap` installs in `node_modules/`; when that one is missing
it fails and says to run `script/bootstrap`, instead of using a `prettier`
from the `PATH`
- `script/fmt-check` — check formatting (read-only), on the host, with the same
`prettier` and the same refusal
- `script/check` — run `script/test`, `script/lint` and `script/fmt-check` - `script/check` — run `script/test`, `script/lint` and `script/fmt-check`
- `script/check-censored` — assert the competitor name RULES.md bars appears - `script/check-censored` — assert the competitor name RULES.md bars appears
nowhere in the working tree or under `dist/` outside its documented nowhere in the working tree or under `dist/` outside its documented
@@ -2160,6 +2164,13 @@ view would leave a wallet one click from deletion.
- Typed data that states no primary type, or one other than the type it - Typed data that states no primary type, or one other than the type it
would be signed as, or that cannot be read → shown with the error line would be signed as, or that cannot be read → shown with the error line
saying so and "Sign" disabled; only "Reject" remains saying so and "Sign" disabled; only "Reject" remains
- Typed data whose domain `chainId` names a chain other than the active
network's → shown with the error line naming both chain ids and "Sign"
disabled; only "Reject" remains. EIP-712 asks for this, because the
signature is valid on the chain the domain names. Typed data with no
`chainId` is not refused. The background refuses such a signature too,
against the network active when the signature reaches it, and answers the
site with the same sentence
- A `personal_sign` or `eth_sign` message that is not hex (`0x` or `0X` and - A `personal_sign` or `eth_sign` message that is not hex (`0x` or `0X` and
an even number of hex digits, the form ethers' `getBytes` reads when an even number of hex digits, the form ethers' `getBytes` reads when
signing) → shown as plain text, with the error line "This message is plain signing) → shown as plain text, with the error line "This message is plain
@@ -2652,7 +2663,8 @@ plaintext list of anyone's domain names.
When a dApp on a blocklisted domain requests a wallet connection, transaction When a dApp on a blocklisted domain requests a wallet connection, transaction
approval, or signature, the approval popup displays a prominent red warning approval, or signature, the approval popup displays a prominent red warning
banner alerting the user. The domain checker matches exact hostnames and all banner alerting the user. The domain checker matches exact hostnames and all
parent domains (subdomain matching). parent domains (subdomain matching), and checks a hostname written with a
trailing dot (`name.`) as `name`.
#### Transaction Decoding #### Transaction Decoding
+7
View File
@@ -25,6 +25,13 @@ run_yarn() {
main() { main() {
cd "$ROOT" cd "$ROOT"
# Without the repo's own prettier, yarn runs whatever prettier is on
# the PATH, which may be a version that formats files differently.
if [ ! -x node_modules/.bin/prettier ]; then
echo "fmt: this repo's prettier is not installed in node_modules/," \
"so run script/bootstrap first." >&2
exit 1
fi
echo "Formatting..." echo "Formatting..."
run_yarn run fmt run_yarn run fmt
} }
+7
View File
@@ -26,6 +26,13 @@ run_yarn() {
main() { main() {
cd "$ROOT" cd "$ROOT"
# Without the repo's own prettier, yarn runs whatever prettier is on
# the PATH, which may be a version that formats files differently.
if [ ! -x node_modules/.bin/prettier ]; then
echo "fmt-check: this repo's prettier is not installed in" \
"node_modules/, so run script/bootstrap first." >&2
exit 1
fi
echo "Checking formatting..." echo "Checking formatting..."
run_yarn run fmt-check run_yarn run fmt-check
} }
+23 -4
View File
@@ -22,6 +22,7 @@ const { debugFetch, log } = require("../shared/log");
const { const {
verifySignedTx, verifySignedTx,
verifySignature, verifySignature,
typedDataChainRefusal,
failureIsRetryable, failureIsRetryable,
describeTxFailure, describeTxFailure,
sameAddress, sameAddress,
@@ -632,8 +633,9 @@ function isExtensionSender(sender) {
// had refused. // had refused.
// //
// TX and sign approvals do not decide here. They stay pending across a // TX and sign approvals do not decide here. They stay pending across a
// disconnect — the user can reopen the toolbar popup — and are rejected by the // disconnect. When their window closes, the windows.onRemoved listener below
// windows.onRemoved listener below. // rejects them, unless an approved attempt is still running, in which case
// that attempt answers the page.
runtime.onConnect.addListener((port) => { runtime.onConnect.addListener((port) => {
if (port.name.startsWith("approval:")) { if (port.name.startsWith("approval:")) {
const id = port.name.split(":")[1]; const id = port.name.split(":")[1];
@@ -661,7 +663,9 @@ runtime.onConnect.addListener((port) => {
const approval = pendingApprovals[id]; const approval = pendingApprovals[id];
if (approval) { if (approval) {
if (approval.type === "tx" || approval.type === "sign") { if (approval.type === "tx" || approval.type === "sign") {
// Keep pending — user can reopen the toolbar popup // When its window closes, windows.onRemoved rejects it,
// unless an approved attempt is still running, in which
// case that attempt answers the page.
return; return;
} }
settleApproval(id, { approved: false, remember: false }); settleApproval(id, { approved: false, remember: false });
@@ -1764,7 +1768,8 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
(async () => { (async () => {
try { try {
const activeAddress = await getActiveAddress(); const s = await getState();
const activeAddress = activeAddressOf(s);
// Same as the transaction path: the address the approval named // Same as the transaction path: the address the approval named
// is the one that must have signed, and a switch since then is // is the one that must have signed, and a switch since then is
// a refusal rather than a signature from another account. // a refusal rather than a signature from another account.
@@ -1783,6 +1788,20 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
signature, signature,
approval.approvedFrom, approval.approvedFrom,
); );
// The sign screen refuses typed data whose domain names
// another chain. A signature for it that arrives anyway is
// refused here, against the network active now.
const sp = approval.signParams;
if (
sp.method === "eth_signTypedData_v4" ||
sp.method === "eth_signTypedData"
) {
const refusal = typedDataChainRefusal(
JSON.parse(sp.typedData).domain,
networkById(s.networkId),
);
if (refusal) throw new ApprovalMismatchError(refusal);
}
settleApproval(msg.id, { signature }, { holdsClaim: true }); settleApproval(msg.id, { signature }, { holdsClaim: true });
sendResponse({ signature }); sendResponse({ signature });
} catch (e) { } catch (e) {
+12 -8
View File
@@ -7,9 +7,10 @@
<link rel="stylesheet" href="styles.css" /> <link rel="stylesheet" href="styles.css" />
</head> </head>
<!-- Chrome gives extension pages a stylesheet of its own that sets the <!-- Chrome gives extension pages a stylesheet of its own that sets the
font on body, and a Tailwind class beats it only when marked font and its size on body, and a Tailwind class beats it only when
important: hence font-mono! rather than font-mono. --> marked important: hence font-mono! and text-sm! rather than
<body class="bg-bg text-fg font-mono! text-sm"> font-mono and text-sm. -->
<body class="bg-bg text-fg font-mono! text-sm!">
<div id="app" class="p-2 pr-5 overflow-x-hidden"> <div id="app" class="p-2 pr-5 overflow-x-hidden">
<!-- ============ GLOBAL TITLE BAR ============ --> <!-- ============ GLOBAL TITLE BAR ============ -->
<div <div
@@ -87,11 +88,14 @@
<!-- Mnemonic form section --> <!-- Mnemonic form section -->
<div id="add-wallet-section-mnemonic"> <div id="add-wallet-section-mnemonic">
<p class="mb-2"> <!-- The die button shares the instruction's row: on a row
Enter your 12 or 24 word recovery phrase below, or click of its own it pushes Import below the fold at
the button to roll the die for a new one. 360x600. -->
</p> <div class="mb-2 flex justify-between items-start gap-2">
<div class="mb-1 flex justify-end"> <p>
Enter your 12 or 24 word recovery phrase below, or
click the button to roll the die for a new one.
</p>
<button <button
id="btn-generate-phrase" id="btn-generate-phrase"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer text-xs" class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer text-xs"
+8 -4
View File
@@ -12,7 +12,7 @@ const {
formatFee, formatFee,
tokenLabel, tokenLabel,
} = require("./helpers"); } = require("./helpers");
const { state, saveState } = require("../../shared/state"); const { state, saveState, currentNetwork } = require("../../shared/state");
const { const {
networkById, networkById,
networkByChainId, networkByChainId,
@@ -46,7 +46,10 @@ const {
const { decryptWithPassword } = require("../../shared/vault"); const { decryptWithPassword } = require("../../shared/vault");
const { getSignerForAddress } = require("../../shared/wallet"); const { getSignerForAddress } = require("../../shared/wallet");
const { walletDefect } = require("../../shared/walletDefects"); const { walletDefect } = require("../../shared/walletDefects");
const { describeSigningFailure } = require("../../shared/approvalVerify"); const {
describeSigningFailure,
typedDataChainRefusal,
} = require("../../shared/approvalVerify");
const txStatus = require("./txStatus"); const txStatus = require("./txStatus");
const uniswap = require("../../shared/uniswap"); const uniswap = require("../../shared/uniswap");
const { notify, runtimeApi, sendMessage } = require("../../shared/browserApi"); const { notify, runtimeApi, sendMessage } = require("../../shared/browserApi");
@@ -443,7 +446,8 @@ function signedPrimaryType(types) {
// Why a signature request cannot be signed, as a sentence for the error line, // Why a signature request cannot be signed, as a sentence for the error line,
// or null when it can. Only typed data is refused: the `primaryType` the page // or null when it can. Only typed data is refused: the `primaryType` the page
// states has to be the type ethers will sign, or this screen would name one // states has to be the type ethers will sign, or this screen would name one
// message while another is signed. // message while another is signed. Its domain may name no chain but the active
// network's.
function typedDataRefusal(sp) { function typedDataRefusal(sp) {
if (sp.method === "personal_sign" || sp.method === "eth_sign") return null; if (sp.method === "personal_sign" || sp.method === "eth_sign") return null;
let data; let data;
@@ -466,7 +470,7 @@ function typedDataRefusal(sp) {
", so it cannot be signed." ", so it cannot be signed."
); );
} }
return null; return typedDataChainRefusal(data.domain, currentNetwork());
} }
// The largest amount a Permit2 allowance can hold, a uint160. Permit2 treats // The largest amount a Permit2 allowance can hold, a uint160. Permit2 treats
+29
View File
@@ -53,6 +53,7 @@ const {
accessListify, accessListify,
formatEther, formatEther,
getAddress, getAddress,
getBigInt,
getBytes, getBytes,
toQuantity, toQuantity,
verifyMessage, verifyMessage,
@@ -635,6 +636,33 @@ function verifySignature(signParams, signature, expectedFrom) {
return recovered; return recovered;
} }
// Why typed data cannot be signed while `network` is the active network, as a
// sentence, or null when it can. EIP-712 has a wallet refuse typed data whose
// domain names another chain, because the signature is valid on that chain.
// ethers signs no chain id when the domain's is null or absent, and reads any
// other with getBigInt(), so 1, "1" and "0x1" are the same chain here too.
function typedDataChainRefusal(domain, network) {
const chainId = domain?.chainId;
if (chainId === undefined || chainId === null) return null;
let named;
try {
named = getBigInt(chainId);
} catch {
return "This typed data cannot be read, so it cannot be signed.";
}
const active = getBigInt(network.chainId);
if (named === active) return null;
return (
"This typed data is for chain id " +
named +
", but the active network is " +
network.name +
" (chain id " +
active +
"), so it cannot be signed."
);
}
// The stage a transaction approval failed at. Which stage it is decides // The stage a transaction approval failed at. Which stage it is decides
// whether the approval survives the failure. // whether the approval survives the failure.
const TX_STAGE_SIGN = "sign"; const TX_STAGE_SIGN = "sign";
@@ -796,6 +824,7 @@ function describeSigningFailure(response, fallbackMessage) {
module.exports = { module.exports = {
verifySignedTx, verifySignedTx,
verifySignature, verifySignature,
typedDataChainRefusal,
assertNoForbiddenFields, assertNoForbiddenFields,
assertNothingUnchecked, assertNothingUnchecked,
assertCanonicalBytes, assertCanonicalBytes,
+4 -1
View File
@@ -110,11 +110,14 @@ function hashListed(hash) {
* Generate hostname variants for subdomain matching. * Generate hostname variants for subdomain matching.
* "sub.evil.com" yields ["sub.evil.com", "evil.com"]. * "sub.evil.com" yields ["sub.evil.com", "evil.com"].
* *
* A trailing dot is dropped: "evil.com." is the fully qualified spelling of
* "evil.com", and the browser loads the same site from either.
*
* @param {string} hostname * @param {string} hostname
* @returns {string[]} * @returns {string[]}
*/ */
function hostnameVariants(hostname) { function hostnameVariants(hostname) {
const h = hostname.toLowerCase(); const h = hostname.toLowerCase().replace(/\.$/, "");
const variants = [h]; const variants = [h];
const parts = h.split("."); const parts = h.split(".");
// Parent domains: a.b.c.d -> b.c.d, c.d // Parent domains: a.b.c.d -> b.c.d, c.d
+56 -1
View File
@@ -397,7 +397,13 @@ function loadBackground(options) {
rpcResult = r; rpcResult = r;
}, },
); );
return { result: () => rpcResult }; return {
id: () =>
new URL(created[created.length - 1].url).searchParams.get(
"approval",
),
result: () => rpcResult,
};
} }
// A dApp asking to connect. The origin defaults to one the persisted // A dApp asking to connect. The origin defaults to one the persisted
@@ -1484,6 +1490,55 @@ describe("what the approval is verified against", () => {
}); });
}); });
// Typed data whose domain names another chain is refused on the sign
// screen; a signature for it that reaches the background anyway is not
// handed to the page (https://git.eeqj.de/sneak/AutistMask/issues/519).
// TYPED_DATA names mainnet.
async function answerTypedData(bg, pending) {
const { domain, types, message } = JSON.parse(TYPED_DATA);
delete types.EIP712Domain;
const signature = await signer.signTypedData(domain, types, message);
const answer = bg.send(
{
type: "AUTISTMASK_SIGN_RESPONSE",
id: pending.id(),
approved: true,
signature,
},
{ url: bg.fromPopup.url },
);
await settle();
return { answer, signature };
}
test("typed data for the active chain is handed back signed", async () => {
const bg = loadBackground();
const pending = bg.requestTypedData();
await settle();
const { answer, signature } = await answerTypedData(bg, pending);
expect(answer.sendResponse).toHaveBeenCalledWith({ signature });
expect(pending.result()).toEqual({ result: signature });
});
test("typed data for another chain is refused, not handed back", async () => {
const bg = loadBackground();
bg.setNetwork(SEPOLIA);
const pending = bg.requestTypedData();
await settle();
const { answer } = await answerTypedData(bg, pending);
const refusal =
"This typed data is for chain id 1, but the active network is Sepolia Testnet (chain id 11155111), so it cannot be signed.";
expect(answer.sendResponse).toHaveBeenCalledWith({
error: refusal,
retryable: false,
});
expect(pending.result()).toEqual({ error: { message: refusal } });
});
test("a sign request from another address raises no approval at all", async () => { test("a sign request from another address raises no approval at all", async () => {
const bg = loadBackground(); const bg = loadBackground();
const pending = bg.requestSign(other.address); const pending = bg.requestSign(other.address);
+12
View File
@@ -128,6 +128,18 @@ step("the popup is drawn in the monospace font it declares", async (env) => {
); );
}); });
// The same check as the Chrome suite's (#456), so both browsers are held to
// the same size.
step("the popup's text is drawn at the 14px text-sm declares", async (env) => {
const size = await env.driver.execute(
"return getComputedStyle(document.body).fontSize;",
);
assert(
size === "14px",
"the popup's text is drawn at " + size + ", not at the 14px of text-sm",
);
});
// The recovery screen (#361): the Chrome suite's four cases, run before any // The recovery screen (#361): the Chrome suite's four cases, run before any
// wallet exists for the same reason. With no wallet nothing saves on a timer, // wallet exists for the same reason. With no wallet nothing saves on a timer,
// so no save can write a good record over the unreadable one. The last of them // so no save can write a good record over the unreadable one. The last of them
+12
View File
@@ -113,6 +113,18 @@ test("the popup is drawn in the monospace font it declares (#418)", async (env)
); );
}); });
// The same stylesheet sets the size on body to 75%, which would draw the
// popup at 12px.
test("the popup's text is drawn at the 14px text-sm declares (#456)", async (env) => {
const size = await env.page.evaluate(
() => getComputedStyle(document.body).fontSize,
);
assert(
size === "14px",
"the popup's text is drawn at " + size + ", not at the 14px of text-sm",
);
});
// The empirical half of #182. The manifest change is only a claim about // The empirical half of #182. The manifest change is only a claim about
// what the CSP permits; this is the observation. Two things have to hold // what the CSP permits; this is the observation. Two things have to hold
// together, and the run covers both: the popup realm compiles WASM (here), // together, and the run covers both: the popup realm compiles WASM (here),
+13
View File
@@ -89,6 +89,12 @@ describe("vendored blocklist", () => {
expect(isPhishingDomain(LISTED[0].toUpperCase())).toBe(true); expect(isPhishingDomain(LISTED[0].toUpperCase())).toBe(true);
}); });
test("a hostname ending in a dot is the same host", () => {
expect(isPhishingDomain(LISTED[1] + ".")).toBe(true);
expect(isPhishingDomain("wallet." + LISTED[1] + ".")).toBe(true);
expect(isPhishingDomain(CLEAN[0] + ".")).toBe(false);
});
test("returns false for an empty or missing hostname", () => { test("returns false for an empty or missing hostname", () => {
expect(isPhishingDomain("")).toBe(false); expect(isPhishingDomain("")).toBe(false);
expect(isPhishingDomain(null)).toBe(false); expect(isPhishingDomain(null)).toBe(false);
@@ -133,6 +139,13 @@ describe("hostnameVariants", () => {
test("lowercases hostnames", () => { test("lowercases hostnames", () => {
expect(hostnameVariants("Evil.COM")).toEqual(["evil.com"]); expect(hostnameVariants("Evil.COM")).toEqual(["evil.com"]);
}); });
test("drops the trailing dot of a fully qualified hostname", () => {
expect(hostnameVariants("sub.evil.com.")).toEqual([
"sub.evil.com",
"evil.com",
]);
});
}); });
describe("domain hashing", () => { describe("domain hashing", () => {
+64
View File
@@ -240,6 +240,7 @@ function request(data) {
beforeEach(() => { beforeEach(() => {
state.trackedTokens = []; state.trackedTokens = [];
state.wallets = []; state.wallets = [];
state.networkId = "mainnet";
}); });
describe("a token permission is warned about, naming spender and amount", () => { describe("a token permission is warned about, naming spender and amount", () => {
@@ -532,3 +533,66 @@ describe("the sign screen refuses a mismatched primary type", () => {
expect(node("btn-approve-sign").disabled).toBe(true); expect(node("btn-approve-sign").disabled).toBe(true);
}); });
}); });
// EIP-712: a wallet refuses typed data whose domain names a chain other than
// the active one, because the signature is valid on the chain it names
// (https://git.eeqj.de/sneak/AutistMask/issues/519). The wallet is on Sepolia
// and the site asks for a mainnet USDC Permit.
describe("the sign screen refuses typed data for another chain", () => {
const REFUSAL =
"This typed data is for chain id 1, but the active network is Sepolia Testnet (chain id 11155111), so it cannot be signed.";
function withChainId(chainId) {
return { ...PERMIT, domain: { ...PERMIT.domain, chainId } };
}
beforeEach(() => {
decryptWithPassword.mockClear();
state.networkId = "sepolia";
});
test("another chain shows the error, with Sign disabled", async () => {
await openSignScreen(PERMIT);
expect(node("approve-sign-error").textContent).toBe(REFUSAL);
expect(node("btn-approve-sign").disabled).toBe(true);
});
test("Sign clicked anyway decrypts and signs nothing", async () => {
const sent = await openSignScreen(PERMIT);
node("approve-sign-password").value = "any password";
await click("btn-approve-sign");
expect(decryptWithPassword).not.toHaveBeenCalled();
expect(sent.map((msg) => msg.type)).not.toContain(
"AUTISTMASK_SIGN_RESPONSE",
);
expect(node("approve-sign-error").textContent).toBe(REFUSAL);
});
// ethers signs 1, "1" and "0x1" as the same chain id.
test("the chain id is refused in each form ethers signs", () => {
for (const chainId of [1, "1", "0x1"]) {
expect(typedDataRefusal(request(withChainId(chainId)))).toBe(
REFUSAL,
);
}
});
test("a chain id that is not a number is refused", () => {
expect(typedDataRefusal(request(withChainId("mainnet")))).toBe(
"This typed data cannot be read, so it cannot be signed.",
);
});
test("the active chain leaves Sign enabled", async () => {
await openSignScreen(withChainId("0xaa36a7"));
expect(node("approve-sign-error").textContent).toBe("");
expect(node("btn-approve-sign").disabled).toBe(false);
});
test("no chain id leaves Sign enabled", async () => {
await openSignScreen(withChainId(undefined));
expect(node("approve-sign-error").textContent).toBe("");
expect(node("btn-approve-sign").disabled).toBe(false);
});
});