From 4b62e31e806c64427cfc71b2787e7405620ca4ee Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Sun, 4 Oct 2026 09:24:41 +0200 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 | 67 +++++++++++++++++++++++++++++++++++--- 4 files changed, 78 insertions(+), 11 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..f6af1d3 100644 --- a/tests/inpageErrors.test.js +++ b/tests/inpageErrors.test.js @@ -49,11 +49,12 @@ class StubCustomEvent extends StubEvent { } } -// Every code the background emits on the RPC path today, read out of -// src/background/index.js. The provider must not know this list — it passes -// through whatever arrived — but the cases below are the real ones. +// Examples of codes the background emits on the RPC path, read out of +// src/background/index.js. The provider must not know any list of codes — it +// passes through whatever arrived — but the cases below are 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( @@ -164,8 +200,9 @@ describe("an EIP-1193 code reaches the page", () => { expect(err.message).toBe(message); }); - // The provider is not allowed to know the list above: a code added to the - // background later must reach the page without this file being edited. + // The provider is not allowed to know the codes above: any other code, + // including one added to the background later, must reach the page + // without inpage.js being edited. test("a code the provider has never heard of is passed through", async () => { const err = await rejectionFrom( (p) => p.request({ method: "eth_accounts" }), @@ -203,6 +240,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 =