All checks were successful
check / check (push) Successful in 9s
## Summary Adds Sepolia testnet support to AutistMask. ### Changes - **New `src/shared/networks.js`** — centralized network definitions (mainnet + Sepolia) with chain IDs, default RPC/Blockscout endpoints, and block explorer URLs - **State management** — `networkId` added to persisted state; defaults to mainnet for backward compatibility - **Settings UI** — network selector dropdown lets users switch between Ethereum Mainnet and Sepolia Testnet - **Dynamic explorer links** — all hardcoded `etherscan.io` URLs replaced with dynamic links from the current network config (`sepolia.etherscan.io` for Sepolia) - **Background service** — `wallet_switchEthereumChain` now accepts both mainnet (0x1) and Sepolia (0xaa36a7); broadcasts `chainChanged` 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/api/v2` for Sepolia - **Etherscan labels** — phishing/scam checks use the correct explorer per network - **Price fetching** — skipped on testnets (testnet tokens have no real market value) - **RPC validation** — checks against the selected network's chain ID, not hardcoded mainnet - **ethers provider** — `getProvider()` uses the correct ethers `Network` for Sepolia ### API Endpoints Verified | Service | Mainnet | Sepolia | |---------|---------|--------| | Etherscan | etherscan.io | sepolia.etherscan.io | | Blockscout | eth.blockscout.com/api/v2 | eth-sepolia.blockscout.com/api/v2 | | RPC | ethereum-rpc.publicnode.com | ethereum-sepolia-rpc.publicnode.com | | CoinDesk (prices) | ✅ | N/A (skipped on testnet) | closes #110 Reviewed-on: #137 THIS WAS ONESHOTTED USING OPUS 4. WTAF Co-authored-by: clawbot <clawbot@noreply.example.org> Co-committed-by: clawbot <clawbot@noreply.example.org>
108 lines
3.8 KiB
JavaScript
108 lines
3.8 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.
|
|
|
|
// 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 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 };
|