Compare commits

..

30 Commits

Author SHA1 Message Date
clawbot
d22c83e6dd fix: move banner creation after loadState() to read correct networkId
All checks were successful
check / check (push) Successful in 13s
2026-03-01 11:36:00 -08:00
18e431ffcf feat: show debug banner on testnet or debug mode, add TESTNET tag
Display the red debug banner when on a testnet OR when DEBUG is enabled.
When on a testnet, a 'TESTNET' label is shown on the far right side of
the banner. The banner label shows the network name when not in debug
mode, and 'DEBUG / INSECURE' when debug is on.

closes #140
2026-03-01 11:36:00 -08:00
a138a36710 fix: suppress USD display on testnet networks (#142)
All checks were successful
check / check (push) Successful in 24s
## Summary

Fixes USD prices still showing on the main view when connected to a testnet (e.g. Sepolia). The root cause was stale mainnet prices lingering in the in-memory price cache after switching networks.

### Root Cause

PR #137 correctly made `refreshPrices()` skip fetching on testnets, but the cached prices from a prior mainnet session remained in the `prices` object. All display functions (`getPrice()`, `getAddressValueUsd()`, etc.) used whatever was cached without checking which network was active.

### Changes

- **`src/shared/prices.js`**
  - `refreshPrices()` now clears the price cache when on a testnet instead of silently returning
  - New `clearPrices()` function empties the cache and resets the fetch timestamp
  - `getPrice()` returns null on testnets (defense-in-depth)
  - `getAddressValueUsd()`, `getWalletValueUsd()`, `getTotalValueUsd()` return null on testnets

- **`src/popup/views/settings.js`**
  - Network switcher immediately clears prices when switching to a testnet, so the UI updates without waiting for the next refresh cycle

closes #139

Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Reviewed-on: #142
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-01 20:31:45 +01:00
6b40fa8836 feat: show estimated USD value for ETH in approve-tx view (#141)
All checks were successful
check / check (push) Successful in 25s
The approve-tx view (dapp-initiated transaction approval) now shows the estimated USD value next to the ETH amount being transferred, using the existing `getPrice`/`formatUsd` from `shared/prices.js`.

This matches the behavior already present in the manual send confirmation view (`confirmTx.js`).

When ETH price is available, the value line shows e.g. `0.5000 ETH ($1,650.00)`. When price is unavailable, it falls back gracefully to just the ETH amount.

closes #138

Co-authored-by: Jeffrey Paul <sneak@noreply.example.org>
Reviewed-on: #141
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-01 20:16:00 +01:00
bc2aedaab6 fix: etherscan link on address-token page goes to token-specific URL (#136)
All checks were successful
check / check (push) Successful in 9s
When viewing the address-token page for our own address with an ERC-20 token, the etherscan link now navigates to the token-specific page (`etherscan.io/token/<contract>?a=<address>`) instead of the plain address page.

Closes #135

Co-authored-by: user <user@Mac.lan guest wan>
Co-authored-by: Jeffrey Paul <sneak@noreply.example.org>
Reviewed-on: #136
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-01 20:13:55 +01:00
e53420f2e2 feat: add Sepolia testnet support (#137)
All checks were successful
check / check (push) Successful in 9s
## Summary

Adds Sepolia testnet support to AutistMask.

### Changes

- **New `src/shared/networks.js`** — centralized network definitions (mainnet + Sepolia) with chain IDs, default RPC/Blockscout endpoints, and block explorer URLs
- **State management** — `networkId` added to persisted state; defaults to mainnet for backward compatibility
- **Settings UI** — network selector dropdown lets users switch between Ethereum Mainnet and Sepolia Testnet
- **Dynamic explorer links** — all hardcoded `etherscan.io` URLs replaced with dynamic links from the current network config (`sepolia.etherscan.io` for Sepolia)
- **Background service** — `wallet_switchEthereumChain` now accepts both mainnet (0x1) and Sepolia (0xaa36a7); broadcasts `chainChanged` to connected dApps
- **Inpage provider** — fetches chain ID on init and updates dynamically via `chainChanged` events (no more hardcoded `0x1`)
- **Blockscout API** — uses `eth-sepolia.blockscout.com/api/v2` for Sepolia
- **Etherscan labels** — phishing/scam checks use the correct explorer per network
- **Price fetching** — skipped on testnets (testnet tokens have no real market value)
- **RPC validation** — checks against the selected network's chain ID, not hardcoded mainnet
- **ethers provider** — `getProvider()` uses the correct ethers `Network` for Sepolia

### API Endpoints Verified

| Service | Mainnet | Sepolia |
|---------|---------|--------|
| Etherscan | etherscan.io | sepolia.etherscan.io |
| Blockscout | eth.blockscout.com/api/v2 | eth-sepolia.blockscout.com/api/v2 |
| RPC | ethereum-rpc.publicnode.com | ethereum-sepolia-rpc.publicnode.com |
| CoinDesk (prices) |  | N/A (skipped on testnet) |

closes #110

Reviewed-on: #137

THIS WAS ONESHOTTED USING OPUS 4.  WTAF
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-01 20:11:22 +01:00
d35bfb7d23 feat: expand confirm-tx warnings — closes #114 (#118)
All checks were successful
check / check (push) Successful in 5s
Expands the confirm-tx warning system with three new warning types, all using the existing `visibility:hidden/visible` pattern from PR #98 (no animations, no layout shift).

## Changes

1. **Scam address list expanded** (7 → 652 addresses): Sourced from [MyEtherWallet/ethereum-lists](https://github.com/MyEtherWallet/ethereum-lists) darklist (MIT license). Checked synchronously before sending.

2. **Contract address warning**: When the recipient is a smart contract (detected via `getCode`), shows a warning that sending directly to a contract may result in permanent loss of funds.

3. **Null/burn address warning**: Detects known burn addresses (`0x0000...0000`, `0x...dead`, `0x...deadbeef`) and warns that funds are permanently destroyed.

4. **No-history warning** (existing from #98): Unchanged, still shows for EOAs with zero transaction history.

All warnings use reserved-space `visibility:hidden/visible` elements — no layout shift, no animations.

closes #114

Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Co-authored-by: user <user@Mac.lan guest wan>
Co-authored-by: clawbot <clawbot@eeqj.de>
Reviewed-on: #118
Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
Co-committed-by: clawbot <sneak+clawbot@sneak.cloud>
2026-03-01 19:34:54 +01:00
3bf60ff162 Reorder transaction detail view: txid first, logical grouping (#133)
All checks were successful
check / check (push) Successful in 9s
Reorganizes the transaction detail view into logical blocks separated by thin horizontal rules for visual clarity.

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

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

closes #131

Co-authored-by: user <user@Mac.lan guest wan>
Co-authored-by: clawbot <clawbot@git.eeqj.de>
Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Reviewed-on: #133
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-01 19:27:23 +01:00
6aeab54e8c Merge pull request 'fix: correct swap display — output token, min received, from address, and amount' (#128) from fix/issue-127-swap-amount-display into main
All checks were successful
check / check (push) Successful in 9s
Reviewed-on: #128
2026-03-01 16:19:41 +01:00
f65764d501 Merge branch 'main' into fix/issue-127-swap-amount-display
All checks were successful
check / check (push) Successful in 21s
2026-03-01 16:19:26 +01:00
4e097c1e32 Merge pull request 'feat: add Type field and on-chain details to transaction detail view' (#130) from fix/issue-95-transaction-type-display into main
All checks were successful
check / check (push) Successful in 8s
Reviewed-on: #130
2026-03-01 15:46:33 +01:00
clawbot
3f6f98dcaf docs: update TransactionDetail screen map with new fields
All checks were successful
check / check (push) Successful in 22s
Add Type, Token contract, Block, Nonce, Transaction fee, Gas price,
and Gas used fields to the TransactionDetail section in the README
screen map to match the implemented UI.
2026-03-01 06:17:40 -08:00
user
3e900dc14c feat: add Type field and on-chain details to transaction detail view
All checks were successful
check / check (push) Successful in 9s
- Always display a Type field as the first item under the Transaction
  heading, identifying the transaction as: Native ETH Transfer, ERC-20
  Token Transfer, Swap, Token Approval, Contract Call, or Contract Creation
- Show token contract address with identicon for ERC-20 transfers
- Fetch and display on-chain details from Blockscout: block number,
  nonce, transaction fee, gas price, and gas used
- All new fields are click-copyable with Etherscan links where applicable

closes #95
2026-03-01 06:11:35 -08:00
user
5dfc6e332b fix: correct swap display — output token, min received, from address, and amount
All checks were successful
check / check (push) Successful in 23s
Fix multi-step Uniswap swap decoding and transaction display:

1. uniswap.js: In multi-step swaps (e.g. V3 → V4), the output token and
   min received amount now come from the LAST swap step instead of the
   first. Previously, an intermediate token's amountOutMin (18 decimals)
   was formatted with the final token's decimals (6), producing
   astronomically wrong 'Min. received' values (~2 trillion USDC).

2. transactions.js: Contract call token transfers (swaps) are now
   consolidated into the original transaction entry instead of creating
   separate entries per token transfer. This prevents intermediate hop
   tokens (e.g. USDS in a USDT→USDS→USDC route) from appearing as the
   transaction's Amount. The received token (swap output) is preferred.

3. transactions.js: The original transaction's from/to addresses are
   preserved for contract calls, so the user sees their own address
   instead of a router or Permit2 contract address.

closes #127
2026-03-01 05:52:10 -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
38 changed files with 233611 additions and 321 deletions

107
LICENSE
View File

@@ -672,3 +672,110 @@ may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
===========================================================================
THIRD-PARTY FILES
===========================================================================
The following files are not original to this project and are distributed
under their own licenses. They are NOT covered by the GPL-3.0 license above.
---------------------------------------------------------------------------
File: src/shared/phishingBlocklist.json
Source: https://github.com/AugurProject/eth-phishing-detect (config.json)
Copyright: Copyright (c) 2018 kumavis
License: Don't Be a Dick Public License (DBAD), Version 1.2
---------------------------------------------------------------------------
DON'T BE A DICK PUBLIC LICENSE
Version 1.2, February 2021
Copyright (C) 2018 kumavis
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document.
DON'T BE A DICK PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
1. Do whatever you like with the original work, just don't be a dick.
Being a dick includes - but is not limited to - the following instances:
1a. Outright copyright infringement - Don't just copy the original
work/works and change the name.
1b. Selling the unmodified original with no work done what-so-ever,
that's REALLY being a dick.
1c. Modifying the original work to contain hidden harmful content.
That would make you a PROPER dick.
2. If you become rich through modifications, related works/services, or
supporting the original work, share the love. Only a dick would make
loads off this work and not buy the original work's creator(s) a pint.
3. Code is provided with no warranty. Using somebody else's code and
bitching when it goes wrong makes you a DONKEY dick. Fix the problem
yourself. A non-dick would submit the fix back or submit a bug report.
4. If you use code, calling it your own would make you a ROYAL dick.
Alternatively, even just a comment giving attribution to where you found
the code would be OK.
---------------------------------------------------------------------------
File: src/shared/scamlist.js (address data from MyEtherWallet ethereum-lists)
Source: https://github.com/MyEtherWallet/ethereum-lists (addresses-darklist.json)
Copyright: Copyright (c) 2020 MyEtherWallet
License: MIT License
---------------------------------------------------------------------------
MIT License
Copyright (c) 2020 MyEtherWallet
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---------------------------------------------------------------------------
File: src/shared/scamlist.js (address data from EtherScamDB)
Source: https://github.com/MrLuit/EtherScamDB (scams.yaml)
Copyright: Copyright (c) 2018 Luit Hollander
License: MIT License
---------------------------------------------------------------------------
MIT License
Copyright (c) 2018 Luit Hollander
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -15,10 +15,12 @@ Hence, a minimally viable ERC20 browser wallet/signer that works cross-platform.
Everything you need, nothing you don't. We import as few libraries as possible,
don't implement any crypto, and don't send user-specific data anywhere but a
(user-configurable) Ethereum RPC endpoint (which defaults to a public node). The
extension contacts exactly three external services: the configured RPC node for
extension contacts three user-configurable services: the configured RPC node for
blockchain interactions, a public CoinDesk API (no API key) for realtime price
information, and a Blockscout block-explorer API for transaction history and
token balances. All three endpoints are user-configurable.
token balances. It also fetches a community-maintained phishing domain blocklist
periodically and performs best-effort Etherscan address label lookups during
transaction confirmation.
In the extension is a hardcoded list of the top ERC20 contract addresses. You
can add any ERC20 contract by contract address if you wish, but the hardcoded
@@ -435,16 +437,29 @@ transitions.
#### TransactionDetail
- **When**: User tapped a transaction row from AddressDetail or AddressToken.
- **Elements**:
- **Elements** (grouped into logical blocks using light well containers; field
labels are self-explanatory so groups have no headings):
- "Transaction" heading, "Back" button
- Status: "Success" or "Failed"
- Time: ISO datetime + relative age in parentheses
- Amount: value + symbol (bold)
- From: blockie + color dot + full address (tap to copy) + etherscan link
- ENS name if available
- To: blockie + color dot + full address (tap to copy) + etherscan link
- ENS name if available
- Transaction hash: full hash (tap to copy) + etherscan link
- Type: transaction classification — one of: Native ETH Transfer, ERC-20
Token Transfer, Swap, Token Approval, Contract Call, Contract Creation
- Status: "Success" or "Failed"
- From: blockie + color dot + full address (tap to copy) + etherscan link;
ENS name if available
- To: blockie + color dot + full address (tap to copy) + etherscan link; ENS
name if available
- Time: ISO datetime + relative age in parentheses
- Block: block number (tap to copy) + etherscan block link
- Amount: value + symbol (bold)
- Native quantity: raw integer + unit (shown when available)
- Token contract: shown for ERC-20 transfers — color dot + full contract
address (tap to copy) + etherscan token link
- Decoded details (shown for contract calls): action name, decoded
parameters, token details, swap steps
- Network details (shown when on-chain data is available): nonce, gas price,
gas used, transaction fee (all tap to copy)
- Raw data (shown when calldata is present): full calldata in monospace
dashed border
- **Transitions**:
- "Back" → **AddressToken** (if `selectedToken` set) or **AddressDetail**
@@ -567,14 +582,25 @@ What the extension does NOT do:
- No analytics or telemetry services
- No token list APIs (user adds tokens manually by contract address)
- No phishing/blocklist APIs
- No Infura/Alchemy dependency (any JSON-RPC endpoint works)
- No backend servers operated by the developer
These three services (RPC endpoint, CoinDesk price API, and Blockscout API) are
the only external services. All three endpoints are user-configurable. Users who
want maximum privacy can point the RPC and Blockscout URLs at their own
self-hosted instances (price fetching can be disabled in a future version).
In addition to the three user-configurable services above (RPC endpoint,
CoinDesk price API, and Blockscout API), AutistMask also contacts:
- **Phishing domain blocklist**: A community-maintained phishing domain
blocklist is vendored into the extension at build time. At runtime, the
extension fetches the live list once every 24 hours to detect newly added
domains. Only the delta (domains not already in the vendored list) is kept in
memory, keeping runtime memory usage small. The delta is persisted to
localStorage if it is under 256 KiB.
- **Etherscan address labels**: When confirming a transaction, the extension
performs a best-effort lookup of the recipient address on Etherscan to check
for phishing/scam labels. This is a direct page fetch with no API key; the
user's browser makes the request.
Users who want maximum privacy can point the RPC and Blockscout URLs at their
own self-hosted instances (price fetching can be disabled in a future version).
### Dependencies
@@ -764,6 +790,21 @@ indexes it as a real token transfer.
designed as a sharp tool — users who understand the risks can configure the
wallet to show everything unfiltered, unix-style.
#### Phishing Domain Protection
AutistMask protects users from known phishing sites when they connect their
wallet or approve transactions/signatures. A community-maintained domain
blocklist is vendored into the extension at build time, providing immediate
protection without any network requests. At runtime, the extension fetches the
live list once every 24 hours and keeps only the delta (newly added domains not
in the vendored list) in memory. This architecture keeps runtime memory usage
small while ensuring fresh coverage of new phishing domains.
When a dApp on a blocklisted domain requests a wallet connection, transaction
approval, or signature, the approval popup displays a prominent red warning
banner alerting the user. The domain checker matches exact hostnames and all
parent domains (subdomain matching).
#### Transaction Decoding
When a dApp asks the user to approve a transaction, AutistMask attempts to
@@ -846,6 +887,21 @@ Currently supported:
GPL-3.0. See [LICENSE](LICENSE).
### Third-Party Data Files
This repository includes data files from third-party projects that are not
covered by the GPL-3.0 license above. These files, their copyright holders, and
their licenses are:
| File | Source | Copyright | License |
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | -------------------------------------------------------------- |
| `src/shared/phishingBlocklist.json` | [eth-phishing-detect](https://github.com/AugurProject/eth-phishing-detect) community-maintained phishing domain blocklist | Copyright (c) 2018 kumavis | [DBAD (Don't Be a Dick)](https://github.com/philsturgeon/dbad) |
| `src/shared/scamlist.js` (address data from MyEtherWallet) | [ethereum-lists](https://github.com/MyEtherWallet/ethereum-lists) `addresses-darklist.json` | Copyright (c) 2020 MyEtherWallet | MIT |
| `src/shared/scamlist.js` (address data from EtherScamDB) | [EtherScamDB](https://github.com/MrLuit/EtherScamDB) `scams.yaml` | Copyright (c) 2018 Luit Hollander | MIT |
The full license texts for these third-party files are included in the
[LICENSE](LICENSE) file.
## Author
[@sneak](https://sneak.berlin)

View File

@@ -2,16 +2,25 @@
// Handles EIP-1193 RPC requests from content scripts and proxies
// non-sensitive calls to the configured Ethereum JSON-RPC endpoint.
const {
ETHEREUM_MAINNET_CHAIN_ID,
DEFAULT_RPC_URL,
} = require("../shared/constants");
const { DEFAULT_RPC_URL } = require("../shared/constants");
const { SUPPORTED_CHAIN_IDS, networkByChainId } = require("../shared/networks");
const { onChainSwitch } = require("../shared/chainSwitch");
const { getBytes } = require("ethers");
const { state, loadState, saveState } = require("../shared/state");
const {
state,
loadState,
saveState,
currentNetwork,
} = require("../shared/state");
const { refreshBalances, getProvider } = require("../shared/balances");
const { debugFetch } = require("../shared/log");
const { decryptWithPassword } = require("../shared/vault");
const { getSignerForAddress } = require("../shared/wallet");
const {
isPhishingDomain,
updatePhishingList,
startPeriodicRefresh,
} = require("../shared/phishingDomains");
const storageApi =
typeof browser !== "undefined"
@@ -324,31 +333,43 @@ async function handleRpc(method, params, origin) {
}
if (method === "eth_chainId") {
return { result: ETHEREUM_MAINNET_CHAIN_ID };
return { result: currentNetwork().chainId };
}
if (method === "net_version") {
return { result: "1" };
return { result: currentNetwork().networkVersion };
}
if (method === "wallet_switchEthereumChain") {
const chainId = params?.[0]?.chainId;
if (chainId === ETHEREUM_MAINNET_CHAIN_ID) {
if (chainId === currentNetwork().chainId) {
return { result: null };
}
if (SUPPORTED_CHAIN_IDS.has(chainId)) {
const target = networkByChainId(chainId);
await onChainSwitch(target.id);
broadcastChainChanged(target.chainId);
return { result: null };
}
return {
error: {
code: 4902,
message: "AutistMask only supports Ethereum mainnet.",
message:
"AutistMask supports Ethereum Mainnet and Sepolia Testnet only.",
},
};
}
if (method === "wallet_addEthereumChain") {
const chainId = params?.[0]?.chainId;
if (SUPPORTED_CHAIN_IDS.has(chainId)) {
return { result: null };
}
return {
error: {
code: 4902,
message: "AutistMask only supports Ethereum mainnet.",
message:
"AutistMask supports Ethereum Mainnet and Sepolia Testnet only.",
},
};
}
@@ -494,6 +515,27 @@ async function handleRpc(method, params, origin) {
return { error: { message: "Unsupported method: " + method } };
}
// Broadcast chainChanged to all tabs when the network is switched.
function broadcastChainChanged(chainId) {
tabsApi.query({}, (tabs) => {
for (const tab of tabs) {
tabsApi.sendMessage(
tab.id,
{
type: "AUTISTMASK_EVENT",
eventName: "chainChanged",
data: chainId,
},
() => {
if (runtime.lastError) {
// expected for tabs without our content script
}
},
);
}
});
}
// Broadcast accountsChanged to all tabs, respecting per-address permissions
async function broadcastAccountsChanged() {
// Clear non-remembered approvals on address switch
@@ -571,6 +613,11 @@ async function backgroundRefresh() {
setInterval(backgroundRefresh, BACKGROUND_REFRESH_INTERVAL);
// Fetch the phishing domain blocklist delta on startup and refresh every 24h.
// The vendored blocklist is bundled at build time; this fetches only new entries.
updatePhishingList();
startPeriodicRefresh();
// When approval window is closed without a response, treat as rejection
if (windowsApi && windowsApi.onRemoved) {
windowsApi.onRemoved.addListener((windowId) => {
@@ -643,6 +690,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

@@ -2,7 +2,10 @@
// Creates window.ethereum (EIP-1193 provider) and announces via EIP-6963.
(function () {
const CHAIN_ID = "0x1"; // Ethereum mainnet
// Defaults to mainnet; updated dynamically via eth_chainId on init and
// chainChanged events from the extension.
let currentChainId = "0x1";
let currentNetworkVersion = "1";
const listeners = {};
let nextId = 1;
@@ -28,6 +31,12 @@
if (event.source !== window) return;
if (event.data?.type !== "AUTISTMASK_EVENT") return;
const { eventName, data } = event.data;
if (eventName === "chainChanged") {
currentChainId = data;
currentNetworkVersion = String(parseInt(data, 16));
provider.chainId = currentChainId;
provider.networkVersion = currentNetworkVersion;
}
emit(eventName, data);
});
@@ -57,8 +66,8 @@
const provider = {
isAutistMask: true,
isMetaMask: true, // compatibility — many dApps check this
chainId: CHAIN_ID,
networkVersion: "1",
chainId: currentChainId,
networkVersion: currentNetworkVersion,
selectedAddress: null,
async request(args) {
@@ -75,6 +84,12 @@
? result[0]
: null;
}
if (args.method === "eth_chainId" && result) {
currentChainId = result;
currentNetworkVersion = String(parseInt(result, 16));
provider.chainId = currentChainId;
provider.networkVersion = currentNetworkVersion;
}
return result;
},
@@ -189,4 +204,19 @@
window.addEventListener("eip6963:requestProvider", announceProvider);
announceProvider();
// Fetch the current chain ID from the extension on load so the provider
// reflects the selected network immediately (covers Sepolia etc.).
sendRequest({ method: "eth_chainId", params: [] })
.then((chainId) => {
if (chainId) {
currentChainId = chainId;
currentNetworkVersion = String(parseInt(chainId, 16));
provider.chainId = currentChainId;
provider.networkVersion = currentNetworkVersion;
}
})
.catch(() => {
// Best-effort — keep defaults.
});
})();

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,47 @@
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-etherscan-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: Etherscan has flagged this address as
phishing/scam. Do not send funds to this address.
</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 +658,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 +773,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 +802,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 +861,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 +869,35 @@
/>
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">
<h3 class="font-bold mb-1">Network</h3>
<p class="text-xs text-muted mb-1">
Select the Ethereum network. Switching networks will
update the RPC and Blockscout endpoints to their
defaults.
</p>
<div class="text-xs flex items-center gap-1">
<select
id="settings-network"
class="border border-border p-1 bg-bg text-fg text-xs cursor-pointer"
>
<option value="mainnet">Ethereum Mainnet</option>
<option value="sepolia">Sepolia Testnet</option>
</select>
</div>
</div>
<div class="bg-well p-3 mx-1 mb-3">
@@ -903,6 +979,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 +1020,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 +1096,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"
@@ -1035,66 +1119,149 @@
<h2 id="tx-detail-heading" class="font-bold mb-2">
Transaction
</h2>
<div id="tx-detail-type-section" class="mb-4 hidden">
<div class="text-xs text-muted mb-1">Type</div>
<div id="tx-detail-type" class="text-xs font-bold"></div>
</div>
<div class="mb-4">
<div class="text-xs text-muted mb-1">Status</div>
<div id="tx-detail-status" class="text-xs"></div>
</div>
<div class="mb-4">
<div class="text-xs text-muted mb-1">Time</div>
<div id="tx-detail-time" class="text-xs"></div>
</div>
<div class="mb-4">
<div class="text-xs text-muted mb-1">Amount</div>
<div id="tx-detail-value" class="text-xs"></div>
</div>
<div class="mb-4 hidden">
<div class="text-xs text-muted mb-1">Native quantity</div>
<div id="tx-detail-native" class="text-xs"></div>
</div>
<div class="mb-4">
<div class="text-xs text-muted mb-1">From</div>
<div id="tx-detail-from" class="text-xs break-all"></div>
</div>
<div class="mb-4">
<div class="text-xs text-muted mb-1">To</div>
<div id="tx-detail-to" class="text-xs break-all"></div>
</div>
<div id="tx-detail-calldata-section" class="mb-4 hidden">
<div
id="tx-detail-calldata-well"
class="mb-3 border border-border border-dashed p-2"
>
<div class="text-xs text-muted mb-1">Action</div>
<!-- ── Identity ── -->
<div class="bg-well p-3 mx-1 mb-3">
<div class="mb-2">
<div class="text-xs text-muted mb-1">
Transaction hash
</div>
<div
id="tx-detail-calldata-action"
class="text-xs font-bold mb-2"
id="tx-detail-hash"
class="text-xs break-all"
></div>
</div>
<div id="tx-detail-type-section" class="mb-2 hidden">
<div class="text-xs text-muted mb-1">Type</div>
<div
id="tx-detail-calldata-details"
class="text-xs"
id="tx-detail-type"
class="text-xs font-bold"
></div>
</div>
<div class="mb-2">
<div class="text-xs text-muted mb-1">Status</div>
<div id="tx-detail-status" class="text-xs"></div>
</div>
<div class="mb-2">
<div class="text-xs text-muted mb-1">From</div>
<div
id="tx-detail-from"
class="text-xs break-all"
></div>
</div>
<div class="mb-2">
<div class="text-xs text-muted mb-1">To</div>
<div id="tx-detail-to" class="text-xs break-all"></div>
</div>
</div>
<!-- ── Timing ── -->
<div class="bg-well p-3 mx-1 mb-3">
<div class="mb-2">
<div class="text-xs text-muted mb-1">Time</div>
<div id="tx-detail-time" class="text-xs"></div>
</div>
<div id="tx-detail-block-section" class="mb-2 hidden">
<div class="text-xs text-muted mb-1">Block</div>
<div id="tx-detail-block" class="text-xs"></div>
</div>
</div>
<!-- ── Value ── -->
<div class="bg-well p-3 mx-1 mb-3">
<div class="mb-2">
<div class="text-xs text-muted mb-1">Amount</div>
<div id="tx-detail-value" class="text-xs"></div>
</div>
<div class="mb-2 hidden">
<div class="text-xs text-muted mb-1">
Native quantity
</div>
<div id="tx-detail-native" class="text-xs"></div>
</div>
<div
id="tx-detail-token-contract-section"
class="mb-2 hidden"
>
<div class="text-xs text-muted mb-1">
Token contract
</div>
<div
id="tx-detail-token-contract"
class="text-xs break-all"
></div>
</div>
</div>
<div class="mb-4">
<div class="text-xs text-muted mb-1">Transaction hash</div>
<div id="tx-detail-hash" class="text-xs break-all"></div>
<!-- ── Decoded details ── -->
<div id="tx-detail-calldata-section" class="hidden">
<div class="bg-well p-3 mx-1 mb-3">
<div id="tx-detail-calldata-well" class="mb-2">
<div class="text-xs text-muted mb-1">Action</div>
<div
id="tx-detail-calldata-action"
class="text-xs font-bold mb-2"
></div>
<div
id="tx-detail-calldata-details"
class="text-xs"
></div>
</div>
</div>
</div>
<div id="tx-detail-rawdata-section" class="mb-4 hidden">
<div class="text-xs text-muted mb-1">Raw data</div>
<div
id="tx-detail-rawdata"
class="text-xs break-all font-mono border border-border border-dashed p-2"
></div>
<!-- ── Network details ── -->
<div id="tx-detail-network-section" class="hidden">
<div class="bg-well p-3 mx-1 mb-3">
<div id="tx-detail-nonce-section" class="mb-2 hidden">
<div class="text-xs text-muted mb-1">Nonce</div>
<div id="tx-detail-nonce" class="text-xs"></div>
</div>
<div
id="tx-detail-gasprice-section"
class="mb-2 hidden"
>
<div class="text-xs text-muted mb-1">Gas price</div>
<div id="tx-detail-gasprice" class="text-xs"></div>
</div>
<div id="tx-detail-gasused-section" class="mb-2 hidden">
<div class="text-xs text-muted mb-1">Gas used</div>
<div id="tx-detail-gasused" class="text-xs"></div>
</div>
<div id="tx-detail-fee-section" class="mb-2 hidden">
<div class="text-xs text-muted mb-1">
Transaction fee
</div>
<div id="tx-detail-fee" class="text-xs"></div>
</div>
</div>
</div>
<!-- ── Raw data ── -->
<div id="tx-detail-rawdata-section" class="hidden">
<div class="bg-well p-3 mx-1 mb-3">
<div class="mb-2">
<div class="text-xs text-muted mb-1">Raw data</div>
<div
id="tx-detail-rawdata"
class="text-xs break-all font-mono border border-border border-dashed p-2"
></div>
</div>
</div>
</div>
</div>
<!-- ============ 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 bg-red-100 text-red-800 border-2 border-red-600 rounded-md"
>
⚠️ PHISHING WARNING: This site is on a known 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 +1306,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 +1328,14 @@
<!-- ============ 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 bg-red-100 text-red-800 border-2 border-red-600 rounded-md"
>
⚠️ PHISHING WARNING: This site is on a known 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 +1343,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 +1383,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 +1405,14 @@
<!-- ============ 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 bg-red-100 text-red-800 border-2 border-red-600 rounded-md"
>
⚠️ PHISHING WARNING: This site is on a known 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

@@ -2,10 +2,16 @@
// Loads state, initializes views, triggers first render.
const { DEBUG } = require("../shared/constants");
const { state, saveState, loadState } = require("../shared/state");
const {
state,
saveState,
loadState,
currentNetwork,
} = 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");
@@ -166,17 +172,25 @@ function fallbackView() {
}
async function init() {
if (DEBUG) {
await loadState();
applyTheme(state.theme);
const net = currentNetwork();
if (DEBUG || net.isTestnet) {
const banner = document.createElement("div");
banner.id = "debug-banner";
banner.textContent = "DEBUG / INSECURE";
if (DEBUG && net.isTestnet) {
banner.textContent = "DEBUG / INSECURE [TESTNET]";
} else if (net.isTestnet) {
banner.textContent = "[TESTNET]";
} else {
banner.textContent = "DEBUG / INSECURE";
}
banner.style.cssText =
"background:#c00;color:#fff;text-align:center;font-size:10px;padding:1px 0;font-family:monospace;position:sticky;top:0;z-index:9999;";
document.body.prepend(banner);
}
await loadState();
// Auto-default active address
if (
state.activeAddress === null &&

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,13 +2,19 @@ const {
$,
showView,
showFlash,
flashCopyFeedback,
balanceLinesForAddress,
addressDotHtml,
addressTitle,
escapeHtml,
truncateMiddle,
} = require("./helpers");
const { state, currentAddress, saveState } = require("../../shared/state");
const {
state,
currentAddress,
saveState,
currentNetwork,
} = require("../../shared/state");
const { formatUsd, getAddressValueUsd } = require("../../shared/prices");
const {
fetchRecentTransactions,
@@ -35,7 +41,7 @@ const EXT_ICON =
`</svg></span>`;
function etherscanAddressLink(address) {
return `https://etherscan.io/address/${address}`;
return `${currentNetwork().explorerUrl}/address/${address}`;
}
function show() {
@@ -94,18 +100,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 +268,7 @@ function init(_ctx) {
if (addr) {
navigator.clipboard.writeText(addr);
showFlash("Copied!");
flashCopyFeedback($("address-full"));
}
});
@@ -310,8 +338,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 +350,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 +371,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 +386,7 @@ function init(_ctx) {
if (key) {
navigator.clipboard.writeText(key);
showFlash("Copied!");
flashCopyFeedback($("export-privkey-value"));
}
});
@@ -366,6 +395,7 @@ function init(_ctx) {
if (full) {
navigator.clipboard.writeText(full);
showFlash("Copied!");
flashCopyFeedback($("export-privkey-address"));
}
});

View File

@@ -5,13 +5,19 @@ const {
$,
showView,
showFlash,
flashCopyFeedback,
addressDotHtml,
addressTitle,
escapeHtml,
truncateMiddle,
balanceLine,
} = require("./helpers");
const { state, currentAddress, saveState } = require("../../shared/state");
const {
state,
currentAddress,
saveState,
currentNetwork,
} = require("../../shared/state");
const { TOKEN_BY_ADDRESS, resolveSymbol } = require("../../shared/tokenList");
const {
formatUsd,
@@ -41,24 +47,49 @@ const EXT_ICON =
`</svg></span>`;
function etherscanAddressLink(address) {
return `https://etherscan.io/address/${address}`;
return `${currentNetwork().explorerUrl}/address/${address}`;
}
function etherscanTokenLink(tokenContract, holderAddress) {
return `${currentNetwork().explorerUrl}/token/${tokenContract}?a=${holderAddress}`;
}
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
);
}
@@ -129,12 +160,15 @@ function show() {
$("address-token-dot").innerHTML = addressDotHtml(addr.address);
$("address-token-full").dataset.full = addr.address;
$("address-token-full").textContent = addr.address;
const addrLink = etherscanAddressLink(addr.address);
const addrLink =
tokenId !== "ETH"
? etherscanTokenLink(tokenId, addr.address)
: etherscanAddressLink(addr.address);
$("address-token-etherscan-link").innerHTML =
`<a href="${addrLink}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
// USD total for this token only
const usdVal = price ? amount * price : 0;
const usdVal = price ? amount * price : null;
const usdStr = formatUsd(usdVal);
$("address-token-usd-total").innerHTML = usdStr || "&nbsp;";
@@ -172,7 +206,7 @@ function show() {
: null;
const tokenHolders = tb && tb.holders != null ? tb.holders : null;
const dot = addressDotHtml(tokenId);
const tokenLink = `https://etherscan.io/token/${escapeHtml(tokenId)}`;
const tokenLink = `${currentNetwork().explorerUrl}/token/${escapeHtml(tokenId)}`;
const projectUrl = knownToken && knownToken.url ? knownToken.url : null;
let infoHtml = `<div class="font-bold mb-2">Contract Address</div>`;
infoHtml +=
@@ -317,6 +351,7 @@ function init(_ctx) {
if (addr) {
navigator.clipboard.writeText(addr);
showFlash("Copied!");
flashCopyFeedback($("address-token-full"));
}
});
@@ -325,6 +360,7 @@ function init(_ctx) {
if (copyEl) {
navigator.clipboard.writeText(copyEl.dataset.copy);
showFlash("Copied!");
flashCopyFeedback(copyEl);
}
});
@@ -357,7 +393,7 @@ function init(_ctx) {
let staticHtml = `<div class="font-bold">${escapeHtml(currentSymbol)}</div>`;
if (tokenId !== "ETH") {
const dot = addressDotHtml(tokenId);
const link = `https://etherscan.io/token/${tokenId}`;
const link = `${currentNetwork().explorerUrl}/token/${tokenId}`;
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
staticHtml +=
`<div class="flex items-center text-xs">${dot}` +
@@ -373,6 +409,7 @@ function init(_ctx) {
copyEl.addEventListener("click", () => {
navigator.clipboard.writeText(copyEl.dataset.copy);
showFlash("Copied!");
flashCopyFeedback(copyEl);
});
}
updateSendBalance();

View File

@@ -7,13 +7,13 @@ const {
showError,
hideError,
} = require("./helpers");
const { state, saveState } = require("../../shared/state");
const { state, saveState, currentNetwork } = require("../../shared/state");
const { formatEther, formatUnits, Interface, toUtf8String } = require("ethers");
const { getPrice, formatUsd } = require("../../shared/prices");
const { ERC20_ABI } = require("../../shared/constants");
const { TOKEN_BY_ADDRESS } = require("../../shared/tokenList");
const txStatus = require("./txStatus");
const uniswap = require("../../shared/uniswap");
const runtime =
typeof browser !== "undefined" ? browser.runtime : chrome.runtime;
@@ -28,7 +28,7 @@ const erc20Iface = new Interface(ERC20_ABI);
function approvalAddressHtml(address) {
const dot = addressDotHtml(address);
const link = `https://etherscan.io/address/${address}`;
const link = `${currentNetwork().explorerUrl}/address/${address}`;
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
const title = addressTitle(address, state.wallets);
let html = "";
@@ -54,7 +54,7 @@ function tokenLabel(address) {
}
function etherscanTokenLink(address) {
return `https://etherscan.io/token/${address}`;
return `${currentNetwork().explorerUrl}/token/${address}`;
}
// Try to decode calldata using known ABIs.
@@ -155,7 +155,24 @@ function decodeCalldata(data, toAddress) {
return null;
}
function showPhishingWarning(elementId, isPhishing) {
const el = $(elementId);
if (!el) return;
// The background script performs the authoritative phishing domain check
// and passes the result via the isPhishingDomain flag.
if (isPhishing) {
el.classList.remove("hidden");
} else {
el.classList.add("hidden");
}
}
function showTxApproval(details) {
showPhishingWarning(
"approve-tx-phishing-warning",
details.isPhishingDomain,
);
const toAddr = details.txParams.to;
const token = toAddr ? TOKEN_BY_ADDRESS.get(toAddr.toLowerCase()) : null;
const ethValue = formatEther(details.txParams.value || "0");
@@ -227,8 +244,14 @@ function showTxApproval(details) {
$("approve-tx-to").innerHTML = escapeHtml("(contract creation)");
}
const ethValueFormatted = formatTxValue(
formatEther(details.txParams.value || "0"),
);
const ethPrice = getPrice("ETH");
const ethUsd = ethPrice ? parseFloat(ethValueFormatted) * ethPrice : null;
const usdStr = formatUsd(ethUsd);
$("approve-tx-value").textContent =
formatTxValue(formatEther(details.txParams.value || "0")) + " ETH";
ethValueFormatted + " ETH" + (usdStr ? " (" + usdStr + ")" : "");
// Decode calldata (reuse decoded from above)
const decodedEl = $("approve-tx-decoded");
@@ -269,7 +292,7 @@ function showTxApproval(details) {
}
$("approve-tx-password").value = "";
$("approve-tx-error").classList.add("hidden");
hideError("approve-tx-error");
showView("approve-tx");
}
@@ -323,6 +346,11 @@ function formatTypedDataHtml(jsonStr) {
}
function showSignApproval(details) {
showPhishingWarning(
"approve-sign-phishing-warning",
details.isPhishingDomain,
);
const sp = details.signParams;
$("approve-sign-hostname").textContent = details.hostname;
@@ -351,10 +379,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 +410,11 @@ function show(id) {
showSignApproval(details);
return;
}
// Site connection approval
showPhishingWarning(
"approve-site-phishing-warning",
details.isPhishingDomain,
);
$("approve-hostname").textContent = details.hostname;
$("approve-address").innerHTML = approvalAddressHtml(
state.activeAddress,

View File

@@ -15,17 +15,21 @@ const {
hideError,
showView,
showFlash,
flashCopyFeedback,
addressTitle,
addressDotHtml,
escapeHtml,
} = require("./helpers");
const { state } = require("../../shared/state");
const { state, currentNetwork } = require("../../shared/state");
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;
@@ -69,11 +51,11 @@ function restore() {
}
function etherscanTokenLink(address) {
return `https://etherscan.io/token/${address}`;
return `${currentNetwork().explorerUrl}/token/${address}`;
}
function etherscanAddressLink(address) {
return `https://etherscan.io/address/${address}`;
return `${currentNetwork().explorerUrl}/address/${address}`;
}
function blockieHtml(address) {
@@ -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,20 @@ 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";
$("confirm-etherscan-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 +300,21 @@ 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";
}
if (w.type === "etherscan-phishing") {
$("confirm-etherscan-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

@@ -6,7 +6,7 @@ const {
getPrice,
getAddressValueUsd,
} = require("../../shared/prices");
const { state, saveState } = require("../../shared/state");
const { state, saveState, currentNetwork } = require("../../shared/state");
// When views are added, removed, or transitions between them change,
// update the view-navigation documentation in README.md to match.
@@ -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) {
@@ -57,10 +59,18 @@ function showView(name) {
clearFlash();
state.currentView = name;
saveState();
if (DEBUG) {
const net = currentNetwork();
if (DEBUG || net.isTestnet) {
const banner = document.getElementById("debug-banner");
if (banner) {
banner.textContent = "DEBUG / INSECURE (" + name + ")";
if (DEBUG && net.isTestnet) {
banner.textContent =
"DEBUG / INSECURE [TESTNET] (" + name + ")";
} else if (net.isTestnet) {
banner.textContent = "[TESTNET]";
} else {
banner.textContent = "DEBUG / INSECURE (" + name + ")";
}
}
}
}
@@ -226,18 +236,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 +289,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,
@@ -10,7 +11,12 @@ const {
escapeHtml,
truncateMiddle,
} = require("./helpers");
const { state, saveState, currentAddress } = require("../../shared/state");
const {
state,
saveState,
currentAddress,
currentNetwork,
} = require("../../shared/state");
const {
updateSendBalance,
renderSendTokenSelect,
@@ -81,13 +87,14 @@ function renderActiveAddress() {
if (state.activeAddress) {
const addr = state.activeAddress;
const dot = addressDotHtml(addr);
const link = `https://etherscan.io/address/${addr}`;
const link = `${currentNetwork().explorerUrl}/address/${addr}`;
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,10 +2,11 @@ const {
$,
showView,
showFlash,
flashCopyFeedback,
formatAddressHtml,
addressTitle,
} = require("./helpers");
const { state, currentAddress } = require("../../shared/state");
const { state, currentAddress, currentNetwork } = require("../../shared/state");
const QRCode = require("qrcode");
const EXT_ICON =
@@ -24,7 +25,8 @@ function show() {
? formatAddressHtml(address, ensName, null, title)
: "";
$("receive-address-block").dataset.full = address;
const link = address ? `https://etherscan.io/address/${address}` : "";
const net = currentNetwork();
const link = address ? `${net.explorerUrl}/address/${address}` : "";
$("receive-etherscan-link").innerHTML = link
? `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`
: "";
@@ -51,20 +53,24 @@ function show() {
warningEl.textContent =
"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");
" on " +
currentNetwork().name +
" to this address. Sending tokens on other networks will result in permanent loss.";
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 +79,7 @@ function init(ctx) {
if (addr) {
navigator.clipboard.writeText(addr);
showFlash("Copied!");
flashCopyFeedback($("receive-address-block"));
}
});

View File

@@ -7,7 +7,7 @@ const {
addressTitle,
escapeHtml,
} = require("./helpers");
const { state, currentAddress } = require("../../shared/state");
const { state, currentAddress, currentNetwork } = require("../../shared/state");
let ctx;
const { getProvider } = require("../../shared/balances");
const { KNOWN_SYMBOLS, resolveSymbol } = require("../../shared/tokenList");
@@ -149,7 +149,7 @@ function updateSendBalance() {
const addr = currentAddress();
if (!addr) return;
const dot = addressDotHtml(addr.address);
const link = `https://etherscan.io/address/${addr.address}`;
const link = `${currentNetwork().explorerUrl}/address/${addr.address}`;
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
const title = addressTitle(addr.address, state.wallets);
let fromHtml = "";

View File

@@ -1,6 +1,8 @@
const { $, showView, showFlash, escapeHtml } = require("./helpers");
const { state, saveState } = require("../../shared/state");
const { ETHEREUM_MAINNET_CHAIN_ID } = require("../../shared/constants");
const { applyTheme } = require("../theme");
const { state, saveState, currentNetwork } = require("../../shared/state");
const { NETWORKS, SUPPORTED_CHAIN_IDS } = require("../../shared/networks");
const { onChainSwitch } = require("../../shared/chainSwitch");
const { log, debugFetch } = require("../../shared/log");
const deleteWallet = require("./deleteWallet");
@@ -124,6 +126,10 @@ function renderWalletListSettings() {
function show() {
$("settings-rpc").value = state.rpcUrl;
$("settings-blockscout").value = state.blockscoutUrl;
const networkSelect = $("settings-network");
if (networkSelect) {
networkSelect.value = state.networkId;
}
renderTrackedTokens();
renderSiteLists();
renderWalletListSettings();
@@ -167,9 +173,12 @@ function init(ctx) {
showFlash("Endpoint returned error: " + json.error.message);
return;
}
if (json.result !== ETHEREUM_MAINNET_CHAIN_ID) {
const net = currentNetwork();
if (json.result !== net.chainId) {
showFlash(
"Wrong network (expected mainnet, got chain " +
"Wrong network (expected " +
net.name +
", got chain " +
json.result +
").",
);
@@ -208,12 +217,30 @@ function init(ctx) {
showFlash("Saved.");
});
const networkSelect = $("settings-network");
if (networkSelect) {
networkSelect.addEventListener("change", async () => {
const newId = networkSelect.value;
const net = await onChainSwitch(newId);
$("settings-rpc").value = state.rpcUrl;
$("settings-blockscout").value = state.blockscoutUrl;
showFlash("Switched to " + net.name + ".");
});
}
$("settings-show-zero-balances").checked = state.showZeroBalanceTokens;
$("settings-show-zero-balances").addEventListener("change", async () => {
state.showZeroBalanceTokens = $("settings-show-zero-balances").checked;
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 +268,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,13 +5,15 @@ const {
$,
showView,
showFlash,
flashCopyFeedback,
addressDotHtml,
addressTitle,
escapeHtml,
isoDate,
timeAgo,
} = require("./helpers");
const { state } = require("../../shared/state");
const { state, currentNetwork } = require("../../shared/state");
const { formatEther, formatUnits } = require("ethers");
const makeBlockie = require("ethereum-blockies-base64");
const { log, debugFetch } = require("../../shared/log");
const { decodeCalldata } = require("./approval");
@@ -25,6 +27,25 @@ const EXT_ICON =
let ctx;
/**
* Determine a human-readable transaction type string from tx fields.
*/
function getTransactionType(tx) {
if (!tx.to) return "Contract Creation";
if (tx.direction === "contract") {
if (tx.directionLabel === "Swap") return "Swap";
if (
tx.method === "approve" ||
tx.directionLabel === "Approve" ||
tx.method === "setApprovalForAll"
)
return "Token Approval";
return "Contract Call";
}
if (tx.symbol && tx.symbol !== "ETH") return "ERC-20 Token Transfer";
return "Native ETH Transfer";
}
function copyableHtml(text, extraClass) {
const cls =
"underline decoration-dashed cursor-pointer" +
@@ -48,7 +69,7 @@ function etherscanLinkHtml(url) {
function txAddressHtml(address, ensName, title) {
const blockie = blockieHtml(address);
const dot = addressDotHtml(address);
const link = `https://etherscan.io/address/${address}`;
const link = `${currentNetwork().explorerUrl}/address/${address}`;
const extLink = etherscanLinkHtml(link);
let html = `<div class="mb-1">${blockie}</div>`;
if (title) {
@@ -74,7 +95,7 @@ function txAddressHtml(address, ensName, title) {
}
function txHashHtml(hash) {
const link = `https://etherscan.io/tx/${hash}`;
const link = `${currentNetwork().explorerUrl}/tx/${hash}`;
const extLink = etherscanLinkHtml(link);
return copyableHtml(hash, "break-all") + extLink;
}
@@ -98,6 +119,7 @@ function show(tx) {
direction: tx.direction || null,
isContractCall: tx.isContractCall || false,
method: tx.method || null,
contractAddress: tx.contractAddress || null,
},
};
render();
@@ -134,30 +156,55 @@ function render() {
nativeEl.parentElement.classList.add("hidden");
}
// Show type label for contract interactions (Swap, Execute, etc.)
// Always show transaction type as the first field
const typeSection = $("tx-detail-type-section");
const typeEl = $("tx-detail-type");
const headingEl = $("tx-detail-heading");
if (tx.direction === "contract" && tx.directionLabel) {
if (typeSection) {
typeEl.textContent = tx.directionLabel;
typeSection.classList.remove("hidden");
}
} else {
if (typeSection) typeSection.classList.add("hidden");
if (typeSection && typeEl) {
typeEl.textContent = getTransactionType(tx);
typeSection.classList.remove("hidden");
}
if (headingEl) headingEl.textContent = "Transaction";
// Hide calldata and raw data sections; re-fetch if this is a contract call
// Token contract address (for ERC-20 transfers)
const tokenContractSection = $("tx-detail-token-contract-section");
const tokenContractEl = $("tx-detail-token-contract");
if (tokenContractSection && tokenContractEl) {
if (tx.contractAddress) {
const dot = addressDotHtml(tx.contractAddress);
const link = `${currentNetwork().explorerUrl}/token/${tx.contractAddress}`;
tokenContractEl.innerHTML =
`<div class="flex items-center">${dot}` +
copyableHtml(tx.contractAddress, "break-all") +
etherscanLinkHtml(link) +
`</div>`;
tokenContractSection.classList.remove("hidden");
} else {
tokenContractSection.classList.add("hidden");
}
}
// Hide calldata and raw data sections; always fetch full tx details
const calldataSection = $("tx-detail-calldata-section");
if (calldataSection) calldataSection.classList.add("hidden");
const rawDataSection = $("tx-detail-rawdata-section");
if (rawDataSection) rawDataSection.classList.add("hidden");
if (tx.isContractCall || tx.direction === "contract") {
loadCalldata(tx.hash, tx.to);
// Hide on-chain detail sections until populated
for (const id of [
"tx-detail-block-section",
"tx-detail-nonce-section",
"tx-detail-fee-section",
"tx-detail-gasprice-section",
"tx-detail-gasused-section",
"tx-detail-network-section",
]) {
const el = $(id);
if (el) el.classList.add("hidden");
}
loadFullTxDetails(tx.hash, tx.to, tx.isContractCall);
const isoStr = isoDate(tx.timestamp);
$("tx-detail-time").innerHTML =
copyableHtml(isoStr) + " (" + escapeHtml(timeAgo(tx.timestamp)) + ")";
@@ -171,11 +218,110 @@ function render() {
el.onclick = () => {
navigator.clipboard.writeText(el.dataset.copy);
showFlash("Copied!");
flashCopyFeedback(el);
};
});
}
async function loadCalldata(txHash, toAddress) {
function showDetailField(sectionId, contentId, value) {
const section = $(sectionId);
const el = $(contentId);
if (!section || !el) return;
el.innerHTML = copyableHtml(value, "");
section.classList.remove("hidden");
}
function populateOnChainDetails(txData) {
// Block number
if (txData.block_number != null) {
const blockLink = `${currentNetwork().explorerUrl}/block/${txData.block_number}`;
const blockSection = $("tx-detail-block-section");
const blockEl = $("tx-detail-block");
if (blockSection && blockEl) {
blockEl.innerHTML =
copyableHtml(String(txData.block_number), "") +
etherscanLinkHtml(blockLink);
blockSection.classList.remove("hidden");
}
}
// Nonce
if (txData.nonce != null) {
showDetailField(
"tx-detail-nonce-section",
"tx-detail-nonce",
String(txData.nonce),
);
}
// Transaction fee
const feeWei = txData.fee?.value || txData.tx_fee;
if (feeWei) {
const feeEth = formatEther(String(feeWei));
showDetailField(
"tx-detail-fee-section",
"tx-detail-fee",
feeEth + " ETH",
);
}
// Gas price
const gasPrice = txData.gas_price;
if (gasPrice) {
const gwei = formatUnits(String(gasPrice), "gwei");
showDetailField(
"tx-detail-gasprice-section",
"tx-detail-gasprice",
gwei + " Gwei",
);
}
// Gas used
const gasUsed = txData.gas_used;
if (gasUsed) {
showDetailField(
"tx-detail-gasused-section",
"tx-detail-gasused",
String(gasUsed),
);
}
// Show the network details wrapper if any child section is visible
const networkWrapper = $("tx-detail-network-section");
if (networkWrapper) {
const hasVisible = [
"tx-detail-nonce-section",
"tx-detail-fee-section",
"tx-detail-gasprice-section",
"tx-detail-gasused-section",
].some((id) => {
const el = $(id);
return el && !el.classList.contains("hidden");
});
if (hasVisible) networkWrapper.classList.remove("hidden");
}
// Bind copy handlers for newly added elements
for (const id of [
"tx-detail-block-section",
"tx-detail-nonce-section",
"tx-detail-fee-section",
"tx-detail-gasprice-section",
"tx-detail-gasused-section",
]) {
const section = $(id);
if (!section) continue;
section.querySelectorAll("[data-copy]").forEach((el) => {
el.onclick = () => {
navigator.clipboard.writeText(el.dataset.copy);
showFlash("Copied!");
flashCopyFeedback(el);
};
});
}
}
async function loadFullTxDetails(txHash, toAddress, isContractCall) {
const section = $("tx-detail-calldata-section");
const actionEl = $("tx-detail-calldata-action");
const detailsEl = $("tx-detail-calldata-details");
@@ -190,6 +336,10 @@ async function loadCalldata(txHash, toAddress) {
);
if (!resp.ok) return;
const txData = await resp.json();
// Populate on-chain detail fields (block, nonce, gas, fee)
populateOnChainDetails(txData);
const inputData = txData.raw_input || txData.input || null;
if (!inputData || inputData === "0x") return;
@@ -211,12 +361,12 @@ async function loadCalldata(txHash, toAddress) {
if (tokenSymbol) {
detailsHtml += `<div class="font-bold">${escapeHtml(tokenSymbol)}</div>`;
}
const etherscanUrl = `https://etherscan.io/token/${d.address}`;
const etherscanUrl = `${currentNetwork().explorerUrl}/token/${d.address}`;
detailsHtml += `<div class="flex items-center">${dot}${copyableHtml(d.address, "break-all")}${etherscanLinkHtml(etherscanUrl)}</div>`;
} else if (d.address) {
// Protocol/contract entry: show name + Etherscan link
const dot = addressDotHtml(d.address);
const etherscanUrl = `https://etherscan.io/address/${d.address}`;
const etherscanUrl = `${currentNetwork().explorerUrl}/address/${d.address}`;
detailsHtml += `<div class="flex items-center">${dot}${copyableHtml(d.value, "break-all")}${etherscanLinkHtml(etherscanUrl)}</div>`;
} else {
detailsHtml += `<div class="font-bold">${escapeHtml(d.value)}</div>`;
@@ -248,6 +398,7 @@ async function loadCalldata(txHash, toAddress) {
el.onclick = () => {
navigator.clipboard.writeText(el.dataset.copy);
showFlash("Copied!");
flashCopyFeedback(el);
};
});
}

View File

@@ -4,12 +4,13 @@ const {
$,
showView,
showFlash,
flashCopyFeedback,
addressDotHtml,
addressTitle,
escapeHtml,
} = require("./helpers");
const { TOKEN_BY_ADDRESS } = require("../../shared/tokenList");
const { state, saveState } = require("../../shared/state");
const { state, saveState, currentNetwork } = require("../../shared/state");
const { getProvider } = require("../../shared/balances");
const { log } = require("../../shared/log");
@@ -37,7 +38,7 @@ function clearTimers() {
function toAddressHtml(address) {
const dot = addressDotHtml(address);
const link = `https://etherscan.io/address/${address}`;
const link = `${currentNetwork().explorerUrl}/address/${address}`;
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
const title = addressTitle(address, state.wallets);
if (title) {
@@ -51,7 +52,7 @@ function toAddressHtml(address) {
}
function txHashHtml(hash) {
const link = `https://etherscan.io/tx/${hash}`;
const link = `${currentNetwork().explorerUrl}/tx/${hash}`;
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
return (
`<span class="underline decoration-dashed cursor-pointer break-all" data-copy="${escapeHtml(hash)}">${escapeHtml(hash)}</span>` +
@@ -61,7 +62,7 @@ function txHashHtml(hash) {
function blockNumberHtml(blockNumber) {
const num = String(blockNumber);
const link = `https://etherscan.io/block/${num}`;
const link = `${currentNetwork().explorerUrl}/block/${num}`;
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
return (
`<span class="underline decoration-dashed cursor-pointer" data-copy="${escapeHtml(num)}">${escapeHtml(num)}</span>` +
@@ -77,6 +78,7 @@ function attachCopyHandlers(viewId) {
el.onclick = () => {
navigator.clipboard.writeText(el.dataset.copy);
showFlash("Copied!");
flashCopyFeedback(el);
};
});
}
@@ -145,7 +147,7 @@ function tokenLabel(address) {
}
function etherscanTokenLink(address) {
return `https://etherscan.io/token/${address}`;
return `${currentNetwork().explorerUrl}/token/${address}`;
}
function decodedDetailsHtml(decoded) {

View File

@@ -0,0 +1,114 @@
// 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);
let isContract = false;
try {
const code = await provider.getCode(address);
if (code && code !== "0x") {
isContract = true;
warnings.push({
type: "contract",
message:
"This address is a smart contract, not a regular wallet.",
severity: "warning",
});
}
} catch (e) {
log.errorf("contract check failed:", e.message);
}
// Skip tx count check for contracts — they may legitimately have
// zero inbound EOA transactions.
if (!isContract) {
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).
// Runs for ALL addresses including contracts, since many dangerous
// flagged addresses on Etherscan (drainers, phishing contracts) are contracts.
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

@@ -15,10 +15,15 @@ const { KNOWN_SYMBOLS, TOKEN_BY_ADDRESS } = require("./tokenList");
// Use a static network to skip auto-detection (which can fail and cause
// "could not coalesce error" on some RPC endpoints like Cloudflare).
const mainnet = Network.from("mainnet");
function getProvider(rpcUrl) {
return new JsonRpcProvider(rpcUrl, mainnet, { staticNetwork: mainnet });
// Accepts an optional networkName ("mainnet" or "sepolia") for the static
// network hint so ethers picks the right chain parameters. When omitted,
// reads the currently selected network from extension state.
function getProvider(rpcUrl, networkName) {
// Lazy require to avoid circular dependency issues at module scope.
const { currentNetwork } = require("./state");
const name = networkName || currentNetwork().id;
const net = Network.from(name);
return new JsonRpcProvider(rpcUrl, net, { staticNetwork: net });
}
function formatBalance(wei) {

57
src/shared/chainSwitch.js Normal file
View File

@@ -0,0 +1,57 @@
// Consolidated chain-switch handler.
//
// Every state change required when the active network changes is
// performed here so that callers (settings UI, background
// wallet_switchEthereumChain, future chain additions) all go
// through a single code path.
//
// Adding a new chain (e.g. ETC) requires only a new entry in
// networks.js — no per-caller wiring is needed.
const { networkById } = require("./networks");
const { clearPrices } = require("./prices");
// Switch the active chain and reset all chain-specific cached state.
// Returns the network configuration object for the new chain.
async function onChainSwitch(newNetworkId) {
const { state, saveState } = require("./state");
const net = networkById(newNetworkId);
// --- core identity ---
state.networkId = net.id;
state.rpcUrl = net.defaultRpcUrl;
state.blockscoutUrl = net.defaultBlockscoutUrl;
// --- price cache ---
// Prices are chain-specific (testnet tokens are worthless,
// ETC has different pricing, etc.).
clearPrices();
// --- balance / refresh state ---
// Reset last-refresh timestamp so the next polling cycle
// triggers an immediate balance refresh on the new chain.
state.lastBalanceRefresh = 0;
// Clear per-address balances and token balances so stale data
// from the previous chain is never displayed while the first
// refresh on the new chain is in flight.
for (const wallet of state.wallets) {
for (const addr of wallet.addresses) {
addr.balance = "0";
addr.tokenBalances = [];
}
}
// --- chain-specific caches ---
// Token holder counts and fraud contract lists are
// chain-specific and must not carry over.
state.tokenHolderCache = {};
state.fraudContracts = [];
await saveState();
return net;
}
module.exports = { onChainSwitch };

View File

@@ -3,6 +3,7 @@ const DEBUG_MNEMONIC =
"cube evolve unfold result inch risk jealous skill hotel bulb night wreck";
const ETHEREUM_MAINNET_CHAIN_ID = "0x1";
const ETHEREUM_SEPOLIA_CHAIN_ID = "0xaa36a7";
const DEFAULT_RPC_URL = "https://ethereum-rpc.publicnode.com";
@@ -20,12 +21,28 @@ 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,
ETHEREUM_MAINNET_CHAIN_ID,
ETHEREUM_SEPOLIA_CHAIN_ID,
DEFAULT_RPC_URL,
DEFAULT_BLOCKSCOUT_URL,
BIP44_ETH_PATH,
ERC20_ABI,
BURN_ADDRESSES,
isBurnAddress,
};

View File

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

57
src/shared/networks.js Normal file
View File

@@ -0,0 +1,57 @@
// Network definitions for supported Ethereum networks.
// Each network specifies its chain ID, default RPC and Blockscout endpoints,
// and the block explorer base URL used for address/tx/token/block links.
const NETWORKS = {
mainnet: {
id: "mainnet",
name: "Ethereum Mainnet",
chainId: "0x1",
networkVersion: "1",
nativeCurrency: "ETH",
defaultRpcUrl: "https://ethereum-rpc.publicnode.com",
defaultBlockscoutUrl: "https://eth.blockscout.com/api/v2",
explorerUrl: "https://etherscan.io",
isTestnet: false,
},
sepolia: {
id: "sepolia",
name: "Sepolia Testnet",
chainId: "0xaa36a7",
networkVersion: "11155111",
nativeCurrency: "SepoliaETH",
defaultRpcUrl: "https://ethereum-sepolia-rpc.publicnode.com",
defaultBlockscoutUrl: "https://eth-sepolia.blockscout.com/api/v2",
explorerUrl: "https://sepolia.etherscan.io",
isTestnet: true,
},
};
const SUPPORTED_CHAIN_IDS = new Set(
Object.values(NETWORKS).map((n) => n.chainId),
);
function networkById(id) {
return NETWORKS[id] || NETWORKS.mainnet;
}
function networkByChainId(chainId) {
for (const net of Object.values(NETWORKS)) {
if (net.chainId === chainId) return net;
}
return null;
}
// Build a block explorer link for the given path type and value.
// type: "address" | "tx" | "token" | "block"
function explorerLink(network, type, value) {
return `${network.explorerUrl}/${type}/${value}`;
}
module.exports = {
NETWORKS,
SUPPORTED_CHAIN_IDS,
networkById,
networkByChainId,
explorerLink,
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,215 @@
// Domain-based phishing detection using a vendored blocklist with delta updates.
//
// A community-maintained phishing domain blocklist is vendored in
// phishingBlocklist.json and bundled at build time. At runtime, we fetch
// the live list periodically and keep only the delta (new entries not in
// the vendored list) in memory. This keeps runtime memory usage small.
//
// The domain-checker checks the in-memory delta first (fresh/recent scam
// sites), then falls back to the vendored list.
//
// If the delta is under 256 KiB it is persisted to localStorage so it
// survives extension/service-worker restarts.
const vendoredConfig = require("./phishingBlocklist.json");
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
const REFRESH_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
const DELTA_STORAGE_KEY = "phishing-delta";
const MAX_DELTA_BYTES = 256 * 1024; // 256 KiB
// Vendored set — built once from the bundled JSON.
const vendoredBlacklist = new Set(
(vendoredConfig.blacklist || []).map((d) => d.toLowerCase()),
);
// Delta set — only entries from live list that are NOT in vendored.
let deltaBlacklist = new Set();
let lastFetchTime = 0;
let fetchPromise = null;
let refreshTimer = null;
/**
* Load delta entries from localStorage on startup.
* Called once during module initialization in the background script.
*/
function loadDeltaFromStorage() {
try {
const raw = localStorage.getItem(DELTA_STORAGE_KEY);
if (!raw) return;
const data = JSON.parse(raw);
if (data.blacklist && Array.isArray(data.blacklist)) {
deltaBlacklist = new Set(
data.blacklist.map((d) => d.toLowerCase()),
);
}
} catch {
// localStorage unavailable or corrupt — start empty
}
}
/**
* Persist delta to localStorage if it fits within MAX_DELTA_BYTES.
*/
function saveDeltaToStorage() {
try {
const data = {
blacklist: Array.from(deltaBlacklist),
};
const json = JSON.stringify(data);
if (json.length < MAX_DELTA_BYTES) {
localStorage.setItem(DELTA_STORAGE_KEY, json);
} else {
// Too large — remove stale key if present
localStorage.removeItem(DELTA_STORAGE_KEY);
}
} catch {
// localStorage unavailable — skip silently
}
}
/**
* Load a pre-parsed config and compute the delta against the vendored list.
* Used for both live fetches and testing.
*
* @param {{ blacklist?: string[] }} config
*/
function loadConfig(config) {
const liveBlacklist = (config.blacklist || []).map((d) => d.toLowerCase());
// Delta = entries in the live list that are NOT in the vendored list
deltaBlacklist = new Set(
liveBlacklist.filter((d) => !vendoredBlacklist.has(d)),
);
lastFetchTime = Date.now();
saveDeltaToStorage();
}
/**
* 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 delta first (fresh/recent scam sites), then vendored list.
*
* @param {string} hostname - The hostname to check.
* @returns {boolean}
*/
function isPhishingDomain(hostname) {
if (!hostname) return false;
const variants = hostnameVariants(hostname);
// Check delta blacklist first (fresh/recent scam sites), then vendored
for (const v of variants) {
if (deltaBlacklist.has(v) || vendoredBlacklist.has(v)) return true;
}
return false;
}
/**
* Fetch the latest blocklist and compute delta against vendored data.
* 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 && lastFetchTime > 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 — vendored list still provides coverage.
// We'll retry next time.
} finally {
fetchPromise = null;
}
})();
return fetchPromise;
}
/**
* Start periodic refresh of the phishing list.
* Should be called once from the background script on startup.
*/
function startPeriodicRefresh() {
if (refreshTimer) return;
refreshTimer = setInterval(updatePhishingList, REFRESH_INTERVAL_MS);
}
/**
* Return the total blocklist size (vendored + delta) for diagnostics.
*
* @returns {number}
*/
function getBlocklistSize() {
return vendoredBlacklist.size + deltaBlacklist.size;
}
/**
* Return the delta blocklist size for diagnostics.
*
* @returns {number}
*/
function getDeltaSize() {
return deltaBlacklist.size;
}
/**
* Reset internal state (for testing).
*/
function _reset() {
deltaBlacklist = new Set();
lastFetchTime = 0;
fetchPromise = null;
if (refreshTimer) {
clearInterval(refreshTimer);
refreshTimer = null;
}
}
// Load persisted delta on module initialization
loadDeltaFromStorage();
module.exports = {
isPhishingDomain,
updatePhishingList,
startPeriodicRefresh,
loadConfig,
getBlocklistSize,
getDeltaSize,
hostnameVariants,
_reset,
// Exposed for testing only
_getVendoredBlacklistSize: () => vendoredBlacklist.size,
_getDeltaBlacklist: () => deltaBlacklist,
};

View File

@@ -8,6 +8,13 @@ const prices = {};
let lastFetchedAt = 0;
async function refreshPrices() {
// Testnet tokens have no real market value — skip price fetching
// and clear any stale mainnet prices so the UI shows no USD values.
const { currentNetwork } = require("./state");
if (currentNetwork().isTestnet) {
clearPrices();
return;
}
const now = Date.now();
if (now - lastFetchedAt < PRICE_CACHE_TTL) return;
try {
@@ -19,7 +26,19 @@ async function refreshPrices() {
}
}
// Clear all cached prices and reset the fetch timestamp so the
// next refreshPrices() call will fetch fresh data.
function clearPrices() {
for (const key of Object.keys(prices)) {
delete prices[key];
}
lastFetchedAt = 0;
}
// Return the USD price for a symbol, or null on testnet / unknown.
function getPrice(symbol) {
const { currentNetwork } = require("./state");
if (currentNetwork().isTestnet) return null;
return prices[symbol] || null;
}
@@ -37,6 +56,8 @@ function formatUsd(amount) {
}
function getAddressValueUsd(addr) {
const { currentNetwork } = require("./state");
if (currentNetwork().isTestnet) return null;
if (!prices.ETH) return null;
let total = 0;
const ethBal = parseFloat(addr.balance || "0");
@@ -51,6 +72,8 @@ function getAddressValueUsd(addr) {
}
function getWalletValueUsd(wallet) {
const { currentNetwork } = require("./state");
if (currentNetwork().isTestnet) return null;
if (!prices.ETH) return null;
let total = 0;
for (const addr of wallet.addresses) {
@@ -60,6 +83,8 @@ function getWalletValueUsd(wallet) {
}
function getTotalValueUsd(wallets) {
const { currentNetwork } = require("./state");
if (currentNetwork().isTestnet) return null;
if (!prices.ETH) return null;
let total = 0;
for (const wallet of wallets) {
@@ -71,6 +96,7 @@ function getTotalValueUsd(wallets) {
module.exports = {
prices,
refreshPrices,
clearPrices,
getPrice,
formatUsd,
getAddressValueUsd,

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

@@ -1,6 +1,7 @@
// State management and extension storage persistence.
const { DEFAULT_RPC_URL, DEFAULT_BLOCKSCOUT_URL } = require("./constants");
const { networkById } = require("./networks");
const storageApi =
typeof browser !== "undefined"
@@ -11,6 +12,7 @@ const DEFAULT_STATE = {
hasWallet: false,
wallets: [],
trackedTokens: [],
networkId: "mainnet",
rpcUrl: DEFAULT_RPC_URL,
blockscoutUrl: DEFAULT_BLOCKSCOUT_URL,
lastBalanceRefresh: 0,
@@ -23,8 +25,10 @@ const DEFAULT_STATE = {
hideFraudContracts: true,
hideDustTransactions: true,
dustThresholdGwei: 100000,
utcTimestamps: false,
fraudContracts: [],
tokenHolderCache: {},
theme: "system",
};
const state = {
@@ -36,11 +40,17 @@ const state = {
viewData: {},
};
// Return the network configuration for the currently selected network.
function currentNetwork() {
return networkById(state.networkId);
}
async function saveState() {
const persisted = {
hasWallet: state.hasWallet,
wallets: state.wallets,
trackedTokens: state.trackedTokens,
networkId: state.networkId,
rpcUrl: state.rpcUrl,
blockscoutUrl: state.blockscoutUrl,
lastBalanceRefresh: state.lastBalanceRefresh,
@@ -53,8 +63,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,
@@ -71,6 +83,7 @@ async function loadState() {
state.hasWallet = saved.hasWallet;
state.wallets = saved.wallets || [];
state.trackedTokens = saved.trackedTokens || [];
state.networkId = saved.networkId || DEFAULT_STATE.networkId;
state.rpcUrl = saved.rpcUrl || DEFAULT_STATE.rpcUrl;
state.blockscoutUrl =
saved.blockscoutUrl || DEFAULT_STATE.blockscoutUrl;
@@ -108,8 +121,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;
@@ -127,4 +143,10 @@ function currentAddress() {
return state.wallets[state.selectedWallet].addresses[state.selectedAddress];
}
module.exports = { state, saveState, loadState, currentAddress };
module.exports = {
state,
saveState,
loadState,
currentAddress,
currentNetwork,
};

View File

@@ -153,24 +153,38 @@ async function fetchRecentTransactions(address, blockscoutUrl, count = 25) {
// When a token transfer shares a hash with a normal tx, the normal tx
// is the contract call (0 ETH) and the token transfer has the real
// amount and symbol. A single transaction (e.g. a swap) can produce
// multiple token transfers (one per token involved), so we key token
// transfers by hash + contract address to keep all of them. We also
// preserve contract-call metadata (direction, label, method) from the
// matching normal tx so swaps display correctly.
// amount and symbol. For contract calls (swaps), a single transaction
// can produce multiple token transfers (input, intermediates, output).
// We consolidate these into the original tx entry using the token
// transfer where the user *receives* tokens (the swap output), so
// the transaction list shows the final result rather than confusing
// intermediate hops. We preserve the original tx's from/to so the
// user sees their own address, not a router or Permit2 contract.
for (const tt of ttJson.items || []) {
const parsed = parseTokenTransfer(tt, addrLower);
const existing = txsByHash.get(parsed.hash);
if (existing && existing.direction === "contract") {
parsed.direction = "contract";
parsed.directionLabel = existing.directionLabel;
parsed.isContractCall = true;
parsed.method = existing.method;
// Remove the bare-hash normal tx so it doesn't appear as a
// duplicate with empty value; token transfers replace it.
txsByHash.delete(parsed.hash);
// For contract calls (swaps), consolidate into the original
// tx entry. Prefer the "received" transfer (swap output)
// for the display amount. If no received transfer exists,
// fall back to the first "sent" transfer (swap input).
const isReceived = parsed.direction === "received";
const needsAmount = !existing.exactValue;
if (isReceived || needsAmount) {
existing.value = parsed.value;
existing.exactValue = parsed.exactValue;
existing.rawAmount = parsed.rawAmount;
existing.rawUnit = parsed.rawUnit;
existing.symbol = parsed.symbol;
existing.contractAddress = parsed.contractAddress;
existing.holders = parsed.holders;
}
// Keep the original tx's from/to (the user's address and the
// contract they called), not the token transfer's from/to
// which may be a router or Permit2 contract.
continue;
}
// Use composite key so multiple token transfers per tx are kept.
// Non-contract token transfers get their own entries.
const ttKey = parsed.hash + ":" + (parsed.contractAddress || "");
txsByHash.set(ttKey, parsed);
}

View File

@@ -359,9 +359,12 @@ function decode(data, toAddress) {
const s = decodeV3SwapExactIn(inputs[i]);
if (s) {
if (!inputToken) inputToken = s.tokenIn;
if (!outputToken) outputToken = s.tokenOut;
if (!inputAmount) inputAmount = s.amountIn;
if (!minOutput) minOutput = s.amountOutMin;
// Always update output: in multi-step swaps (V3 → V4),
// the last swap step determines the final output token
// and minimum received amount.
outputToken = s.tokenOut;
minOutput = s.amountOutMin;
}
}
@@ -369,9 +372,9 @@ function decode(data, toAddress) {
const s = decodeV2SwapExactIn(inputs[i]);
if (s) {
if (!inputToken) inputToken = s.tokenIn;
if (!outputToken) outputToken = s.tokenOut;
if (!inputAmount) inputAmount = s.amountIn;
if (!minOutput) minOutput = s.amountOutMin;
outputToken = s.tokenOut;
minOutput = s.amountOutMin;
}
}
@@ -388,12 +391,11 @@ function decode(data, toAddress) {
const v4 = decodeV4Swap(inputs[i]);
if (v4) {
if (!inputToken && v4.tokenIn) inputToken = v4.tokenIn;
if (!outputToken && v4.tokenOut)
outputToken = v4.tokenOut;
if (!inputAmount && v4.amountIn)
inputAmount = v4.amountIn;
if (!minOutput && v4.amountOutMin)
minOutput = v4.amountOutMin;
// Always update output: last swap step wins
if (v4.tokenOut) outputToken = v4.tokenOut;
if (v4.amountOutMin) minOutput = v4.amountOutMin;
}
}

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,205 @@
// Provide a localStorage mock for Node.js test environment.
// Must be set before requiring the module since it calls loadDeltaFromStorage()
// at module load time.
const localStorageStore = {};
global.localStorage = {
getItem: (key) =>
Object.prototype.hasOwnProperty.call(localStorageStore, key)
? localStorageStore[key]
: null,
setItem: (key, value) => {
localStorageStore[key] = String(value);
},
removeItem: (key) => {
delete localStorageStore[key];
},
};
const {
isPhishingDomain,
loadConfig,
getBlocklistSize,
getDeltaSize,
hostnameVariants,
_reset,
_getVendoredBlacklistSize,
_getDeltaBlacklist,
} = require("../src/shared/phishingDomains");
// Reset delta state before each test to avoid cross-test contamination.
// Note: vendored sets are immutable and always present.
beforeEach(() => {
_reset();
// Clear localStorage mock between tests
for (const key of Object.keys(localStorageStore)) {
delete localStorageStore[key];
}
});
describe("phishingDomains", () => {
describe("vendored blocklist", () => {
test("vendored blacklist is loaded from bundled JSON", () => {
// The vendored blocklist should have a large number of entries
expect(_getVendoredBlacklistSize()).toBeGreaterThan(100000);
});
test("detects domains from vendored blacklist", () => {
// These are well-known phishing domains in the vendored list
expect(isPhishingDomain("hopprotocol.pro")).toBe(true);
expect(isPhishingDomain("blast-pools.pages.dev")).toBe(true);
});
test("getBlocklistSize includes vendored entries", () => {
expect(getBlocklistSize()).toBeGreaterThan(100000);
});
});
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("delta computation via loadConfig", () => {
test("loadConfig computes delta of new entries not in vendored list", () => {
loadConfig({
blacklist: [
"brand-new-scam-site-xyz123.com",
"hopprotocol.pro", // already in vendored
],
});
// Only the new domain should be in the delta
expect(
_getDeltaBlacklist().has("brand-new-scam-site-xyz123.com"),
).toBe(true);
expect(_getDeltaBlacklist().has("hopprotocol.pro")).toBe(false);
expect(getDeltaSize()).toBe(1);
});
test("re-loading config replaces previous delta", () => {
loadConfig({
blacklist: ["first-scam-xyz.com"],
});
expect(isPhishingDomain("first-scam-xyz.com")).toBe(true);
loadConfig({
blacklist: ["second-scam-xyz.com"],
});
expect(isPhishingDomain("first-scam-xyz.com")).toBe(false);
expect(isPhishingDomain("second-scam-xyz.com")).toBe(true);
});
test("getBlocklistSize includes both vendored and delta", () => {
const baseSize = getBlocklistSize();
loadConfig({
blacklist: ["delta-only-scam-xyz.com"],
});
expect(getBlocklistSize()).toBe(baseSize + 1);
});
});
describe("isPhishingDomain with delta + vendored", () => {
test("detects domain from delta blacklist", () => {
loadConfig({
blacklist: ["fresh-scam-xyz.com"],
});
expect(isPhishingDomain("fresh-scam-xyz.com")).toBe(true);
});
test("detects domain from vendored blacklist", () => {
// No delta loaded — vendored still works
expect(isPhishingDomain("hopprotocol.pro")).toBe(true);
});
test("returns false for clean domains", () => {
expect(isPhishingDomain("etherscan.io")).toBe(false);
expect(isPhishingDomain("example.com")).toBe(false);
});
test("detects subdomain of blacklisted domain (vendored)", () => {
expect(isPhishingDomain("app.hopprotocol.pro")).toBe(true);
});
test("detects subdomain of blacklisted domain (delta)", () => {
loadConfig({
blacklist: ["delta-phish-xyz.com"],
});
expect(isPhishingDomain("sub.delta-phish-xyz.com")).toBe(true);
});
test("case-insensitive matching", () => {
loadConfig({
blacklist: ["Delta-Scam-XYZ.COM"],
});
expect(isPhishingDomain("delta-scam-xyz.com")).toBe(true);
expect(isPhishingDomain("DELTA-SCAM-XYZ.COM")).toBe(true);
});
test("returns false for empty/null hostname", () => {
expect(isPhishingDomain("")).toBe(false);
expect(isPhishingDomain(null)).toBe(false);
});
test("handles config with no blacklist key", () => {
loadConfig({});
expect(getDeltaSize()).toBe(0);
// Vendored list still works
expect(isPhishingDomain("hopprotocol.pro")).toBe(true);
});
});
describe("localStorage persistence", () => {
test("saveDeltaToStorage persists delta under 256KiB", () => {
loadConfig({
blacklist: ["persisted-scam-xyz.com"],
});
const stored = localStorage.getItem("phishing-delta");
expect(stored).not.toBeNull();
const data = JSON.parse(stored);
expect(data.blacklist).toContain("persisted-scam-xyz.com");
});
test("delta is cleared on _reset", () => {
loadConfig({
blacklist: ["temp-scam-xyz.com"],
});
expect(getDeltaSize()).toBe(1);
_reset();
expect(getDeltaSize()).toBe(0);
});
});
describe("real-world blocklist patterns", () => {
test("detects known phishing domains from vendored list", () => {
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", () => {
expect(isPhishingDomain("opensea.io")).toBe(false);
expect(isPhishingDomain("etherscan.io")).toBe(false);
});
});
});