Compare commits
29 Commits
feature/ex
...
06324158aa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06324158aa | ||
|
|
293d781385 | ||
|
|
5927dfd45b | ||
| cb1446067f | |||
|
|
2e4cf32211 | ||
|
|
e737574038 | ||
| 3bf60ff162 | |||
| 6aeab54e8c | |||
| f65764d501 | |||
| 4e097c1e32 | |||
|
|
3f6f98dcaf | ||
|
|
3e900dc14c | ||
|
|
5dfc6e332b | ||
| a182aa534b | |||
| a388100262 | |||
| dd3cabf816 | |||
|
|
235e5e7fa7 | ||
|
|
be06bd8f0c | ||
|
|
a72359432b | ||
|
|
2bdb547995 | ||
| 834228b572 | |||
|
|
813993f17c | ||
| 5f01d9f111 | |||
|
|
d78af3ec80 | ||
| 753fb5658a | |||
| bdb2031d46 | |||
| 25ecaee128 | |||
|
|
ff4b5ee24d | ||
|
|
ca6e9054f9 |
107
LICENSE
107
LICENSE
@@ -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.
|
||||
|
||||
86
README.md
86
README.md
@@ -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)
|
||||
|
||||
@@ -12,6 +12,11 @@ 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"
|
||||
@@ -571,6 +576,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 +653,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);
|
||||
|
||||
@@ -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,17 @@
|
||||
/>
|
||||
Show tracked tokens with zero balance
|
||||
</label>
|
||||
<div class="text-xs flex items-center gap-1">
|
||||
<label for="settings-theme">Theme:</label>
|
||||
<select
|
||||
id="settings-theme"
|
||||
class="border border-border p-1 bg-bg text-fg text-xs cursor-pointer"
|
||||
>
|
||||
<option value="system">System</option>
|
||||
<option value="light">Light</option>
|
||||
<option value="dark">Dark</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-well p-3 mx-1 mb-3">
|
||||
@@ -903,6 +961,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 +1002,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 +1078,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 +1101,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 +1288,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 +1310,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 +1325,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 +1365,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 +1387,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>
|
||||
|
||||
@@ -6,6 +6,7 @@ const { state, saveState, loadState } = require("../shared/state");
|
||||
const { refreshPrices } = require("../shared/prices");
|
||||
const { refreshBalances } = require("../shared/balances");
|
||||
const { $, showView } = require("./views/helpers");
|
||||
const { applyTheme } = require("./theme");
|
||||
|
||||
const home = require("./views/home");
|
||||
const welcome = require("./views/welcome");
|
||||
@@ -176,6 +177,7 @@ async function init() {
|
||||
}
|
||||
|
||||
await loadState();
|
||||
applyTheme(state.theme);
|
||||
|
||||
// Auto-default active address
|
||||
if (
|
||||
|
||||
@@ -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
33
src/popup/theme.js
Normal 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 };
|
||||
@@ -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";
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,6 +2,7 @@ const {
|
||||
$,
|
||||
showView,
|
||||
showFlash,
|
||||
flashCopyFeedback,
|
||||
balanceLinesForAddress,
|
||||
addressDotHtml,
|
||||
addressTitle,
|
||||
@@ -94,18 +95,39 @@ function show() {
|
||||
function isoDate(timestamp) {
|
||||
const d = new Date(timestamp * 1000);
|
||||
const pad = (n) => String(n).padStart(2, "0");
|
||||
if (state.utcTimestamps) {
|
||||
return (
|
||||
d.getUTCFullYear() +
|
||||
"-" +
|
||||
pad(d.getUTCMonth() + 1) +
|
||||
"-" +
|
||||
pad(d.getUTCDate()) +
|
||||
"T" +
|
||||
pad(d.getUTCHours()) +
|
||||
":" +
|
||||
pad(d.getUTCMinutes()) +
|
||||
":" +
|
||||
pad(d.getUTCSeconds()) +
|
||||
"Z"
|
||||
);
|
||||
}
|
||||
const offsetMin = -d.getTimezoneOffset();
|
||||
const sign = offsetMin >= 0 ? "+" : "-";
|
||||
const absOff = Math.abs(offsetMin);
|
||||
const tzStr = sign + pad(Math.floor(absOff / 60)) + ":" + pad(absOff % 60);
|
||||
return (
|
||||
d.getFullYear() +
|
||||
"-" +
|
||||
pad(d.getMonth() + 1) +
|
||||
"-" +
|
||||
pad(d.getDate()) +
|
||||
" " +
|
||||
"T" +
|
||||
pad(d.getHours()) +
|
||||
":" +
|
||||
pad(d.getMinutes()) +
|
||||
":" +
|
||||
pad(d.getSeconds())
|
||||
pad(d.getSeconds()) +
|
||||
tzStr
|
||||
);
|
||||
}
|
||||
|
||||
@@ -241,6 +263,7 @@ function init(_ctx) {
|
||||
if (addr) {
|
||||
navigator.clipboard.writeText(addr);
|
||||
showFlash("Copied!");
|
||||
flashCopyFeedback($("address-full"));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -310,8 +333,8 @@ function init(_ctx) {
|
||||
$("export-privkey-address").textContent = addr.address;
|
||||
$("export-privkey-address").dataset.full = addr.address;
|
||||
$("export-privkey-password").value = "";
|
||||
$("export-privkey-flash").classList.add("hidden");
|
||||
$("export-privkey-flash").textContent = "";
|
||||
$("export-privkey-flash").style.visibility = "hidden";
|
||||
$("export-privkey-password-section").classList.remove("hidden");
|
||||
$("export-privkey-result").classList.add("hidden");
|
||||
$("export-privkey-value").textContent = "";
|
||||
@@ -322,7 +345,7 @@ function init(_ctx) {
|
||||
const password = $("export-privkey-password").value;
|
||||
if (!password) {
|
||||
$("export-privkey-flash").textContent = "Password is required.";
|
||||
$("export-privkey-flash").classList.remove("hidden");
|
||||
$("export-privkey-flash").style.visibility = "visible";
|
||||
return;
|
||||
}
|
||||
const btn = $("btn-export-privkey-confirm");
|
||||
@@ -343,10 +366,10 @@ function init(_ctx) {
|
||||
$("export-privkey-password-section").classList.add("hidden");
|
||||
$("export-privkey-value").textContent = privateKey;
|
||||
$("export-privkey-result").classList.remove("hidden");
|
||||
$("export-privkey-flash").classList.add("hidden");
|
||||
$("export-privkey-flash").style.visibility = "hidden";
|
||||
} catch {
|
||||
$("export-privkey-flash").textContent = "Wrong password.";
|
||||
$("export-privkey-flash").classList.remove("hidden");
|
||||
$("export-privkey-flash").style.visibility = "visible";
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.classList.remove("text-muted");
|
||||
@@ -358,6 +381,7 @@ function init(_ctx) {
|
||||
if (key) {
|
||||
navigator.clipboard.writeText(key);
|
||||
showFlash("Copied!");
|
||||
flashCopyFeedback($("export-privkey-value"));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -366,6 +390,7 @@ function init(_ctx) {
|
||||
if (full) {
|
||||
navigator.clipboard.writeText(full);
|
||||
showFlash("Copied!");
|
||||
flashCopyFeedback($("export-privkey-address"));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ const {
|
||||
$,
|
||||
showView,
|
||||
showFlash,
|
||||
flashCopyFeedback,
|
||||
addressDotHtml,
|
||||
addressTitle,
|
||||
escapeHtml,
|
||||
@@ -47,18 +48,39 @@ function etherscanAddressLink(address) {
|
||||
function isoDate(timestamp) {
|
||||
const d = new Date(timestamp * 1000);
|
||||
const pad = (n) => String(n).padStart(2, "0");
|
||||
if (state.utcTimestamps) {
|
||||
return (
|
||||
d.getUTCFullYear() +
|
||||
"-" +
|
||||
pad(d.getUTCMonth() + 1) +
|
||||
"-" +
|
||||
pad(d.getUTCDate()) +
|
||||
"T" +
|
||||
pad(d.getUTCHours()) +
|
||||
":" +
|
||||
pad(d.getUTCMinutes()) +
|
||||
":" +
|
||||
pad(d.getUTCSeconds()) +
|
||||
"Z"
|
||||
);
|
||||
}
|
||||
const offsetMin = -d.getTimezoneOffset();
|
||||
const sign = offsetMin >= 0 ? "+" : "-";
|
||||
const absOff = Math.abs(offsetMin);
|
||||
const tzStr = sign + pad(Math.floor(absOff / 60)) + ":" + pad(absOff % 60);
|
||||
return (
|
||||
d.getFullYear() +
|
||||
"-" +
|
||||
pad(d.getMonth() + 1) +
|
||||
"-" +
|
||||
pad(d.getDate()) +
|
||||
" " +
|
||||
"T" +
|
||||
pad(d.getHours()) +
|
||||
":" +
|
||||
pad(d.getMinutes()) +
|
||||
":" +
|
||||
pad(d.getSeconds())
|
||||
pad(d.getSeconds()) +
|
||||
tzStr
|
||||
);
|
||||
}
|
||||
|
||||
@@ -317,6 +339,7 @@ function init(_ctx) {
|
||||
if (addr) {
|
||||
navigator.clipboard.writeText(addr);
|
||||
showFlash("Copied!");
|
||||
flashCopyFeedback($("address-token-full"));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -325,6 +348,7 @@ function init(_ctx) {
|
||||
if (copyEl) {
|
||||
navigator.clipboard.writeText(copyEl.dataset.copy);
|
||||
showFlash("Copied!");
|
||||
flashCopyFeedback(copyEl);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -373,6 +397,7 @@ function init(_ctx) {
|
||||
copyEl.addEventListener("click", () => {
|
||||
navigator.clipboard.writeText(copyEl.dataset.copy);
|
||||
showFlash("Copied!");
|
||||
flashCopyFeedback(copyEl);
|
||||
});
|
||||
}
|
||||
updateSendBalance();
|
||||
|
||||
@@ -13,7 +13,6 @@ 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;
|
||||
|
||||
@@ -155,7 +154,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");
|
||||
@@ -269,7 +285,7 @@ function showTxApproval(details) {
|
||||
}
|
||||
|
||||
$("approve-tx-password").value = "";
|
||||
$("approve-tx-error").classList.add("hidden");
|
||||
hideError("approve-tx-error");
|
||||
|
||||
showView("approve-tx");
|
||||
}
|
||||
@@ -323,6 +339,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 +372,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 +403,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,
|
||||
|
||||
@@ -15,6 +15,7 @@ const {
|
||||
hideError,
|
||||
showView,
|
||||
showFlash,
|
||||
flashCopyFeedback,
|
||||
addressTitle,
|
||||
addressDotHtml,
|
||||
escapeHtml,
|
||||
@@ -24,8 +25,11 @@ const { getSignerForAddress } = require("../../shared/wallet");
|
||||
const { decryptWithPassword } = require("../../shared/vault");
|
||||
const { formatUsd, getPrice } = require("../../shared/prices");
|
||||
const { getProvider } = require("../../shared/balances");
|
||||
const { isScamAddress, isNullOrBurnAddress } = require("../../shared/scamlist");
|
||||
const { ERC20_ABI } = require("../../shared/constants");
|
||||
const {
|
||||
getLocalWarnings,
|
||||
getFullWarnings,
|
||||
} = require("../../shared/addressWarnings");
|
||||
const { ERC20_ABI, isBurnAddress } = require("../../shared/constants");
|
||||
const { log } = require("../../shared/log");
|
||||
const makeBlockie = require("ethereum-blockies-base64");
|
||||
const txStatus = require("./txStatus");
|
||||
@@ -38,28 +42,6 @@ const EXT_ICON =
|
||||
`</svg></span>`;
|
||||
|
||||
let pendingTx = null;
|
||||
// Track active warnings so async checks can append without overwriting.
|
||||
let activeWarnings = [];
|
||||
|
||||
function renderWarnings(el, warnings) {
|
||||
activeWarnings = warnings.slice();
|
||||
if (warnings.length > 0) {
|
||||
el.innerHTML = warnings
|
||||
.map(
|
||||
(w) =>
|
||||
`<div class="border border-border border-dashed p-2 mb-1 text-xs font-bold">WARNING: ${w}</div>`,
|
||||
)
|
||||
.join("");
|
||||
el.classList.remove("hidden");
|
||||
} else {
|
||||
el.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function appendWarning(el, message) {
|
||||
activeWarnings.push(message);
|
||||
renderWarnings(el, activeWarnings);
|
||||
}
|
||||
|
||||
function restore() {
|
||||
const d = state.viewData;
|
||||
@@ -139,6 +121,7 @@ function show(txInfo) {
|
||||
copyEl.onclick = () => {
|
||||
navigator.clipboard.writeText(copyEl.dataset.copy);
|
||||
showFlash("Copied!");
|
||||
flashCopyFeedback(copyEl);
|
||||
};
|
||||
}
|
||||
} else {
|
||||
@@ -187,24 +170,24 @@ function show(txInfo) {
|
||||
$("confirm-balance").textContent = valueWithUsd(bal + " ETH", balUsd);
|
||||
}
|
||||
|
||||
// Check for warnings (synchronous checks first, async checks added later)
|
||||
const warnings = [];
|
||||
if (isScamAddress(txInfo.to)) {
|
||||
warnings.push(
|
||||
"This address is on a known scam/fraud list. Do not send funds to this address.",
|
||||
);
|
||||
}
|
||||
if (isNullOrBurnAddress(txInfo.to)) {
|
||||
warnings.push(
|
||||
"This is a null or burn address. Funds sent here will be permanently lost.",
|
||||
);
|
||||
}
|
||||
if (txInfo.to.toLowerCase() === txInfo.from.toLowerCase()) {
|
||||
warnings.push("You are sending to your own address.");
|
||||
}
|
||||
// Check for warnings (synchronous local checks)
|
||||
const localWarnings = getLocalWarnings(txInfo.to, {
|
||||
fromAddress: txInfo.from,
|
||||
});
|
||||
|
||||
const warningsEl = $("confirm-warnings");
|
||||
renderWarnings(warningsEl, warnings);
|
||||
if (localWarnings.length > 0) {
|
||||
warningsEl.innerHTML = localWarnings
|
||||
.map(
|
||||
(w) =>
|
||||
`<div class="border border-border border-dashed p-2 mb-1 text-xs font-bold">WARNING: ${w.message}</div>`,
|
||||
)
|
||||
.join("");
|
||||
warningsEl.style.visibility = "visible";
|
||||
} else {
|
||||
warningsEl.innerHTML = "";
|
||||
warningsEl.style.visibility = "hidden";
|
||||
}
|
||||
|
||||
// Check for errors
|
||||
const errors = [];
|
||||
@@ -241,11 +224,12 @@ function show(txInfo) {
|
||||
errorsEl.innerHTML = errors
|
||||
.map((e) => `<div class="text-xs">${e}</div>`)
|
||||
.join("");
|
||||
errorsEl.classList.remove("hidden");
|
||||
errorsEl.style.visibility = "visible";
|
||||
sendBtn.disabled = true;
|
||||
sendBtn.classList.add("text-muted");
|
||||
} else {
|
||||
errorsEl.classList.add("hidden");
|
||||
errorsEl.innerHTML = "";
|
||||
errorsEl.style.visibility = "hidden";
|
||||
sendBtn.disabled = false;
|
||||
sendBtn.classList.remove("text-muted");
|
||||
}
|
||||
@@ -255,15 +239,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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -40,11 +40,13 @@ function $(id) {
|
||||
function showError(id, msg) {
|
||||
const el = $(id);
|
||||
el.textContent = msg;
|
||||
el.classList.remove("hidden");
|
||||
el.style.visibility = "visible";
|
||||
}
|
||||
|
||||
function hideError(id) {
|
||||
$(id).classList.add("hidden");
|
||||
const el = $(id);
|
||||
el.textContent = "";
|
||||
el.style.visibility = "hidden";
|
||||
}
|
||||
|
||||
function showView(name) {
|
||||
@@ -226,18 +228,39 @@ function formatAddressHtml(address, ensName, maxLen, title) {
|
||||
function isoDate(timestamp) {
|
||||
const d = new Date(timestamp * 1000);
|
||||
const pad = (n) => String(n).padStart(2, "0");
|
||||
if (state.utcTimestamps) {
|
||||
return (
|
||||
d.getUTCFullYear() +
|
||||
"-" +
|
||||
pad(d.getUTCMonth() + 1) +
|
||||
"-" +
|
||||
pad(d.getUTCDate()) +
|
||||
"T" +
|
||||
pad(d.getUTCHours()) +
|
||||
":" +
|
||||
pad(d.getUTCMinutes()) +
|
||||
":" +
|
||||
pad(d.getUTCSeconds()) +
|
||||
"Z"
|
||||
);
|
||||
}
|
||||
const offsetMin = -d.getTimezoneOffset();
|
||||
const sign = offsetMin >= 0 ? "+" : "-";
|
||||
const absOff = Math.abs(offsetMin);
|
||||
const tzStr = sign + pad(Math.floor(absOff / 60)) + ":" + pad(absOff % 60);
|
||||
return (
|
||||
d.getFullYear() +
|
||||
"-" +
|
||||
pad(d.getMonth() + 1) +
|
||||
"-" +
|
||||
pad(d.getDate()) +
|
||||
" " +
|
||||
"T" +
|
||||
pad(d.getHours()) +
|
||||
":" +
|
||||
pad(d.getMinutes()) +
|
||||
":" +
|
||||
pad(d.getSeconds())
|
||||
pad(d.getSeconds()) +
|
||||
tzStr
|
||||
);
|
||||
}
|
||||
|
||||
@@ -258,12 +281,26 @@ function timeAgo(timestamp) {
|
||||
return years + " year" + (years !== 1 ? "s" : "") + " ago";
|
||||
}
|
||||
|
||||
function flashCopyFeedback(el) {
|
||||
if (!el) return;
|
||||
el.classList.remove("copy-flash-fade");
|
||||
el.classList.add("copy-flash-active");
|
||||
setTimeout(() => {
|
||||
el.classList.remove("copy-flash-active");
|
||||
el.classList.add("copy-flash-fade");
|
||||
setTimeout(() => {
|
||||
el.classList.remove("copy-flash-fade");
|
||||
}, 275);
|
||||
}, 75);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
$,
|
||||
showError,
|
||||
hideError,
|
||||
showView,
|
||||
showFlash,
|
||||
flashCopyFeedback,
|
||||
balanceLine,
|
||||
balanceLinesForAddress,
|
||||
addressColor,
|
||||
|
||||
@@ -2,6 +2,7 @@ const {
|
||||
$,
|
||||
showView,
|
||||
showFlash,
|
||||
flashCopyFeedback,
|
||||
balanceLinesForAddress,
|
||||
isoDate,
|
||||
timeAgo,
|
||||
@@ -85,9 +86,10 @@ function renderActiveAddress() {
|
||||
el.innerHTML =
|
||||
`<span class="underline decoration-dashed cursor-pointer" id="active-addr-copy">${dot}${escapeHtml(addr)}</span>` +
|
||||
`<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
|
||||
$("active-addr-copy").addEventListener("click", () => {
|
||||
$("active-addr-copy").addEventListener("click", (e) => {
|
||||
navigator.clipboard.writeText(addr);
|
||||
showFlash("Copied!");
|
||||
flashCopyFeedback(e.currentTarget);
|
||||
});
|
||||
} else {
|
||||
el.textContent = "";
|
||||
|
||||
@@ -2,6 +2,7 @@ const {
|
||||
$,
|
||||
showView,
|
||||
showFlash,
|
||||
flashCopyFeedback,
|
||||
formatAddressHtml,
|
||||
addressTitle,
|
||||
} = require("./helpers");
|
||||
@@ -52,19 +53,21 @@ function show() {
|
||||
"This is an ERC-20 token. Only send " +
|
||||
symbol +
|
||||
" on the Ethereum network to this address. Sending tokens on other networks will result in permanent loss.";
|
||||
warningEl.classList.remove("hidden");
|
||||
warningEl.style.visibility = "visible";
|
||||
} else {
|
||||
warningEl.classList.add("hidden");
|
||||
warningEl.textContent = "";
|
||||
warningEl.style.visibility = "hidden";
|
||||
}
|
||||
showView("receive");
|
||||
}
|
||||
|
||||
function init(ctx) {
|
||||
$("receive-address-block").addEventListener("click", () => {
|
||||
$("receive-address-block").addEventListener("click", (e) => {
|
||||
const addr = $("receive-address-block").dataset.full;
|
||||
if (addr) {
|
||||
navigator.clipboard.writeText(addr);
|
||||
showFlash("Copied!");
|
||||
flashCopyFeedback(e.currentTarget);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -73,6 +76,7 @@ function init(ctx) {
|
||||
if (addr) {
|
||||
navigator.clipboard.writeText(addr);
|
||||
showFlash("Copied!");
|
||||
flashCopyFeedback($("receive-address-block"));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const { $, showView, showFlash, escapeHtml } = require("./helpers");
|
||||
const { applyTheme } = require("../theme");
|
||||
const { state, saveState } = require("../../shared/state");
|
||||
const { ETHEREUM_MAINNET_CHAIN_ID } = require("../../shared/constants");
|
||||
const { log, debugFetch } = require("../../shared/log");
|
||||
@@ -214,6 +215,13 @@ function init(ctx) {
|
||||
await saveState();
|
||||
});
|
||||
|
||||
$("settings-theme").value = state.theme;
|
||||
$("settings-theme").addEventListener("change", async () => {
|
||||
state.theme = $("settings-theme").value;
|
||||
await saveState();
|
||||
applyTheme(state.theme);
|
||||
});
|
||||
|
||||
$("settings-hide-low-holders").checked = state.hideLowHolderTokens;
|
||||
$("settings-hide-low-holders").addEventListener("change", async () => {
|
||||
state.hideLowHolderTokens = $("settings-hide-low-holders").checked;
|
||||
@@ -241,6 +249,12 @@ function init(ctx) {
|
||||
}
|
||||
});
|
||||
|
||||
$("settings-utc-timestamps").checked = state.utcTimestamps;
|
||||
$("settings-utc-timestamps").addEventListener("change", async () => {
|
||||
state.utcTimestamps = $("settings-utc-timestamps").checked;
|
||||
await saveState();
|
||||
});
|
||||
|
||||
$("btn-main-add-wallet").addEventListener("click", ctx.showAddWalletView);
|
||||
|
||||
$("btn-settings-add-token").addEventListener(
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ const {
|
||||
$,
|
||||
showView,
|
||||
showFlash,
|
||||
flashCopyFeedback,
|
||||
addressDotHtml,
|
||||
addressTitle,
|
||||
escapeHtml,
|
||||
@@ -12,6 +13,7 @@ const {
|
||||
timeAgo,
|
||||
} = require("./helpers");
|
||||
const { state } = 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" +
|
||||
@@ -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 = `https://etherscan.io/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 = `https://etherscan.io/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;
|
||||
|
||||
@@ -248,6 +398,7 @@ async function loadCalldata(txHash, toAddress) {
|
||||
el.onclick = () => {
|
||||
navigator.clipboard.writeText(el.dataset.copy);
|
||||
showFlash("Copied!");
|
||||
flashCopyFeedback(el);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ const {
|
||||
$,
|
||||
showView,
|
||||
showFlash,
|
||||
flashCopyFeedback,
|
||||
addressDotHtml,
|
||||
addressTitle,
|
||||
escapeHtml,
|
||||
@@ -77,6 +78,7 @@ function attachCopyHandlers(viewId) {
|
||||
el.onclick = () => {
|
||||
navigator.clipboard.writeText(el.dataset.copy);
|
||||
showFlash("Copied!");
|
||||
flashCopyFeedback(el);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
114
src/shared/addressWarnings.js
Normal file
114
src/shared/addressWarnings.js
Normal 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 };
|
||||
@@ -20,6 +20,19 @@ const ERC20_ABI = [
|
||||
"function approve(address spender, uint256 amount) returns (bool)",
|
||||
];
|
||||
|
||||
// Known null/burn addresses that permanently destroy funds.
|
||||
const BURN_ADDRESSES = new Set([
|
||||
"0x0000000000000000000000000000000000000000",
|
||||
"0x0000000000000000000000000000000000000001",
|
||||
"0x000000000000000000000000000000000000dead",
|
||||
"0xdead000000000000000000000000000000000000",
|
||||
"0x00000000000000000000000000000000deadbeef",
|
||||
]);
|
||||
|
||||
function isBurnAddress(address) {
|
||||
return BURN_ADDRESSES.has(address.toLowerCase());
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DEBUG,
|
||||
DEBUG_MNEMONIC,
|
||||
@@ -28,4 +41,6 @@ module.exports = {
|
||||
DEFAULT_BLOCKSCOUT_URL,
|
||||
BIP44_ETH_PATH,
|
||||
ERC20_ABI,
|
||||
BURN_ADDRESSES,
|
||||
isBurnAddress,
|
||||
};
|
||||
|
||||
102
src/shared/etherscanLabels.js
Normal file
102
src/shared/etherscanLabels.js
Normal file
@@ -0,0 +1,102 @@
|
||||
// Etherscan address label lookup via page scraping.
|
||||
// Extension users make the requests directly to Etherscan — no proxy needed.
|
||||
// This is a best-effort enrichment: network failures return null silently.
|
||||
|
||||
const ETHERSCAN_BASE = "https://etherscan.io/address/";
|
||||
|
||||
// Patterns in the page title that indicate a flagged address.
|
||||
// Title format: "Fake_Phishing184810 | Address: 0x... | Etherscan"
|
||||
const PHISHING_LABEL_PATTERNS = [/^Fake_Phishing/i, /^Phish:/i, /^Exploiter/i];
|
||||
|
||||
// Patterns in the page body that indicate a scam/phishing warning.
|
||||
const SCAM_BODY_PATTERNS = [
|
||||
/used in a\s+(?:\w+\s+)?phishing scam/i,
|
||||
/used in a\s+(?:\w+\s+)?scam/i,
|
||||
/wallet\s+drainer/i,
|
||||
];
|
||||
|
||||
/**
|
||||
* Parse the Etherscan address page HTML to extract label info.
|
||||
* Exported for unit testing (no fetch needed).
|
||||
*
|
||||
* @param {string} html - Raw HTML of the Etherscan address page.
|
||||
* @returns {{ label: string|null, isPhishing: boolean, warning: string|null }}
|
||||
*/
|
||||
function parseEtherscanPage(html) {
|
||||
// Extract <title> content
|
||||
const titleMatch = html.match(/<title[^>]*>([^<]+)<\/title>/i);
|
||||
let label = null;
|
||||
let isPhishing = false;
|
||||
let warning = null;
|
||||
|
||||
if (titleMatch) {
|
||||
const title = titleMatch[1].trim();
|
||||
// Title: "LABEL | Address: 0x... | Etherscan" or "Address: 0x... | Etherscan"
|
||||
const labelMatch = title.match(/^(.+?)\s*\|\s*Address:/);
|
||||
if (labelMatch) {
|
||||
const candidate = labelMatch[1].trim();
|
||||
// Only treat as a label if it's not just "Address" (unlabeled addresses)
|
||||
if (candidate.toLowerCase() !== "address") {
|
||||
label = candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check label against phishing patterns
|
||||
if (label) {
|
||||
for (const pat of PHISHING_LABEL_PATTERNS) {
|
||||
if (pat.test(label)) {
|
||||
isPhishing = true;
|
||||
warning = `Etherscan labels this address as "${label}" (Phish/Hack).`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check page body for scam warning banners
|
||||
if (!isPhishing) {
|
||||
for (const pat of SCAM_BODY_PATTERNS) {
|
||||
if (pat.test(html)) {
|
||||
isPhishing = true;
|
||||
warning = label
|
||||
? `Etherscan labels this address as "${label}" and reports it was used in a scam.`
|
||||
: "Etherscan reports this address was flagged for phishing/scam activity.";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { label, isPhishing, warning };
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch an address page from Etherscan and check for scam/phishing labels.
|
||||
* Returns a warning object if the address is flagged, or null.
|
||||
* Network failures return null silently (best-effort check).
|
||||
*
|
||||
* @param {string} address - Ethereum address to check.
|
||||
* @returns {Promise<{type: string, message: string, severity: string}|null>}
|
||||
*/
|
||||
async function checkEtherscanLabel(address) {
|
||||
try {
|
||||
const resp = await fetch(ETHERSCAN_BASE + address, {
|
||||
headers: { Accept: "text/html" },
|
||||
});
|
||||
if (!resp.ok) return null;
|
||||
const html = await resp.text();
|
||||
const result = parseEtherscanPage(html);
|
||||
if (result.isPhishing) {
|
||||
return {
|
||||
type: "etherscan-phishing",
|
||||
message: result.warning,
|
||||
severity: "critical",
|
||||
};
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
// Network errors are expected — Etherscan may rate-limit or block.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { parseEtherscanPage, checkEtherscanLabel };
|
||||
231418
src/shared/phishingBlocklist.json
Normal file
231418
src/shared/phishingBlocklist.json
Normal file
File diff suppressed because it is too large
Load Diff
215
src/shared/phishingDomains.js
Normal file
215
src/shared/phishingDomains.js
Normal 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,
|
||||
};
|
||||
@@ -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 };
|
||||
|
||||
@@ -23,8 +23,10 @@ const DEFAULT_STATE = {
|
||||
hideFraudContracts: true,
|
||||
hideDustTransactions: true,
|
||||
dustThresholdGwei: 100000,
|
||||
utcTimestamps: false,
|
||||
fraudContracts: [],
|
||||
tokenHolderCache: {},
|
||||
theme: "system",
|
||||
};
|
||||
|
||||
const state = {
|
||||
@@ -53,8 +55,10 @@ async function saveState() {
|
||||
hideFraudContracts: state.hideFraudContracts,
|
||||
hideDustTransactions: state.hideDustTransactions,
|
||||
dustThresholdGwei: state.dustThresholdGwei,
|
||||
utcTimestamps: state.utcTimestamps,
|
||||
fraudContracts: state.fraudContracts,
|
||||
tokenHolderCache: state.tokenHolderCache,
|
||||
theme: state.theme,
|
||||
currentView: state.currentView,
|
||||
selectedWallet: state.selectedWallet,
|
||||
selectedAddress: state.selectedAddress,
|
||||
@@ -108,8 +112,11 @@ async function loadState() {
|
||||
saved.dustThresholdGwei !== undefined
|
||||
? saved.dustThresholdGwei
|
||||
: 100000;
|
||||
state.utcTimestamps =
|
||||
saved.utcTimestamps !== undefined ? saved.utcTimestamps : false;
|
||||
state.fraudContracts = saved.fraudContracts || [];
|
||||
state.tokenHolderCache = saved.tokenHolderCache || {};
|
||||
state.theme = saved.theme || "system";
|
||||
state.currentView = saved.currentView || null;
|
||||
state.selectedWallet =
|
||||
saved.selectedWallet !== undefined ? saved.selectedWallet : null;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
100
tests/etherscanLabels.test.js
Normal file
100
tests/etherscanLabels.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
205
tests/phishingDomains.test.js
Normal file
205
tests/phishingDomains.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user