Compare commits

..

18 Commits

Author SHA1 Message Date
user
01839d9c47 feat: add Etherscan label scraping and MetaMask phishing domain blocklist
All checks were successful
check / check (push) Successful in 22s
- 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 05:03:42 -08:00
clawbot
9eef2ea602 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 05:03:42 -08:00
a182aa534b Merge pull request 'fix: include timezone offset in all displayed timestamps (closes #116)' (#120) from fix/issue-116-timestamp-timezone into main
All checks were successful
check / check (push) Successful in 9s
Reviewed-on: #120
2026-03-01 13:36:04 +01:00
a388100262 Merge branch 'main' into fix/issue-116-timestamp-timezone
All checks were successful
check / check (push) Successful in 22s
2026-03-01 13:35:26 +01:00
dd3cabf816 Merge pull request 'feat: add theme setting (Light/Dark/System) with dark mode — closes #125' (#126) from feat/issue-125-dark-mode into main
All checks were successful
check / check (push) Successful in 10s
Reviewed-on: #126
2026-03-01 13:35:11 +01:00
user
235e5e7fa7 fix: improve dark mode contrast for hover, well, section, and border colors
All checks were successful
check / check (push) Successful in 22s
2026-03-01 03:49:18 -08:00
user
be06bd8f0c fix: improve dark mode contrast for wells and balance display
All checks were successful
check / check (push) Successful in 10s
- Change dark mode --color-well from #0a0a0a to #111111 for visible
  contrast against #000000 background
- Add explicit text-fg class to balance display element to ensure
  white text in dark mode
2026-03-01 03:38:27 -08:00
user
a72359432b fix: include timezone offset in all displayed timestamps
All checks were successful
check / check (push) Successful in 21s
All isoDate() functions now output proper ISO 8601 format with timezone
offset (e.g. 2026-02-28T15:30:00-08:00) instead of bare datetime strings.
Also uses 'T' separator per ISO 8601.

closes #116
2026-03-01 03:36:49 -08:00
user
2bdb547995 feat: add theme setting (Light/Dark/System) with dark mode
Add theme preference (light/dark/system) stored in extension state.
System mode follows prefers-color-scheme and listens for changes.
Dark mode inverts the monochrome palette (white-on-black).
Theme selector added to Display section in settings.

Closes #125
2026-03-01 03:36:42 -08:00
834228b572 Merge pull request 'fix: reserve space for all error/status messages — closes #123' (#124) from fix/issue-123-layout-shift-audit into main
All checks were successful
check / check (push) Successful in 8s
Reviewed-on: #124
2026-03-01 12:33:08 +01:00
clawbot
813993f17c fix: reserve space for all error/status messages — closes #123
All checks were successful
check / check (push) Successful in 22s
Replace display:none (hidden class) with visibility:hidden/visible for all
error, warning, and status message elements across the extension UI. This
prevents layout shift when messages appear or disappear.

Changes:
- helpers.js: showError/hideError now use visibility instead of hidden class
- index.html: all error/status divs use visibility:hidden + min-height
- confirmTx.js: warnings, errors, fee section use visibility
- approval.js: tx-error, sign-error, danger-warning use visibility
- addressDetail.js: export-privkey-flash uses visibility
- deleteWallet.js: delete-wallet-flash uses visibility
- addWallet.js: phrase-warning uses visibility
- receive.js: erc20-warning uses visibility
- addToken.js: add-token-info uses visibility
- settingsAddToken.js: settings-addtoken-info uses visibility
2026-02-28 16:30:43 -08:00
5f01d9f111 Merge pull request 'feat: speed up copy-flash timing by ~25% — follow-up to #113' (#121) from fix/issue-100-faster-copy-flash into main
All checks were successful
check / check (push) Successful in 11s
Reviewed-on: #121
2026-03-01 01:21:24 +01:00
user
d78af3ec80 feat: speed up copy-flash timing by ~25%
All checks were successful
check / check (push) Successful in 20s
Reduce active phase from 100ms to 75ms, fade transition from 300ms to
225ms, and cleanup delay from 350ms to 275ms for snappier feedback.

Refs #100
2026-02-28 16:17:07 -08:00
753fb5658a Merge pull request 'fix: cross-wallet-type duplicate detection — closes #111' (#115) from fix/issue-111-cross-wallet-dedup into main
All checks were successful
check / check (push) Successful in 9s
Reviewed-on: #115
2026-03-01 01:13:15 +01:00
bdb2031d46 Merge branch 'main' into fix/issue-111-cross-wallet-dedup
All checks were successful
check / check (push) Successful in 21s
2026-03-01 01:13:06 +01:00
25ecaee128 Merge pull request 'feat: add copy-flash visual feedback — closes #100' (#113) from fix/issue-100-copy-flash-feedback into main
All checks were successful
check / check (push) Successful in 21s
Reviewed-on: #113
2026-03-01 01:12:40 +01:00
user
ff4b5ee24d feat: add copy-flash visual feedback on click-to-copy
All checks were successful
check / check (push) Successful in 9s
When a user clicks to copy text (addresses, tx hashes, etc.), the copied
element now briefly flashes with inverted colors (bg/fg swap) and fades
back over ~300ms. This provides localized visual feedback in addition to
the existing flash message.

Applied to all click-to-copy elements across all views.

closes #100
2026-03-01 01:01:34 +01:00
user
ca6e9054f9 fix: cross-wallet-type duplicate detection for all import methods
All checks were successful
check / check (push) Successful in 22s
- Private key import now checks ALL wallets (hd, xprv, key) for address conflicts
- xprv import now checks xpub against existing xpubs and addresses across all wallet types
- Mnemonic import now checks xpub against xprv wallets and addresses across all types
- Extract findWalletByAddress() and findWalletByXpub() helpers for consistent dedup

closes #111
2026-02-28 15:58:47 -08:00
27 changed files with 1188 additions and 163 deletions

View File

@@ -12,6 +12,10 @@ const { refreshBalances, getProvider } = require("../shared/balances");
const { debugFetch } = require("../shared/log");
const { decryptWithPassword } = require("../shared/vault");
const { getSignerForAddress } = require("../shared/wallet");
const {
isPhishingDomain,
updatePhishingList,
} = require("../shared/phishingDomains");
const storageApi =
typeof browser !== "undefined"
@@ -571,6 +575,10 @@ async function backgroundRefresh() {
setInterval(backgroundRefresh, BACKGROUND_REFRESH_INTERVAL);
// Fetch the MetaMask eth-phishing-detect domain blocklist on startup.
// Refreshes every 24 hours automatically.
updatePhishingList();
// When approval window is closed without a response, treat as rejection
if (windowsApi && windowsApi.onRemoved) {
windowsApi.onRemoved.addListener((windowId) => {
@@ -643,6 +651,8 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
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);

View File

@@ -107,7 +107,8 @@
</div>
<div
id="add-wallet-phrase-warning"
class="text-xs mb-2 border border-border border-dashed p-2 hidden"
class="text-xs mb-2 border border-border border-dashed p-2"
style="visibility: hidden"
>
Write these words down and keep them safe. Anyone with
them can take your funds; if you lose them, your wallet
@@ -184,7 +185,7 @@
<!-- active address headline -->
<div
id="total-value"
class="text-2xl font-bold min-h-[2rem]"
class="text-2xl font-bold min-h-[2rem] text-fg"
></div>
<div
id="total-value-sub"
@@ -375,7 +376,8 @@
</p>
<div
id="export-privkey-flash"
class="text-xs mb-2 hidden"
class="text-xs mb-2 min-h-[1.25rem]"
style="visibility: hidden"
></div>
<div id="export-privkey-password-section" class="mb-2">
<label class="block mb-1">Password</label>
@@ -579,13 +581,17 @@
<div class="text-xs text-muted mb-1">Your balance</div>
<div id="confirm-balance" class="text-xs"></div>
</div>
<div id="confirm-fee" class="mb-3 hidden">
<div id="confirm-fee" class="mb-3" style="visibility: hidden">
<div class="text-xs text-muted mb-1">
Estimated network fee
</div>
<div id="confirm-fee-amount" class="text-xs"></div>
</div>
<div id="confirm-warnings" class="mb-2 hidden"></div>
<div
id="confirm-warnings"
class="mb-2"
style="visibility: hidden"
></div>
<div
id="confirm-recipient-warning"
class="mb-2"
@@ -599,9 +605,35 @@
Double-check the address before sending.
</div>
</div>
<div
id="confirm-contract-warning"
class="mb-2"
style="visibility: hidden"
>
<div
class="border border-red-500 border-dashed p-2 text-xs font-bold text-red-500"
>
WARNING: The recipient is a smart contract. Sending ETH
or tokens directly to a contract may result in permanent
loss of funds.
</div>
</div>
<div
id="confirm-burn-warning"
class="mb-2"
style="visibility: hidden"
>
<div
class="border border-red-500 border-dashed p-2 text-xs font-bold text-red-500"
>
WARNING: This is a known null/burn address. Funds sent
here are permanently destroyed and cannot be recovered.
</div>
</div>
<div
id="confirm-errors"
class="mb-2 border border-border border-dashed p-2 hidden"
class="mb-2 border border-border border-dashed p-2"
style="visibility: hidden; min-height: 1.25rem"
></div>
<div class="mb-2">
<label class="block mb-1 text-xs">Password</label>
@@ -614,6 +646,7 @@
<div
id="confirm-tx-password-error"
class="text-xs mb-2 min-h-[1.25rem]"
style="visibility: hidden"
></div>
<button
id="btn-confirm-send"
@@ -728,7 +761,8 @@
</button>
<div
id="receive-erc20-warning"
class="text-xs border border-border border-dashed p-2 mt-3 hidden"
class="text-xs border border-border border-dashed p-2 mt-3"
style="visibility: hidden"
></div>
</div>
@@ -756,7 +790,8 @@
</div>
<div
id="add-token-info"
class="text-xs text-muted mb-2 hidden"
class="text-xs text-muted mb-2 min-h-[1.25rem]"
style="visibility: hidden"
></div>
<div class="mb-2">
<label class="block mb-1 text-xs text-muted"
@@ -814,7 +849,7 @@
<div class="bg-well p-3 mx-1 mb-3">
<h3 class="font-bold mb-1">Display</h3>
<label
class="text-xs flex items-center gap-1 cursor-pointer"
class="text-xs flex items-center gap-1 cursor-pointer mb-2"
>
<input
type="checkbox"
@@ -822,6 +857,17 @@
/>
Show tracked tokens with zero balance
</label>
<div class="text-xs flex items-center gap-1">
<label for="settings-theme">Theme:</label>
<select
id="settings-theme"
class="border border-border p-1 bg-bg text-fg text-xs cursor-pointer"
>
<option value="system">System</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</div>
</div>
<div class="bg-well p-3 mx-1 mb-3">
@@ -903,6 +949,12 @@
/>
<span class="text-xs text-muted">gwei</span>
</div>
<label
class="text-xs flex items-center gap-1 cursor-pointer mb-1"
>
<input type="checkbox" id="settings-utc-timestamps" />
UTC Timestamps
</label>
</div>
<div class="bg-well p-3 mx-1 mb-3">
@@ -938,7 +990,8 @@
</p>
<div
id="delete-wallet-flash"
class="text-xs text-red-500 mb-2 hidden"
class="text-xs text-red-500 mb-2 min-h-[1.25rem]"
style="visibility: hidden"
></div>
<div class="mb-2">
<label class="block mb-1">Password</label>
@@ -1013,7 +1066,8 @@
/>
<div
id="settings-addtoken-info"
class="text-xs text-muted mt-1 hidden"
class="text-xs text-muted mt-1 min-h-[1.25rem]"
style="visibility: hidden"
></div>
<button
id="btn-settings-addtoken-manual"
@@ -1095,6 +1149,20 @@
<!-- ============ TRANSACTION APPROVAL ============ -->
<div id="view-approve-tx" class="view hidden">
<h2 class="font-bold mb-2">Transaction Request</h2>
<div
id="approve-tx-phishing-warning"
class="mb-3 p-2 text-xs font-bold hidden"
style="
background: #fee2e2;
color: #991b1b;
border: 2px solid #dc2626;
border-radius: 6px;
"
>
⚠️ PHISHING WARNING: This site is on MetaMask's phishing
blocklist. This transaction may steal your funds. Proceed
with extreme caution.
</div>
<p class="mb-2">
<span id="approve-tx-hostname" class="font-bold"></span>
wants to send a transaction.
@@ -1139,7 +1207,8 @@
</div>
<div
id="approve-tx-error"
class="text-xs mb-2 border border-border border-dashed p-1 min-h-[1.25rem] hidden"
class="text-xs mb-2 border border-border border-dashed p-1 min-h-[1.25rem]"
style="visibility: hidden"
></div>
<div class="flex justify-between">
<button
@@ -1160,6 +1229,20 @@
<!-- ============ SIGNATURE APPROVAL ============ -->
<div id="view-approve-sign" class="view hidden">
<h2 class="font-bold mb-2">Signature Request</h2>
<div
id="approve-sign-phishing-warning"
class="mb-3 p-2 text-xs font-bold hidden"
style="
background: #fee2e2;
color: #991b1b;
border: 2px solid #dc2626;
border-radius: 6px;
"
>
⚠️ PHISHING WARNING: This site is on MetaMask's phishing
blocklist. Signing this message may authorize theft of your
funds. Proceed with extreme caution.
</div>
<p class="mb-2">
<span id="approve-sign-hostname" class="font-bold"></span>
wants you to sign a message.
@@ -1167,8 +1250,10 @@
<div
id="approve-sign-danger-warning"
class="hidden mb-3 p-2 text-xs font-bold"
class="mb-3 p-2 text-xs font-bold"
style="
visibility: hidden;
min-height: 1.25rem;
background: #fee2e2;
color: #991b1b;
border: 2px solid #dc2626;
@@ -1205,7 +1290,8 @@
</div>
<div
id="approve-sign-error"
class="text-xs mb-2 border border-border border-dashed p-1 min-h-[1.25rem] hidden"
class="text-xs mb-2 border border-border border-dashed p-1 min-h-[1.25rem]"
style="visibility: hidden"
></div>
<div class="flex justify-between">
<button
@@ -1226,6 +1312,20 @@
<!-- ============ SITE APPROVAL ============ -->
<div id="view-approve-site" class="view hidden">
<h2 class="font-bold mb-2">Connection Request</h2>
<div
id="approve-site-phishing-warning"
class="mb-3 p-2 text-xs font-bold hidden"
style="
background: #fee2e2;
color: #991b1b;
border: 2px solid #dc2626;
border-radius: 6px;
"
>
⚠️ PHISHING WARNING: This site is on MetaMask's phishing
blocklist. Connecting your wallet may result in loss of
funds. Proceed with extreme caution.
</div>
<div class="mb-3">
<p class="mb-2">
<span id="approve-hostname" class="font-bold"></span>

View File

@@ -6,6 +6,7 @@ const { state, saveState, loadState } = require("../shared/state");
const { refreshPrices } = require("../shared/prices");
const { refreshBalances } = require("../shared/balances");
const { $, showView } = require("./views/helpers");
const { applyTheme } = require("./theme");
const home = require("./views/home");
const welcome = require("./views/welcome");
@@ -176,6 +177,7 @@ async function init() {
}
await loadState();
applyTheme(state.theme);
// Auto-default active address
if (

View File

@@ -15,7 +15,32 @@
--color-section: #dddddd;
}
html.dark {
--color-bg: #000000;
--color-fg: #ffffff;
--color-muted: #aaaaaa;
--color-border: #ffffff;
--color-border-light: #444444;
--color-hover: #222222;
--color-well: #1a1a1a;
--color-danger-well: #2a0a0a;
--color-section: #2a2a2a;
}
body {
width: 396px;
overflow-x: hidden;
}
/* Copy-flash feedback: inverts colors then fades back */
.copy-flash-active {
background-color: var(--color-fg) !important;
color: var(--color-bg) !important;
transition: none;
}
.copy-flash-fade {
transition:
background-color 225ms ease-out,
color 225ms ease-out;
}

33
src/popup/theme.js Normal file
View File

@@ -0,0 +1,33 @@
// Theme management: applies light/dark class to <html> based on preference.
let mediaQuery = null;
let mediaHandler = null;
function applyTheme(theme) {
// Clean up previous system listener
if (mediaQuery && mediaHandler) {
mediaQuery.removeEventListener("change", mediaHandler);
mediaHandler = null;
}
if (theme === "dark") {
document.documentElement.classList.add("dark");
} else if (theme === "light") {
document.documentElement.classList.remove("dark");
} else {
// system
mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const update = () => {
if (mediaQuery.matches) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
};
mediaHandler = update;
mediaQuery.addEventListener("change", update);
update();
}
}
module.exports = { applyTheme };

View File

@@ -7,7 +7,8 @@ const { log } = require("../../shared/log");
function show() {
$("add-token-address").value = "";
$("add-token-info").classList.add("hidden");
$("add-token-info").textContent = "";
$("add-token-info").style.visibility = "hidden";
const list = $("common-token-list");
list.innerHTML = getTopTokens(25)
.map(
@@ -45,7 +46,7 @@ function init(ctx) {
}
const infoEl = $("add-token-info");
infoEl.textContent = "Looking up token...";
infoEl.classList.remove("hidden");
infoEl.style.visibility = "visible";
log.debugf("Looking up token contract", contractAddr);
try {
const info = await lookupTokenInfo(contractAddr, state.rpcUrl);
@@ -63,7 +64,8 @@ function init(ctx) {
const detail = e.shortMessage || e.message || String(e);
log.errorf("Token lookup failed for", contractAddr, detail);
showFlash(detail);
infoEl.classList.add("hidden");
infoEl.textContent = "";
infoEl.style.visibility = "hidden";
}
});

View File

@@ -11,6 +11,25 @@ const { encryptWithPassword } = require("../../shared/vault");
const { state, saveState } = require("../../shared/state");
const { scanForAddresses } = require("../../shared/balances");
/**
* Check if an address already exists in ANY wallet (hd, xprv, or key).
* Returns the wallet object if found, or undefined.
*/
function findWalletByAddress(addr) {
const lower = addr.toLowerCase();
return state.wallets.find((w) =>
w.addresses.some((a) => a.address.toLowerCase() === lower),
);
}
/**
* Check if an xpub already exists in any HD-type wallet (hd or xprv).
* Returns the wallet object if found, or undefined.
*/
function findWalletByXpub(xpub) {
return state.wallets.find((w) => w.xpub && w.xpub === xpub);
}
let currentMode = "mnemonic";
const MODES = ["mnemonic", "privkey", "xprv"];
@@ -52,7 +71,7 @@ function show() {
$("import-xprv-key").value = "";
$("add-wallet-password").value = "";
$("add-wallet-password-confirm").value = "";
$("add-wallet-phrase-warning").classList.add("hidden");
$("add-wallet-phrase-warning").style.visibility = "hidden";
switchMode("mnemonic");
showView("add-wallet");
}
@@ -97,18 +116,18 @@ async function importMnemonic(ctx) {
const pw = validatePassword();
if (!pw) return;
const { xpub, firstAddress } = hdWalletFromMnemonic(mnemonic);
const duplicate = state.wallets.find(
(w) =>
w.type === "hd" &&
w.addresses[0] &&
w.addresses[0].address.toLowerCase() === firstAddress.toLowerCase(),
);
if (duplicate) {
const xpubDup = findWalletByXpub(xpub);
if (xpubDup) {
showFlash(
"This recovery phrase is already added (" + duplicate.name + ").",
"This recovery phrase is already added (" + xpubDup.name + ").",
);
return;
}
const addrDup = findWalletByAddress(firstAddress);
if (addrDup) {
showFlash("Address already exists in wallet (" + addrDup.name + ").");
return;
}
const encrypted = await encryptWithPassword(mnemonic, pw);
const walletNum = state.wallets.length + 1;
const wallet = {
@@ -162,15 +181,10 @@ async function importPrivateKey(ctx) {
}
const pw = validatePassword();
if (!pw) return;
const duplicate = state.wallets.find(
(w) =>
w.type === "key" &&
w.addresses[0] &&
w.addresses[0].address.toLowerCase() === addr.toLowerCase(),
);
const duplicate = findWalletByAddress(addr);
if (duplicate) {
showFlash(
"This private key is already added (" + duplicate.name + ").",
"This address already exists in wallet (" + duplicate.name + ").",
);
return;
}
@@ -208,14 +222,14 @@ async function importXprvKey(ctx) {
return;
}
const { xpub, firstAddress } = result;
const duplicate = state.wallets.find(
(w) =>
(w.type === "hd" || w.type === "xprv") &&
w.addresses[0] &&
w.addresses[0].address.toLowerCase() === firstAddress.toLowerCase(),
);
if (duplicate) {
showFlash("This key is already added (" + duplicate.name + ").");
const xpubDup = findWalletByXpub(xpub);
if (xpubDup) {
showFlash("This key is already added (" + xpubDup.name + ").");
return;
}
const addrDup = findWalletByAddress(firstAddress);
if (addrDup) {
showFlash("Address already exists in wallet (" + addrDup.name + ").");
return;
}
const pw = validatePassword();
@@ -267,7 +281,7 @@ function init(ctx) {
// Generate mnemonic
$("btn-generate-phrase").addEventListener("click", () => {
$("wallet-mnemonic").value = generateMnemonic();
$("add-wallet-phrase-warning").classList.remove("hidden");
$("add-wallet-phrase-warning").style.visibility = "visible";
});
// Import / confirm

View File

@@ -2,6 +2,7 @@ const {
$,
showView,
showFlash,
flashCopyFeedback,
balanceLinesForAddress,
addressDotHtml,
addressTitle,
@@ -94,18 +95,39 @@ function show() {
function isoDate(timestamp) {
const d = new Date(timestamp * 1000);
const pad = (n) => String(n).padStart(2, "0");
if (state.utcTimestamps) {
return (
d.getUTCFullYear() +
"-" +
pad(d.getUTCMonth() + 1) +
"-" +
pad(d.getUTCDate()) +
"T" +
pad(d.getUTCHours()) +
":" +
pad(d.getUTCMinutes()) +
":" +
pad(d.getUTCSeconds()) +
"Z"
);
}
const offsetMin = -d.getTimezoneOffset();
const sign = offsetMin >= 0 ? "+" : "-";
const absOff = Math.abs(offsetMin);
const tzStr = sign + pad(Math.floor(absOff / 60)) + ":" + pad(absOff % 60);
return (
d.getFullYear() +
"-" +
pad(d.getMonth() + 1) +
"-" +
pad(d.getDate()) +
" " +
"T" +
pad(d.getHours()) +
":" +
pad(d.getMinutes()) +
":" +
pad(d.getSeconds())
pad(d.getSeconds()) +
tzStr
);
}
@@ -241,6 +263,7 @@ function init(_ctx) {
if (addr) {
navigator.clipboard.writeText(addr);
showFlash("Copied!");
flashCopyFeedback($("address-full"));
}
});
@@ -310,8 +333,8 @@ function init(_ctx) {
$("export-privkey-address").textContent = addr.address;
$("export-privkey-address").dataset.full = addr.address;
$("export-privkey-password").value = "";
$("export-privkey-flash").classList.add("hidden");
$("export-privkey-flash").textContent = "";
$("export-privkey-flash").style.visibility = "hidden";
$("export-privkey-password-section").classList.remove("hidden");
$("export-privkey-result").classList.add("hidden");
$("export-privkey-value").textContent = "";
@@ -322,7 +345,7 @@ function init(_ctx) {
const password = $("export-privkey-password").value;
if (!password) {
$("export-privkey-flash").textContent = "Password is required.";
$("export-privkey-flash").classList.remove("hidden");
$("export-privkey-flash").style.visibility = "visible";
return;
}
const btn = $("btn-export-privkey-confirm");
@@ -343,10 +366,10 @@ function init(_ctx) {
$("export-privkey-password-section").classList.add("hidden");
$("export-privkey-value").textContent = privateKey;
$("export-privkey-result").classList.remove("hidden");
$("export-privkey-flash").classList.add("hidden");
$("export-privkey-flash").style.visibility = "hidden";
} catch {
$("export-privkey-flash").textContent = "Wrong password.";
$("export-privkey-flash").classList.remove("hidden");
$("export-privkey-flash").style.visibility = "visible";
} finally {
btn.disabled = false;
btn.classList.remove("text-muted");
@@ -358,6 +381,7 @@ function init(_ctx) {
if (key) {
navigator.clipboard.writeText(key);
showFlash("Copied!");
flashCopyFeedback($("export-privkey-value"));
}
});
@@ -366,6 +390,7 @@ function init(_ctx) {
if (full) {
navigator.clipboard.writeText(full);
showFlash("Copied!");
flashCopyFeedback($("export-privkey-address"));
}
});

View File

@@ -5,6 +5,7 @@ const {
$,
showView,
showFlash,
flashCopyFeedback,
addressDotHtml,
addressTitle,
escapeHtml,
@@ -47,18 +48,39 @@ function etherscanAddressLink(address) {
function isoDate(timestamp) {
const d = new Date(timestamp * 1000);
const pad = (n) => String(n).padStart(2, "0");
if (state.utcTimestamps) {
return (
d.getUTCFullYear() +
"-" +
pad(d.getUTCMonth() + 1) +
"-" +
pad(d.getUTCDate()) +
"T" +
pad(d.getUTCHours()) +
":" +
pad(d.getUTCMinutes()) +
":" +
pad(d.getUTCSeconds()) +
"Z"
);
}
const offsetMin = -d.getTimezoneOffset();
const sign = offsetMin >= 0 ? "+" : "-";
const absOff = Math.abs(offsetMin);
const tzStr = sign + pad(Math.floor(absOff / 60)) + ":" + pad(absOff % 60);
return (
d.getFullYear() +
"-" +
pad(d.getMonth() + 1) +
"-" +
pad(d.getDate()) +
" " +
"T" +
pad(d.getHours()) +
":" +
pad(d.getMinutes()) +
":" +
pad(d.getSeconds())
pad(d.getSeconds()) +
tzStr
);
}
@@ -317,6 +339,7 @@ function init(_ctx) {
if (addr) {
navigator.clipboard.writeText(addr);
showFlash("Copied!");
flashCopyFeedback($("address-token-full"));
}
});
@@ -325,6 +348,7 @@ function init(_ctx) {
if (copyEl) {
navigator.clipboard.writeText(copyEl.dataset.copy);
showFlash("Copied!");
flashCopyFeedback(copyEl);
}
});
@@ -373,6 +397,7 @@ function init(_ctx) {
copyEl.addEventListener("click", () => {
navigator.clipboard.writeText(copyEl.dataset.copy);
showFlash("Copied!");
flashCopyFeedback(copyEl);
});
}
updateSendBalance();

View File

@@ -13,6 +13,7 @@ const { ERC20_ABI } = require("../../shared/constants");
const { TOKEN_BY_ADDRESS } = require("../../shared/tokenList");
const txStatus = require("./txStatus");
const uniswap = require("../../shared/uniswap");
const { isPhishingDomain } = require("../../shared/phishingDomains");
const runtime =
typeof browser !== "undefined" ? browser.runtime : chrome.runtime;
@@ -155,7 +156,24 @@ function decodeCalldata(data, toAddress) {
return null;
}
function showPhishingWarning(elementId, hostname, isPhishing) {
const el = $(elementId);
if (!el) return;
// Check both the flag from background and a local re-check
if (isPhishing || isPhishingDomain(hostname)) {
el.classList.remove("hidden");
} else {
el.classList.add("hidden");
}
}
function showTxApproval(details) {
showPhishingWarning(
"approve-tx-phishing-warning",
details.hostname,
details.isPhishingDomain,
);
const toAddr = details.txParams.to;
const token = toAddr ? TOKEN_BY_ADDRESS.get(toAddr.toLowerCase()) : null;
const ethValue = formatEther(details.txParams.value || "0");
@@ -269,7 +287,7 @@ function showTxApproval(details) {
}
$("approve-tx-password").value = "";
$("approve-tx-error").classList.add("hidden");
hideError("approve-tx-error");
showView("approve-tx");
}
@@ -323,6 +341,12 @@ function formatTypedDataHtml(jsonStr) {
}
function showSignApproval(details) {
showPhishingWarning(
"approve-sign-phishing-warning",
details.hostname,
details.isPhishingDomain,
);
const sp = details.signParams;
$("approve-sign-hostname").textContent = details.hostname;
@@ -351,10 +375,10 @@ function showSignApproval(details) {
if (warningEl) {
if (sp.dangerWarning) {
warningEl.textContent = sp.dangerWarning;
warningEl.classList.remove("hidden");
warningEl.style.visibility = "visible";
} else {
warningEl.textContent = "";
warningEl.classList.add("hidden");
warningEl.style.visibility = "hidden";
}
}
@@ -382,6 +406,12 @@ function show(id) {
showSignApproval(details);
return;
}
// Site connection approval
showPhishingWarning(
"approve-site-phishing-warning",
details.hostname,
details.isPhishingDomain,
);
$("approve-hostname").textContent = details.hostname;
$("approve-address").innerHTML = approvalAddressHtml(
state.activeAddress,

View File

@@ -15,6 +15,7 @@ const {
hideError,
showView,
showFlash,
flashCopyFeedback,
addressTitle,
addressDotHtml,
escapeHtml,
@@ -24,8 +25,11 @@ const { getSignerForAddress } = require("../../shared/wallet");
const { decryptWithPassword } = require("../../shared/vault");
const { formatUsd, getPrice } = require("../../shared/prices");
const { getProvider } = require("../../shared/balances");
const { isScamAddress, isNullOrBurnAddress } = require("../../shared/scamlist");
const { ERC20_ABI } = require("../../shared/constants");
const {
getLocalWarnings,
getFullWarnings,
} = require("../../shared/addressWarnings");
const { ERC20_ABI, isBurnAddress } = require("../../shared/constants");
const { log } = require("../../shared/log");
const makeBlockie = require("ethereum-blockies-base64");
const txStatus = require("./txStatus");
@@ -38,28 +42,6 @@ const EXT_ICON =
`</svg></span>`;
let pendingTx = null;
// Track active warnings so async checks can append without overwriting.
let activeWarnings = [];
function renderWarnings(el, warnings) {
activeWarnings = warnings.slice();
if (warnings.length > 0) {
el.innerHTML = warnings
.map(
(w) =>
`<div class="border border-border border-dashed p-2 mb-1 text-xs font-bold">WARNING: ${w}</div>`,
)
.join("");
el.classList.remove("hidden");
} else {
el.classList.add("hidden");
}
}
function appendWarning(el, message) {
activeWarnings.push(message);
renderWarnings(el, activeWarnings);
}
function restore() {
const d = state.viewData;
@@ -139,6 +121,7 @@ function show(txInfo) {
copyEl.onclick = () => {
navigator.clipboard.writeText(copyEl.dataset.copy);
showFlash("Copied!");
flashCopyFeedback(copyEl);
};
}
} else {
@@ -187,24 +170,24 @@ function show(txInfo) {
$("confirm-balance").textContent = valueWithUsd(bal + " ETH", balUsd);
}
// Check for warnings (synchronous checks first, async checks added later)
const warnings = [];
if (isScamAddress(txInfo.to)) {
warnings.push(
"This address is on a known scam/fraud list. Do not send funds to this address.",
);
}
if (isNullOrBurnAddress(txInfo.to)) {
warnings.push(
"This is a null or burn address. Funds sent here will be permanently lost.",
);
}
if (txInfo.to.toLowerCase() === txInfo.from.toLowerCase()) {
warnings.push("You are sending to your own address.");
}
// Check for warnings (synchronous local checks)
const localWarnings = getLocalWarnings(txInfo.to, {
fromAddress: txInfo.from,
});
const warningsEl = $("confirm-warnings");
renderWarnings(warningsEl, warnings);
if (localWarnings.length > 0) {
warningsEl.innerHTML = localWarnings
.map(
(w) =>
`<div class="border border-border border-dashed p-2 mb-1 text-xs font-bold">WARNING: ${w.message}</div>`,
)
.join("");
warningsEl.style.visibility = "visible";
} else {
warningsEl.innerHTML = "";
warningsEl.style.visibility = "hidden";
}
// Check for errors
const errors = [];
@@ -241,11 +224,12 @@ function show(txInfo) {
errorsEl.innerHTML = errors
.map((e) => `<div class="text-xs">${e}</div>`)
.join("");
errorsEl.classList.remove("hidden");
errorsEl.style.visibility = "visible";
sendBtn.disabled = true;
sendBtn.classList.add("text-muted");
} else {
errorsEl.classList.add("hidden");
errorsEl.innerHTML = "";
errorsEl.style.visibility = "hidden";
sendBtn.disabled = false;
sendBtn.classList.remove("text-muted");
}
@@ -255,15 +239,19 @@ function show(txInfo) {
hideError("confirm-tx-password-error");
// Gas estimate — show placeholder then fetch async
$("confirm-fee").classList.remove("hidden");
$("confirm-fee").style.visibility = "visible";
$("confirm-fee-amount").textContent = "Estimating...";
state.viewData = { pendingTx: txInfo };
showView("confirm-tx");
// Hide the legacy recipient warning element (warnings now unified)
const legacyWarningEl = $("confirm-recipient-warning");
if (legacyWarningEl) {
legacyWarningEl.style.display = "none";
// Reset async warnings to hidden (space always reserved, no layout shift)
$("confirm-recipient-warning").style.visibility = "hidden";
$("confirm-contract-warning").style.visibility = "hidden";
$("confirm-burn-warning").style.visibility = "hidden";
// Show burn warning via reserved element (in addition to inline warning)
if (isBurnAddress(txInfo.to)) {
$("confirm-burn-warning").style.visibility = "visible";
}
estimateGas(txInfo);
@@ -311,24 +299,18 @@ async function estimateGas(txInfo) {
}
async function checkRecipientHistory(txInfo) {
const warningsEl = $("confirm-warnings");
try {
const provider = getProvider(state.rpcUrl);
const code = await provider.getCode(txInfo.to);
if (code && code !== "0x") {
// Recipient is a contract address — warn the user
appendWarning(
warningsEl,
"The recipient is a contract address. Sending tokens directly to a contract may result in permanent loss of funds.",
);
return;
}
const txCount = await provider.getTransactionCount(txInfo.to);
if (txCount === 0) {
appendWarning(
warningsEl,
"The recipient address has ZERO transaction history. This may indicate a fresh or unused address. Double-check the address before sending.",
);
const asyncWarnings = await getFullWarnings(txInfo.to, provider, {
fromAddress: txInfo.from,
});
for (const w of asyncWarnings) {
if (w.type === "contract") {
$("confirm-contract-warning").style.visibility = "visible";
}
if (w.type === "new-address") {
$("confirm-recipient-warning").style.visibility = "visible";
}
}
} catch (e) {
log.errorf("recipient history check failed:", e.message);

View File

@@ -12,7 +12,7 @@ function show(walletIdx) {
wallet.name || "Wallet " + (walletIdx + 1);
$("delete-wallet-password").value = "";
$("delete-wallet-flash").textContent = "";
$("delete-wallet-flash").classList.add("hidden");
$("delete-wallet-flash").style.visibility = "hidden";
showView("delete-wallet-confirm");
}
@@ -29,14 +29,14 @@ function init(_ctx) {
if (!pw) {
$("delete-wallet-flash").textContent =
"Please enter your password.";
$("delete-wallet-flash").classList.remove("hidden");
$("delete-wallet-flash").style.visibility = "visible";
return;
}
if (deleteWalletIndex === null) {
$("delete-wallet-flash").textContent =
"No wallet selected for deletion.";
$("delete-wallet-flash").classList.remove("hidden");
$("delete-wallet-flash").style.visibility = "visible";
return;
}
@@ -52,7 +52,7 @@ function init(_ctx) {
await decryptWithPassword(wallet.encryptedSecret, pw);
} catch (_e) {
$("delete-wallet-flash").textContent = "Wrong password.";
$("delete-wallet-flash").classList.remove("hidden");
$("delete-wallet-flash").style.visibility = "visible";
btn.disabled = false;
btn.classList.remove("text-muted");
return;

View File

@@ -40,11 +40,13 @@ function $(id) {
function showError(id, msg) {
const el = $(id);
el.textContent = msg;
el.classList.remove("hidden");
el.style.visibility = "visible";
}
function hideError(id) {
$(id).classList.add("hidden");
const el = $(id);
el.textContent = "";
el.style.visibility = "hidden";
}
function showView(name) {
@@ -226,18 +228,39 @@ function formatAddressHtml(address, ensName, maxLen, title) {
function isoDate(timestamp) {
const d = new Date(timestamp * 1000);
const pad = (n) => String(n).padStart(2, "0");
if (state.utcTimestamps) {
return (
d.getUTCFullYear() +
"-" +
pad(d.getUTCMonth() + 1) +
"-" +
pad(d.getUTCDate()) +
"T" +
pad(d.getUTCHours()) +
":" +
pad(d.getUTCMinutes()) +
":" +
pad(d.getUTCSeconds()) +
"Z"
);
}
const offsetMin = -d.getTimezoneOffset();
const sign = offsetMin >= 0 ? "+" : "-";
const absOff = Math.abs(offsetMin);
const tzStr = sign + pad(Math.floor(absOff / 60)) + ":" + pad(absOff % 60);
return (
d.getFullYear() +
"-" +
pad(d.getMonth() + 1) +
"-" +
pad(d.getDate()) +
" " +
"T" +
pad(d.getHours()) +
":" +
pad(d.getMinutes()) +
":" +
pad(d.getSeconds())
pad(d.getSeconds()) +
tzStr
);
}
@@ -258,12 +281,26 @@ function timeAgo(timestamp) {
return years + " year" + (years !== 1 ? "s" : "") + " ago";
}
function flashCopyFeedback(el) {
if (!el) return;
el.classList.remove("copy-flash-fade");
el.classList.add("copy-flash-active");
setTimeout(() => {
el.classList.remove("copy-flash-active");
el.classList.add("copy-flash-fade");
setTimeout(() => {
el.classList.remove("copy-flash-fade");
}, 275);
}, 75);
}
module.exports = {
$,
showError,
hideError,
showView,
showFlash,
flashCopyFeedback,
balanceLine,
balanceLinesForAddress,
addressColor,

View File

@@ -2,6 +2,7 @@ const {
$,
showView,
showFlash,
flashCopyFeedback,
balanceLinesForAddress,
isoDate,
timeAgo,
@@ -85,9 +86,10 @@ function renderActiveAddress() {
el.innerHTML =
`<span class="underline decoration-dashed cursor-pointer" id="active-addr-copy">${dot}${escapeHtml(addr)}</span>` +
`<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
$("active-addr-copy").addEventListener("click", () => {
$("active-addr-copy").addEventListener("click", (e) => {
navigator.clipboard.writeText(addr);
showFlash("Copied!");
flashCopyFeedback(e.currentTarget);
});
} else {
el.textContent = "";

View File

@@ -2,6 +2,7 @@ const {
$,
showView,
showFlash,
flashCopyFeedback,
formatAddressHtml,
addressTitle,
} = require("./helpers");
@@ -52,19 +53,21 @@ function show() {
"This is an ERC-20 token. Only send " +
symbol +
" on the Ethereum network to this address. Sending tokens on other networks will result in permanent loss.";
warningEl.classList.remove("hidden");
warningEl.style.visibility = "visible";
} else {
warningEl.classList.add("hidden");
warningEl.textContent = "";
warningEl.style.visibility = "hidden";
}
showView("receive");
}
function init(ctx) {
$("receive-address-block").addEventListener("click", () => {
$("receive-address-block").addEventListener("click", (e) => {
const addr = $("receive-address-block").dataset.full;
if (addr) {
navigator.clipboard.writeText(addr);
showFlash("Copied!");
flashCopyFeedback(e.currentTarget);
}
});
@@ -73,6 +76,7 @@ function init(ctx) {
if (addr) {
navigator.clipboard.writeText(addr);
showFlash("Copied!");
flashCopyFeedback($("receive-address-block"));
}
});

View File

@@ -1,4 +1,5 @@
const { $, showView, showFlash, escapeHtml } = require("./helpers");
const { applyTheme } = require("../theme");
const { state, saveState } = require("../../shared/state");
const { ETHEREUM_MAINNET_CHAIN_ID } = require("../../shared/constants");
const { log, debugFetch } = require("../../shared/log");
@@ -214,6 +215,13 @@ function init(ctx) {
await saveState();
});
$("settings-theme").value = state.theme;
$("settings-theme").addEventListener("change", async () => {
state.theme = $("settings-theme").value;
await saveState();
applyTheme(state.theme);
});
$("settings-hide-low-holders").checked = state.hideLowHolderTokens;
$("settings-hide-low-holders").addEventListener("change", async () => {
state.hideLowHolderTokens = $("settings-hide-low-holders").checked;
@@ -241,6 +249,12 @@ function init(ctx) {
}
});
$("settings-utc-timestamps").checked = state.utcTimestamps;
$("settings-utc-timestamps").addEventListener("change", async () => {
state.utcTimestamps = $("settings-utc-timestamps").checked;
await saveState();
});
$("btn-main-add-wallet").addEventListener("click", ctx.showAddWalletView);
$("btn-settings-add-token").addEventListener(

View File

@@ -73,7 +73,8 @@ function renderDropdown() {
function show() {
$("settings-addtoken-address").value = "";
$("settings-addtoken-info").classList.add("hidden");
$("settings-addtoken-info").textContent = "";
$("settings-addtoken-info").style.visibility = "hidden";
renderTop10();
renderDropdown();
showView("settings-addtoken");
@@ -129,7 +130,7 @@ function init(_ctx) {
}
const infoEl = $("settings-addtoken-info");
infoEl.textContent = "Looking up token...";
infoEl.classList.remove("hidden");
infoEl.style.visibility = "visible";
log.debugf("Looking up token contract", addr);
try {
const info = await lookupTokenInfo(addr, state.rpcUrl);
@@ -143,7 +144,8 @@ function init(_ctx) {
await saveState();
showFlash("Added " + info.symbol);
$("settings-addtoken-address").value = "";
infoEl.classList.add("hidden");
infoEl.textContent = "";
infoEl.style.visibility = "hidden";
renderTop10();
renderDropdown();
ctx.doRefreshAndRender();
@@ -151,7 +153,8 @@ function init(_ctx) {
const detail = e.shortMessage || e.message || String(e);
log.errorf("Token lookup failed for", addr, detail);
showFlash(detail);
infoEl.classList.add("hidden");
infoEl.textContent = "";
infoEl.style.visibility = "hidden";
}
});
}

View File

@@ -5,6 +5,7 @@ const {
$,
showView,
showFlash,
flashCopyFeedback,
addressDotHtml,
addressTitle,
escapeHtml,
@@ -171,6 +172,7 @@ function render() {
el.onclick = () => {
navigator.clipboard.writeText(el.dataset.copy);
showFlash("Copied!");
flashCopyFeedback(el);
};
});
}
@@ -248,6 +250,7 @@ async function loadCalldata(txHash, toAddress) {
el.onclick = () => {
navigator.clipboard.writeText(el.dataset.copy);
showFlash("Copied!");
flashCopyFeedback(el);
};
});
}

View File

@@ -4,6 +4,7 @@ const {
$,
showView,
showFlash,
flashCopyFeedback,
addressDotHtml,
addressTitle,
escapeHtml,
@@ -77,6 +78,7 @@ function attachCopyHandlers(viewId) {
el.onclick = () => {
navigator.clipboard.writeText(el.dataset.copy);
showFlash("Copied!");
flashCopyFeedback(el);
};
});
}

View File

@@ -0,0 +1,109 @@
// Address warning module.
// Provides local and async (RPC-based) warning checks for Ethereum addresses.
// Returns arrays of {type, message, severity} objects.
const { isScamAddress } = require("./scamlist");
const { isBurnAddress } = require("./constants");
const { checkEtherscanLabel } = require("./etherscanLabels");
const { log } = require("./log");
/**
* Check an address against local-only lists (scam, burn, self-send).
* Synchronous — no network calls.
*
* @param {string} address - The target address to check.
* @param {object} [options] - Optional context.
* @param {string} [options.fromAddress] - Sender address (for self-send check).
* @returns {Array<{type: string, message: string, severity: string}>}
*/
function getLocalWarnings(address, options = {}) {
const warnings = [];
const addr = address.toLowerCase();
if (isScamAddress(addr)) {
warnings.push({
type: "scam",
message:
"This address is on a known scam/fraud list. Do not send funds to this address.",
severity: "critical",
});
}
if (isBurnAddress(addr)) {
warnings.push({
type: "burn",
message:
"This is a known null/burn address. Funds sent here are permanently destroyed and cannot be recovered.",
severity: "critical",
});
}
if (options.fromAddress && addr === options.fromAddress.toLowerCase()) {
warnings.push({
type: "self-send",
message: "You are sending to your own address.",
severity: "warning",
});
}
return warnings;
}
/**
* Check an address against local lists AND via RPC queries.
* Async — performs network calls to check contract status and tx history.
*
* @param {string} address - The target address to check.
* @param {object} provider - An ethers.js provider instance.
* @param {object} [options] - Optional context.
* @param {string} [options.fromAddress] - Sender address (for self-send check).
* @returns {Promise<Array<{type: string, message: string, severity: string}>>}
*/
async function getFullWarnings(address, provider, options = {}) {
const warnings = getLocalWarnings(address, options);
try {
const code = await provider.getCode(address);
if (code && code !== "0x") {
warnings.push({
type: "contract",
message:
"This address is a smart contract, not a regular wallet.",
severity: "warning",
});
// If it's a contract, skip the tx count check — contracts
// may legitimately have zero inbound EOA transactions.
return warnings;
}
} catch (e) {
log.errorf("contract check failed:", e.message);
}
try {
const txCount = await provider.getTransactionCount(address);
if (txCount === 0) {
warnings.push({
type: "new-address",
message:
"This address has never sent a transaction. Double-check it is correct.",
severity: "info",
});
}
} catch (e) {
log.errorf("tx count check failed:", e.message);
}
// Etherscan label check (best-effort async — network failures are silent).
try {
const etherscanWarning = await checkEtherscanLabel(address);
if (etherscanWarning) {
warnings.push(etherscanWarning);
}
} catch (e) {
log.errorf("etherscan label check failed:", e.message);
}
return warnings;
}
module.exports = { getLocalWarnings, getFullWarnings };

View File

@@ -20,6 +20,19 @@ const ERC20_ABI = [
"function approve(address spender, uint256 amount) returns (bool)",
];
// Known null/burn addresses that permanently destroy funds.
const BURN_ADDRESSES = new Set([
"0x0000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000001",
"0x000000000000000000000000000000000000dead",
"0xdead000000000000000000000000000000000000",
"0x00000000000000000000000000000000deadbeef",
]);
function isBurnAddress(address) {
return BURN_ADDRESSES.has(address.toLowerCase());
}
module.exports = {
DEBUG,
DEBUG_MNEMONIC,
@@ -28,4 +41,6 @@ module.exports = {
DEFAULT_BLOCKSCOUT_URL,
BIP44_ETH_PATH,
ERC20_ABI,
BURN_ADDRESSES,
isBurnAddress,
};

View File

@@ -0,0 +1,102 @@
// 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 ETHERSCAN_BASE = "https://etherscan.io/address/";
// 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).
*
* @param {string} address - Ethereum address to check.
* @returns {Promise<{type: string, message: string, severity: string}|null>}
*/
async function checkEtherscanLabel(address) {
try {
const resp = await fetch(ETHERSCAN_BASE + 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 };

View File

@@ -0,0 +1,133 @@
// Domain-based phishing detection using MetaMask's eth-phishing-detect blocklist.
// Fetches the blocklist at runtime, caches it in memory, and checks hostnames.
//
// The blocklist source:
// https://github.com/MetaMask/eth-phishing-detect (src/config.json)
//
// The config uses { blacklist: [...], whitelist: [...], fuzzylist: [...] }.
// We check exact hostname and parent-domain matches against the blacklist,
// with whitelist overrides.
const BLOCKLIST_URL =
"https://raw.githubusercontent.com/MetaMask/eth-phishing-detect/main/src/config.json";
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
let blacklistSet = new Set();
let whitelistSet = new Set();
let lastFetchTime = 0;
let fetchPromise = null;
/**
* Load a pre-parsed config into the in-memory sets.
* Used for testing and for loading from cache.
*
* @param {{ blacklist?: string[], whitelist?: string[] }} config
*/
function loadConfig(config) {
blacklistSet = new Set(
(config.blacklist || []).map((d) => d.toLowerCase()),
);
whitelistSet = new Set(
(config.whitelist || []).map((d) => d.toLowerCase()),
);
lastFetchTime = Date.now();
}
/**
* Generate hostname variants for subdomain matching.
* "sub.evil.com" yields ["sub.evil.com", "evil.com"].
*
* @param {string} hostname
* @returns {string[]}
*/
function hostnameVariants(hostname) {
const h = hostname.toLowerCase();
const variants = [h];
const parts = h.split(".");
// Parent domains: a.b.c.d -> b.c.d, c.d
for (let i = 1; i < parts.length - 1; i++) {
variants.push(parts.slice(i).join("."));
}
return variants;
}
/**
* Check if a hostname is on the phishing blocklist.
* Checks exact hostname and all parent domains.
* Whitelisted domains are never flagged.
*
* @param {string} hostname - The hostname to check.
* @returns {boolean}
*/
function isPhishingDomain(hostname) {
if (!hostname) return false;
const variants = hostnameVariants(hostname);
// Whitelist takes priority
for (const v of variants) {
if (whitelistSet.has(v)) return false;
}
for (const v of variants) {
if (blacklistSet.has(v)) return true;
}
return false;
}
/**
* Fetch the latest blocklist from the MetaMask repo.
* De-duplicates concurrent fetches. Results are cached for CACHE_TTL_MS.
*
* @returns {Promise<void>}
*/
async function updatePhishingList() {
// Skip if recently fetched
if (Date.now() - lastFetchTime < CACHE_TTL_MS && blacklistSet.size > 0) {
return;
}
// De-duplicate concurrent calls
if (fetchPromise) return fetchPromise;
fetchPromise = (async () => {
try {
const resp = await fetch(BLOCKLIST_URL);
if (!resp.ok) throw new Error("HTTP " + resp.status);
const config = await resp.json();
loadConfig(config);
} catch {
// Silently fail — we'll retry next time.
} finally {
fetchPromise = null;
}
})();
return fetchPromise;
}
/**
* Return the current blocklist size (for diagnostics).
*
* @returns {number}
*/
function getBlocklistSize() {
return blacklistSet.size;
}
/**
* Reset internal state (for testing).
*/
function _reset() {
blacklistSet = new Set();
whitelistSet = new Set();
lastFetchTime = 0;
fetchPromise = null;
}
module.exports = {
isPhishingDomain,
updatePhishingList,
loadConfig,
getBlocklistSize,
hostnameVariants,
_reset,
};

View File

@@ -8,8 +8,10 @@
// and does not enforce jurisdiction-specific sanctions.
//
// Sources:
// - MyEtherWallet/ethereum-lists addresses-darklist.json (MIT)
// - CryptoScamDB/EtherScamDB scams.yaml (MIT)
// - MyEtherWallet/ethereum-lists addresses-darklist.json (MIT license)
// https://github.com/MyEtherWallet/ethereum-lists
// - EtherScamDB scams.yaml (MIT license)
// https://github.com/MrLuit/EtherScamDB
// - Known wallet-drainer contracts identified via Etherscan labels,
// MistTrack alerts, and community incident reports.
//
@@ -27,6 +29,7 @@ const SCAM_ADDRESSES = new Set([
"0x00c8bd3cd1e649a3fd2a89b3edc1c2ab631227a0",
"0x00d07f8c2194f14c8d694680f2b8c0be66d4d5e9",
"0x00e01a648ff41346cdeb873182383333d2184dd1",
"0x00eb4cc5eeac114294f3beef4007bcdaec293f59",
"0x00eb6f5199cd0b671da371969b1a0f948e982fea",
"0x011da0ab16577cbb73374a5b9b869d66253150e7",
"0x0153775362c3071c1860e8dbfd53ccc82fa226f5",
@@ -60,6 +63,7 @@ const SCAM_ADDRESSES = new Set([
"0x052f585fa599bfb4bed290ff30c057627ccd8059",
"0x054aa7a66dcdf5fd76a3914fb5e02650c1afa65b",
"0x058e49378461c239dae065114cd9fa1c0dbc25c1",
"0x0591a42188996397fc7cd6db729045146c37696c",
"0x059615ea15f7f0e2a276103127bbace30223d294",
"0x0596e5e05fd430f7d83b38b23e250bfa98900d7f",
"0x05dd62c007cde143b402fa5da3937c40c70b4b14",
@@ -108,6 +112,7 @@ const SCAM_ADDRESSES = new Set([
"0x096c39ab4fa36ca74a36fe1f4767ff487762f0aa",
"0x09750ad360fdb7a2ee23669c4503c974d86d8694",
"0x09909c850f0e3019a645e0530aa37798dba59eb1",
"0x099218972a0f693151b54d09617f5b25ecdd187b",
"0x09cc69f6b484cada8e152d2002adfca496d723f3",
"0x09f8b64a3992ab516fc34de1157d2b4a39d45301",
"0x09faf25e57abd0a401bb5a2341d7f926c389f8d1",
@@ -139,6 +144,7 @@ const SCAM_ADDRESSES = new Set([
"0x0bdd81d2a676166f2a28691e8af64ebeeca67fae",
"0x0c2cb2d98f9f5ed558d9560d0e38e47187b2bfe1",
"0x0c4762bc7b1af1dc9448a827f53861c118b712de",
"0x0c626d05fb5805362f2fbd5f4291211b691f129b",
"0x0c6a415effaa3a2310c0db718090f4c5fd633982",
"0x0c8b0b3e963becf16cda410fe1eb4b5d64022c94",
"0x0c8b740e3377f2be7a108aeba6a2f660588c728d",
@@ -202,7 +208,7 @@ const SCAM_ADDRESSES = new Set([
"0x1177d4f07c6ef70f51cf23493c66e713f7acccda",
"0x118db57c34621acd0a91f02c8c18cd1a3ad7c213",
"0x119240ef17333e8dcb19a0dd4f5fc848981b0ff4",
"0x11c058c3efbf53939fb6872b09a2b5cf2410a1e2c3f3c867664e43a626d878c0",
"0x11c058c3efbf53939fb6872b09a2b5cf2410a1e2",
"0x122c7f492c51c247e293b0f996fa63de61474959",
"0x124c0c9a23e1c4c04b10cc1eda4934f9818c7ec6",
"0x12736c6b02381c3c50e41db3a69d7bb651a77d57",
@@ -219,6 +225,7 @@ const SCAM_ADDRESSES = new Set([
"0x14258a5ddcad7001e8ac89b539df7e0f08a34500",
"0x143494359bbacc878308075c1c9fa05fcda96651",
"0x1447b1cb205158d98fdbd312b37ed9dd1481fb62",
"0x144826ded37a161f4d0aa7e5884131750022703c",
"0x1494403137159bb0dc545d11963fdf797ea1ecab",
"0x14a11e2ef83895918da176165e33025e02e472de",
"0x14b7716c688875bcf54d5ff47cad4fca6b3834fa",
@@ -248,6 +255,7 @@ const SCAM_ADDRESSES = new Set([
"0x17897c5c38362d8f620e70fee0adba109af5db2d",
"0x17935dffbc228397d2f5a67061867804cecacae8",
"0x17980b9ff9b318c93263ed52d19f3ca7ad24ed9a",
"0x179f6011f23e68fcbefbe3bf816240e09a85a700",
"0x17bd6f815fb71a77dc20e12177c4d763a3f67632",
"0x17eb36408edccd62df38938f9f85ac6a590b1f34",
"0x181c71726f12ce2514e8b93019eb22645a79f966",
@@ -264,6 +272,7 @@ const SCAM_ADDRESSES = new Set([
"0x18e81898ac31a43850fb8d0224477349de6d8d9a",
"0x18ec1d727320cbed7c0c63cd655ade997b292c5e",
"0x18f3489d959fd0e3fac366646e08f9aeabea4d75",
"0x18ffda6e10338378159411135f78bfa98f18298a",
"0x1901d590f5ddbe8d58c69b324c13f884ca1d0b31",
"0x196333b00abf440c044b7164868cd5a4682e0276",
"0x197f986c640aedfd3ce94553c61ece11937bc2bb",
@@ -298,7 +307,7 @@ const SCAM_ADDRESSES = new Set([
"0x1c39d6e0278d1a28ce21dbd73826559b010224e5",
"0x1c40d1a1cac7c586b9509c565296f91c8441af9f",
"0x1c50139c266559b29d7cb27635e0da13bef76a09",
"0x1c6e3348a7ea72ffe6a384e51bd1f36ac1bcb4264f461889a318a3bb2251bf19",
"0x1c6e3348a7ea72ffe6a384e51bd1f36ac1bcb426",
"0x1cbc864ea74e0f23f103df8623ba56a01b1eda59",
"0x1ccc9b2769741ab0e1620721df7cf8ff1d70716b",
"0x1ccd65d573057c388ea96cc8be30c18a6d21185d",
@@ -423,6 +432,7 @@ const SCAM_ADDRESSES = new Set([
"0x2a206bca8ee7c324af1b67ae05373a59a3d4502d",
"0x2a21180d900cddc46416117903edc0ee83286408",
"0x2a37a183eb7f18cae3fcb967404e472729f21e96",
"0x2a5b8e165bcae0e6e1524e9966a20b0631ee8bbf",
"0x2a6c7867e605d0f0f940cec1669d98a33cf4c299",
"0x2a6d8021861f27ab992572d8689017b7a83c989d",
"0x2a7d04018d7295d8069ec9721ee415c4bdc57909",
@@ -436,6 +446,7 @@ const SCAM_ADDRESSES = new Set([
"0x2b3f15a55a68c4c81ae8331c2fe8e90008993f51",
"0x2b5268fde5041f6b1afd77166de4e9ea5d9e967a",
"0x2b5d022d3396c412a58c7be8897bc8d3e0c823c3",
"0x2b8cdceb358ee31c2ba6c9707ad09df66bf7bc88",
"0x2bcc8aefa2fc9d8e52dad098778bb0a8d08a4aa3",
"0x2bd267ad20e8c42c85f9c16fd4c6f24770dc492c",
"0x2c05d74cd9f878b9834a50092043bde944a60ef8",
@@ -444,6 +455,7 @@ const SCAM_ADDRESSES = new Set([
"0x2c30e423d30e78c416b3d8938e2e14d09af4753f",
"0x2cbc78b7db97576674cc4e442d3f4d792b43a3a9",
"0x2cc762c27a50d18e50efcd3bee9e3154e3bb094d",
"0x2ccbff3a042c68716ed2a2cb0c544a9f1d1935e1",
"0x2cf904052d9ff15c18d6061e569b09925a6f0ac0",
"0x2d146aa23645950fdefbb23f636a5d1674fe1047",
"0x2d19f9fece6da18f2899944b0621c5ecebaa249b",
@@ -458,7 +470,7 @@ const SCAM_ADDRESSES = new Set([
"0x2dc5616ec2b9d24906f0e11dc8d8a736392dfedd",
"0x2df62e8d020984d77a5baaec76aa3473daf2d313",
"0x2dfc601db0c0b5eefc302249acdf936dce83cfb3",
"0x2dfe2e0522cc1f050edcc7a05213bb55bbb36884ec9468fc39eccc013c65b5e4",
"0x2dfe2e0522cc1f050edcc7a05213bb55bbb36884",
"0x2e3a90d922c6d5f73136f83eb62a86e27b792487",
"0x2e3fd2b400a269f7a0b1aaf448ef02679cd557e9",
"0x2e462fdef8cde7db82aa3f7c627731be94f7c9c4",
@@ -491,6 +503,7 @@ const SCAM_ADDRESSES = new Set([
"0x31a732f783baff201d7a1bf530e3411636e6df2d",
"0x31e2f659e5371f95de47be79e1d5027caf0da39d",
"0x31e437cfa7dd0c6bad194e4865c3696551715c4a",
"0x31f530b58efa42dab4bd1789184b4cc6b884dea0",
"0x31fca2139b07c2a97287de51f0219a34f4181710",
"0x3219ebdcbc9f79f59c8c53105f3e74e0289f3ea9",
"0x3232d99007e509d29564099c622a89e143fdc05c",
@@ -519,6 +532,7 @@ const SCAM_ADDRESSES = new Set([
"0x34eea3e741f9a3e77fb34e0793b1af36edc2b0ae",
"0x35124e63425645e352b56a06482cea32bedec838",
"0x3533104fd03ff652d7e041ec259080f75f243f3f",
"0x355ed5aaa382d0443a03bf4b3b4cfc85eb80e4ca",
"0x356149c2eeb5aaa1030022d22134abed97bd496e",
"0x35647d709bcd5b451f8c62228876975e8d31f85b",
"0x35810a676a84ec4db68c0a9286f7a740fda10b29",
@@ -555,6 +569,7 @@ const SCAM_ADDRESSES = new Set([
"0x3853ba76ec6ae97818e2d0e0839c9eda6c396690",
"0x3869db12591382ed94eefe6b55c4e82c6b8c1489",
"0x387ba35be8c8cbf2f4d3ddb571cb80069bf100e2",
"0x3883f5e181fccaf8410fa61e12b59bad963fb645",
"0x3884eb0ae2a04bce65b5b0ca9c1bd069cbd52c66",
"0x388cf3c02c034e7fe8ef164a2b414534fc212119",
"0x38b7ca3b78c51364095bc56146d25f55aee0af21",
@@ -574,6 +589,7 @@ const SCAM_ADDRESSES = new Set([
"0x3a0086083aa90c4692507bb82dc14b8754ebc663",
"0x3a3999e6501e2a36dd3c0b8fc2bd165fc4a22e54",
"0x3a85774c434a4cc51fda217cf0e5caefd6c0af2f",
"0x3a878eddb991ebcbc7c8052055b2e5ed5d0d1ba4",
"0x3a992c15aae6d9b9062f6533cd12fc9f89e9a3be",
"0x3aa65f17cde339df49afd2f88b3c8495842d5fb0",
"0x3ad44a16451d65d97394ac793b0a2d90c8530499",
@@ -587,6 +603,7 @@ const SCAM_ADDRESSES = new Set([
"0x3bd49b98ffcc5f717eed0c9e78a276ae979de6e4",
"0x3bf351a62df57dce8512b136daa4cd6ebe2dda91",
"0x3bff0d893f8f595bc40ad266772b583da492b20f",
"0x3c3284e18511e6fadb62be6e090991540dc8972e",
"0x3c3b85b2ae785a8cc16c3d4df12cb27c6983dff5",
"0x3c924cbbd8ffb34b3da99c1501afb9bb3cf5e4f4",
"0x3c98b49694c18ab0492a048ee213e2c3d3b3e0ff",
@@ -635,6 +652,7 @@ const SCAM_ADDRESSES = new Set([
"0x41075e21827263e149b8bb26ff7eb5b185c7b0ca",
"0x410ebb88525ea799ea9e15a03f794f7f6f2fbdcd",
"0x4114fb8b1879f61b18f7d2e623569a847a03e15a",
"0x4121cc82607ebab3f334e067f37fe2709c403bf6",
"0x41466413f0c7f11c5b1af39cc1344af9d21a6d57",
"0x41467711f2d1f6021119ffb113ae0e78251b45fa",
"0x414bca672494b8f078112c52ae258f9e8de1a4a0",
@@ -645,6 +663,7 @@ const SCAM_ADDRESSES = new Set([
"0x4217c883eecd9581644c31794f695553ccc505e1",
"0x4256117a02ac880335f8bfbeed63f92ec0001a5a",
"0x425e0406395243f087100c8f7b67c02217974123",
"0x4287c8db304f036068520292fe263b9412f4d30d",
"0x429d386adc915ade21593609b1b89b976b2f2af4",
"0x42a777e0f24390e7ed461a7af325526e53ac57d9",
"0x42c5459911ae51d1d005cbe39749bd8d8e533c22",
@@ -671,6 +690,7 @@ const SCAM_ADDRESSES = new Set([
"0x4491492c21605d923f1ef9e947eeaa74799dbdd6",
"0x4496370b4f0993152f761bd3b3d1dd4e7b1b3139",
"0x44a7ff01f7d38c73530c279e19d31527bdcf8c78",
"0x44af22ab4b6cf9deb599566163aa4c1b4f4196a8",
"0x44b7fd8c984d3d66d8eaa6af4a4ba9976f257e49",
"0x44ce1de2379aa6d6d4ad46c3531b9eebb71f7f54",
"0x45029af827c652f47b1f678456b2cd009647c8ad",
@@ -715,6 +735,7 @@ const SCAM_ADDRESSES = new Set([
"0x484f46f9dd32a0147126f07da4be61efc26ef42a",
"0x4886df279cc4946941e26de30b4011a2e92d2009",
"0x4899f3371fb9f8e68a0b639bf1fd75220a089c42",
"0x48a17df119f20d50b829f15e43e0e59a658c7476",
"0x48bd2b7b486a5c6f9560510bc0f7c915509ef7f7",
"0x48d0a447b1d7b9a89112578db4536032d3047b2e",
"0x48dcc01a31e38e14f6a63846e371c6e9cb0c3f3a",
@@ -743,6 +764,7 @@ const SCAM_ADDRESSES = new Set([
"0x4a7d4264291d6bee4d4ee3eb01006445e19ef366",
"0x4a8264933388e0f526fee7b5e2b8a9ad74d1a11c",
"0x4a8a747408e23b8b5314a85df21b0f8f8307ab40",
"0x4a8b69bd663e6967a118456a30a51495bdba704c",
"0x4a8d9b64ee9c7e058fe9c8cbf96375b02da006e2",
"0x4ae9971e28d7f422b406ee1f3bb38e1b96637239",
"0x4b2acbb4160ddf25fb45842bfecbaea9a1919103",
@@ -779,6 +801,7 @@ const SCAM_ADDRESSES = new Set([
"0x4ee78087a5cab2a24c49edcdc3d6bf61045d2a53",
"0x4efbb8a016b8d28d261fe31be1404cf12f4010a2",
"0x4f1872383be22878af5d4795b69be61b35ec5d10",
"0x4f223fcb9beb2d560f8d55c17d7ce5aa4b7bdc5b",
"0x4f3ca8c465170c0988295e22b5347ef8bb38f2b7",
"0x4f484c32c4810ae8129f1b724b09c663f8341713",
"0x4f53c9882ba87d2d7c525df2aef2540efb6e32e5",
@@ -797,10 +820,13 @@ const SCAM_ADDRESSES = new Set([
"0x5093c4029acab3aff80140023099f5ec6ca7d52f",
"0x50a9a8c8ce2bb0ea10f4d4a586ac2d77257b8247",
"0x50e8de98570b8a2ddff80a9d2e8adcdbc35f3f95",
"0x50fe59792a325084604a524a8f148de04182769a",
"0x5123bc9d86e99ea6b6c67c7ba66507f5b4356d7c",
"0x5127e8d79160f4cd177f5ac5a1e860acaa59a34b",
"0x514e9ca5406f112aba902b0cba87395b914d861e",
"0x5167052b83f36952d1a9901e0de2b2038c3dd1a3",
"0x5188d13bdd9d7ac30324a569e2ce42488374738e",
"0x51af777899f0e81fbb69836e9255cc5bab7a5842",
"0x51dcd13361c5d921d2c4818e419011301e7be34e",
"0x52005b77fbebf53cfd9527a388f58d0431aaaa3e",
"0x5208d7f63a089906889a5a9caed81e9c889e64f8",
@@ -815,18 +841,23 @@ const SCAM_ADDRESSES = new Set([
"0x52c9dfd68c67aafc9e3e615fb38824b17def6432",
"0x53645080083e0037976455ef903bc88679d1029f",
"0x536a6ba0d913d5d6a4ce2c6eb7ed0de3c0f0b89e",
"0x536de519a2ab6fd0cc8055e5119b3c2a8dd2464b",
"0x539832908a06ff5cdd5abc84db81c2a95f70eb33",
"0x53db08c95d0d6de34d8318970bbcb063f82fcc41",
"0x53ffe7422c7c1769e5183377bbf2f6858b0d74de",
"0x5400cff7aa5537881b305d838a951c3fec123b10",
"0x540c6f30ae667319e032a8988dcb81b28a960433",
"0x5416400b0d5798dfdcefa2d22335f18982ee22fa",
"0x54189eab44661be1b081b4535c050230a787d816",
"0x5457bb4430d8b8d1a3708576100aef0e027c5b9b",
"0x545b0fdcbb2b404b26546521d4d183c2538509c2",
"0x545f5e5c54d8ac72af1c7de8c070387b73841a24",
"0x548821e41d89303810f3886f986b3e0c1f6f8858",
"0x548ac87629ceb858aa09b419f5b9898ec5735fba",
"0x548f0678ff6b82c2c97913ef3b0f2c515ef2594c",
"0x5496d2e076c2c467bef865f7e23fb2ff83a17ac9",
"0x54b623c82365ef20680b17301bd949bcd5599eb9",
"0x54d2fb4f6ef43bc3b0ec0efe62400c2603ed136f",
"0x54e50eec86d757ef26269c061deace691b269116",
"0x5526dc369bbe6b9b2c5e64bb368c1d3ccfa58271",
"0x552c94e34f62ca14d9f2bec6f1e59b11efb25b89",
@@ -863,10 +894,12 @@ const SCAM_ADDRESSES = new Set([
"0x57d4e4ea0a207074d7e45fe60c939d2f4d3ed06b",
"0x57dcf20135b0cb9d167e8ebfe13b84bfd67645ed",
"0x58049616f5cd4b2ccd3654d7015d271a985ff2e7",
"0x58192acd70c8f198337ee9eb90d96b255643636a",
"0x581cc257051a34972641d008c3915a75771be274",
"0x581ffaadeeb6d183a73d93413e137cb8c6057603",
"0x584dab561e3c4ba02c74fb76e7c48a0a3b7d8097",
"0x585f148c35a5f50829a29f6ecfaf02d83303c3b5",
"0x588c0e4af4bb0cd6e63e898bf8ad555452297326",
"0x58a166f9a6ff996e2349eea90d42cc198529a037",
"0x58b62d0e9cf118c6eebe4ce7e9680a8ec533c094",
"0x58dce1959623e1210fe465ca3afa3a05590cc4e7",
@@ -894,6 +927,7 @@ const SCAM_ADDRESSES = new Set([
"0x5c0133dc7e8811b3faddd1016d0800dbf5bec2d8",
"0x5c0ea45d4d78d062667f9ea17534306e409acf0a",
"0x5c2f45f806683785e59d4b41547e3729b681d806",
"0x5c3a228510d246b78a3765c20221cbf3082b44a4",
"0x5c5452d1a4fb7175812de3b4a4f1c34d5bc5a261",
"0x5c9b3fa74e0700a3cbffd168c7342799a163ba53",
"0x5caa556d0b8fa8622b1746bcc1211429737dcafc",
@@ -904,8 +938,10 @@ const SCAM_ADDRESSES = new Set([
"0x5d1bcbde56db05bead0ff7c87c9dc85baf98ab32",
"0x5d3f32f4b2e99fb79d2f6a1cbf3aa7390f8fc751",
"0x5d445ce69ff9d8096d57444906c193f0ee577bf2",
"0x5d4d57cd06fa7fe99e26fdc481b468f77f05073c",
"0x5d82db63cf0c54d47006d416bdc7dab09ea2f3f1",
"0x5d840db230c397acc22ab28053d1a1ff7f14583e",
"0x5dc8ed6cfe7df7c66744ff4f68c53e6f482875e8",
"0x5ddd20ac4bacff3f148af4c8c24194a1c102cfe5",
"0x5de81caefee2f158964a65f0e19bd5e5ee94602a",
"0x5dfae1c25ec5232f1d725d5265ceddd7102be5b4",
@@ -926,11 +962,14 @@ const SCAM_ADDRESSES = new Set([
"0x5fe584cb2d2a937c4abbd0cad57841cc8db97df1",
"0x602026bd56bf9ca9d86926669e4353035aca2a88",
"0x603ab01878daea6fa97bc7a02a6a4e6d6f52205c",
"0x6040e8deec06c448671b11676ced4bf742e10031",
"0x604cac9ce467021908814f5a36a006e236ae10a2",
"0x604da69d33a0df6414c30270434ba4e0e95d8cf6",
"0x606188614094946e0fcd05a0c94d51ff586fb01b",
"0x6067a95e7bb071a5b73741628a0a5cb5cc164203",
"0x607c78aeaabadb84adc3298fb3077fc75429e8bd",
"0x60931fbd64b2a4418b24c5ef8bc33f93f6fafa4e",
"0x60afc5761212ed59608b0559b4ccbaad24fc74b1",
"0x60c9c2de546be5fa9f5f1a05d174376d9bb7ad48",
"0x60e4ec00cc96b57792f89ad05eaaa7dca643800b",
"0x60e6a89060b325148fe247e9e1c0d090e2764ec2",
@@ -961,6 +1000,7 @@ const SCAM_ADDRESSES = new Set([
"0x6429f18602cfbe936a75d4bb707cdc15f33bb3fa",
"0x6442dc8bcb92fcd556c78d91f551cdaad62654f4",
"0x645125230e924d2db84be4cabf946c87d5be8935",
"0x645a58a563ab8c94207e8faea54bcb97e02d23c9",
"0x6488b63be2fc20c1f25983a53c447f6530351971",
"0x64aaf2fd9910e9ad85dbbb695664ff8812664e10",
"0x64b212d2fe54db498df6099e5a9afa8298068f89",
@@ -1008,6 +1048,7 @@ const SCAM_ADDRESSES = new Set([
"0x6973c24fadd0bcab33ee5cb325c8a70e81c67c20",
"0x6997726e03707bf08032b8b6da935cae80524bca",
"0x699abf8d1053243411280400bfcdb3b408f61e67",
"0x69b9053691d7caca5d76e0f7d5a9744b53aea0a5",
"0x69f8e87518129498da751f26ea2309db05e7270b",
"0x6a0e59af84f7a9afa5708b5e43c4845b17aedaac",
"0x6a13e6d0251fba7b474cf4468aa157ee0c6a736c",
@@ -1019,6 +1060,7 @@ const SCAM_ADDRESSES = new Set([
"0x6a60dc30ef7e29abbd5a92b7697270dbd2115c18",
"0x6a9c2ec4f4888d7338fc1b28584aaafeb01e6db3",
"0x6aa83b12973f3364bc7f17211cfd85c7586fb371",
"0x6ab2a9088916bf97c248888009b669aa37bab681",
"0x6af114154a8850b1c54d48bd9103bdcdb420611b",
"0x6b0a6c142905ce6c1db7276d8878a3e7846675d9",
"0x6b4888b308d6013407190dfeefbb4c7dc2eaa61f",
@@ -1027,6 +1069,7 @@ const SCAM_ADDRESSES = new Set([
"0x6bed8445ada86419243f95a5bb14bc599d62b295",
"0x6c20ef21e604028c649247c581e8e49fda6191ca",
"0x6c4f56b116e91f15e790508d4212906b75835b63",
"0x6c653ca88eff4c9301023244ff49d506e5a2004c",
"0x6c712ae4986342773edc37a9ff7fa90b1b52ca6e",
"0x6cca6dcd9843b74fd6118687ad14b572bcf99ee6",
"0x6cf481c7090d88ca6f1fa3c9ffd0911ef5359808",
@@ -1050,7 +1093,9 @@ const SCAM_ADDRESSES = new Set([
"0x6f0041b3906048528a6760a1bc9627a201a83e38",
"0x6f431a1b6c368cd3abb7bacdcf0625d6749f1980",
"0x6fc7bd37879f85815e0d34e520ff502167a4e49c",
"0x6fce6d2ceca868ac809d1e8d52e9fc3b8690118f",
"0x6fcf42fcd9c6a54b64d1b52083700142952a2805",
"0x701888e9c68f40201de48d9d4bcdedeb2ef2b088",
"0x701cb175e6ca0ee8dcb61b4598b5a08e2a60bc2a",
"0x702d965332e3082dc976c3b4013cfe0e2c540bcb",
"0x703f0116bf2ef4c80efac751a319f097fd2dfe6c",
@@ -1065,6 +1110,8 @@ const SCAM_ADDRESSES = new Set([
"0x710b1d822e0e5b9d55f123598f383667502c3a51",
"0x71108be9c232722dd6839a6fbad171cf44d3ffaf",
"0x713a6f26525b24581cbe20679811af8680abdddd",
"0x713ea0a714cfdc3ed7d0d39a7732fc4293da1f81",
"0x71481c3ac7853ef63c87f0e9f91b6a5e16e04438",
"0x71486336caed5fa8e37ff8b31b9d67d08fbdc262",
"0x714b56c2454e2ef5e908daa7ccfce1a73184f2b9",
"0x717d2041c516573a0e27f552cdb6c5c3ec7b4e09",
@@ -1106,6 +1153,7 @@ const SCAM_ADDRESSES = new Set([
"0x7613a475b7fe61775f579bc148300c8171eccae9",
"0x76256547a138343164fe4a37ffb7b2bed27924f9",
"0x76524f023c42cb4018d24b8097959b2ecf8fa8ea",
"0x766316f65c88486bcb59fd57d5ce0087780c1424",
"0x76638de7e1a2ae5128ff808d17a0a636ba16324a",
"0x76bb5b6177096b337c79f2f948aa08b0db5f5211",
"0x76c88cd1ec2442c4f929b0f87280be006d7ba725",
@@ -1153,6 +1201,7 @@ const SCAM_ADDRESSES = new Set([
"0x7bb386c33486fe345168d0af94bef03897e16022",
"0x7bbcee71e2283c445bdb076bb7fa187726a45d53",
"0x7bc060459dd2730d912b654ec98cacdee2b6927d",
"0x7c030200a2e8abab418bc58b4f43c46ceb4d9202",
"0x7c14369f68d7812c105a597beb13ff8da64520c5",
"0x7c203685291149d5dad4308781f42a6b945df4e1",
"0x7c57981af4cee87567774b53f9da1bfceb8b944b",
@@ -1161,10 +1210,13 @@ const SCAM_ADDRESSES = new Set([
"0x7c9001c50ea57c1b2ec1e3e63cf04c297534bfc1",
"0x7c99f9cc14994f4b0d2e8cdcfda4811bbfb69a57",
"0x7cb16e15f66b8dca14385409f44336a9f679632c",
"0x7cb3ce3248e7c0cd320f3440d437a92a54f541f0",
"0x7cbc4e0d168744912c5afc081499145bbdc51e69",
"0x7cd912f8df67b1e56e7aa82a66f4a518c76c0108",
"0x7cdf272058894f70e3a7ace593628ad2bc0f574d",
"0x7d067a450b17a2936cc27c44e5499359d9940155",
"0x7d5f788097e80d797064fde23a62caf8d18ee5a6",
"0x7d66c5111f9369b1b7ecc2b80e97dc98585344bd",
"0x7d69d57b025aa1e5029194582eb85608db84cfb5",
"0x7d73f38770997f30bb6dfa2b8da184f2cfeba6c6",
"0x7d7f49f628e88413eb736bf059410bbdfecadcd5",
@@ -1176,6 +1228,7 @@ const SCAM_ADDRESSES = new Set([
"0x7debe420a36349fb8014c181c9a7e835e83efb23",
"0x7e160f5cf87e44cbe6b1337bb883f453445391de",
"0x7e1ed8b2c8ee37b4afa078b31c7bcba9fb0efa5a",
"0x7e25f33c1e3402f2197dab251ce2d73cdbcb77a3",
"0x7e3d7d8e31b3f472fcd3c1552a9f009131c50c6c",
"0x7e8ba7b52ae83be8885a613c2b249db4a2d0d06c",
"0x7ea4270d456bc7b3e1c72f8a21a88a90c3d60147",
@@ -1195,10 +1248,12 @@ const SCAM_ADDRESSES = new Set([
"0x7fc94073a3adf4553d59d09e0b0808bb9d408735",
"0x7fdfaa3154d3cb82e90f5fde8c52a95d66211705",
"0x7fe5f97dd063f39386207bcc27b5581b3ef2e600",
"0x7ff8caeb28f91ad0ee150027af4d51754461e055",
"0x801d03f4d053242afbc2949c141bab4270ca6707",
"0x8042980dbae8dde334b16df3870014c7bbd62ed6",
"0x804a67880f461c2ba4b927a10e99e371962b0f3f",
"0x806466179736c54b62dc6e9f844f02b8d67a49e7",
"0x809826cceab68c387726af962713b64cb5cb3cca",
"0x80b81748dd5c55baaa87731aa469ec17e33dac9a",
"0x80e02ae8c5d5e482558caf32004e7d6281445f28",
"0x80f43e67169c917c68391f823fcff41b9b786d69",
@@ -1209,6 +1264,7 @@ const SCAM_ADDRESSES = new Set([
"0x81c01024deaad32b015707f6f3ac948e88463204",
"0x81e4b65b9330c5693d38430111d7eb174615bdd6",
"0x81e7eae5baa0979398ba5c4b09cafcb8c690062b",
"0x81fbb23ce259d141339e49ece91599ed97cc8b49",
"0x8202b590eca4662446102b3a97e3536aac8ab941",
"0x820c415a17bf165a174e6b55232d956202d9470f",
"0x821006cd8ad55c74936d277217ecea6863231e48",
@@ -1243,6 +1299,7 @@ const SCAM_ADDRESSES = new Set([
"0x858457daa7e087ad74cdeeceab8419079bc2ca03",
"0x85d642eafb5f15ef54852b57302040ab066ff2c4",
"0x85e150d832942b10383b6d83211224c1ef37fad8",
"0x85f3e26a776c06338722d263406f260dcd79962e",
"0x860b62006ea05a05c7638d16a106858450bde336",
"0x860f3b648111294346897c6a8309ffbf22265526",
"0x8645ec394f7af95316639dce6f99c01476b0d888",
@@ -1296,6 +1353,7 @@ const SCAM_ADDRESSES = new Set([
"0x89f4d39667aa080502c517b613a6c92786aa14bd",
"0x8a04b6a0223af79e4acc0a55c7b166fe0794269a",
"0x8a0e85901849b40a7f80399a00f7115193c0bdb2",
"0x8a16589539096694a829d68ef8db4a95b628deef",
"0x8a46aa04725b2e2029bdcb924d671fd6a9c11dab",
"0x8a65c8565504a2bde31f9c09125f5b9d53b2b07e",
"0x8a6762e718beb7e6af9ab3868320c3c5137adc6c",
@@ -1304,12 +1362,15 @@ const SCAM_ADDRESSES = new Set([
"0x8a8869a8b53573294b0e03820253607d5528679d",
"0x8abe0a9b8a1c8a003354e61f3ed8befdeb7d2cec",
"0x8ac2d87f4308b0718aa9345b409277e01282fe03",
"0x8acacabed9eb9170e72c840e4e21f1fe67f8bfca",
"0x8af1380c436fc019b8055c61d09e3ee5f182278c",
"0x8b0987e0bccb30a53c5294ad73a80ed776457c4b",
"0x8b0e77fe177d555408339e26921be7d76d398d8a",
"0x8b170540b63d2f826846f3c0b9e6410b08548ec3",
"0x8b1b4f3a25444005ecd37f7b0c12bc5078c13443",
"0x8b3b831d767fad1ab0ad52a38777acb98630f8a7",
"0x8b6e20440a46881713f8f9da764b4094781e0f12",
"0x8b9310e47cd2d4aad77735a3218d9420cb152709",
"0x8baa4757f0a110dc4e5c365e7376d8449b084d16",
"0x8bb8a410e770a693c169a6b2b5286297d9eccbed",
"0x8bba1a1c5e9f72c2ce97a240a7878dd4ae803b61",
@@ -1373,6 +1434,7 @@ const SCAM_ADDRESSES = new Set([
"0x920adc9a060cda345fdec2fdebad6ebf38edf83d",
"0x92127085c609bde93cab077e60f417d852222299",
"0x92370e8b926f29038104a1fbafa024389e753fbe",
"0x9273d7cccd00d63da1f5f03e5f8e7399d2c45e25",
"0x92cd726ce03ceaa4bf7198ce9c52d05d63cee575",
"0x92d43d2f55e077d1bebc6e348a9f4ff64fd4f21a",
"0x92fa227d01dededae2a50b9da02413c48a872424",
@@ -1403,6 +1465,7 @@ const SCAM_ADDRESSES = new Set([
"0x955427a36b5c92edee90a1448bfc7e854e9caef5",
"0x956729a9e9b2ff42a30c8bd8fbe380b2c714825b",
"0x95c5db5bd9a0260fc457af4f6fc6de7819d9b1a6",
"0x95d986f907ea7aed17c7b09b9689af7819545fb1",
"0x95ffcdf36129e5e256190609cd97841d03e52356",
"0x960cf466795efc2d47cd37ad01bb43bfaaae3618",
"0x964a7c09e3db065ece947fdb7f90bf0068a6f98e",
@@ -1423,6 +1486,7 @@ const SCAM_ADDRESSES = new Set([
"0x983bda798a24720bb4fe3dba287ec352e7b440fc",
"0x9844f5c5f9aa7146a74ffc7b9227742acfa71dea",
"0x9849424a2f3516b70b56d70330c45c25dccb2c02",
"0x984ac2891fbaf6a10038c25cc1ba20e787416cfe",
"0x9852286460022e5151381361ba4271c7272cf966",
"0x988caa328c1f5f802c936dd1ae9e3da24729b687",
"0x98a630280447461ab2156919b0941ebdeae5bc41",
@@ -1436,6 +1500,7 @@ const SCAM_ADDRESSES = new Set([
"0x99bad2f2fe5856a02489440568d22fc8852deab5",
"0x99f93d05059f074e893ab369f71adb4569a3da12",
"0x9a27f038a711d84b4b5da0d3b96827e11b1de6db",
"0x9a4a8d9133a7d7401904d72789b1a925b20499ca",
"0x9a4afeaecd8efe2a7186365b55e72503d555713b",
"0x9a4b3419f7e74ffaac61aa7ae8d345dc4b8f0758",
"0x9a827b3aa6dd9781e25dc7bb58de67b2fa721d16",
@@ -1463,6 +1528,7 @@ const SCAM_ADDRESSES = new Set([
"0x9d03a3ad580cc2fb820349d3ff002fb2fc7a6b19",
"0x9d0b8447f16d6bc1de2b52d63e9c487bd778a91d",
"0x9d4b62503b4b7993182323effe6245f6d77e4413",
"0x9d61de20d2987eeabaa3b384ff6b5001ca570787",
"0x9d748d8f305b54127c57752716b48f23364ac3cd",
"0x9d79aeece49694b01b2d2a44ba2c240de48959c9",
"0x9da01df0eeae50b30845a1cafb27e1f75887b887",
@@ -1474,9 +1540,11 @@ const SCAM_ADDRESSES = new Set([
"0x9e12d932c429107608a8ad0d65c60021a371f9c1",
"0x9e5a6b7a73c6b390aa418f5b4cd3a8f6d1572810",
"0x9e78d3aa219e4255d86a7b71fd10a5f6612c8739",
"0x9e91ea0c442dd2b41cc7fe2d54faecd34f59c134",
"0x9e9e3b124370c763d1a4e838069da3dfdbfca059",
"0x9eb041af96d44583110565abec79aedf22eb60b5",
"0x9ede16fd45a94c5be23a6ab09651b7e26ce171c3",
"0x9eea7fda3c78cd8c096ae229de8217a94cd1bcfe",
"0x9f4562c9be26c7020909b50ccde3447f1b8c4b21",
"0x9fd4351644ea4c47b127ef3e237476570c01884e",
"0x9fe0627303fe40f1631231b4dd016d30c13eafc7",
@@ -1485,8 +1553,10 @@ const SCAM_ADDRESSES = new Set([
"0xa07ddf9ce7dc9c8492a9a307c1bf85cabeb6a8de",
"0xa0a51563ca933ecdf030f84425def24ab8cc6733",
"0xa0c7d4f3fc7f8a46fb58990228d0b82a0354f371",
"0xa0d4bee1beeafce031c56cfb40210746cc6ab2d0",
"0xa0d67bf1ae91b6b705abd4f695cc13edaacd0d02",
"0xa0db4acb24e92167341320fcd882bbfb641cd12d",
"0xa125929ba78213a27d8695b276f55d6aba567b01",
"0xa153a6ef80fb5d60de18688bdc82684d48fc8de1",
"0xa1b04a60a35854d0749ae39b0346bceb55247ecc",
"0xa1b70dc70fb74767cd380985cf93c4fb132fc4f7",
@@ -1534,6 +1604,7 @@ const SCAM_ADDRESSES = new Set([
"0xa5712afa19d70bb5dd57b91c43fffcaaf077dc3d",
"0xa5797ea738abf85db7b3f2e04a4a40a5180044a8",
"0xa592f4891ff2d233c93b641af65069ed663295bc",
"0xa59381a01daeae334730ef3ee81bfa5099a95faa",
"0xa59fc1920284607dd1d95bcc43f821601faef7d3",
"0xa5b254aea2e59ab3ce3bec470fe1882403c41be0",
"0xa5e83199a7ecb6669064d492f15ddc096b6cbab8",
@@ -1548,12 +1619,14 @@ const SCAM_ADDRESSES = new Set([
"0xa69ea6ab7707c551eeae1d443e179318a9dc73ee",
"0xa6b60dd3be491aafe5aba8622b35c0ead608d3fd",
"0xa6ba0996684fcff6167128a13c8b0a1648310e6e",
"0xa6caf53fc4aa0094306f304e4f235e2be98297e6",
"0xa6f2814c0020c38882e82d9bfde7b485a7297a1d",
"0xa7229e0de15d36a58a79ee61ab93744241ff001b",
"0xa746d613d9b3a267ad470e5ce980dbc12473247c",
"0xa7886aa3b1d05b72c88dd467d5ada7809f419566",
"0xa78959e1568448a3cf866968872431e710458552",
"0xa7a020ae798a0a026c57ed6cbe48b21d3dbbec5b",
"0xa7b8edb0583b478e2b7c431feb4bc6629a6cd1e5",
"0xa7be709f13d9b0283fb43211c85bbb12b7273e9a",
"0xa7ce02b8195fe8e3116a7e1248f2725eaac86fec",
"0xa7f21d9c638881ffbbab011b29aae0a5ec2c3739",
@@ -1599,6 +1672,7 @@ const SCAM_ADDRESSES = new Set([
"0xabc490af011c8e354b7d2112648883fe9e511695",
"0xac14590ad8184a48302f59a8baaf924b865de4c5",
"0xac1a4f4d49715e31f54cb8e0bf867bb9170c10cb",
"0xac1ee036c52d20ad437a1dc46056b9abf0cf1ca0",
"0xac2530772075b7576a7b27390e348fadf3345c29",
"0xac3800002e45ed2e1a55dedfa2aca137f6dba61e",
"0xac513396ee50091972ee6fc07d120b6ad360b233",
@@ -1652,6 +1726,7 @@ const SCAM_ADDRESSES = new Set([
"0xb23452b9e14f7770c0ad879fb4da1e70bf661eee",
"0xb23fb6cc17026d1ce9cd543cb69a023569503e4c",
"0xb25711e2493378eabb02e74de6653129846d5434",
"0xb29a0b9b4d32c9cd39c02d0f19a6a8bbb3047ceb",
"0xb2a3fcf38979898e695c88947e3373bf1c2e9b37",
"0xb2ac8e363ea34201df532a01522a006fcaa389ee",
"0xb2b8d4337ade6f8e6da4954844b05b9a1aa9358a",
@@ -1690,6 +1765,7 @@ const SCAM_ADDRESSES = new Set([
"0xb6751b16add74242ea28131ae7008f093734e455",
"0xb6adacf355031c10b4fd70ee7fb4aca88a7c216d",
"0xb6ecd180f5cbb9167147394841d31f94eff77dbf",
"0xb6ed7644c69416d67b522e20bc294a9a9b405b31",
"0xb70557d641af8ea5e94ac1569e6121d6a9bb30dd",
"0xb71df0dbbb9754c1aa9115253f4ebb7ef3f8a57c",
"0xb733a811edf703701d2403d8196df87306bed6d6",
@@ -1705,6 +1781,7 @@ const SCAM_ADDRESSES = new Set([
"0xb86fabbb253b69906ab8311de95a9008c738e92e",
"0xb8ba72c75af66cbe7e8dd72c7176aae678eb0fe5",
"0xb8ed23977f745865abbc645f09fc07f82cfde421",
"0xb8f2b53063ded859a3fdb96d43dd3d37253f47bf",
"0xb916873ded41b00b2691103d2b675bb3818e6c6e",
"0xb93184f8cfd3012e77114c93bf6ef08e9d6779a3",
"0xb9556a1cb3150148443e9e9e9c29491ff48640ee",
@@ -1746,8 +1823,10 @@ const SCAM_ADDRESSES = new Set([
"0xbc75274d1713e07df13b777c1d9067e7eb3dd885",
"0xbc83d48dd0cd9c3f47bab6436defeb334b563f4c",
"0xbc85a12364c9e375801c00aad17b893fc4c8f5b6",
"0xbc86727e770de68b1060c91f6bb6945c73e10388",
"0xbc8b85b1515e45fb2d74333310a1d37b879732c0",
"0xbc91d37841f58fc8611ab045c240bd5241090b86",
"0xbca08e0db1634b272e42630e2f1e7c4b92148b8d",
"0xbca6294a6c80ee0f20173547b8d85d4948a0cb39",
"0xbca804d30e8602f3ca47f8c8b3a44f8e03fe1594",
"0xbcc6c0fef89b87a12773db7a9a8ecbccccdb7aff",
@@ -1767,6 +1846,7 @@ const SCAM_ADDRESSES = new Set([
"0xbefa0509207c8834003af4bd82d13876f1a58fdd",
"0xbf0a4a57d66ec0eb94dce2fd0a4aa57f8b0a2530",
"0xbf0c85867fcdd4064d22b0dfd91561a52134e035",
"0xbf30cbf5dd5fbf040a1fa8b7388f756f624afdef",
"0xbf3d7084d5669f4a2dda9acf96fd60ad814734d7",
"0xbf7c1fa00de07b3041b4a099c2c435a5a27f259f",
"0xbf863b57322a1e634388946f942306393a5d4c69",
@@ -1808,6 +1888,7 @@ const SCAM_ADDRESSES = new Set([
"0xc4856c6c04ee3416daeaed3e5abae3622bbe1ebe",
"0xc4b1a260735fdc817409183db8594baf9f0b0f2f",
"0xc4b51a247514901faf1b6b1da9f0836066e64407",
"0xc4bd211e5b92235e6426113cd735b74bc4335d08",
"0xc4c474507cc8bbb5c8cf06f7351bd5395e83175d",
"0xc4ce40af23a0619119e7f59730131c22650d5d11",
"0xc4dca8f7d121ad79057a8382fc9fa9898727ddd8",
@@ -1864,6 +1945,7 @@ const SCAM_ADDRESSES = new Set([
"0xcbcd4b518890429c0ffef3d782ed99b3adff009b",
"0xcbf99a459b71aa633ff40eb14db95e0618c64b26",
"0xcc02b920ae227f1be7d01fc241c27e5f74d40436",
"0xcc1a8a506cd613694d705c92c26d92369f544052",
"0xcc2c4c1b9bee3e9ae45b5c9024c3d032387830b8",
"0xcc3a7e3c3cdcba86761de4fb3311b8add77761f2",
"0xcc49d1f23f01decd4e18b6aaacccb038c9648e30",
@@ -1881,6 +1963,7 @@ const SCAM_ADDRESSES = new Set([
"0xcd88045ecc901dd6e0beb11381d42c10429042b4",
"0xcda5dd8e13fdae006b270769b1a18fa6c5524ce0",
"0xcdae97fb0bd5539641a31b2228d95246b0ac2b6f",
"0xcdb26199db086d54f7b11e50ca4374b4dd9ce13f",
"0xcdba82888e4698c485abacf8c7ad87dc2221f378",
"0xcdd1c19dcc3f473eaef6edb0a28e1e796d6e1767",
"0xcde79393c6f0deb9b7b51579a386c853feb8e104",
@@ -1895,6 +1978,7 @@ const SCAM_ADDRESSES = new Set([
"0xcee56c3d77d814d29b9410c0f74c12781fce03ff",
"0xcee9ee01d48050415f1b104277bd493c5dbe645e",
"0xceebb2d0cdc85e9774a232bab74c45fc883c15e4",
"0xcf02680e5f057d41a68c3da6221ade6609833e5a",
"0xcf060f72ea615dc395f08d6205d3540a7142c712",
"0xcf1d62627baf1a84bed11e30cf6cdae0f1b5c296",
"0xcf75d0bf4e47f3db616a28d1cf8d4153d3957eda",
@@ -1903,6 +1987,7 @@ const SCAM_ADDRESSES = new Set([
"0xd03e202205168b8d0ef6c7fa9c27cb71b42f9e06",
"0xd055610c2d5151adb3eaf994e08abe45dee936e0",
"0xd06f80a4b932d7247aba4a85decce6c2458c0654",
"0xd0929d411954c47438dc1d871dd6081f5c5e149c",
"0xd0ac32ab01d9d3ec145ef63f73bf4e222dbcc0fb",
"0xd0acac843aaab4cef20b322405405e67e90147d1",
"0xd0b473271e9d38dfd11925d62ef8c0a2cf033a9c",
@@ -1914,9 +1999,11 @@ const SCAM_ADDRESSES = new Set([
"0xd11d565ba23b523ef7737aa24bdbb75a06521d87",
"0xd1381f89b4feea63e9c6bc97dc9fc2b0c96bf12f",
"0xd1bdd067f5f1cbd358e2dde444f8d9f41de8ae76",
"0xd1dc670608c00e8e6378062b48ef5911b068ae3d",
"0xd22066c4e511698b626aea89cf70fba5fd3f37d4",
"0xd22167d083a5cfc2f1ccadeb842a8093b2174c5f",
"0xd243018b2825ad409512a200e744529bc1b129f2",
"0xd26114cd6ee289accf82350c8d8487fedb8a0c07",
"0xd261e98abd0ebc6ccaa3c57a9bb017b0720c1343",
"0xd289bc0a919e75b824f4dfc376e9f24c119ed3f3",
"0xd2c42e8ec5e691bfb6f2e00565cb4455c565d9d3",
@@ -1945,6 +2032,7 @@ const SCAM_ADDRESSES = new Set([
"0xd51b910a21f091995a2fdbc54f8ae2f981506b5f",
"0xd522663a8a4dfe76a0bf1e608fc3b7aa2b9ccfde",
"0xd52fcaf53cd3efd7e1d484fd9cd2dd21355063e5",
"0xd53ec8a1478e7128d2d5f131a36462ca3190fcce",
"0xd550eada44d72c8a840d2aeb5cbe70469c9c0bac",
"0xd582cec308d9320b2ec3dc2868b56549fc8ace5a",
"0xd585209338fdc9eaa15ec3a2b7ec589fc99b9c5d",
@@ -1972,6 +2060,7 @@ const SCAM_ADDRESSES = new Set([
"0xd88b17314696afcbba531f1bbade53fcf5bc6018",
"0xd8959bd6983757fd0f883808f2a7ea1ad18b8d6a",
"0xd8d0506ed425364ee126819e8b73fc3160c39d49",
"0xd8ddfd63696127b18911546b13856cf98a246ff4",
"0xd8f203a2cfd7c647cd5a6619e90003724895d570",
"0xd912289c9f079b0655737a55fd5d745501ecefc7",
"0xd914c1152a0e4974dc3985e9b6f6905e002ebb3d",
@@ -1990,6 +2079,7 @@ const SCAM_ADDRESSES = new Set([
"0xda5f6405808111de82084e6804adf7153edaa8ed",
"0xda657e9fa116900fab8178e5580a3a6cedd89f3c",
"0xda7a139e03fe696b760af98f9df89466077bc12e",
"0xda8f192b292516e912e2323081618e61c00aa604",
"0xda917961872ae8e0c8b96f6925a4d7cc7b27aea3",
"0xda9c7d72d902db64b42b663358c47559be293f80",
"0xdaa29859836d97c810c7f9d350d4a1b3e8cafc9a",
@@ -2024,6 +2114,7 @@ const SCAM_ADDRESSES = new Set([
"0xdde66bf2a852ea1603686a0f858b45f8f0695d70",
"0xddff022e4befa69cbb5262446a8ae564700bea24",
"0xde5886e65cbf1a9d21267f5ef7d5ed444cc63938",
"0xde60e7ea42f99851b8dc4234880ac121eef6bd5f",
"0xde72b9575b532ab7cf37c677d95e9ce612b05ace",
"0xdef05b512d405f4fb930e252c6c11f054832c93b",
"0xdefb014b9e2f3bd81cdb084821f99b681cfca695",
@@ -2048,6 +2139,7 @@ const SCAM_ADDRESSES = new Set([
"0xe08c6d06107ff83a2be9177e2dbe91c87f0a9914",
"0xe0b13c073e8173b06062c69a160ebb54e2af86c3",
"0xe0ea79c1d13a86eb4abc78e8a2ab83651c9e49bf",
"0xe0eec9b102facc20e299b3adafb20b5003a4350f",
"0xe10270bfb1ed82e120bfc392efb3c94a1604ded6",
"0xe10708068c2c17b1d13ccd2f0b572fb737fc69c4",
"0xe10c24ca7bf18640fcb35e059919348891922a3b",
@@ -2075,6 +2167,7 @@ const SCAM_ADDRESSES = new Set([
"0xe3400442ed7754cb2a43becb83f801cce1055db9",
"0xe344e4b209e8eabaa2a6ddd1b0aa120b7599af25",
"0xe349b26753eca84a2858901d414c612c8c8e20f9",
"0xe34e1944e776f39b9252790a0527ebda647ae668",
"0xe350160e3c8a07e92ec58ddcb8df81a73aedc6f9",
"0xe3bbcdf129da8cf2b4e9c4950e343a693e9229cc",
"0xe3d474f3686a831bf380498d1dbd57fdf972ca30",
@@ -2105,10 +2198,12 @@ const SCAM_ADDRESSES = new Set([
"0xe6693620c549d35c52496a1cb0105f2712baf771",
"0xe695c41faebe1a4753d5ede23db32541b137079b",
"0xe6b39dad6d7a50b233da23e510697422e9d6351a",
"0xe6bf49e5ba7787a3550be485f074ec07f9c86403",
"0xe6bfb31042d652f365a855a77cb1891bcd17e9cf",
"0xe6c51d563f92a23dee9a7093bb1be33bd35c05d8",
"0xe6d3486a5fe2742e313a3266af8ff4f43e597d27",
"0xe6e9bbfb6f9f240ac42b2f4b39223e91ac5882b8",
"0xe6eb070bcef81fd12a198ba03fe393e9ee4cb671",
"0xe7300a38788ee039b20ae36c80dfeeb3a53d3a06",
"0xe74b02131ed2184eb94fd357b4f303e6935367f5",
"0xe79392c79832287f9a07d0af9fa87fd150014e18",
@@ -2123,6 +2218,7 @@ const SCAM_ADDRESSES = new Set([
"0xe88379631857c0d8174efefdf8dca25b29610f08",
"0xe886bfcfa373c17dbc07c7b73f1d368339ac5dfd",
"0xe889b0d59fd4181857edb19f5fecafa8510f2fad",
"0xe8f42cb54991a25eae58d2602f21c8d5d5105a7d",
"0xe949a4c861089fbde6ef1175c23e1485d5970567",
"0xe977419ef28e71ed541ff6318cea9a6392709a48",
"0xe9aa3a74e3d62274f221eca42736cadc14ccffaf",
@@ -2165,10 +2261,12 @@ const SCAM_ADDRESSES = new Set([
"0xed44fc770eaa76db9ade24d86ce3b409f4aed009",
"0xed51c3c2fb6e6965aed7beec167b0596dc36116d",
"0xed6b90c028310122af361ce84a4604afbed40910",
"0xed7e838d657dd5beff27c62178d285ba7834e022",
"0xed8c8830cfc51f306c4765598cf3ee50ad8d978a",
"0xedc0c49946b93aa296a11298480ae7913bb65222",
"0xedc702ed96accae315a0f1e8fa2d5a6bc4feea69",
"0xedf202629bb7e9f72d4c62c325d198513fa7a3d3",
"0xedf6ca1c856a97c1f91355f863f7c04b61643756",
"0xedfe1d963b32a89e05dd0bc0e8f595b4b9afb544",
"0xee07f244d1bc5b077296975bd062c930b9ab9ea6",
"0xee14140f20fb737809ca206c238382b3f802ca6c",
@@ -2182,6 +2280,7 @@ const SCAM_ADDRESSES = new Set([
"0xeed48722238b98317d849fa591d96b7efbe9b06f",
"0xeed7072fcf46733833249d1e979c44fe4f2a23a2",
"0xeef2a09be2a136bba76f04cf056e36947dbf0b0c",
"0xeefa861d00e68aab019df8bf3a9cb689196df41d",
"0xef0683bef79b7ad85573415c781edfde8bec65b1",
"0xef0d5aa61af54a8d932734b3f1949bf40b873bb7",
"0xef35af4a5037a1e416514f1f016550cf7c865ffc",
@@ -2205,6 +2304,7 @@ const SCAM_ADDRESSES = new Set([
"0xf1a4b668d15f0e66543e9f1f795cc2b0f97e3ef2",
"0xf1c058ae62f2d25efebdd809eedc609cbb9a5090",
"0xf1e250f2a27ab7ec5b82b287f2799260448cd51d",
"0xf203d1985247e9a93f31535c004fa783199fa6d3",
"0xf23a5c61e951b198adbc59e1a05a729c043d33d5",
"0xf23f9dda63ed0628609272dc0544c7a2f7189f51",
"0xf245e09a1b42f847b120558f0c6e08f821be23f7",
@@ -2215,6 +2315,7 @@ const SCAM_ADDRESSES = new Set([
"0xf31b4f7550833a746f788b36f2b292e5fa49a248",
"0xf33068d5e798f6519349ce32669d1ec940db1193",
"0xf33142f5bb228516f93e4267fb5a7241dc241614",
"0xf346fdcd5a205ba8f25edf0ffcddcc7a6583ac65",
"0xf34bdf2c9057b186499a7bc8afad1629c808e263",
"0xf364e3d3fcbefc297dc3724fea7ff86b2e14e740",
"0xf36965e734ce6f1abe3b66f0f819b3a8a9bd547c",
@@ -2264,6 +2365,7 @@ const SCAM_ADDRESSES = new Set([
"0xf8178b379ec4fd758230d28c55c89c064708dfe5",
"0xf847860a334c63b347030b3e6dc1d18136ce6f65",
"0xf855fee50a8915634a105385cb6cb9e442d15457",
"0xf87f6f77d695039599412b9fe04ddc89a5e7eb79",
"0xf880b70ce700297f70ffad94eebcbc7ead6b1b48",
"0xf8abbdc8578b940b82906a2e8da893b164d4fa59",
"0xf8e676094628776690dbf83fa31f08aa14fd3fb8",
@@ -2293,6 +2395,7 @@ const SCAM_ADDRESSES = new Set([
"0xfad05ee13c8e0ec94e5ee9dc056dc451b2ad6b1d",
"0xfad17c9da2bebf758e3bc0c99d1abe8af7e96ed1",
"0xfb03e59c83b984da0f6a5575b955541af28ccc65",
"0xfb1817f17820466490a1a67bfd81df32eeea679a",
"0xfb1a7ccf5bcd436dcc0acb49ba1fb9f57bb4d064",
"0xfb2050604a065cf9699bcf07b33accb6f5c27231",
"0xfb5e36b888bc15528b6bd42fe0b1b2af62693eb9",
@@ -2314,9 +2417,11 @@ const SCAM_ADDRESSES = new Set([
"0xfcf10f54105223dfcd8edce0c62352a059ce1e19",
"0xfd2a63d44ff0799dc0dad7dcce74b4ac4bec2528",
"0xfd477bf560e59941796b398cea662b393298abc0",
"0xfd684df5ed6bff686ef5b5e1b959eb94c687c78e",
"0xfd8999b60a72c51ea892db66f5ef0c58f2ecd6d3",
"0xfd8b13415d9dd061bb665438632fba566267bba4",
"0xfddbfa1b0b93612b95e3296690b63b74d019370c",
"0xfdf02f6beef524df60a066cbe795957f78c63b1d",
"0xfdff5c88b7ce9b45037c360c380904e780de17a4",
"0xfe68de56a07cd3af0ec40c22b0193115ecdd0501",
"0xfe68f28599b19c5d8a562e8cc7f7b07c36e0a99d",
@@ -2332,33 +2437,8 @@ const SCAM_ADDRESSES = new Set([
"0xffde23396d57e10abf58bd929bb1e856c7718218",
]);
// Well-known null and burn addresses.
const NULL_BURN_ADDRESSES = new Set([
"0x0000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000001",
"0x0000000000000000000000000000000000000002",
"0x0000000000000000000000000000000000000003",
"0x0000000000000000000000000000000000000004",
"0x0000000000000000000000000000000000000005",
"0x0000000000000000000000000000000000000006",
"0x0000000000000000000000000000000000000007",
"0x0000000000000000000000000000000000000008",
"0x0000000000000000000000000000000000000009",
"0x000000000000000000000000000000000000dead",
"0xdead000000000000000000000000000000000000",
]);
function isScamAddress(address) {
return SCAM_ADDRESSES.has(address.toLowerCase());
}
function isNullOrBurnAddress(address) {
return NULL_BURN_ADDRESSES.has(address.toLowerCase());
}
module.exports = {
isScamAddress,
isNullOrBurnAddress,
SCAM_ADDRESSES,
NULL_BURN_ADDRESSES,
};
module.exports = { isScamAddress, SCAM_ADDRESSES };

View File

@@ -23,8 +23,10 @@ const DEFAULT_STATE = {
hideFraudContracts: true,
hideDustTransactions: true,
dustThresholdGwei: 100000,
utcTimestamps: false,
fraudContracts: [],
tokenHolderCache: {},
theme: "system",
};
const state = {
@@ -53,8 +55,10 @@ async function saveState() {
hideFraudContracts: state.hideFraudContracts,
hideDustTransactions: state.hideDustTransactions,
dustThresholdGwei: state.dustThresholdGwei,
utcTimestamps: state.utcTimestamps,
fraudContracts: state.fraudContracts,
tokenHolderCache: state.tokenHolderCache,
theme: state.theme,
currentView: state.currentView,
selectedWallet: state.selectedWallet,
selectedAddress: state.selectedAddress,
@@ -108,8 +112,11 @@ async function loadState() {
saved.dustThresholdGwei !== undefined
? saved.dustThresholdGwei
: 100000;
state.utcTimestamps =
saved.utcTimestamps !== undefined ? saved.utcTimestamps : false;
state.fraudContracts = saved.fraudContracts || [];
state.tokenHolderCache = saved.tokenHolderCache || {};
state.theme = saved.theme || "system";
state.currentView = saved.currentView || null;
state.selectedWallet =
saved.selectedWallet !== undefined ? saved.selectedWallet : null;

View File

@@ -0,0 +1,100 @@
const { parseEtherscanPage } = require("../src/shared/etherscanLabels");
describe("etherscanLabels", () => {
describe("parseEtherscanPage", () => {
test("detects Fake_Phishing label in title", () => {
const html = `<html><head><title>Fake_Phishing184810 | Address: 0x00000c07...3ea470000 | Etherscan</title></head><body></body></html>`;
const result = parseEtherscanPage(html);
expect(result.label).toBe("Fake_Phishing184810");
expect(result.isPhishing).toBe(true);
expect(result.warning).toContain("Fake_Phishing184810");
expect(result.warning).toContain("Phish/Hack");
});
test("detects Fake_Phishing with different number", () => {
const html = `<html><head><title>Fake_Phishing5169 | Address: 0x3e0defb8...99a7a8a74 | Etherscan</title></head><body></body></html>`;
const result = parseEtherscanPage(html);
expect(result.label).toBe("Fake_Phishing5169");
expect(result.isPhishing).toBe(true);
});
test("detects Exploiter label", () => {
const html = `<html><head><title>Exploiter 42 | Address: 0xabcdef...1234 | Etherscan</title></head><body></body></html>`;
const result = parseEtherscanPage(html);
expect(result.label).toBe("Exploiter 42");
expect(result.isPhishing).toBe(true);
});
test("detects scam warning in body text", () => {
const html =
`<html><head><title>Address: 0xabcdef...1234 | Etherscan</title></head>` +
`<body>There are reports that this address was used in a Phishing scam.</body></html>`;
const result = parseEtherscanPage(html);
expect(result.label).toBeNull();
expect(result.isPhishing).toBe(true);
expect(result.warning).toContain("phishing/scam");
});
test("detects scam warning with label in body", () => {
const html =
`<html><head><title>SomeScammer | Address: 0xabcdef...1234 | Etherscan</title></head>` +
`<body>There are reports that this address was used in a scam.</body></html>`;
const result = parseEtherscanPage(html);
expect(result.label).toBe("SomeScammer");
expect(result.isPhishing).toBe(true);
expect(result.warning).toContain("SomeScammer");
});
test("returns clean result for legitimate address", () => {
const html = `<html><head><title>vitalik.eth | Address: 0xd8dA6BF2...37aA96045 | Etherscan</title></head><body>Overview</body></html>`;
const result = parseEtherscanPage(html);
expect(result.label).toBe("vitalik.eth");
expect(result.isPhishing).toBe(false);
expect(result.warning).toBeNull();
});
test("returns clean result for unlabeled address", () => {
const html = `<html><head><title>Address: 0x1234567890...abcdef | Etherscan</title></head><body>Overview</body></html>`;
const result = parseEtherscanPage(html);
expect(result.label).toBeNull();
expect(result.isPhishing).toBe(false);
expect(result.warning).toBeNull();
});
test("handles exchange labels correctly (not phishing)", () => {
const html = `<html><head><title>Coinbase 10 | Address: 0xa9d1e08c...b81d3e43 | Etherscan</title></head><body>Overview</body></html>`;
const result = parseEtherscanPage(html);
expect(result.label).toBe("Coinbase 10");
expect(result.isPhishing).toBe(false);
});
test("handles contract names correctly (not phishing)", () => {
const html = `<html><head><title>Beacon Deposit Contract | Address: 0x00000000...03d7705Fa | Etherscan</title></head><body>Overview</body></html>`;
const result = parseEtherscanPage(html);
expect(result.label).toBe("Beacon Deposit Contract");
expect(result.isPhishing).toBe(false);
});
test("handles empty HTML gracefully", () => {
const result = parseEtherscanPage("");
expect(result.label).toBeNull();
expect(result.isPhishing).toBe(false);
expect(result.warning).toBeNull();
});
test("handles malformed title tag", () => {
const html = `<html><head><title></title></head><body></body></html>`;
const result = parseEtherscanPage(html);
expect(result.label).toBeNull();
expect(result.isPhishing).toBe(false);
});
test("detects wallet drainer warning", () => {
const html =
`<html><head><title>Address: 0xabc...def | Etherscan</title></head>` +
`<body>This is a known wallet drainer contract.</body></html>`;
const result = parseEtherscanPage(html);
expect(result.isPhishing).toBe(true);
});
});
});

View File

@@ -0,0 +1,166 @@
const {
isPhishingDomain,
loadConfig,
getBlocklistSize,
hostnameVariants,
_reset,
} = require("../src/shared/phishingDomains");
// Reset state before each test to avoid cross-test contamination.
beforeEach(() => {
_reset();
});
describe("phishingDomains", () => {
describe("hostnameVariants", () => {
test("returns exact hostname plus parent domains", () => {
const variants = hostnameVariants("sub.evil.com");
expect(variants).toEqual(["sub.evil.com", "evil.com"]);
});
test("returns just the hostname for a bare domain", () => {
const variants = hostnameVariants("example.com");
expect(variants).toEqual(["example.com"]);
});
test("handles deep subdomain chains", () => {
const variants = hostnameVariants("a.b.c.d.com");
expect(variants).toEqual([
"a.b.c.d.com",
"b.c.d.com",
"c.d.com",
"d.com",
]);
});
test("lowercases hostnames", () => {
const variants = hostnameVariants("Evil.COM");
expect(variants).toEqual(["evil.com"]);
});
});
describe("loadConfig + isPhishingDomain", () => {
test("detects exact blacklisted domain", () => {
loadConfig({
blacklist: ["evil-phishing.com", "scam-swap.xyz"],
whitelist: [],
});
expect(isPhishingDomain("evil-phishing.com")).toBe(true);
expect(isPhishingDomain("scam-swap.xyz")).toBe(true);
});
test("returns false for clean domains", () => {
loadConfig({
blacklist: ["evil-phishing.com"],
whitelist: [],
});
expect(isPhishingDomain("etherscan.io")).toBe(false);
expect(isPhishingDomain("uniswap.org")).toBe(false);
});
test("detects subdomain of blacklisted domain", () => {
loadConfig({
blacklist: ["evil-phishing.com"],
whitelist: [],
});
expect(isPhishingDomain("app.evil-phishing.com")).toBe(true);
expect(isPhishingDomain("sub.app.evil-phishing.com")).toBe(true);
});
test("whitelist overrides blacklist", () => {
loadConfig({
blacklist: ["metamask.io"],
whitelist: ["metamask.io"],
});
expect(isPhishingDomain("metamask.io")).toBe(false);
});
test("whitelist on parent domain overrides blacklist", () => {
loadConfig({
blacklist: ["sub.legit.com"],
whitelist: ["legit.com"],
});
expect(isPhishingDomain("sub.legit.com")).toBe(false);
});
test("case-insensitive matching", () => {
loadConfig({
blacklist: ["Evil-Phishing.COM"],
whitelist: [],
});
expect(isPhishingDomain("evil-phishing.com")).toBe(true);
expect(isPhishingDomain("EVIL-PHISHING.COM")).toBe(true);
});
test("returns false for empty/null hostname", () => {
loadConfig({
blacklist: ["evil.com"],
whitelist: [],
});
expect(isPhishingDomain("")).toBe(false);
expect(isPhishingDomain(null)).toBe(false);
});
test("getBlocklistSize reflects loaded config", () => {
loadConfig({
blacklist: ["a.com", "b.com", "c.com"],
whitelist: ["d.com"],
});
expect(getBlocklistSize()).toBe(3);
});
test("handles config with no blacklist/whitelist keys", () => {
loadConfig({});
expect(isPhishingDomain("anything.com")).toBe(false);
expect(getBlocklistSize()).toBe(0);
});
test("re-loading config replaces previous data", () => {
loadConfig({
blacklist: ["old-scam.com"],
whitelist: [],
});
expect(isPhishingDomain("old-scam.com")).toBe(true);
loadConfig({
blacklist: ["new-scam.com"],
whitelist: [],
});
expect(isPhishingDomain("old-scam.com")).toBe(false);
expect(isPhishingDomain("new-scam.com")).toBe(true);
});
});
describe("real-world MetaMask blocklist patterns", () => {
test("detects known phishing domains from MetaMask list", () => {
loadConfig({
blacklist: [
"uniswap-trade.web.app",
"hopprotocol.pro",
"blast-pools.pages.dev",
],
whitelist: [],
});
expect(isPhishingDomain("uniswap-trade.web.app")).toBe(true);
expect(isPhishingDomain("hopprotocol.pro")).toBe(true);
expect(isPhishingDomain("blast-pools.pages.dev")).toBe(true);
});
test("does not flag legitimate domains whitelisted by MetaMask", () => {
loadConfig({
blacklist: ["opensea.pro"],
whitelist: [
"opensea.io",
"metamask.io",
"etherscan.io",
"opensea.pro",
],
});
expect(isPhishingDomain("opensea.io")).toBe(false);
expect(isPhishingDomain("metamask.io")).toBe(false);
expect(isPhishingDomain("etherscan.io")).toBe(false);
// opensea.pro is both blacklisted and whitelisted — whitelist wins
expect(isPhishingDomain("opensea.pro")).toBe(false);
});
});
});