From 6324e6c22d7d49bcffb3676c229e029592ddd66f Mon Sep 17 00:00:00 2001 From: sneak Date: Sun, 4 Oct 2026 04:46:47 +0000 Subject: [PATCH] fix: refuse an unsupported method with EIP-1193 code 4200 (closes #279) handleRpc() answered a method it does not implement with "Unsupported 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 --- TODO.md | 7 +++++ src/background/index.js | 4 ++- src/content/inpage.js | 11 ++++---- tests/inpageErrors.test.js | 56 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 6 deletions(-) diff --git a/TODO.md b/TODO.md index 49525b6..7004d95 100644 --- a/TODO.md +++ b/TODO.md @@ -45,6 +45,13 @@ but the review is broader than any of them. # 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: ` 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 removing a site there disconnects it ([#406](https://git.eeqj.de/sneak/AutistMask/issues/406)). Such a connection diff --git a/src/background/index.js b/src/background/index.js index 42b5c27..5172cba 100644 --- a/src/background/index.js +++ b/src/background/index.js @@ -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 diff --git a/src/content/inpage.js b/src/content/inpage.js index 604e5e4..38f57a8 100644 --- a/src/content/inpage.js +++ b/src/content/inpage.js @@ -31,11 +31,12 @@ // an error instead of accepting the refusal. // // Whatever code arrived is passed through verbatim rather than being - // matched against a list: the extension emits 4001, 4100 and 4902 today, - // and a code this file has never heard of is still the truth about what - // happened. An error reported with no code at all stays a plain Error — - // a ProviderRpcError whose `code` is undefined would advertise a - // conformance it does not have. `message` is untouched in every case. + // matched against a list: the extension emits codes such as 4001, 4100, + // 4200 and 4902, and a code this file has never heard of is still the + // truth about what happened. An error reported with no code at all stays + // a plain Error — a ProviderRpcError whose `code` is undefined would + // advertise a conformance it does not have. `message` is untouched in + // every case. function toPageError(error) { const message = (error && error.message) || "Request failed"; if (error && error.code !== undefined && error.code !== null) { diff --git a/tests/inpageErrors.test.js b/tests/inpageErrors.test.js index c45446b..7498f74 100644 --- a/tests/inpageErrors.test.js +++ b/tests/inpageErrors.test.js @@ -54,6 +54,7 @@ class StubCustomEvent extends StubEvent { // through whatever arrived — but the cases below are the real ones. const REJECTED = 4001; // user rejected the request 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 // A stub window with the four things inpage.js touches: message listeners, @@ -115,6 +116,41 @@ async function rejectionFrom(start, response) { 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", () => { test("a user rejection arrives as code 4001", async () => { 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", () => { test("a coded error keeps the message byte for byte", async () => { const message =