ConfirmTx set the transaction's chain id to the active network's every time it drew the screen, so a popup reopened onto a saved confirmation after a network switch in Settings described the reviewed transfer as the new network's and sent it there. show() now sets the chain id only on a transaction Send hands over, and the screen describes the transaction entirely on that network: native currency, USD values, explorer links, and the node and explorer its checks ask. explorerUrl(), renderAddressHtml(), getPrice(), formatFee() and checkEtherscanLabel() take an optional chain id for this. A saved transaction whose chain id names no network is not restored. Model: opus-5-5
113 lines
4.1 KiB
JavaScript
113 lines
4.1 KiB
JavaScript
// 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.
|
|
|
|
const { networkByChainId } = require("./networks");
|
|
|
|
// 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 <title> 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 explorer of the network with `chainId`, or of the active network
|
|
* when it is omitted: etherscan.io on mainnet, sepolia.etherscan.io on
|
|
* Sepolia.
|
|
*
|
|
* @param {string} address - Ethereum address to check.
|
|
* @param {string} [chainId] - Chain id of the network to look it up on.
|
|
* @returns {Promise<{type: string, message: string, severity: string}|null>}
|
|
*/
|
|
async function checkEtherscanLabel(address, chainId) {
|
|
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 network = chainId ? networkByChainId(chainId) : currentNetwork();
|
|
const etherscanBase = network.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 };
|