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
254 lines
8.9 KiB
JavaScript
254 lines
8.9 KiB
JavaScript
// AutistMask inpage script — injected into the page's JS context.
|
|
// Creates window.ethereum (EIP-1193 provider) and announces via EIP-6963.
|
|
|
|
(function () {
|
|
// Defaults to mainnet; updated dynamically via eth_chainId on init and
|
|
// chainChanged events from the extension.
|
|
let currentChainId = "0x1";
|
|
let currentNetworkVersion = "1";
|
|
|
|
const listeners = {};
|
|
let nextId = 1;
|
|
const pending = {};
|
|
|
|
// EIP-1193 ProviderRpcError: `code`, `message`, optional `data`. A class
|
|
// rather than properties bolted onto an Error because this object crosses
|
|
// no boundary after construction — it is built in the page's own realm and
|
|
// handed straight to the caller's catch — so the prototype survives and
|
|
// `error.name` is a stable thing for a dApp to see.
|
|
class ProviderRpcError extends Error {
|
|
constructor(code, message, data) {
|
|
super(message);
|
|
this.name = "ProviderRpcError";
|
|
this.code = code;
|
|
if (data !== undefined) this.data = data;
|
|
}
|
|
}
|
|
|
|
// Rebuild a boundary error as the error the page catches, carrying the
|
|
// code (and data) the extension reported. Without this a dApp cannot tell
|
|
// a user's refusal (4001) from a wallet that broke, and retries or shows
|
|
// an error instead of accepting the refusal.
|
|
//
|
|
// Whatever code arrived is passed through verbatim rather than being
|
|
// 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) {
|
|
return new ProviderRpcError(error.code, message, error.data);
|
|
}
|
|
return new Error(message);
|
|
}
|
|
|
|
// Listen for responses from the content script
|
|
window.addEventListener("message", (event) => {
|
|
if (event.source !== window) return;
|
|
if (event.data?.type !== "AUTISTMASK_RESPONSE") return;
|
|
const { id, result, error } = event.data;
|
|
const p = pending[id];
|
|
if (!p) return;
|
|
delete pending[id];
|
|
if (error) {
|
|
p.reject(toPageError(error));
|
|
} else {
|
|
p.resolve(result);
|
|
}
|
|
});
|
|
|
|
// Listen for events pushed from the extension
|
|
window.addEventListener("message", (event) => {
|
|
if (event.source !== window) return;
|
|
if (event.data?.type !== "AUTISTMASK_EVENT") return;
|
|
const { eventName, data } = event.data;
|
|
if (eventName === "chainChanged") {
|
|
currentChainId = data;
|
|
currentNetworkVersion = String(parseInt(data, 16));
|
|
provider.chainId = currentChainId;
|
|
provider.networkVersion = currentNetworkVersion;
|
|
}
|
|
emit(eventName, data);
|
|
});
|
|
|
|
function emit(eventName, data) {
|
|
const cbs = listeners[eventName];
|
|
if (!cbs) return;
|
|
for (const cb of cbs) {
|
|
try {
|
|
cb(data);
|
|
} catch {
|
|
// ignore listener errors
|
|
}
|
|
}
|
|
}
|
|
|
|
function sendRequest(args) {
|
|
return new Promise((resolve, reject) => {
|
|
const id = nextId++;
|
|
pending[id] = { resolve, reject };
|
|
window.postMessage(
|
|
{ type: "AUTISTMASK_REQUEST", id, ...args },
|
|
"*",
|
|
);
|
|
});
|
|
}
|
|
|
|
const provider = {
|
|
isAutistMask: true,
|
|
isMetaMask: true, // compatibility — many dApps check this
|
|
chainId: currentChainId,
|
|
networkVersion: currentNetworkVersion,
|
|
selectedAddress: null,
|
|
|
|
async request(args) {
|
|
const result = await sendRequest({
|
|
method: args.method,
|
|
params: args.params || [],
|
|
});
|
|
if (
|
|
args.method === "eth_requestAccounts" ||
|
|
args.method === "eth_accounts"
|
|
) {
|
|
provider.selectedAddress =
|
|
Array.isArray(result) && result.length > 0
|
|
? result[0]
|
|
: null;
|
|
}
|
|
if (args.method === "eth_chainId" && result) {
|
|
currentChainId = result;
|
|
currentNetworkVersion = String(parseInt(result, 16));
|
|
provider.chainId = currentChainId;
|
|
provider.networkVersion = currentNetworkVersion;
|
|
}
|
|
return result;
|
|
},
|
|
|
|
// Legacy methods (still used by some dApps)
|
|
enable() {
|
|
return this.request({ method: "eth_requestAccounts" });
|
|
},
|
|
|
|
send(methodOrPayload, paramsOrCallback) {
|
|
// Handle both send(method, params) and send({method, params})
|
|
if (typeof methodOrPayload === "string") {
|
|
return this.request({
|
|
method: methodOrPayload,
|
|
params: paramsOrCallback || [],
|
|
});
|
|
}
|
|
return this.request({
|
|
method: methodOrPayload.method,
|
|
params: methodOrPayload.params || [],
|
|
});
|
|
},
|
|
|
|
sendAsync(payload, callback) {
|
|
this.request({
|
|
method: payload.method,
|
|
params: payload.params || [],
|
|
})
|
|
.then((result) =>
|
|
callback(null, { id: payload.id, jsonrpc: "2.0", result }),
|
|
)
|
|
.catch((err) => callback(err));
|
|
},
|
|
|
|
on(event, cb) {
|
|
if (!listeners[event]) listeners[event] = [];
|
|
listeners[event].push(cb);
|
|
return this;
|
|
},
|
|
|
|
removeListener(event, cb) {
|
|
if (!listeners[event]) return this;
|
|
listeners[event] = listeners[event].filter((c) => c !== cb);
|
|
return this;
|
|
},
|
|
|
|
removeAllListeners(event) {
|
|
if (event) {
|
|
delete listeners[event];
|
|
} else {
|
|
for (const key of Object.keys(listeners)) {
|
|
delete listeners[key];
|
|
}
|
|
}
|
|
return this;
|
|
},
|
|
|
|
// Some dApps (wagmi) probe this object to decide whether the provider
|
|
// supports the de-facto standard extras. The name is theirs, not ours.
|
|
_metamask: {
|
|
isUnlocked() {
|
|
return Promise.resolve(provider.selectedAddress !== null);
|
|
},
|
|
},
|
|
};
|
|
|
|
// Set window.ethereum if no other wallet has claimed it
|
|
if (typeof window.ethereum === "undefined") {
|
|
window.ethereum = provider;
|
|
}
|
|
window.dispatchEvent(new Event("ethereum#initialized"));
|
|
|
|
// EIP-6963: Multi Injected Provider Discovery
|
|
const ICON_SVG =
|
|
"data:image/svg+xml," +
|
|
encodeURIComponent(
|
|
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">' +
|
|
'<rect width="32" height="32" rx="6" fill="#000"/>' +
|
|
'<text x="16" y="23" text-anchor="middle" font-family="monospace" font-size="20" font-weight="bold" fill="#fff">A</text>' +
|
|
"</svg>",
|
|
);
|
|
|
|
// EIP-6963 asks for one UUIDv4 per provider for the life of the page: one
|
|
// per page load, shared by every announcement in that load. It is
|
|
// generated here and never stored: announcing one persisted value to every
|
|
// site, on every load and across restarts, turned it into a stable
|
|
// cross-site, cross-session tracking identifier any page could read
|
|
// (https://git.eeqj.de/sneak/AutistMask/issues/398).
|
|
const providerUuid = crypto.randomUUID();
|
|
|
|
function buildProviderInfo() {
|
|
return {
|
|
uuid: providerUuid,
|
|
name: "AutistMask",
|
|
icon: ICON_SVG,
|
|
rdns: "berlin.sneak.autistmask",
|
|
};
|
|
}
|
|
|
|
function announceProvider() {
|
|
window.dispatchEvent(
|
|
new CustomEvent("eip6963:announceProvider", {
|
|
detail: Object.freeze({
|
|
info: buildProviderInfo(),
|
|
provider,
|
|
}),
|
|
}),
|
|
);
|
|
}
|
|
|
|
window.addEventListener("eip6963:requestProvider", announceProvider);
|
|
announceProvider();
|
|
|
|
// Fetch the current chain ID from the extension on load so the provider
|
|
// reflects the selected network immediately (covers Sepolia etc.).
|
|
sendRequest({ method: "eth_chainId", params: [] })
|
|
.then((chainId) => {
|
|
if (chainId) {
|
|
currentChainId = chainId;
|
|
currentNetworkVersion = String(parseInt(chainId, 16));
|
|
provider.chainId = currentChainId;
|
|
provider.networkVersion = currentNetworkVersion;
|
|
}
|
|
})
|
|
.catch(() => {
|
|
// Best-effort — keep defaults.
|
|
});
|
|
})();
|