All checks were successful
check / check (push) Successful in 28s
The provider rebuilt every rejection as a bare Error carrying only a message, so a dApp checking err.code === 4001 saw undefined and could not tell a user's deliberate refusal from a failure. Well-behaved sites therefore showed an error or retried instead of accepting the refusal. The code was produced correctly and did cross the extension boundary; it was lost in the last hop. Rejections now reach the page as a ProviderRpcError carrying code, and data where present. The code is passed through verbatim rather than matched against a whitelist, so a code added upstream later needs no change here. An error that genuinely has no code stays a plain Error with no code property at all, rather than advertising code: undefined -- 'code' in err is what a careful dApp asks. Messages are unchanged for every path, verified byte-for-byte against the previous provider across every background error shape. The end-to-end assertion that printed the observed code now requires it.
256 lines
8.9 KiB
JavaScript
256 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 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.
|
|
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", function onUuid(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", function onUuid(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 (e) {
|
|
// 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) check this to confirm MetaMask-like behavior
|
|
_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>",
|
|
);
|
|
|
|
let providerUuid = crypto.randomUUID(); // fallback until real UUID arrives
|
|
|
|
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,
|
|
}),
|
|
}),
|
|
);
|
|
}
|
|
|
|
// Listen for the persisted UUID from the content script
|
|
function onProviderUuid(event) {
|
|
if (event.source !== window) return;
|
|
if (event.data?.type !== "AUTISTMASK_PROVIDER_UUID") return;
|
|
window.removeEventListener("message", onProviderUuid);
|
|
providerUuid = event.data.uuid;
|
|
announceProvider();
|
|
}
|
|
window.addEventListener("message", onProviderUuid);
|
|
|
|
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.
|
|
});
|
|
})();
|