// Etherscan address label lookup via page scraping. // Extension users make the requests directly to Etherscan — no proxy needed. // This is a best-effort enrichment: network failures return null silently. // Patterns in the page title that indicate a flagged address. // Title format: "Fake_Phishing184810 | Address: 0x... | Etherscan" const PHISHING_LABEL_PATTERNS = [/^Fake_Phishing/i, /^Phish:/i, /^Exploiter/i]; // Patterns in the page body that indicate a scam/phishing warning. const SCAM_BODY_PATTERNS = [ /used in a\s+(?:\w+\s+)?phishing scam/i, /used in a\s+(?:\w+\s+)?scam/i, /wallet\s+drainer/i, ]; /** * Parse the Etherscan address page HTML to extract label info. * Exported for unit testing (no fetch needed). * * @param {string} html - Raw HTML of the Etherscan address page. * @returns {{ label: string|null, isPhishing: boolean, warning: string|null }} */ function parseEtherscanPage(html) { // Extract content const titleMatch = html.match(/<title[^>]*>([^<]+)<\/title>/i); let label = null; let isPhishing = false; let warning = null; if (titleMatch) { const title = titleMatch[1].trim(); // Title: "LABEL | Address: 0x... | Etherscan" or "Address: 0x... | Etherscan" const labelMatch = title.match(/^(.+?)\s*\|\s*Address:/); if (labelMatch) { const candidate = labelMatch[1].trim(); // Only treat as a label if it's not just "Address" (unlabeled addresses) if (candidate.toLowerCase() !== "address") { label = candidate; } } } // Check label against phishing patterns if (label) { for (const pat of PHISHING_LABEL_PATTERNS) { if (pat.test(label)) { isPhishing = true; warning = `Etherscan labels this address as "${label}" (Phish/Hack).`; break; } } } // Check page body for scam warning banners if (!isPhishing) { for (const pat of SCAM_BODY_PATTERNS) { if (pat.test(html)) { isPhishing = true; warning = label ? `Etherscan labels this address as "${label}" and reports it was used in a scam.` : "Etherscan reports this address was flagged for phishing/scam activity."; break; } } } return { label, isPhishing, warning }; } /** * Fetch an address page from Etherscan and check for scam/phishing labels. * Returns a warning object if the address is flagged, or null. * Network failures return null silently (best-effort check). * * Uses the current network's explorer URL so the lookup works on both * mainnet (etherscan.io) and Sepolia (sepolia.etherscan.io). * * @param {string} address - Ethereum address to check. * @returns {Promise<{type: string, message: string, severity: string}|null>} */ async function checkEtherscanLabel(address) { try { // Lazy require to avoid pulling in chrome.storage at module scope // (which breaks unit tests that only exercise parseEtherscanPage). const { currentNetwork } = require("./state"); const etherscanBase = currentNetwork().explorerUrl + "/address/"; const resp = await fetch(etherscanBase + address, { headers: { Accept: "text/html" }, }); if (!resp.ok) return null; const html = await resp.text(); const result = parseEtherscanPage(html); if (result.isPhishing) { return { type: "etherscan-phishing", message: result.warning, severity: "critical", }; } return null; } catch { // Network errors are expected — Etherscan may rate-limit or block. return null; } } module.exports = { parseEtherscanPage, checkEtherscanLabel };