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..2a9da49 100644 --- a/src/content/inpage.js +++ b/src/content/inpage.js @@ -31,7 +31,7 @@ // 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, + // matched against a list: the extension emits 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 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 =