Compare commits

..

7 Commits

Author SHA1 Message Date
clawbot
06324158aa remove phishing domain whitelist support
All checks were successful
check / check (push) Successful in 13s
Remove all whitelist functionality from the phishing domain system.
The blocklist now only checks the blacklist — no whitelist overrides.

- Remove vendoredWhitelist and deltaWhitelist Sets
- Remove whitelist checks in isPhishingDomain()
- Remove whitelist from delta storage persistence
- Remove whitelist from loadConfig() delta computation
- Remove whitelist-specific test cases
- Update README to remove whitelist mention

Closes #114
2026-03-01 10:29:00 -08:00
user
293d781385 docs: add third-party file attribution to LICENSE and README 2026-03-01 10:29:00 -08:00
clawbot
5927dfd45b refactor: vendor phishing blocklist, delta-only memory model
- Vendor community-maintained phishing domain blocklist into
  src/shared/phishingBlocklist.json (bundled at build time by esbuild)
- Refactor phishingDomains.js: build vendored Sets at module load,
  fetch live list periodically, keep only delta (new entries not in
  vendored) in memory for small runtime footprint
- Domain checker checks delta first (fresh scam sites), then vendored
- Persist delta to localStorage if under 256 KiB
- Load delta from localStorage on startup for instant coverage
- Add startPeriodicRefresh() with 24h setInterval in background script
- Remove dead code: popup's local isPhishingDomain() re-check was inert
  (popup never called updatePhishingList so its blacklistSet was always
  empty); now relies solely on background's authoritative flag
- Remove all competitor name mentions from UI warning text and comments
- Update README: document phishing domain protection architecture,
  update external services list
- Update tests: cover vendored blocklist loading, delta computation,
  localStorage persistence, delta+vendored interaction

Closes #114
2026-03-01 10:29:00 -08:00
cb1446067f fix: etherscan label check runs for contracts, UI displays etherscan-phishing warnings
Bug 1: getFullWarnings returned early for contract addresses, skipping
checkEtherscanLabel. Restructured to use isContract flag so the Etherscan
check runs for all addresses (contracts are often the most dangerous).

Bug 2: confirmTx.js only handled 'contract' and 'new-address' warning types,
silently discarding 'etherscan-phishing'. Added confirm-etherscan-warning
HTML element and handler in the async warnings loop.

Style: converted inline style attributes on phishing warning banners
(approve-tx, approve-sign, approve-site) to Tailwind utility classes
(bg-red-100 text-red-800 border-2 border-red-600 rounded-md).
2026-03-01 10:29:00 -08:00
user
2e4cf32211 feat: add Etherscan label scraping and MetaMask phishing domain blocklist
- Add etherscanLabels module: scrapes Etherscan address pages for
  phishing/scam labels (Fake_Phishing*, Exploiter, scam warnings).
  Integrated as best-effort async check in addressWarnings.

- Add phishingDomains module: fetches MetaMask's eth-phishing-detect
  blocklist (~231K domains) at runtime, caches in memory, refreshes
  every 24h. Checks hostnames with subdomain matching and whitelist
  overrides.

- Integrate domain phishing checks into all approval flows:
  connection requests, transaction approvals, and signature requests
  show a prominent red warning banner when the requesting site is on
  the MetaMask blocklist.

- Add unit tests for both modules (12 tests for etherscanLabels
  parsing, 15 tests for phishingDomains matching).

Closes #114
2026-03-01 10:29:00 -08:00
clawbot
e737574038 feat: expand confirm-tx warnings — closes #114
- Refactor address warnings into src/shared/addressWarnings.js module
  - getLocalWarnings(address, options): sync checks against local lists
  - getFullWarnings(address, provider, options): async local + RPC checks
- Expand scam address list from 652 to 2417 addresses
  - Added EtherScamDB (MIT) as additional source
- Update confirmTx.js to use the new addressWarnings module
2026-03-01 10:29:00 -08:00
3bf60ff162 Reorder transaction detail view: txid first, logical grouping (#133)
All checks were successful
check / check (push) Successful in 9s
Reorganizes the transaction detail view into logical blocks separated by thin horizontal rules for visual clarity.

**Identity block**: Transaction hash (first!), Type, Status
**Timing block**: Time, Block number
**Value block**: Amount, Native quantity, Token contract, From, To
**Decoded details**: Action/protocol/steps (for contract calls)
**Network details**: Nonce, Gas price, Gas used, Transaction fee
**Raw data**: Full calldata

README TransactionDetail screen map updated to reflect the new ordering and grouping.

closes #131

Co-authored-by: user <user@Mac.lan guest wan>
Co-authored-by: clawbot <clawbot@git.eeqj.de>
Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Reviewed-on: #133
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-01 19:27:23 +01:00
5 changed files with 161 additions and 171 deletions

View File

@@ -437,25 +437,29 @@ transitions.
#### TransactionDetail #### TransactionDetail
- **When**: User tapped a transaction row from AddressDetail or AddressToken. - **When**: User tapped a transaction row from AddressDetail or AddressToken.
- **Elements**: - **Elements** (grouped into logical blocks using light well containers; field
labels are self-explanatory so groups have no headings):
- "Transaction" heading, "Back" button - "Transaction" heading, "Back" button
- Transaction hash: full hash (tap to copy) + etherscan link
- Type: transaction classification — one of: Native ETH Transfer, ERC-20 - Type: transaction classification — one of: Native ETH Transfer, ERC-20
Token Transfer, Swap, Token Approval, Contract Call, Contract Creation Token Transfer, Swap, Token Approval, Contract Call, Contract Creation
- Status: "Success" or "Failed"
- From: blockie + color dot + full address (tap to copy) + etherscan link;
ENS name if available
- To: blockie + color dot + full address (tap to copy) + etherscan link; ENS
name if available
- Time: ISO datetime + relative age in parentheses
- Block: block number (tap to copy) + etherscan block link
- Amount: value + symbol (bold)
- Native quantity: raw integer + unit (shown when available)
- Token contract: shown for ERC-20 transfers — color dot + full contract - Token contract: shown for ERC-20 transfers — color dot + full contract
address (tap to copy) + etherscan token link address (tap to copy) + etherscan token link
- Status: "Success" or "Failed" - Decoded details (shown for contract calls): action name, decoded
- Time: ISO datetime + relative age in parentheses parameters, token details, swap steps
- Amount: value + symbol (bold) - Network details (shown when on-chain data is available): nonce, gas price,
- From: blockie + color dot + full address (tap to copy) + etherscan link gas used, transaction fee (all tap to copy)
- ENS name if available - Raw data (shown when calldata is present): full calldata in monospace
- To: blockie + color dot + full address (tap to copy) + etherscan link dashed border
- ENS name if available
- Transaction hash: full hash (tap to copy) + etherscan link
- Block: block number (tap to copy) + etherscan block link
- Nonce: transaction nonce (tap to copy)
- Transaction fee: ETH amount (tap to copy)
- Gas price: value in Gwei (tap to copy)
- Gas used: integer (tap to copy)
- **Transitions**: - **Transitions**:
- "Back" → **AddressToken** (if `selectedToken` set) or **AddressDetail** - "Back" → **AddressToken** (if `selectedToken` set) or **AddressDetail**
@@ -799,8 +803,7 @@ small while ensuring fresh coverage of new phishing domains.
When a dApp on a blocklisted domain requests a wallet connection, transaction When a dApp on a blocklisted domain requests a wallet connection, transaction
approval, or signature, the approval popup displays a prominent red warning approval, or signature, the approval popup displays a prominent red warning
banner alerting the user. The domain checker matches exact hostnames and all banner alerting the user. The domain checker matches exact hostnames and all
parent domains (subdomain matching), with whitelist overrides for legitimate parent domains (subdomain matching).
sites that share a parent domain with a blocklisted entry.
#### Transaction Decoding #### Transaction Decoding

View File

@@ -1101,87 +1101,135 @@
<h2 id="tx-detail-heading" class="font-bold mb-2"> <h2 id="tx-detail-heading" class="font-bold mb-2">
Transaction Transaction
</h2> </h2>
<div id="tx-detail-type-section" class="mb-4 hidden">
<div class="text-xs text-muted mb-1">Type</div> <!-- ── Identity ── -->
<div id="tx-detail-type" class="text-xs font-bold"></div> <div class="bg-well p-3 mx-1 mb-3">
</div> <div class="mb-2">
<div class="mb-4"> <div class="text-xs text-muted mb-1">
<div class="text-xs text-muted mb-1">Status</div> Transaction hash
<div id="tx-detail-status" class="text-xs"></div> </div>
</div>
<div class="mb-4">
<div class="text-xs text-muted mb-1">Time</div>
<div id="tx-detail-time" class="text-xs"></div>
</div>
<div class="mb-4">
<div class="text-xs text-muted mb-1">Amount</div>
<div id="tx-detail-value" class="text-xs"></div>
</div>
<div class="mb-4 hidden">
<div class="text-xs text-muted mb-1">Native quantity</div>
<div id="tx-detail-native" class="text-xs"></div>
</div>
<div class="mb-4">
<div class="text-xs text-muted mb-1">From</div>
<div id="tx-detail-from" class="text-xs break-all"></div>
</div>
<div class="mb-4">
<div class="text-xs text-muted mb-1">To</div>
<div id="tx-detail-to" class="text-xs break-all"></div>
</div>
<div id="tx-detail-token-contract-section" class="mb-4 hidden">
<div class="text-xs text-muted mb-1">Token contract</div>
<div
id="tx-detail-token-contract"
class="text-xs break-all"
></div>
</div>
<div id="tx-detail-calldata-section" class="mb-4 hidden">
<div
id="tx-detail-calldata-well"
class="mb-3 border border-border border-dashed p-2"
>
<div class="text-xs text-muted mb-1">Action</div>
<div <div
id="tx-detail-calldata-action" id="tx-detail-hash"
class="text-xs font-bold mb-2" class="text-xs break-all"
></div> ></div>
</div>
<div id="tx-detail-type-section" class="mb-2 hidden">
<div class="text-xs text-muted mb-1">Type</div>
<div <div
id="tx-detail-calldata-details" id="tx-detail-type"
class="text-xs" class="text-xs font-bold"
></div>
</div>
<div class="mb-2">
<div class="text-xs text-muted mb-1">Status</div>
<div id="tx-detail-status" class="text-xs"></div>
</div>
<div class="mb-2">
<div class="text-xs text-muted mb-1">From</div>
<div
id="tx-detail-from"
class="text-xs break-all"
></div>
</div>
<div class="mb-2">
<div class="text-xs text-muted mb-1">To</div>
<div id="tx-detail-to" class="text-xs break-all"></div>
</div>
</div>
<!-- ── Timing ── -->
<div class="bg-well p-3 mx-1 mb-3">
<div class="mb-2">
<div class="text-xs text-muted mb-1">Time</div>
<div id="tx-detail-time" class="text-xs"></div>
</div>
<div id="tx-detail-block-section" class="mb-2 hidden">
<div class="text-xs text-muted mb-1">Block</div>
<div id="tx-detail-block" class="text-xs"></div>
</div>
</div>
<!-- ── Value ── -->
<div class="bg-well p-3 mx-1 mb-3">
<div class="mb-2">
<div class="text-xs text-muted mb-1">Amount</div>
<div id="tx-detail-value" class="text-xs"></div>
</div>
<div class="mb-2 hidden">
<div class="text-xs text-muted mb-1">
Native quantity
</div>
<div id="tx-detail-native" class="text-xs"></div>
</div>
<div
id="tx-detail-token-contract-section"
class="mb-2 hidden"
>
<div class="text-xs text-muted mb-1">
Token contract
</div>
<div
id="tx-detail-token-contract"
class="text-xs break-all"
></div> ></div>
</div> </div>
</div> </div>
<div class="mb-4">
<div class="text-xs text-muted mb-1">Transaction hash</div> <!-- ── Decoded details ── -->
<div id="tx-detail-hash" class="text-xs break-all"></div> <div id="tx-detail-calldata-section" class="hidden">
<div class="bg-well p-3 mx-1 mb-3">
<div id="tx-detail-calldata-well" class="mb-2">
<div class="text-xs text-muted mb-1">Action</div>
<div
id="tx-detail-calldata-action"
class="text-xs font-bold mb-2"
></div>
<div
id="tx-detail-calldata-details"
class="text-xs"
></div>
</div>
</div>
</div> </div>
<div id="tx-detail-block-section" class="mb-4 hidden">
<div class="text-xs text-muted mb-1">Block</div> <!-- ── Network details ── -->
<div id="tx-detail-block" class="text-xs"></div> <div id="tx-detail-network-section" class="hidden">
<div class="bg-well p-3 mx-1 mb-3">
<div id="tx-detail-nonce-section" class="mb-2 hidden">
<div class="text-xs text-muted mb-1">Nonce</div>
<div id="tx-detail-nonce" class="text-xs"></div>
</div>
<div
id="tx-detail-gasprice-section"
class="mb-2 hidden"
>
<div class="text-xs text-muted mb-1">Gas price</div>
<div id="tx-detail-gasprice" class="text-xs"></div>
</div>
<div id="tx-detail-gasused-section" class="mb-2 hidden">
<div class="text-xs text-muted mb-1">Gas used</div>
<div id="tx-detail-gasused" class="text-xs"></div>
</div>
<div id="tx-detail-fee-section" class="mb-2 hidden">
<div class="text-xs text-muted mb-1">
Transaction fee
</div>
<div id="tx-detail-fee" class="text-xs"></div>
</div>
</div>
</div> </div>
<div id="tx-detail-nonce-section" class="mb-4 hidden">
<div class="text-xs text-muted mb-1">Nonce</div> <!-- ── Raw data ── -->
<div id="tx-detail-nonce" class="text-xs"></div> <div id="tx-detail-rawdata-section" class="hidden">
</div> <div class="bg-well p-3 mx-1 mb-3">
<div id="tx-detail-fee-section" class="mb-4 hidden"> <div class="mb-2">
<div class="text-xs text-muted mb-1">Transaction fee</div> <div class="text-xs text-muted mb-1">Raw data</div>
<div id="tx-detail-fee" class="text-xs"></div> <div
</div> id="tx-detail-rawdata"
<div id="tx-detail-gasprice-section" class="mb-4 hidden"> class="text-xs break-all font-mono border border-border border-dashed p-2"
<div class="text-xs text-muted mb-1">Gas price</div> ></div>
<div id="tx-detail-gasprice" class="text-xs"></div> </div>
</div> </div>
<div id="tx-detail-gasused-section" class="mb-4 hidden">
<div class="text-xs text-muted mb-1">Gas used</div>
<div id="tx-detail-gasused" class="text-xs"></div>
</div>
<div id="tx-detail-rawdata-section" class="mb-4 hidden">
<div class="text-xs text-muted mb-1">Raw data</div>
<div
id="tx-detail-rawdata"
class="text-xs break-all font-mono border border-border border-dashed p-2"
></div>
</div> </div>
</div> </div>

View File

@@ -197,6 +197,7 @@ function render() {
"tx-detail-fee-section", "tx-detail-fee-section",
"tx-detail-gasprice-section", "tx-detail-gasprice-section",
"tx-detail-gasused-section", "tx-detail-gasused-section",
"tx-detail-network-section",
]) { ]) {
const el = $(id); const el = $(id);
if (el) el.classList.add("hidden"); if (el) el.classList.add("hidden");
@@ -285,6 +286,21 @@ function populateOnChainDetails(txData) {
); );
} }
// Show the network details wrapper if any child section is visible
const networkWrapper = $("tx-detail-network-section");
if (networkWrapper) {
const hasVisible = [
"tx-detail-nonce-section",
"tx-detail-fee-section",
"tx-detail-gasprice-section",
"tx-detail-gasused-section",
].some((id) => {
const el = $(id);
return el && !el.classList.contains("hidden");
});
if (hasVisible) networkWrapper.classList.remove("hidden");
}
// Bind copy handlers for newly added elements // Bind copy handlers for newly added elements
for (const id of [ for (const id of [
"tx-detail-block-section", "tx-detail-block-section",

View File

@@ -21,17 +21,13 @@ const REFRESH_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
const DELTA_STORAGE_KEY = "phishing-delta"; const DELTA_STORAGE_KEY = "phishing-delta";
const MAX_DELTA_BYTES = 256 * 1024; // 256 KiB const MAX_DELTA_BYTES = 256 * 1024; // 256 KiB
// Vendored sets — built once from the bundled JSON. // Vendored set — built once from the bundled JSON.
const vendoredBlacklist = new Set( const vendoredBlacklist = new Set(
(vendoredConfig.blacklist || []).map((d) => d.toLowerCase()), (vendoredConfig.blacklist || []).map((d) => d.toLowerCase()),
); );
const vendoredWhitelist = new Set(
(vendoredConfig.whitelist || []).map((d) => d.toLowerCase()),
);
// Delta sets — only entries from live list that are NOT in vendored. // Delta set — only entries from live list that are NOT in vendored.
let deltaBlacklist = new Set(); let deltaBlacklist = new Set();
let deltaWhitelist = new Set();
let lastFetchTime = 0; let lastFetchTime = 0;
let fetchPromise = null; let fetchPromise = null;
let refreshTimer = null; let refreshTimer = null;
@@ -50,11 +46,6 @@ function loadDeltaFromStorage() {
data.blacklist.map((d) => d.toLowerCase()), data.blacklist.map((d) => d.toLowerCase()),
); );
} }
if (data.whitelist && Array.isArray(data.whitelist)) {
deltaWhitelist = new Set(
data.whitelist.map((d) => d.toLowerCase()),
);
}
} catch { } catch {
// localStorage unavailable or corrupt — start empty // localStorage unavailable or corrupt — start empty
} }
@@ -67,7 +58,6 @@ function saveDeltaToStorage() {
try { try {
const data = { const data = {
blacklist: Array.from(deltaBlacklist), blacklist: Array.from(deltaBlacklist),
whitelist: Array.from(deltaWhitelist),
}; };
const json = JSON.stringify(data); const json = JSON.stringify(data);
if (json.length < MAX_DELTA_BYTES) { if (json.length < MAX_DELTA_BYTES) {
@@ -85,19 +75,15 @@ function saveDeltaToStorage() {
* Load a pre-parsed config and compute the delta against the vendored list. * Load a pre-parsed config and compute the delta against the vendored list.
* Used for both live fetches and testing. * Used for both live fetches and testing.
* *
* @param {{ blacklist?: string[], whitelist?: string[] }} config * @param {{ blacklist?: string[] }} config
*/ */
function loadConfig(config) { function loadConfig(config) {
const liveBlacklist = (config.blacklist || []).map((d) => d.toLowerCase()); const liveBlacklist = (config.blacklist || []).map((d) => d.toLowerCase());
const liveWhitelist = (config.whitelist || []).map((d) => d.toLowerCase());
// Delta = entries in the live list that are NOT in the vendored list // Delta = entries in the live list that are NOT in the vendored list
deltaBlacklist = new Set( deltaBlacklist = new Set(
liveBlacklist.filter((d) => !vendoredBlacklist.has(d)), liveBlacklist.filter((d) => !vendoredBlacklist.has(d)),
); );
deltaWhitelist = new Set(
liveWhitelist.filter((d) => !vendoredWhitelist.has(d)),
);
lastFetchTime = Date.now(); lastFetchTime = Date.now();
saveDeltaToStorage(); saveDeltaToStorage();
@@ -124,7 +110,6 @@ function hostnameVariants(hostname) {
/** /**
* Check if a hostname is on the phishing blocklist. * Check if a hostname is on the phishing blocklist.
* Checks delta first (fresh/recent scam sites), then vendored list. * Checks delta first (fresh/recent scam sites), then vendored list.
* Whitelisted domains (delta + vendored) are never flagged.
* *
* @param {string} hostname - The hostname to check. * @param {string} hostname - The hostname to check.
* @returns {boolean} * @returns {boolean}
@@ -133,11 +118,6 @@ function isPhishingDomain(hostname) {
if (!hostname) return false; if (!hostname) return false;
const variants = hostnameVariants(hostname); const variants = hostnameVariants(hostname);
// Whitelist takes priority — check delta whitelist first, then vendored
for (const v of variants) {
if (deltaWhitelist.has(v) || vendoredWhitelist.has(v)) return false;
}
// Check delta blacklist first (fresh/recent scam sites), then vendored // Check delta blacklist first (fresh/recent scam sites), then vendored
for (const v of variants) { for (const v of variants) {
if (deltaBlacklist.has(v) || vendoredBlacklist.has(v)) return true; if (deltaBlacklist.has(v) || vendoredBlacklist.has(v)) return true;
@@ -209,7 +189,6 @@ function getDeltaSize() {
*/ */
function _reset() { function _reset() {
deltaBlacklist = new Set(); deltaBlacklist = new Set();
deltaWhitelist = new Set();
lastFetchTime = 0; lastFetchTime = 0;
fetchPromise = null; fetchPromise = null;
if (refreshTimer) { if (refreshTimer) {
@@ -232,7 +211,5 @@ module.exports = {
_reset, _reset,
// Exposed for testing only // Exposed for testing only
_getVendoredBlacklistSize: () => vendoredBlacklist.size, _getVendoredBlacklistSize: () => vendoredBlacklist.size,
_getVendoredWhitelistSize: () => vendoredWhitelist.size,
_getDeltaBlacklist: () => deltaBlacklist, _getDeltaBlacklist: () => deltaBlacklist,
_getDeltaWhitelist: () => deltaWhitelist,
}; };

View File

@@ -23,9 +23,7 @@ const {
hostnameVariants, hostnameVariants,
_reset, _reset,
_getVendoredBlacklistSize, _getVendoredBlacklistSize,
_getVendoredWhitelistSize,
_getDeltaBlacklist, _getDeltaBlacklist,
_getDeltaWhitelist,
} = require("../src/shared/phishingDomains"); } = require("../src/shared/phishingDomains");
// Reset delta state before each test to avoid cross-test contamination. // Reset delta state before each test to avoid cross-test contamination.
@@ -45,21 +43,12 @@ describe("phishingDomains", () => {
expect(_getVendoredBlacklistSize()).toBeGreaterThan(100000); expect(_getVendoredBlacklistSize()).toBeGreaterThan(100000);
}); });
test("vendored whitelist is loaded from bundled JSON", () => {
expect(_getVendoredWhitelistSize()).toBeGreaterThan(0);
});
test("detects domains from vendored blacklist", () => { test("detects domains from vendored blacklist", () => {
// These are well-known phishing domains in the vendored list // These are well-known phishing domains in the vendored list
expect(isPhishingDomain("hopprotocol.pro")).toBe(true); expect(isPhishingDomain("hopprotocol.pro")).toBe(true);
expect(isPhishingDomain("blast-pools.pages.dev")).toBe(true); expect(isPhishingDomain("blast-pools.pages.dev")).toBe(true);
}); });
test("vendored whitelist overrides vendored blacklist", () => {
// opensea.pro is whitelisted in the vendored config
expect(isPhishingDomain("opensea.pro")).toBe(false);
});
test("getBlocklistSize includes vendored entries", () => { test("getBlocklistSize includes vendored entries", () => {
expect(getBlocklistSize()).toBeGreaterThan(100000); expect(getBlocklistSize()).toBeGreaterThan(100000);
}); });
@@ -99,7 +88,6 @@ describe("phishingDomains", () => {
"brand-new-scam-site-xyz123.com", "brand-new-scam-site-xyz123.com",
"hopprotocol.pro", // already in vendored "hopprotocol.pro", // already in vendored
], ],
whitelist: [],
}); });
// Only the new domain should be in the delta // Only the new domain should be in the delta
expect( expect(
@@ -109,30 +97,14 @@ describe("phishingDomains", () => {
expect(getDeltaSize()).toBe(1); expect(getDeltaSize()).toBe(1);
}); });
test("delta whitelist entries are computed correctly", () => {
loadConfig({
blacklist: [],
whitelist: [
"new-safe-site-xyz789.com",
"opensea.pro", // already in vendored whitelist
],
});
expect(_getDeltaWhitelist().has("new-safe-site-xyz789.com")).toBe(
true,
);
expect(_getDeltaWhitelist().has("opensea.pro")).toBe(false);
});
test("re-loading config replaces previous delta", () => { test("re-loading config replaces previous delta", () => {
loadConfig({ loadConfig({
blacklist: ["first-scam-xyz.com"], blacklist: ["first-scam-xyz.com"],
whitelist: [],
}); });
expect(isPhishingDomain("first-scam-xyz.com")).toBe(true); expect(isPhishingDomain("first-scam-xyz.com")).toBe(true);
loadConfig({ loadConfig({
blacklist: ["second-scam-xyz.com"], blacklist: ["second-scam-xyz.com"],
whitelist: [],
}); });
expect(isPhishingDomain("first-scam-xyz.com")).toBe(false); expect(isPhishingDomain("first-scam-xyz.com")).toBe(false);
expect(isPhishingDomain("second-scam-xyz.com")).toBe(true); expect(isPhishingDomain("second-scam-xyz.com")).toBe(true);
@@ -142,7 +114,6 @@ describe("phishingDomains", () => {
const baseSize = getBlocklistSize(); const baseSize = getBlocklistSize();
loadConfig({ loadConfig({
blacklist: ["delta-only-scam-xyz.com"], blacklist: ["delta-only-scam-xyz.com"],
whitelist: [],
}); });
expect(getBlocklistSize()).toBe(baseSize + 1); expect(getBlocklistSize()).toBe(baseSize + 1);
}); });
@@ -152,7 +123,6 @@ describe("phishingDomains", () => {
test("detects domain from delta blacklist", () => { test("detects domain from delta blacklist", () => {
loadConfig({ loadConfig({
blacklist: ["fresh-scam-xyz.com"], blacklist: ["fresh-scam-xyz.com"],
whitelist: [],
}); });
expect(isPhishingDomain("fresh-scam-xyz.com")).toBe(true); expect(isPhishingDomain("fresh-scam-xyz.com")).toBe(true);
}); });
@@ -174,34 +144,13 @@ describe("phishingDomains", () => {
test("detects subdomain of blacklisted domain (delta)", () => { test("detects subdomain of blacklisted domain (delta)", () => {
loadConfig({ loadConfig({
blacklist: ["delta-phish-xyz.com"], blacklist: ["delta-phish-xyz.com"],
whitelist: [],
}); });
expect(isPhishingDomain("sub.delta-phish-xyz.com")).toBe(true); expect(isPhishingDomain("sub.delta-phish-xyz.com")).toBe(true);
}); });
test("delta whitelist overrides vendored blacklist", () => {
// hopprotocol.pro is in the vendored blacklist
expect(isPhishingDomain("hopprotocol.pro")).toBe(true);
loadConfig({
blacklist: [],
whitelist: ["hopprotocol.pro"],
});
// Now whitelisted via delta — should not be flagged
expect(isPhishingDomain("hopprotocol.pro")).toBe(false);
});
test("vendored whitelist overrides delta blacklist", () => {
loadConfig({
blacklist: ["opensea.pro"], // opensea.pro is vendored-whitelisted
whitelist: [],
});
expect(isPhishingDomain("opensea.pro")).toBe(false);
});
test("case-insensitive matching", () => { test("case-insensitive matching", () => {
loadConfig({ loadConfig({
blacklist: ["Delta-Scam-XYZ.COM"], blacklist: ["Delta-Scam-XYZ.COM"],
whitelist: [],
}); });
expect(isPhishingDomain("delta-scam-xyz.com")).toBe(true); expect(isPhishingDomain("delta-scam-xyz.com")).toBe(true);
expect(isPhishingDomain("DELTA-SCAM-XYZ.COM")).toBe(true); expect(isPhishingDomain("DELTA-SCAM-XYZ.COM")).toBe(true);
@@ -212,7 +161,7 @@ describe("phishingDomains", () => {
expect(isPhishingDomain(null)).toBe(false); expect(isPhishingDomain(null)).toBe(false);
}); });
test("handles config with no blacklist/whitelist keys", () => { test("handles config with no blacklist key", () => {
loadConfig({}); loadConfig({});
expect(getDeltaSize()).toBe(0); expect(getDeltaSize()).toBe(0);
// Vendored list still works // Vendored list still works
@@ -224,19 +173,16 @@ describe("phishingDomains", () => {
test("saveDeltaToStorage persists delta under 256KiB", () => { test("saveDeltaToStorage persists delta under 256KiB", () => {
loadConfig({ loadConfig({
blacklist: ["persisted-scam-xyz.com"], blacklist: ["persisted-scam-xyz.com"],
whitelist: ["persisted-safe-xyz.com"],
}); });
const stored = localStorage.getItem("phishing-delta"); const stored = localStorage.getItem("phishing-delta");
expect(stored).not.toBeNull(); expect(stored).not.toBeNull();
const data = JSON.parse(stored); const data = JSON.parse(stored);
expect(data.blacklist).toContain("persisted-scam-xyz.com"); expect(data.blacklist).toContain("persisted-scam-xyz.com");
expect(data.whitelist).toContain("persisted-safe-xyz.com");
}); });
test("delta is cleared on _reset", () => { test("delta is cleared on _reset", () => {
loadConfig({ loadConfig({
blacklist: ["temp-scam-xyz.com"], blacklist: ["temp-scam-xyz.com"],
whitelist: [],
}); });
expect(getDeltaSize()).toBe(1); expect(getDeltaSize()).toBe(1);
_reset(); _reset();
@@ -251,7 +197,7 @@ describe("phishingDomains", () => {
expect(isPhishingDomain("blast-pools.pages.dev")).toBe(true); expect(isPhishingDomain("blast-pools.pages.dev")).toBe(true);
}); });
test("does not flag legitimate whitelisted domains", () => { test("does not flag legitimate domains", () => {
expect(isPhishingDomain("opensea.io")).toBe(false); expect(isPhishingDomain("opensea.io")).toBe(false);
expect(isPhishingDomain("etherscan.io")).toBe(false); expect(isPhishingDomain("etherscan.io")).toBe(false);
}); });