Files
AutistMask/src/content/inpage.js
user d4f2f34c89
All checks were successful
check / check (push) Successful in 25s
feat: add Sepolia testnet support
Add support for the Sepolia testnet alongside Ethereum mainnet:

- New src/shared/networks.js with network definitions (mainnet + sepolia)
  including chain IDs, default RPC/Blockscout endpoints, and explorer URLs
- State now tracks networkId; defaults to mainnet for backward compatibility
- Network selector in Settings lets users switch between mainnet and Sepolia
- Switching networks updates RPC URL, Blockscout URL, and chain ID
- All hardcoded etherscan.io URLs replaced with dynamic explorer links
  from the current network config (sepolia.etherscan.io for Sepolia)
- Background handles wallet_switchEthereumChain for both supported chains
  and broadcasts chainChanged events to connected dApps
- Inpage provider fetches chain ID on init and updates dynamically via
  chainChanged events (no more hardcoded 0x1)
- Blockscout API uses eth-sepolia.blockscout.com for Sepolia
- Etherscan label/phishing checks use the correct explorer per network
- Price fetching skipped on testnets (tokens have no real value)
- RPC validation checks against the selected network's chain ID
- getProvider() uses the correct ethers Network for Sepolia

API endpoints verified:
- Etherscan: sepolia.etherscan.io
- Blockscout: eth-sepolia.blockscout.com/api/v2
- RPC: ethereum-sepolia-rpc.publicnode.com

closes #110
2026-03-01 10:45:21 -08:00

223 lines
7.3 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 = {};
// 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(new Error(error.message || "Request failed"));
} 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.
});
})();