fix: refuse an unsupported method with EIP-1193 code 4200 (closes #279)
check / check (push) Successful in 2m27s
e2e / e2e-chrome (push) Successful in 3m0s
e2e / e2e-firefox (push) Successful in 3m9s

handleRpc() answered a method it does not implement with
"Unsupported method: <method>" and no code, so a site probing for an
optional method could not tell "not implemented" from "the call failed"
and fall back. It now carries code 4200, which EIP-1193 defines for this
case; the message is unchanged. The provider already passes any code
through to the page.

The new test hands the real background's reply to the provider and
checks the page sees 4200.

Model: opus-5-5
This commit is contained in:
2026-10-04 05:42:20 +00:00
parent 49a7da87e8
commit 6324e6c22d
4 changed files with 72 additions and 6 deletions
+7
View File
@@ -45,6 +45,13 @@ but the review is broader than any of them.
# Completed Steps # Completed Steps
- 2026-10-04: A method the wallet does not implement is refused with EIP-1193
code `4200` ([#279](https://git.eeqj.de/sneak/AutistMask/issues/279)). The
background's `Unsupported method: <method>` error carried no code, so a site
probing for an optional method could not tell "not implemented" from "the call
failed". The message is unchanged; the background's other errors with no code
are untouched.
- 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
+3 -1
View File
@@ -932,7 +932,9 @@ async function handleRpc(method, params, origin) {
} }
} }
return { error: { message: "Unsupported method: " + method } }; // EIP-1193 4200 lets a site tell "this wallet does not implement that"
// from "that call failed", and fall back.
return { error: { code: 4200, message: "Unsupported method: " + method } };
} }
// The body of eth_sendTransaction, from the connection check through to the // The body of eth_sendTransaction, from the connection check through to the
+6 -5
View File
@@ -31,11 +31,12 @@
// an error instead of accepting the refusal. // an error instead of accepting the refusal.
// //
// Whatever code arrived is passed through verbatim rather than being // Whatever code arrived is passed through verbatim rather than being
// matched against a list: the extension emits 4001, 4100 and 4902 today, // matched against a list: the extension emits codes such as 4001, 4100,
// and a code this file has never heard of is still the truth about what // 4200 and 4902, and a code this file has never heard of is still the
// happened. An error reported with no code at all stays a plain Error — // truth about what happened. An error reported with no code at all stays
// a ProviderRpcError whose `code` is undefined would advertise a // a plain Error — a ProviderRpcError whose `code` is undefined would
// conformance it does not have. `message` is untouched in every case. // advertise a conformance it does not have. `message` is untouched in
// every case.
function toPageError(error) { function toPageError(error) {
const message = (error && error.message) || "Request failed"; const message = (error && error.message) || "Request failed";
if (error && error.code !== undefined && error.code !== null) { if (error && error.code !== undefined && error.code !== null) {
+56
View File
@@ -54,6 +54,7 @@ class StubCustomEvent extends StubEvent {
// through whatever arrived — but the cases below are the real ones. // through whatever arrived — but the cases below are the real ones.
const REJECTED = 4001; // user rejected the request const REJECTED = 4001; // user rejected the request
const UNAUTHORIZED = 4100; // site not connected / wrong address const UNAUTHORIZED = 4100; // site not connected / wrong address
const UNSUPPORTED_METHOD = 4200; // a method the wallet does not implement
const UNRECOGNIZED_CHAIN = 4902; // switch/add to an unsupported chain const UNRECOGNIZED_CHAIN = 4902; // switch/add to an unsupported chain
// A stub window with the four things inpage.js touches: message listeners, // A stub window with the four things inpage.js touches: message listeners,
@@ -115,6 +116,41 @@ async function rejectionFrom(start, response) {
return outcome.error; return outcome.error;
} }
// The reply the real background worker (src/background/index.js) sends for
// `method`, loaded against just enough of the extension API to receive one
// RPC message. Same shape as tests/coldWorkerChainId.test.js.
function backgroundReply(method) {
jest.resetModules();
jest.doMock("../src/shared/alarms", () => ({
BALANCE_REFRESH_ALARM: "balance",
BALANCE_REFRESH_PERIOD_MINUTES: 1,
ensureRecurringAlarms: async () => {},
registerAlarmHandlers: () => {},
}));
let messageListener = null;
global.chrome = {
runtime: {
onMessage: {
addListener: (fn) => {
messageListener = fn;
},
},
onConnect: { addListener: () => {} },
},
};
require("../src/background/index");
return new Promise((resolve) => {
messageListener(
{ type: "AUTISTMASK_RPC", method, params: [] },
{ origin: "https://dapp.example" },
resolve,
);
});
}
describe("an EIP-1193 code reaches the page", () => { describe("an EIP-1193 code reaches the page", () => {
test("a user rejection arrives as code 4001", async () => { test("a user rejection arrives as code 4001", async () => {
const err = await rejectionFrom( const err = await rejectionFrom(
@@ -203,6 +239,26 @@ describe("an EIP-1193 code reaches the page", () => {
}); });
}); });
// The reply here is the background's own, not one written in this file: it
// used to carry no code for a method the wallet does not implement
// (https://git.eeqj.de/sneak/AutistMask/issues/279), so a site probing for an
// optional method could not tell "not implemented" from "the call failed".
describe("a method the wallet does not implement", () => {
afterEach(() => {
delete global.chrome;
});
test("reaches the page as code 4200", async () => {
const method = "wallet_noSuchMethod";
const err = await rejectionFrom(
(p) => p.request({ method }),
await backgroundReply(method),
);
expect(err.code).toBe(UNSUPPORTED_METHOD);
expect(err.message).toBe("Unsupported method: " + method);
});
});
describe("the message is untouched", () => { describe("the message is untouched", () => {
test("a coded error keeps the message byte for byte", async () => { test("a coded error keeps the message byte for byte", async () => {
const message = const message =