All checks were successful
check / check (push) Successful in 25s
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
862 lines
29 KiB
JavaScript
862 lines
29 KiB
JavaScript
// AutistMask background service worker
|
|
// Handles EIP-1193 RPC requests from content scripts and proxies
|
|
// non-sensitive calls to the configured Ethereum JSON-RPC endpoint.
|
|
|
|
const { DEFAULT_RPC_URL } = require("../shared/constants");
|
|
const { SUPPORTED_CHAIN_IDS, networkByChainId } = require("../shared/networks");
|
|
const { getBytes } = require("ethers");
|
|
const {
|
|
state,
|
|
loadState,
|
|
saveState,
|
|
currentNetwork,
|
|
} = require("../shared/state");
|
|
const { refreshBalances, getProvider } = require("../shared/balances");
|
|
const { debugFetch } = require("../shared/log");
|
|
const { decryptWithPassword } = require("../shared/vault");
|
|
const { getSignerForAddress } = require("../shared/wallet");
|
|
const {
|
|
isPhishingDomain,
|
|
updatePhishingList,
|
|
startPeriodicRefresh,
|
|
} = require("../shared/phishingDomains");
|
|
|
|
const storageApi =
|
|
typeof browser !== "undefined"
|
|
? browser.storage.local
|
|
: chrome.storage.local;
|
|
const runtime =
|
|
typeof browser !== "undefined" ? browser.runtime : chrome.runtime;
|
|
const windowsApi =
|
|
typeof browser !== "undefined" ? browser.windows : chrome.windows;
|
|
const tabsApi = typeof browser !== "undefined" ? browser.tabs : chrome.tabs;
|
|
const actionApi =
|
|
typeof browser !== "undefined" ? browser.browserAction : chrome.action;
|
|
|
|
// Connected sites (in-memory, non-persisted): { "origin:address": true }
|
|
const connectedSites = {};
|
|
|
|
// Pending approval requests: { id: { origin, hostname, resolve } }
|
|
const pendingApprovals = {};
|
|
|
|
async function getState() {
|
|
const result = await storageApi.get("autistmask");
|
|
return (
|
|
result.autistmask || {
|
|
wallets: [],
|
|
rpcUrl: DEFAULT_RPC_URL,
|
|
activeAddress: null,
|
|
allowedSites: {},
|
|
deniedSites: {},
|
|
}
|
|
);
|
|
}
|
|
|
|
async function getActiveAddress() {
|
|
const s = await getState();
|
|
if (s.activeAddress) return s.activeAddress;
|
|
// Fall back to first address
|
|
if (s.wallets.length > 0 && s.wallets[0].addresses.length > 0) {
|
|
return s.wallets[0].addresses[0].address;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async function getRpcUrl() {
|
|
const s = await getState();
|
|
return s.rpcUrl || DEFAULT_RPC_URL;
|
|
}
|
|
|
|
function extractHostname(origin) {
|
|
try {
|
|
return new URL(origin).hostname;
|
|
} catch {
|
|
return origin;
|
|
}
|
|
}
|
|
|
|
// Proxy an RPC call to the Ethereum node
|
|
async function proxyRpc(method, params) {
|
|
const rpcUrl = await getRpcUrl();
|
|
const resp = await debugFetch(rpcUrl, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method,
|
|
params,
|
|
}),
|
|
});
|
|
const json = await resp.json();
|
|
if (json.error) {
|
|
throw new Error(json.error.message || "RPC error");
|
|
}
|
|
return json.result;
|
|
}
|
|
|
|
function resetPopupUrl() {
|
|
if (actionApi && typeof actionApi.setPopup === "function") {
|
|
actionApi.setPopup({ popup: "src/popup/index.html" });
|
|
}
|
|
}
|
|
|
|
// Open approval in a separate popup window.
|
|
// This is the primary mechanism for tx/sign approvals (triggered programmatically,
|
|
// not from a user gesture) and the fallback for site-connection approvals.
|
|
function openApprovalWindow(id) {
|
|
const popupUrl = runtime.getURL("src/popup/index.html?approval=" + id);
|
|
const popupWidth = 360;
|
|
const popupHeight = 600;
|
|
|
|
windowsApi.getLastFocused((currentWin) => {
|
|
const opts = {
|
|
url: popupUrl,
|
|
type: "popup",
|
|
width: popupWidth,
|
|
height: popupHeight,
|
|
};
|
|
if (currentWin) {
|
|
opts.left = Math.round(
|
|
currentWin.left + (currentWin.width - popupWidth) / 2,
|
|
);
|
|
opts.top = Math.round(
|
|
currentWin.top + (currentWin.height - popupHeight) / 2,
|
|
);
|
|
}
|
|
windowsApi.create(opts, (win) => {
|
|
if (win) {
|
|
pendingApprovals[id].windowId = win.id;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Open an approval popup and return a promise that resolves with the user decision.
|
|
// Prefers the browser-action popup (anchored to toolbar, no macOS Space switch).
|
|
function requestApproval(origin, hostname) {
|
|
return new Promise((resolve) => {
|
|
const id = crypto.randomUUID();
|
|
pendingApprovals[id] = { origin, hostname, resolve };
|
|
|
|
if (actionApi && typeof actionApi.openPopup === "function") {
|
|
actionApi.setPopup({
|
|
popup: "src/popup/index.html?approval=" + id,
|
|
});
|
|
try {
|
|
const result = actionApi.openPopup();
|
|
if (result && typeof result.catch === "function") {
|
|
result.catch(() => openApprovalWindow(id));
|
|
}
|
|
} catch {
|
|
openApprovalWindow(id);
|
|
}
|
|
} else {
|
|
openApprovalWindow(id);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Open a tx-approval popup and return a promise that resolves with txHash or error.
|
|
// Uses windows.create() directly because tx approvals are triggered programmatically
|
|
// (from a dApp RPC call), not from a user gesture, so action.openPopup() is
|
|
// unreliable in this context.
|
|
function requestTxApproval(origin, hostname, txParams) {
|
|
return new Promise((resolve) => {
|
|
const id = crypto.randomUUID();
|
|
pendingApprovals[id] = {
|
|
origin,
|
|
hostname,
|
|
txParams,
|
|
resolve,
|
|
type: "tx",
|
|
};
|
|
|
|
openApprovalWindow(id);
|
|
});
|
|
}
|
|
|
|
// Open a sign-approval popup and return a promise that resolves with { signature } or { error }.
|
|
// Uses windows.create() directly because sign approvals are triggered programmatically
|
|
// (from a dApp RPC call), not from a user gesture, so action.openPopup() is
|
|
// unreliable in this context.
|
|
function requestSignApproval(origin, hostname, signParams) {
|
|
return new Promise((resolve) => {
|
|
const id = crypto.randomUUID();
|
|
pendingApprovals[id] = {
|
|
origin,
|
|
hostname,
|
|
signParams,
|
|
resolve,
|
|
type: "sign",
|
|
};
|
|
|
|
openApprovalWindow(id);
|
|
});
|
|
}
|
|
|
|
// Detect when an approval popup (browser-action) closes without a response.
|
|
// TX and sign approvals now use windows.create() and are handled by the
|
|
// windowsApi.onRemoved listener below, but we still handle site-connection
|
|
// approval disconnects here.
|
|
runtime.onConnect.addListener((port) => {
|
|
if (port.name.startsWith("approval:")) {
|
|
const id = port.name.split(":")[1];
|
|
port.onDisconnect.addListener(() => {
|
|
const approval = pendingApprovals[id];
|
|
if (approval) {
|
|
if (approval.type === "tx" || approval.type === "sign") {
|
|
// Keep pending — user can reopen the toolbar popup
|
|
return;
|
|
}
|
|
approval.resolve({ approved: false, remember: false });
|
|
delete pendingApprovals[id];
|
|
}
|
|
resetPopupUrl();
|
|
});
|
|
}
|
|
});
|
|
|
|
// Handle connection requests (eth_requestAccounts, wallet_requestPermissions)
|
|
async function handleConnectionRequest(origin) {
|
|
const s = await getState();
|
|
const activeAddress = await getActiveAddress();
|
|
if (!activeAddress) {
|
|
return { error: { message: "No accounts available" } };
|
|
}
|
|
|
|
const hostname = extractHostname(origin);
|
|
const allowed = s.allowedSites[activeAddress] || [];
|
|
const denied = s.deniedSites[activeAddress] || [];
|
|
|
|
// Check denied list
|
|
if (denied.includes(hostname)) {
|
|
return {
|
|
error: {
|
|
code: 4001,
|
|
message: "User rejected the request.",
|
|
},
|
|
};
|
|
}
|
|
|
|
// Check allowed list or in-memory connected
|
|
if (
|
|
allowed.includes(hostname) ||
|
|
connectedSites[origin + ":" + activeAddress]
|
|
) {
|
|
return { result: [activeAddress] };
|
|
}
|
|
|
|
// Open approval popup
|
|
const decision = await requestApproval(origin, hostname);
|
|
|
|
if (decision.approved) {
|
|
if (decision.remember) {
|
|
// Reload state to get latest, add to allowed, persist
|
|
await loadState();
|
|
if (!state.allowedSites[activeAddress]) {
|
|
state.allowedSites[activeAddress] = [];
|
|
}
|
|
if (!state.allowedSites[activeAddress].includes(hostname)) {
|
|
state.allowedSites[activeAddress].push(hostname);
|
|
}
|
|
await saveState();
|
|
} else {
|
|
connectedSites[origin + ":" + activeAddress] = true;
|
|
}
|
|
return { result: [activeAddress] };
|
|
} else {
|
|
if (decision.remember) {
|
|
await loadState();
|
|
if (!state.deniedSites[activeAddress]) {
|
|
state.deniedSites[activeAddress] = [];
|
|
}
|
|
if (!state.deniedSites[activeAddress].includes(hostname)) {
|
|
state.deniedSites[activeAddress].push(hostname);
|
|
}
|
|
await saveState();
|
|
}
|
|
return {
|
|
error: {
|
|
code: 4001,
|
|
message: "User rejected the request.",
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
// Methods that are safe to proxy directly to the RPC node
|
|
const PROXY_METHODS = [
|
|
"eth_blockNumber",
|
|
"eth_call",
|
|
"eth_chainId",
|
|
"eth_estimateGas",
|
|
"eth_gasPrice",
|
|
"eth_getBalance",
|
|
"eth_getBlockByHash",
|
|
"eth_getBlockByNumber",
|
|
"eth_getCode",
|
|
"eth_getLogs",
|
|
"eth_getStorageAt",
|
|
"eth_getTransactionByHash",
|
|
"eth_getTransactionCount",
|
|
"eth_getTransactionReceipt",
|
|
"eth_maxPriorityFeePerGas",
|
|
"eth_sendRawTransaction",
|
|
"net_version",
|
|
"web3_clientVersion",
|
|
"eth_feeHistory",
|
|
"eth_getBlockTransactionCountByHash",
|
|
"eth_getBlockTransactionCountByNumber",
|
|
];
|
|
|
|
async function handleRpc(method, params, origin) {
|
|
// Connection requests — go through approval flow
|
|
if (method === "eth_requestAccounts") {
|
|
return handleConnectionRequest(origin);
|
|
}
|
|
|
|
if (method === "eth_accounts") {
|
|
const s = await getState();
|
|
const activeAddress = await getActiveAddress();
|
|
if (!activeAddress) return { result: [] };
|
|
const hostname = extractHostname(origin);
|
|
const allowed = s.allowedSites[activeAddress] || [];
|
|
if (
|
|
allowed.includes(hostname) ||
|
|
connectedSites[origin + ":" + activeAddress]
|
|
) {
|
|
return { result: [activeAddress] };
|
|
}
|
|
return { result: [] };
|
|
}
|
|
|
|
if (method === "eth_chainId") {
|
|
return { result: currentNetwork().chainId };
|
|
}
|
|
|
|
if (method === "net_version") {
|
|
return { result: currentNetwork().networkVersion };
|
|
}
|
|
|
|
if (method === "wallet_switchEthereumChain") {
|
|
const chainId = params?.[0]?.chainId;
|
|
if (chainId === currentNetwork().chainId) {
|
|
return { result: null };
|
|
}
|
|
if (SUPPORTED_CHAIN_IDS.has(chainId)) {
|
|
// Switch to the requested network
|
|
const target = networkByChainId(chainId);
|
|
state.networkId = target.id;
|
|
state.rpcUrl = target.defaultRpcUrl;
|
|
state.blockscoutUrl = target.defaultBlockscoutUrl;
|
|
await saveState();
|
|
broadcastChainChanged(target.chainId);
|
|
return { result: null };
|
|
}
|
|
return {
|
|
error: {
|
|
code: 4902,
|
|
message:
|
|
"AutistMask supports Ethereum Mainnet and Sepolia Testnet only.",
|
|
},
|
|
};
|
|
}
|
|
|
|
if (method === "wallet_addEthereumChain") {
|
|
const chainId = params?.[0]?.chainId;
|
|
if (SUPPORTED_CHAIN_IDS.has(chainId)) {
|
|
return { result: null };
|
|
}
|
|
return {
|
|
error: {
|
|
code: 4902,
|
|
message:
|
|
"AutistMask supports Ethereum Mainnet and Sepolia Testnet only.",
|
|
},
|
|
};
|
|
}
|
|
|
|
if (method === "wallet_requestPermissions") {
|
|
const connResult = await handleConnectionRequest(origin);
|
|
if (connResult.error) return connResult;
|
|
return {
|
|
result: [
|
|
{
|
|
parentCapability: "eth_accounts",
|
|
caveats: [
|
|
{
|
|
type: "restrictReturnedAccounts",
|
|
value: connResult.result,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
if (method === "wallet_getPermissions") {
|
|
const s = await getState();
|
|
const activeAddress = await getActiveAddress();
|
|
const hostname = extractHostname(origin);
|
|
const allowed = s.allowedSites[activeAddress] || [];
|
|
const isConnected =
|
|
allowed.includes(hostname) ||
|
|
connectedSites[origin + ":" + activeAddress];
|
|
if (!isConnected || !activeAddress) {
|
|
return { result: [] };
|
|
}
|
|
return {
|
|
result: [
|
|
{
|
|
parentCapability: "eth_accounts",
|
|
caveats: [
|
|
{
|
|
type: "restrictReturnedAccounts",
|
|
value: [activeAddress],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
if (method === "personal_sign" || method === "eth_sign") {
|
|
const s = await getState();
|
|
const activeAddress = await getActiveAddress();
|
|
if (!activeAddress)
|
|
return { error: { message: "No accounts available" } };
|
|
|
|
const hostname = extractHostname(origin);
|
|
const allowed = s.allowedSites[activeAddress] || [];
|
|
if (
|
|
!allowed.includes(hostname) &&
|
|
!connectedSites[origin + ":" + activeAddress]
|
|
) {
|
|
return { error: { code: 4100, message: "Unauthorized" } };
|
|
}
|
|
|
|
// personal_sign: params[0]=message, params[1]=address
|
|
// eth_sign: params[0]=address, params[1]=message
|
|
const signParams =
|
|
method === "personal_sign"
|
|
? { method, message: params[0], from: params[1] }
|
|
: { method, message: params[1], from: params[0] };
|
|
|
|
if (method === "eth_sign") {
|
|
signParams.dangerWarning =
|
|
"\u26a0\ufe0f DANGER: This site is requesting to sign a raw hash. " +
|
|
"This can be used to sign transactions that drain your funds. " +
|
|
"Only proceed if you fully understand what you are signing.";
|
|
}
|
|
|
|
const decision = await requestSignApproval(
|
|
origin,
|
|
hostname,
|
|
signParams,
|
|
);
|
|
if (decision.error) return { error: decision.error };
|
|
return { result: decision.signature };
|
|
}
|
|
|
|
if (method === "eth_signTypedData_v4" || method === "eth_signTypedData") {
|
|
const s = await getState();
|
|
const activeAddress = await getActiveAddress();
|
|
if (!activeAddress)
|
|
return { error: { message: "No accounts available" } };
|
|
|
|
const hostname = extractHostname(origin);
|
|
const allowed = s.allowedSites[activeAddress] || [];
|
|
if (
|
|
!allowed.includes(hostname) &&
|
|
!connectedSites[origin + ":" + activeAddress]
|
|
) {
|
|
return { error: { code: 4100, message: "Unauthorized" } };
|
|
}
|
|
|
|
const signParams = { method, typedData: params[1], from: params[0] };
|
|
const decision = await requestSignApproval(
|
|
origin,
|
|
hostname,
|
|
signParams,
|
|
);
|
|
if (decision.error) return { error: decision.error };
|
|
return { result: decision.signature };
|
|
}
|
|
|
|
if (method === "eth_sendTransaction") {
|
|
const s = await getState();
|
|
const activeAddress = await getActiveAddress();
|
|
if (!activeAddress)
|
|
return { error: { message: "No accounts available" } };
|
|
|
|
const hostname = extractHostname(origin);
|
|
const allowed = s.allowedSites[activeAddress] || [];
|
|
if (
|
|
!allowed.includes(hostname) &&
|
|
!connectedSites[origin + ":" + activeAddress]
|
|
) {
|
|
return { error: { code: 4100, message: "Unauthorized" } };
|
|
}
|
|
|
|
const txParams = params?.[0] || {};
|
|
const decision = await requestTxApproval(origin, hostname, txParams);
|
|
if (decision.error) return { error: decision.error };
|
|
return { result: decision.txHash };
|
|
}
|
|
|
|
// Proxy safe read-only methods to the RPC node
|
|
if (PROXY_METHODS.includes(method)) {
|
|
try {
|
|
const result = await proxyRpc(method, params);
|
|
return { result };
|
|
} catch (e) {
|
|
return { error: { message: e.message } };
|
|
}
|
|
}
|
|
|
|
return { error: { message: "Unsupported method: " + method } };
|
|
}
|
|
|
|
// Broadcast chainChanged to all tabs when the network is switched.
|
|
function broadcastChainChanged(chainId) {
|
|
tabsApi.query({}, (tabs) => {
|
|
for (const tab of tabs) {
|
|
tabsApi.sendMessage(
|
|
tab.id,
|
|
{
|
|
type: "AUTISTMASK_EVENT",
|
|
eventName: "chainChanged",
|
|
data: chainId,
|
|
},
|
|
() => {
|
|
if (runtime.lastError) {
|
|
// expected for tabs without our content script
|
|
}
|
|
},
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Broadcast accountsChanged to all tabs, respecting per-address permissions
|
|
async function broadcastAccountsChanged() {
|
|
// Clear non-remembered approvals on address switch
|
|
for (const key of Object.keys(connectedSites)) {
|
|
delete connectedSites[key];
|
|
}
|
|
// Reject and close any pending approval popups so they don't hang
|
|
for (const [id, approval] of Object.entries(pendingApprovals)) {
|
|
if (approval.type === "tx" || approval.type === "sign") {
|
|
approval.resolve({
|
|
error: { code: 4001, message: "User rejected the request." },
|
|
});
|
|
} else {
|
|
approval.resolve({ approved: false, remember: false });
|
|
}
|
|
if (approval.windowId) {
|
|
windowsApi.remove(approval.windowId, () => {
|
|
if (runtime.lastError) {
|
|
// window already closed
|
|
}
|
|
});
|
|
}
|
|
delete pendingApprovals[id];
|
|
}
|
|
resetPopupUrl();
|
|
const s = await getState();
|
|
const activeAddress = await getActiveAddress();
|
|
const allowed = activeAddress ? s.allowedSites[activeAddress] || [] : [];
|
|
tabsApi.query({}, (tabs) => {
|
|
for (const tab of tabs) {
|
|
const origin = tab.url ? new URL(tab.url).origin : "";
|
|
const hostname = extractHostname(origin);
|
|
const hasPermission =
|
|
activeAddress &&
|
|
(allowed.includes(hostname) ||
|
|
connectedSites[origin + ":" + activeAddress]);
|
|
tabsApi.sendMessage(
|
|
tab.id,
|
|
{
|
|
type: "AUTISTMASK_EVENT",
|
|
eventName: "accountsChanged",
|
|
data: hasPermission ? [activeAddress] : [],
|
|
},
|
|
() => {
|
|
// Ignore errors for tabs without content script
|
|
if (runtime.lastError) {
|
|
// expected for tabs without our content script
|
|
}
|
|
},
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Background balance refresh: every 60 seconds when the popup isn't open.
|
|
// When the popup IS open, its 10-second interval keeps lastBalanceRefresh
|
|
// fresh, so this naturally skips.
|
|
const BACKGROUND_REFRESH_INTERVAL = 60000;
|
|
|
|
async function backgroundRefresh() {
|
|
await loadState();
|
|
const now = Date.now();
|
|
if (now - (state.lastBalanceRefresh || 0) < BACKGROUND_REFRESH_INTERVAL)
|
|
return;
|
|
if (state.wallets.length === 0) return;
|
|
await refreshBalances(
|
|
state.wallets,
|
|
state.rpcUrl,
|
|
state.blockscoutUrl,
|
|
state.trackedTokens,
|
|
);
|
|
state.lastBalanceRefresh = now;
|
|
await saveState();
|
|
}
|
|
|
|
setInterval(backgroundRefresh, BACKGROUND_REFRESH_INTERVAL);
|
|
|
|
// Fetch the phishing domain blocklist delta on startup and refresh every 24h.
|
|
// The vendored blocklist is bundled at build time; this fetches only new entries.
|
|
updatePhishingList();
|
|
startPeriodicRefresh();
|
|
|
|
// When approval window is closed without a response, treat as rejection
|
|
if (windowsApi && windowsApi.onRemoved) {
|
|
windowsApi.onRemoved.addListener((windowId) => {
|
|
for (const [id, approval] of Object.entries(pendingApprovals)) {
|
|
if (approval.windowId === windowId) {
|
|
if (approval.type === "tx" || approval.type === "sign") {
|
|
approval.resolve({
|
|
error: {
|
|
code: 4001,
|
|
message: "User rejected the request.",
|
|
},
|
|
});
|
|
} else {
|
|
approval.resolve({ approved: false, remember: false });
|
|
}
|
|
delete pendingApprovals[id];
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Listen for messages from content scripts and popup
|
|
runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|
if (msg.type === "AUTISTMASK_RPC") {
|
|
// Derive origin from trusted sender info to prevent origin spoofing.
|
|
// Chrome MV3 provides sender.origin; Firefox MV2 fallback uses sender.tab.url.
|
|
let trustedOrigin = msg.origin; // fallback only if sender info unavailable
|
|
if (sender.origin) {
|
|
trustedOrigin = sender.origin;
|
|
} else if (sender.tab && sender.tab.url) {
|
|
try {
|
|
trustedOrigin = new URL(sender.tab.url).origin;
|
|
} catch {
|
|
// keep fallback
|
|
}
|
|
}
|
|
handleRpc(msg.method, msg.params, trustedOrigin).then((response) => {
|
|
sendResponse(response);
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// Validate that popup-only messages originate from the extension itself.
|
|
const POPUP_ONLY_TYPES = [
|
|
"AUTISTMASK_GET_APPROVAL",
|
|
"AUTISTMASK_APPROVAL_RESPONSE",
|
|
"AUTISTMASK_TX_RESPONSE",
|
|
"AUTISTMASK_SIGN_RESPONSE",
|
|
];
|
|
if (POPUP_ONLY_TYPES.includes(msg.type)) {
|
|
const extUrl = runtime.getURL("");
|
|
if (!sender.url || !sender.url.startsWith(extUrl)) {
|
|
sendResponse({ error: "Unauthorized sender" });
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (msg.type === "AUTISTMASK_GET_APPROVAL") {
|
|
const approval = pendingApprovals[msg.id];
|
|
if (approval) {
|
|
const resp = {
|
|
hostname: approval.hostname,
|
|
origin: approval.origin,
|
|
};
|
|
if (approval.type === "tx") {
|
|
resp.type = "tx";
|
|
resp.txParams = approval.txParams;
|
|
}
|
|
if (approval.type === "sign") {
|
|
resp.type = "sign";
|
|
resp.signParams = approval.signParams;
|
|
}
|
|
// Flag if the requesting domain is on the phishing blocklist.
|
|
resp.isPhishingDomain = isPhishingDomain(approval.hostname);
|
|
sendResponse(resp);
|
|
} else {
|
|
sendResponse(null);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (msg.type === "AUTISTMASK_APPROVAL_RESPONSE") {
|
|
const approval = pendingApprovals[msg.id];
|
|
if (approval) {
|
|
approval.resolve({
|
|
approved: msg.approved,
|
|
remember: msg.remember,
|
|
});
|
|
delete pendingApprovals[msg.id];
|
|
}
|
|
resetPopupUrl();
|
|
return false;
|
|
}
|
|
|
|
if (msg.type === "AUTISTMASK_TX_RESPONSE") {
|
|
const approval = pendingApprovals[msg.id];
|
|
if (!approval) return false;
|
|
delete pendingApprovals[msg.id];
|
|
resetPopupUrl();
|
|
|
|
if (!msg.approved) {
|
|
approval.resolve({
|
|
error: { code: 4001, message: "User rejected the request." },
|
|
});
|
|
return true;
|
|
}
|
|
|
|
(async () => {
|
|
try {
|
|
await loadState();
|
|
const activeAddress = await getActiveAddress();
|
|
let wallet, addrIndex;
|
|
for (const w of state.wallets) {
|
|
for (let i = 0; i < w.addresses.length; i++) {
|
|
if (w.addresses[i].address === activeAddress) {
|
|
wallet = w;
|
|
addrIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
if (wallet) break;
|
|
}
|
|
if (!wallet) throw new Error("Wallet not found");
|
|
// TODO(security): Move decryption to popup to avoid sending password via runtime.sendMessage
|
|
let decrypted = await decryptWithPassword(
|
|
wallet.encryptedSecret,
|
|
msg.password,
|
|
);
|
|
const signer = getSignerForAddress(
|
|
wallet,
|
|
addrIndex,
|
|
decrypted,
|
|
);
|
|
// Best-effort: clear decrypted secret after use.
|
|
// Note: JS strings are immutable; this nulls the reference but
|
|
// the original string may persist in memory until GC.
|
|
decrypted = null;
|
|
const provider = getProvider(state.rpcUrl);
|
|
const connected = signer.connect(provider);
|
|
const tx = await connected.sendTransaction(approval.txParams);
|
|
approval.resolve({ txHash: tx.hash });
|
|
sendResponse({ txHash: tx.hash });
|
|
} catch (e) {
|
|
const errMsg = e.shortMessage || e.message;
|
|
approval.resolve({
|
|
error: { message: errMsg },
|
|
});
|
|
sendResponse({ error: errMsg });
|
|
}
|
|
})();
|
|
return true;
|
|
}
|
|
|
|
if (msg.type === "AUTISTMASK_SIGN_RESPONSE") {
|
|
const approval = pendingApprovals[msg.id];
|
|
if (!approval) return false;
|
|
delete pendingApprovals[msg.id];
|
|
resetPopupUrl();
|
|
|
|
if (!msg.approved) {
|
|
approval.resolve({
|
|
error: { code: 4001, message: "User rejected the request." },
|
|
});
|
|
return true;
|
|
}
|
|
|
|
(async () => {
|
|
try {
|
|
await loadState();
|
|
const activeAddress = await getActiveAddress();
|
|
let wallet, addrIndex;
|
|
for (const w of state.wallets) {
|
|
for (let i = 0; i < w.addresses.length; i++) {
|
|
if (w.addresses[i].address === activeAddress) {
|
|
wallet = w;
|
|
addrIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
if (wallet) break;
|
|
}
|
|
if (!wallet) throw new Error("Wallet not found");
|
|
// TODO(security): Move decryption to popup to avoid sending password via runtime.sendMessage
|
|
let decrypted = await decryptWithPassword(
|
|
wallet.encryptedSecret,
|
|
msg.password,
|
|
);
|
|
const signer = getSignerForAddress(
|
|
wallet,
|
|
addrIndex,
|
|
decrypted,
|
|
);
|
|
// Best-effort: clear decrypted secret after use.
|
|
// Note: JS strings are immutable; this nulls the reference but
|
|
// the original string may persist in memory until GC.
|
|
decrypted = null;
|
|
|
|
const sp = approval.signParams;
|
|
let signature;
|
|
|
|
if (sp.method === "personal_sign" || sp.method === "eth_sign") {
|
|
signature = await signer.signMessage(getBytes(sp.message));
|
|
} else {
|
|
// eth_signTypedData_v4 / eth_signTypedData
|
|
const typedData = JSON.parse(sp.typedData);
|
|
const { domain, types, message } = typedData;
|
|
// ethers handles EIP712Domain internally
|
|
delete types.EIP712Domain;
|
|
signature = await signer.signTypedData(
|
|
domain,
|
|
types,
|
|
message,
|
|
);
|
|
}
|
|
|
|
approval.resolve({ signature });
|
|
sendResponse({ signature });
|
|
} catch (e) {
|
|
const errMsg = e.shortMessage || e.message;
|
|
approval.resolve({
|
|
error: { message: errMsg },
|
|
});
|
|
sendResponse({ error: errMsg });
|
|
}
|
|
})();
|
|
return true;
|
|
}
|
|
|
|
if (msg.type === "AUTISTMASK_ACTIVE_CHANGED") {
|
|
broadcastAccountsChanged();
|
|
return false;
|
|
}
|
|
|
|
if (msg.type === "AUTISTMASK_REMOVE_SITE") {
|
|
// Popup already saved state; nothing else needed
|
|
return false;
|
|
}
|
|
});
|