Compare commits

..

1 Commits

Author SHA1 Message Date
user
f01a662000 feat: expand warning types for send confirmation
All checks were successful
check / check (push) Successful in 23s
- Combine MEW darklist (652) and CryptoScamDB (2043) into 2314 scam addresses
- Add null/burn address detection with permanent loss warning
- Add contract address detection warning (sending directly to contracts)
- Unify all warnings into single warnings element (sync + async)
- Zero-history warning now uses unified warning system

Closes #114
2026-02-28 16:11:02 -08:00
38 changed files with 636 additions and 233690 deletions

107
LICENSE
View File

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

View File

@@ -15,12 +15,10 @@ 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 three user-configurable services: the configured RPC node for
extension contacts exactly three external 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. It also fetches a community-maintained phishing domain blocklist
periodically and performs best-effort Etherscan address label lookups during
transaction confirmation.
token balances. All three endpoints are user-configurable.
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
@@ -437,29 +435,16 @@ transitions.
#### TransactionDetail
- **When**: User tapped a transaction row from AddressDetail or AddressToken.
- **Elements** (grouped into logical blocks using light well containers; field
labels are self-explanatory so groups have no headings):
- **Elements**:
- "Transaction" heading, "Back" button
- 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
- 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
- **Transitions**:
- "Back" → **AddressToken** (if `selectedToken` set) or **AddressDetail**
@@ -582,25 +567,14 @@ 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
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).
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).
### Dependencies
@@ -790,21 +764,6 @@ 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
@@ -887,21 +846,6 @@ Currently supported:
GPL-3.0. See [LICENSE](LICENSE).
### Third-Party Data Files
This repository includes data files from third-party projects that are not
covered by the GPL-3.0 license above. These files, their copyright holders, and
their licenses are:
| File | Source | Copyright | License |
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | -------------------------------------------------------------- |
| `src/shared/phishingBlocklist.json` | [eth-phishing-detect](https://github.com/AugurProject/eth-phishing-detect) community-maintained phishing domain blocklist | Copyright (c) 2018 kumavis | [DBAD (Don't Be a Dick)](https://github.com/philsturgeon/dbad) |
| `src/shared/scamlist.js` (address data from MyEtherWallet) | [ethereum-lists](https://github.com/MyEtherWallet/ethereum-lists) `addresses-darklist.json` | Copyright (c) 2020 MyEtherWallet | MIT |
| `src/shared/scamlist.js` (address data from EtherScamDB) | [EtherScamDB](https://github.com/MrLuit/EtherScamDB) `scams.yaml` | Copyright (c) 2018 Luit Hollander | MIT |
The full license texts for these third-party files are included in the
[LICENSE](LICENSE) file.
## Author
[@sneak](https://sneak.berlin)

View File

@@ -2,25 +2,16 @@
// Handles EIP-1193 RPC requests from content scripts and proxies
// non-sensitive calls to the configured Ethereum JSON-RPC endpoint.
const { DEFAULT_RPC_URL } = require("../shared/constants");
const { SUPPORTED_CHAIN_IDS, networkByChainId } = require("../shared/networks");
const { onChainSwitch } = require("../shared/chainSwitch");
const { getBytes } = require("ethers");
const {
state,
loadState,
saveState,
currentNetwork,
} = require("../shared/state");
ETHEREUM_MAINNET_CHAIN_ID,
DEFAULT_RPC_URL,
} = require("../shared/constants");
const { getBytes } = require("ethers");
const { state, loadState, saveState } = require("../shared/state");
const { refreshBalances, getProvider } = require("../shared/balances");
const { debugFetch } = require("../shared/log");
const { decryptWithPassword } = require("../shared/vault");
const { getSignerForAddress } = require("../shared/wallet");
const {
isPhishingDomain,
updatePhishingList,
startPeriodicRefresh,
} = require("../shared/phishingDomains");
const storageApi =
typeof browser !== "undefined"
@@ -333,43 +324,31 @@ async function handleRpc(method, params, origin) {
}
if (method === "eth_chainId") {
return { result: currentNetwork().chainId };
return { result: ETHEREUM_MAINNET_CHAIN_ID };
}
if (method === "net_version") {
return { result: currentNetwork().networkVersion };
return { result: "1" };
}
if (method === "wallet_switchEthereumChain") {
const chainId = params?.[0]?.chainId;
if (chainId === currentNetwork().chainId) {
return { result: null };
}
if (SUPPORTED_CHAIN_IDS.has(chainId)) {
const target = networkByChainId(chainId);
await onChainSwitch(target.id);
broadcastChainChanged(target.chainId);
if (chainId === ETHEREUM_MAINNET_CHAIN_ID) {
return { result: null };
}
return {
error: {
code: 4902,
message:
"AutistMask supports Ethereum Mainnet and Sepolia Testnet only.",
message: "AutistMask only supports Ethereum mainnet.",
},
};
}
if (method === "wallet_addEthereumChain") {
const chainId = params?.[0]?.chainId;
if (SUPPORTED_CHAIN_IDS.has(chainId)) {
return { result: null };
}
return {
error: {
code: 4902,
message:
"AutistMask supports Ethereum Mainnet and Sepolia Testnet only.",
message: "AutistMask only supports Ethereum mainnet.",
},
};
}
@@ -515,27 +494,6 @@ async function handleRpc(method, params, origin) {
return { error: { message: "Unsupported method: " + method } };
}
// Broadcast chainChanged to all tabs when the network is switched.
function broadcastChainChanged(chainId) {
tabsApi.query({}, (tabs) => {
for (const tab of tabs) {
tabsApi.sendMessage(
tab.id,
{
type: "AUTISTMASK_EVENT",
eventName: "chainChanged",
data: chainId,
},
() => {
if (runtime.lastError) {
// expected for tabs without our content script
}
},
);
}
});
}
// Broadcast accountsChanged to all tabs, respecting per-address permissions
async function broadcastAccountsChanged() {
// Clear non-remembered approvals on address switch
@@ -613,11 +571,6 @@ 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) => {
@@ -690,8 +643,6 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
resp.type = "sign";
resp.signParams = approval.signParams;
}
// Flag if the requesting domain is on the phishing blocklist.
resp.isPhishingDomain = isPhishingDomain(approval.hostname);
sendResponse(resp);
} else {
sendResponse(null);

View File

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

View File

@@ -107,8 +107,7 @@
</div>
<div
id="add-wallet-phrase-warning"
class="text-xs mb-2 border border-border border-dashed p-2"
style="visibility: hidden"
class="text-xs mb-2 border border-border border-dashed p-2 hidden"
>
Write these words down and keep them safe. Anyone with
them can take your funds; if you lose them, your wallet
@@ -185,7 +184,7 @@
<!-- active address headline -->
<div
id="total-value"
class="text-2xl font-bold min-h-[2rem] text-fg"
class="text-2xl font-bold min-h-[2rem]"
></div>
<div
id="total-value-sub"
@@ -376,8 +375,7 @@
</p>
<div
id="export-privkey-flash"
class="text-xs mb-2 min-h-[1.25rem]"
style="visibility: hidden"
class="text-xs mb-2 hidden"
></div>
<div id="export-privkey-password-section" class="mb-2">
<label class="block mb-1">Password</label>
@@ -581,17 +579,13 @@
<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" style="visibility: hidden">
<div id="confirm-fee" class="mb-3 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"
style="visibility: hidden"
></div>
<div id="confirm-warnings" class="mb-2 hidden"></div>
<div
id="confirm-recipient-warning"
class="mb-2"
@@ -605,47 +599,9 @@
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"
style="visibility: hidden; min-height: 1.25rem"
class="mb-2 border border-border border-dashed p-2 hidden"
></div>
<div class="mb-2">
<label class="block mb-1 text-xs">Password</label>
@@ -658,7 +614,6 @@
<div
id="confirm-tx-password-error"
class="text-xs mb-2 min-h-[1.25rem]"
style="visibility: hidden"
></div>
<button
id="btn-confirm-send"
@@ -773,8 +728,7 @@
</button>
<div
id="receive-erc20-warning"
class="text-xs border border-border border-dashed p-2 mt-3"
style="visibility: hidden"
class="text-xs border border-border border-dashed p-2 mt-3 hidden"
></div>
</div>
@@ -802,8 +756,7 @@
</div>
<div
id="add-token-info"
class="text-xs text-muted mb-2 min-h-[1.25rem]"
style="visibility: hidden"
class="text-xs text-muted mb-2 hidden"
></div>
<div class="mb-2">
<label class="block mb-1 text-xs text-muted"
@@ -861,7 +814,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 mb-2"
class="text-xs flex items-center gap-1 cursor-pointer"
>
<input
type="checkbox"
@@ -869,35 +822,6 @@
/>
Show tracked tokens with zero balance
</label>
<div class="text-xs flex items-center gap-1">
<label for="settings-theme">Theme:</label>
<select
id="settings-theme"
class="border border-border p-1 bg-bg text-fg text-xs cursor-pointer"
>
<option value="system">System</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</div>
</div>
<div class="bg-well p-3 mx-1 mb-3">
<h3 class="font-bold mb-1">Network</h3>
<p class="text-xs text-muted mb-1">
Select the Ethereum network. Switching networks will
update the RPC and Blockscout endpoints to their
defaults.
</p>
<div class="text-xs flex items-center gap-1">
<select
id="settings-network"
class="border border-border p-1 bg-bg text-fg text-xs cursor-pointer"
>
<option value="mainnet">Ethereum Mainnet</option>
<option value="sepolia">Sepolia Testnet</option>
</select>
</div>
</div>
<div class="bg-well p-3 mx-1 mb-3">
@@ -979,12 +903,6 @@
/>
<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">
@@ -1020,8 +938,7 @@
</p>
<div
id="delete-wallet-flash"
class="text-xs text-red-500 mb-2 min-h-[1.25rem]"
style="visibility: hidden"
class="text-xs text-red-500 mb-2 hidden"
></div>
<div class="mb-2">
<label class="block mb-1">Password</label>
@@ -1096,8 +1013,7 @@
/>
<div
id="settings-addtoken-info"
class="text-xs text-muted mt-1 min-h-[1.25rem]"
style="visibility: hidden"
class="text-xs text-muted mt-1 hidden"
></div>
<button
id="btn-settings-addtoken-manual"
@@ -1119,149 +1035,66 @@
<h2 id="tx-detail-heading" class="font-bold mb-2">
Transaction
</h2>
<!-- ── 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-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-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 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>
<!-- ── 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 class="mb-4">
<div class="text-xs text-muted mb-1">Status</div>
<div id="tx-detail-status" class="text-xs"></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 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-token-contract-section"
class="mb-2 hidden"
id="tx-detail-calldata-well"
class="mb-3 border border-border border-dashed p-2"
>
<div class="text-xs text-muted mb-1">
Token contract
</div>
<div class="text-xs text-muted mb-1">Action</div>
<div
id="tx-detail-token-contract"
class="text-xs break-all"
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>
<!-- ── 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 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>
</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 id="tx-detail-rawdata-section" class="mb-4 hidden">
<div class="text-xs text-muted mb-1">Raw data</div>
<div
id="tx-detail-rawdata"
class="text-xs break-all font-mono border border-border border-dashed p-2"
></div>
</div>
</div>
<!-- ============ 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.
@@ -1306,8 +1139,7 @@
</div>
<div
id="approve-tx-error"
class="text-xs mb-2 border border-border border-dashed p-1 min-h-[1.25rem]"
style="visibility: hidden"
class="text-xs mb-2 border border-border border-dashed p-1 min-h-[1.25rem] hidden"
></div>
<div class="flex justify-between">
<button
@@ -1328,14 +1160,6 @@
<!-- ============ 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.
@@ -1343,10 +1167,8 @@
<div
id="approve-sign-danger-warning"
class="mb-3 p-2 text-xs font-bold"
class="hidden mb-3 p-2 text-xs font-bold"
style="
visibility: hidden;
min-height: 1.25rem;
background: #fee2e2;
color: #991b1b;
border: 2px solid #dc2626;
@@ -1383,8 +1205,7 @@
</div>
<div
id="approve-sign-error"
class="text-xs mb-2 border border-border border-dashed p-1 min-h-[1.25rem]"
style="visibility: hidden"
class="text-xs mb-2 border border-border border-dashed p-1 min-h-[1.25rem] hidden"
></div>
<div class="flex justify-between">
<button
@@ -1405,14 +1226,6 @@
<!-- ============ SITE APPROVAL ============ -->
<div id="view-approve-site" class="view hidden">
<h2 class="font-bold mb-2">Connection Request</h2>
<div
id="approve-site-phishing-warning"
class="mb-3 p-2 text-xs font-bold hidden bg-red-100 text-red-800 border-2 border-red-600 rounded-md"
>
⚠️ PHISHING WARNING: This site is on a known phishing
blocklist. Connecting your wallet may result in loss of
funds. Proceed with extreme caution.
</div>
<div class="mb-3">
<p class="mb-2">
<span id="approve-hostname" class="font-bold"></span>

View File

@@ -6,7 +6,6 @@ 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");
@@ -177,7 +176,6 @@ async function init() {
}
await loadState();
applyTheme(state.theme);
// Auto-default active address
if (

View File

@@ -15,32 +15,7 @@
--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;
}

View File

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

View File

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

View File

@@ -11,25 +11,6 @@ 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"];
@@ -71,7 +52,7 @@ function show() {
$("import-xprv-key").value = "";
$("add-wallet-password").value = "";
$("add-wallet-password-confirm").value = "";
$("add-wallet-phrase-warning").style.visibility = "hidden";
$("add-wallet-phrase-warning").classList.add("hidden");
switchMode("mnemonic");
showView("add-wallet");
}
@@ -116,18 +97,18 @@ async function importMnemonic(ctx) {
const pw = validatePassword();
if (!pw) return;
const { xpub, firstAddress } = hdWalletFromMnemonic(mnemonic);
const xpubDup = findWalletByXpub(xpub);
if (xpubDup) {
const duplicate = state.wallets.find(
(w) =>
w.type === "hd" &&
w.addresses[0] &&
w.addresses[0].address.toLowerCase() === firstAddress.toLowerCase(),
);
if (duplicate) {
showFlash(
"This recovery phrase is already added (" + xpubDup.name + ").",
"This recovery phrase is already added (" + duplicate.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 = {
@@ -181,10 +162,15 @@ async function importPrivateKey(ctx) {
}
const pw = validatePassword();
if (!pw) return;
const duplicate = findWalletByAddress(addr);
const duplicate = state.wallets.find(
(w) =>
w.type === "key" &&
w.addresses[0] &&
w.addresses[0].address.toLowerCase() === addr.toLowerCase(),
);
if (duplicate) {
showFlash(
"This address already exists in wallet (" + duplicate.name + ").",
"This private key is already added (" + duplicate.name + ").",
);
return;
}
@@ -222,14 +208,14 @@ async function importXprvKey(ctx) {
return;
}
const { xpub, firstAddress } = result;
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 + ").");
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 + ").");
return;
}
const pw = validatePassword();
@@ -281,7 +267,7 @@ function init(ctx) {
// Generate mnemonic
$("btn-generate-phrase").addEventListener("click", () => {
$("wallet-mnemonic").value = generateMnemonic();
$("add-wallet-phrase-warning").style.visibility = "visible";
$("add-wallet-phrase-warning").classList.remove("hidden");
});
// Import / confirm

View File

@@ -2,14 +2,11 @@ const {
$,
showView,
showFlash,
flashCopyFeedback,
balanceLinesForAddress,
addressDotHtml,
addressTitle,
escapeHtml,
truncateMiddle,
renderAddressHtml,
attachCopyHandlers,
} = require("./helpers");
const { state, currentAddress, saveState } = require("../../shared/state");
const { formatUsd, getAddressValueUsd } = require("../../shared/prices");
@@ -30,6 +27,17 @@ const { getSignerForAddress } = require("../../shared/wallet");
let ctx;
const EXT_ICON =
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
`<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5">` +
`<path d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5"/>` +
`<path d="M7 1.5h3.5V5M7 5.5L10.5 1.5"/>` +
`</svg></span>`;
function etherscanAddressLink(address) {
return `https://etherscan.io/address/${address}`;
}
function show() {
state.selectedToken = null;
const wallet = state.wallets[state.selectedWallet];
@@ -47,18 +55,22 @@ function show() {
img.style.imageRendering = "pixelated";
img.style.borderRadius = "50%";
blockieEl.appendChild(img);
const addrTitle = addressTitle(addr.address, state.wallets);
$("address-line").innerHTML = renderAddressHtml(addr.address, {
title: addrTitle,
ensName: addr.ensName,
});
$("address-line").dataset.full = addr.address;
attachCopyHandlers($("address-line"));
$("address-dot").innerHTML = addressDotHtml(addr.address);
$("address-full").dataset.full = addr.address;
$("address-full").textContent = addr.address;
const addrLink = etherscanAddressLink(addr.address);
$("address-etherscan-link").innerHTML =
`<a href="${addrLink}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
const usdTotal = formatUsd(getAddressValueUsd(addr));
$("address-usd-total").innerHTML = usdTotal || "&nbsp;";
const ensEl = $("address-ens");
// ENS is now shown inside renderAddressHtml, hide the separate element
ensEl.classList.add("hidden");
if (addr.ensName) {
ensEl.innerHTML =
addressDotHtml(addr.address) + escapeHtml(addr.ensName);
ensEl.classList.remove("hidden");
} else {
ensEl.classList.add("hidden");
}
$("address-balances").innerHTML = balanceLinesForAddress(
addr,
state.trackedTokens,
@@ -82,39 +94,18 @@ 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()) +
tzStr
pad(d.getSeconds())
);
}
@@ -245,6 +236,13 @@ function renderTransactions(txs) {
function init(_ctx) {
ctx = _ctx;
$("address-full").addEventListener("click", () => {
const addr = $("address-full").dataset.full;
if (addr) {
navigator.clipboard.writeText(addr);
showFlash("Copied!");
}
});
$("btn-address-back").addEventListener("click", () => {
ctx.renderWalletList();
@@ -308,12 +306,12 @@ function init(_ctx) {
blockieEl.appendChild(bImg);
$("export-privkey-title").textContent =
wallet.name + " \u2014 Address " + (state.selectedAddress + 1);
const exportAddrContainer = $("export-privkey-dot").parentElement;
exportAddrContainer.innerHTML = renderAddressHtml(addr.address);
attachCopyHandlers(exportAddrContainer);
$("export-privkey-dot").innerHTML = addressDotHtml(addr.address);
$("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 = "";
@@ -324,7 +322,7 @@ function init(_ctx) {
const password = $("export-privkey-password").value;
if (!password) {
$("export-privkey-flash").textContent = "Password is required.";
$("export-privkey-flash").style.visibility = "visible";
$("export-privkey-flash").classList.remove("hidden");
return;
}
const btn = $("btn-export-privkey-confirm");
@@ -345,10 +343,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").style.visibility = "hidden";
$("export-privkey-flash").classList.add("hidden");
} catch {
$("export-privkey-flash").textContent = "Wrong password.";
$("export-privkey-flash").style.visibility = "visible";
$("export-privkey-flash").classList.remove("hidden");
} finally {
btn.disabled = false;
btn.classList.remove("text-muted");
@@ -360,7 +358,14 @@ function init(_ctx) {
if (key) {
navigator.clipboard.writeText(key);
showFlash("Copied!");
flashCopyFeedback($("export-privkey-value"));
}
});
$("export-privkey-address").addEventListener("click", () => {
const full = $("export-privkey-address").dataset.full;
if (full) {
navigator.clipboard.writeText(full);
showFlash("Copied!");
}
});

View File

@@ -5,14 +5,11 @@ const {
$,
showView,
showFlash,
flashCopyFeedback,
addressDotHtml,
addressTitle,
escapeHtml,
truncateMiddle,
balanceLine,
renderAddressHtml,
attachCopyHandlers,
} = require("./helpers");
const { state, currentAddress, saveState } = require("../../shared/state");
const { TOKEN_BY_ADDRESS, resolveSymbol } = require("../../shared/tokenList");
@@ -36,42 +33,32 @@ const makeBlockie = require("ethereum-blockies-base64");
let ctx;
const EXT_ICON =
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
`<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5">` +
`<path d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5"/>` +
`<path d="M7 1.5h3.5V5M7 5.5L10.5 1.5"/>` +
`</svg></span>`;
function etherscanAddressLink(address) {
return `https://etherscan.io/address/${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()) +
tzStr
pad(d.getSeconds())
);
}
@@ -139,16 +126,15 @@ function show() {
blockieEl.appendChild(img);
// Address line
const addrTitle = addressTitle(addr.address, state.wallets);
$("address-token-line").innerHTML = renderAddressHtml(addr.address, {
title: addrTitle,
ensName: addr.ensName,
});
$("address-token-line").dataset.full = addr.address;
attachCopyHandlers($("address-token-line"));
$("address-token-dot").innerHTML = addressDotHtml(addr.address);
$("address-token-full").dataset.full = addr.address;
$("address-token-full").textContent = addr.address;
const addrLink = etherscanAddressLink(addr.address);
$("address-token-etherscan-link").innerHTML =
`<a href="${addrLink}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
// USD total for this token only
const usdVal = price ? amount * price : null;
const usdVal = price ? amount * price : 0;
const usdStr = formatUsd(usdVal);
$("address-token-usd-total").innerHTML = usdStr || "&nbsp;";
@@ -185,9 +171,15 @@ function show() {
? knownToken.decimals
: null;
const tokenHolders = tb && tb.holders != null ? tb.holders : null;
const dot = addressDotHtml(tokenId);
const tokenLink = `https://etherscan.io/token/${escapeHtml(tokenId)}`;
const projectUrl = knownToken && knownToken.url ? knownToken.url : null;
let infoHtml = `<div class="font-bold mb-2">Contract Address</div>`;
infoHtml += `<div class="mb-2">${renderAddressHtml(tokenId)}</div>`;
infoHtml +=
`<div class="flex items-center mb-2">${dot}` +
`<span class="break-all underline decoration-dashed cursor-pointer" id="address-token-contract-copy" data-copy="${escapeHtml(tokenId)}">${escapeHtml(tokenId)}</span>` +
`<a href="${tokenLink}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>` +
`</div>`;
if (tokenName)
infoHtml += `<div class="mb-1"><span class="text-muted">Name:</span> ${tokenName}</div>`;
if (tokenSymbol)
@@ -199,7 +191,6 @@ function show() {
if (projectUrl)
infoHtml += `<div class="mb-1"><span class="text-muted">Website:</span> <a href="${escapeHtml(projectUrl)}" target="_blank" rel="noopener" class="underline decoration-dashed">${escapeHtml(projectUrl)}</a></div>`;
contractInfo.innerHTML = infoHtml;
attachCopyHandlers(contractInfo);
contractInfo.classList.remove("hidden");
} else {
contractInfo.innerHTML = "";
@@ -321,12 +312,19 @@ function renderTransactions(txs) {
function init(_ctx) {
ctx = _ctx;
$("address-token-full").addEventListener("click", () => {
const addr = $("address-token-full").dataset.full;
if (addr) {
navigator.clipboard.writeText(addr);
showFlash("Copied!");
}
});
$("address-token-contract-info").addEventListener("click", (e) => {
const copyEl = e.target.closest("[data-copy]");
if (copyEl) {
navigator.clipboard.writeText(copyEl.dataset.copy);
showFlash("Copied!");
flashCopyFeedback(copyEl);
}
});
@@ -358,11 +356,25 @@ function init(_ctx) {
$("send-token").classList.add("hidden");
let staticHtml = `<div class="font-bold">${escapeHtml(currentSymbol)}</div>`;
if (tokenId !== "ETH") {
staticHtml += `<div class="text-xs">${renderAddressHtml(tokenId)}</div>`;
const dot = addressDotHtml(tokenId);
const link = `https://etherscan.io/token/${tokenId}`;
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
staticHtml +=
`<div class="flex items-center text-xs">${dot}` +
`<span class="break-all underline decoration-dashed cursor-pointer" data-copy="${escapeHtml(tokenId)}">${escapeHtml(tokenId)}</span>` +
extLink +
`</div>`;
}
$("send-token-static").innerHTML = staticHtml;
$("send-token-static").classList.remove("hidden");
attachCopyHandlers($("send-token-static"));
// Attach copy handler for the contract address
const copyEl = $("send-token-static").querySelector("[data-copy]");
if (copyEl) {
copyEl.addEventListener("click", () => {
navigator.clipboard.writeText(copyEl.dataset.copy);
showFlash("Copied!");
});
}
updateSendBalance();
resetSendValidation();
showView("send");

View File

@@ -1,28 +1,44 @@
const {
$,
addressDotHtml,
addressTitle,
escapeHtml,
showView,
showError,
hideError,
renderAddressHtml,
attachCopyHandlers,
} = require("./helpers");
const { state, saveState, currentNetwork } = require("../../shared/state");
const { state, saveState } = require("../../shared/state");
const { formatEther, formatUnits, Interface, toUtf8String } = require("ethers");
const { getPrice, formatUsd } = require("../../shared/prices");
const { ERC20_ABI } = require("../../shared/constants");
const { TOKEN_BY_ADDRESS } = require("../../shared/tokenList");
const txStatus = require("./txStatus");
const uniswap = require("../../shared/uniswap");
const runtime =
typeof browser !== "undefined" ? browser.runtime : chrome.runtime;
const EXT_ICON =
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
`<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5">` +
`<path d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5"/>` +
`<path d="M7 1.5h3.5V5M7 5.5L10.5 1.5"/>` +
`</svg></span>`;
const erc20Iface = new Interface(ERC20_ABI);
function approvalAddressHtml(address) {
const dot = addressDotHtml(address);
const link = `https://etherscan.io/address/${address}`;
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
const title = addressTitle(address, state.wallets);
return renderAddressHtml(address, { title });
let html = "";
if (title) {
html += `<div class="flex items-center font-bold">${dot}${escapeHtml(title)}</div>`;
html += `<div class="break-all">${escapeHtml(address)}${extLink}</div>`;
} else {
html += `<div class="flex items-center">${dot}<span class="break-all">${escapeHtml(address)}</span>${extLink}</div>`;
}
return html;
}
function formatTxValue(val) {
@@ -37,6 +53,10 @@ function tokenLabel(address) {
return t ? t.symbol : null;
}
function etherscanTokenLink(address) {
return `https://etherscan.io/token/${address}`;
}
// Try to decode calldata using known ABIs.
// Returns { name, description, details } or null.
function decodeCalldata(data, toAddress) {
@@ -135,24 +155,7 @@ 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");
@@ -215,19 +218,17 @@ function showTxApproval(details) {
toHtml += `<div class="font-bold mb-1">${escapeHtml(symbol)}</div>`;
}
toHtml += approvalAddressHtml(toAddr);
if (symbol) {
const link = etherscanTokenLink(toAddr);
toHtml = toHtml.replace("</div>", "") + ""; // approvalAddressHtml already has etherscan link
}
$("approve-tx-to").innerHTML = toHtml;
} else {
$("approve-tx-to").innerHTML = escapeHtml("(contract creation)");
}
const ethValueFormatted = formatTxValue(
formatEther(details.txParams.value || "0"),
);
const ethPrice = getPrice("ETH");
const ethUsd = ethPrice ? parseFloat(ethValueFormatted) * ethPrice : null;
const usdStr = formatUsd(ethUsd);
$("approve-tx-value").textContent =
ethValueFormatted + " ETH" + (usdStr ? " (" + usdStr + ")" : "");
formatTxValue(formatEther(details.txParams.value || "0")) + " ETH";
// Decode calldata (reuse decoded from above)
const decodedEl = $("approve-tx-decoded");
@@ -242,9 +243,12 @@ function showTxApproval(details) {
detailsHtml += `<div class="text-muted">${escapeHtml(d.label)}</div>`;
if (d.address) {
if (d.isToken) {
const tLink = etherscanTokenLink(d.address);
detailsHtml += `<div class="font-bold">${escapeHtml(tokenLabel(d.address) || "Unknown token")}</div>`;
detailsHtml += approvalAddressHtml(d.address);
} else {
detailsHtml += approvalAddressHtml(d.address);
}
detailsHtml += approvalAddressHtml(d.address);
} else {
detailsHtml += `<div class="font-bold">${escapeHtml(d.value)}</div>`;
}
@@ -265,10 +269,9 @@ function showTxApproval(details) {
}
$("approve-tx-password").value = "";
hideError("approve-tx-error");
$("approve-tx-error").classList.add("hidden");
showView("approve-tx");
attachCopyHandlers("view-approve-tx");
}
function decodeHexMessage(hex) {
@@ -320,11 +323,6 @@ function formatTypedDataHtml(jsonStr) {
}
function showSignApproval(details) {
showPhishingWarning(
"approve-sign-phishing-warning",
details.isPhishingDomain,
);
const sp = details.signParams;
$("approve-sign-hostname").textContent = details.hostname;
@@ -353,10 +351,10 @@ function showSignApproval(details) {
if (warningEl) {
if (sp.dangerWarning) {
warningEl.textContent = sp.dangerWarning;
warningEl.style.visibility = "visible";
warningEl.classList.remove("hidden");
} else {
warningEl.textContent = "";
warningEl.style.visibility = "hidden";
warningEl.classList.add("hidden");
}
}
@@ -366,7 +364,6 @@ function showSignApproval(details) {
$("btn-approve-sign").classList.remove("text-muted");
showView("approve-sign");
attachCopyHandlers("view-approve-sign");
}
function show(id) {
@@ -385,16 +382,10 @@ 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,
);
attachCopyHandlers("view-approve-site");
$("approve-remember").checked = state.rememberSiteChoice;
});
}

View File

@@ -15,27 +15,51 @@ const {
hideError,
showView,
showFlash,
flashCopyFeedback,
addressTitle,
addressDotHtml,
escapeHtml,
renderAddressHtml,
attachCopyHandlers,
} = require("./helpers");
const { state, currentNetwork } = require("../../shared/state");
const { state } = require("../../shared/state");
const { getSignerForAddress } = require("../../shared/wallet");
const { decryptWithPassword } = require("../../shared/vault");
const { formatUsd, getPrice } = require("../../shared/prices");
const { getProvider } = require("../../shared/balances");
const {
getLocalWarnings,
getFullWarnings,
} = require("../../shared/addressWarnings");
const { ERC20_ABI, isBurnAddress } = require("../../shared/constants");
const { isScamAddress, isNullOrBurnAddress } = require("../../shared/scamlist");
const { ERC20_ABI } = require("../../shared/constants");
const { log } = require("../../shared/log");
const makeBlockie = require("ethereum-blockies-base64");
const txStatus = require("./txStatus");
const EXT_ICON =
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
`<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5">` +
`<path d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5"/>` +
`<path d="M7 1.5h3.5V5M7 5.5L10.5 1.5"/>` +
`</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;
@@ -44,6 +68,14 @@ function restore() {
}
}
function etherscanTokenLink(address) {
return `https://etherscan.io/token/${address}`;
}
function etherscanAddressLink(address) {
return `https://etherscan.io/address/${address}`;
}
function blockieHtml(address) {
const src = makeBlockie(address);
return `<img src="${src}" width="48" height="48" style="image-rendering:pixelated;border-radius:50%;display:inline-block">`;
@@ -51,10 +83,22 @@ function blockieHtml(address) {
function confirmAddressHtml(address, ensName, title) {
const blockie = blockieHtml(address);
return (
`<div class="mb-1">${blockie}</div>` +
renderAddressHtml(address, { title, ensName })
);
const dot = addressDotHtml(address);
const link = etherscanAddressLink(address);
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
let html = `<div class="mb-1">${blockie}</div>`;
if (title) {
html += `<div class="flex items-center font-bold">${dot}${escapeHtml(title)}</div>`;
}
if (ensName) {
html += `<div class="flex items-center font-bold">${title ? "" : dot}${escapeHtml(ensName)}</div>`;
}
html +=
`<div class="flex items-center">${title || ensName ? "" : dot}` +
`<span class="break-all">${escapeHtml(address)}</span>` +
extLink +
`</div>`;
return html;
}
function valueWithUsd(text, usdAmount) {
@@ -81,12 +125,22 @@ function show(txInfo) {
// Token contract section (ERC-20 only)
const tokenSection = $("confirm-token-section");
if (isErc20) {
$("confirm-token-contract").innerHTML = renderAddressHtml(
txInfo.token,
{},
);
const dot = addressDotHtml(txInfo.token);
const link = etherscanTokenLink(txInfo.token);
$("confirm-token-contract").innerHTML =
`<div class="flex items-center">${dot}` +
`<span class="break-all underline decoration-dashed cursor-pointer" data-copy="${escapeHtml(txInfo.token)}">${escapeHtml(txInfo.token)}</span>` +
`<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>` +
`</div>`;
tokenSection.classList.remove("hidden");
attachCopyHandlers(tokenSection);
// Attach click-to-copy on the contract address
const copyEl = tokenSection.querySelector("[data-copy]");
if (copyEl) {
copyEl.onclick = () => {
navigator.clipboard.writeText(copyEl.dataset.copy);
showFlash("Copied!");
};
}
} else {
tokenSection.classList.add("hidden");
}
@@ -133,24 +187,24 @@ function show(txInfo) {
$("confirm-balance").textContent = valueWithUsd(bal + " ETH", balUsd);
}
// Check for warnings (synchronous local checks)
const localWarnings = getLocalWarnings(txInfo.to, {
fromAddress: txInfo.from,
});
// 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.");
}
const warningsEl = $("confirm-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";
}
renderWarnings(warningsEl, warnings);
// Check for errors
const errors = [];
@@ -187,12 +241,11 @@ function show(txInfo) {
errorsEl.innerHTML = errors
.map((e) => `<div class="text-xs">${e}</div>`)
.join("");
errorsEl.style.visibility = "visible";
errorsEl.classList.remove("hidden");
sendBtn.disabled = true;
sendBtn.classList.add("text-muted");
} else {
errorsEl.innerHTML = "";
errorsEl.style.visibility = "hidden";
errorsEl.classList.add("hidden");
sendBtn.disabled = false;
sendBtn.classList.remove("text-muted");
}
@@ -202,21 +255,15 @@ function show(txInfo) {
hideError("confirm-tx-password-error");
// Gas estimate — show placeholder then fetch async
$("confirm-fee").style.visibility = "visible";
$("confirm-fee").classList.remove("hidden");
$("confirm-fee-amount").textContent = "Estimating...";
state.viewData = { pendingTx: txInfo };
showView("confirm-tx");
attachCopyHandlers("view-confirm-tx");
// 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";
// Hide the legacy recipient warning element (warnings now unified)
const legacyWarningEl = $("confirm-recipient-warning");
if (legacyWarningEl) {
legacyWarningEl.style.display = "none";
}
estimateGas(txInfo);
@@ -264,21 +311,24 @@ async function estimateGas(txInfo) {
}
async function checkRecipientHistory(txInfo) {
const warningsEl = $("confirm-warnings");
try {
const provider = getProvider(state.rpcUrl);
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";
}
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.",
);
}
} catch (e) {
log.errorf("recipient history check failed:", e.message);

View File

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

View File

@@ -6,7 +6,7 @@ const {
getPrice,
getAddressValueUsd,
} = require("../../shared/prices");
const { state, saveState, currentNetwork } = require("../../shared/state");
const { state, saveState } = require("../../shared/state");
// When views are added, removed, or transitions between them change,
// update the view-navigation documentation in README.md to match.
@@ -40,13 +40,11 @@ function $(id) {
function showError(id, msg) {
const el = $(id);
el.textContent = msg;
el.style.visibility = "visible";
el.classList.remove("hidden");
}
function hideError(id) {
const el = $(id);
el.textContent = "";
el.style.visibility = "hidden";
$(id).classList.add("hidden");
}
function showView(name) {
@@ -208,47 +206,38 @@ function addressTitle(address, wallets) {
// Render an address with color dot, optional ENS name, optional title,
// and optional truncation. Title and ENS are shown as bold labels above
// the full address.
// Delegates to renderAddressHtml for consistent output.
function formatAddressHtml(address, ensName, maxLen, title) {
return renderAddressHtml(address, { title, ensName, maxLen });
const dot = addressDotHtml(address);
const displayAddr = maxLen ? truncateMiddle(address, maxLen) : address;
if (title || ensName) {
let html = "";
if (title) {
html += `<div class="flex items-center font-bold">${dot}${escapeHtml(title)}</div>`;
}
if (ensName) {
html += `<div class="flex items-center font-bold">${title ? "" : dot}${escapeHtml(ensName)}</div>`;
}
html += `<div class="break-all">${escapeHtml(displayAddr)}</div>`;
return html;
}
return `<div class="flex items-center">${dot}<span class="break-all">${escapeHtml(displayAddr)}</span></div>`;
}
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()) +
tzStr
pad(d.getSeconds())
);
}
@@ -269,111 +258,12 @@ function timeAgo(timestamp) {
return years + " year" + (years !== 1 ? "s" : "") + " ago";
}
// Shared external-link icon SVG used across all views.
const EXT_ICON =
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
`<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5">` +
`<path d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5"/>` +
`<path d="M7 1.5h3.5V5M7 5.5L10.5 1.5"/>` +
`</svg></span>`;
function etherscanAddressUrl(address) {
return `${currentNetwork().explorerUrl}/address/${address}`;
}
function etherscanLinkHtml(url) {
return (
`<a href="${url}" target="_blank" rel="noopener" ` +
`class="inline-flex items-center">${EXT_ICON}</a>`
);
}
// Render a copyable text span with dashed underline affordance.
// The caller must attach click handlers via attachCopyHandlers() or
// manually wire up [data-copy] elements after inserting the HTML.
function copyableHtml(text, extraClass) {
const cls =
"underline decoration-dashed cursor-pointer" +
(extraClass ? " " + extraClass : "");
return `<span class="${cls}" data-copy="${escapeHtml(text)}">${escapeHtml(text)}</span>`;
}
// Attach click-to-copy handlers to all [data-copy] elements within
// a container. Safe to call multiple times on the same container.
function attachCopyHandlers(container) {
const root =
typeof container === "string"
? document.getElementById(container)
: container;
if (!root) return;
root.querySelectorAll("[data-copy]").forEach((el) => {
el.onclick = () => {
navigator.clipboard.writeText(el.dataset.copy);
showFlash("Copied!");
flashCopyFeedback(el);
};
});
}
// Unified address rendering.
//
// Produces consistent HTML for any Ethereum address:
// • Color dot
// • Optional title (e.g. "Wallet 1 — Address 2") shown bold above address
// • Optional ENS name shown bold above address
// • Full address (or truncated via maxLen) with dashed-underline click-to-copy
// • Etherscan external link icon
//
// Options object:
// title — wallet title string (from addressTitle)
// ensName — ENS name string
// maxLen — if set, truncate address display (min 32 chars enforced)
// noLink — if true, omit etherscan link
//
// After inserting the returned HTML into the DOM, call
// attachCopyHandlers() on the parent to wire up click-to-copy.
function renderAddressHtml(address, opts) {
const { title, ensName, maxLen, noLink } = opts || {};
const dot = addressDotHtml(address);
const displayAddr = maxLen ? truncateMiddle(address, maxLen) : address;
const link = etherscanAddressUrl(address);
const extLink = noLink ? "" : etherscanLinkHtml(link);
let html = "";
if (title) {
html += `<div class="flex items-center font-bold">${dot}${escapeHtml(title)}</div>`;
}
if (ensName) {
html += `<div class="flex items-center font-bold">${title ? "" : dot}${escapeHtml(ensName)}</div>`;
}
if (title || ensName) {
html += `<div class="flex items-center">${copyableHtml(displayAddr, "break-all")}${extLink}</div>`;
} else {
html += `<div class="flex items-center">${dot}${copyableHtml(displayAddr, "break-all")}${extLink}</div>`;
}
return html;
}
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,
@@ -381,12 +271,6 @@ module.exports = {
escapeHtml,
addressTitle,
formatAddressHtml,
renderAddressHtml,
copyableHtml,
attachCopyHandlers,
etherscanAddressUrl,
etherscanLinkHtml,
EXT_ICON,
truncateMiddle,
isoDate,
timeAgo,

View File

@@ -2,7 +2,6 @@ const {
$,
showView,
showFlash,
flashCopyFeedback,
balanceLinesForAddress,
isoDate,
timeAgo,
@@ -10,8 +9,6 @@ const {
addressTitle,
escapeHtml,
truncateMiddle,
renderAddressHtml,
attachCopyHandlers,
} = require("./helpers");
const { state, saveState, currentAddress } = require("../../shared/state");
const {
@@ -71,12 +68,27 @@ function renderTotalValue() {
}
}
const EXT_ICON =
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
`<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5">` +
`<path d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5"/>` +
`<path d="M7 1.5h3.5V5M7 5.5L10.5 1.5"/>` +
`</svg></span>`;
function renderActiveAddress() {
const el = $("active-address-display");
if (!el) return;
if (state.activeAddress) {
el.innerHTML = renderAddressHtml(state.activeAddress);
attachCopyHandlers(el);
const addr = state.activeAddress;
const dot = addressDotHtml(addr);
const link = `https://etherscan.io/address/${addr}`;
el.innerHTML =
`<span class="underline decoration-dashed cursor-pointer" id="active-addr-copy">${dot}${escapeHtml(addr)}</span>` +
`<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
$("active-addr-copy").addEventListener("click", () => {
navigator.clipboard.writeText(addr);
showFlash("Copied!");
});
} else {
el.textContent = "";
}

View File

@@ -2,14 +2,19 @@ const {
$,
showView,
showFlash,
flashCopyFeedback,
formatAddressHtml,
addressTitle,
attachCopyHandlers,
} = require("./helpers");
const { state, currentAddress, currentNetwork } = require("../../shared/state");
const { state, currentAddress } = require("../../shared/state");
const QRCode = require("qrcode");
const EXT_ICON =
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
`<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5">` +
`<path d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5"/>` +
`<path d="M7 1.5h3.5V5M7 5.5L10.5 1.5"/>` +
`</svg></span>`;
function show() {
const addr = currentAddress();
const address = addr ? addr.address : "";
@@ -19,8 +24,10 @@ function show() {
? formatAddressHtml(address, ensName, null, title)
: "";
$("receive-address-block").dataset.full = address;
// Etherscan link is now included in formatAddressHtml via renderAddressHtml
$("receive-etherscan-link").innerHTML = "";
const link = address ? `https://etherscan.io/address/${address}` : "";
$("receive-etherscan-link").innerHTML = link
? `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`
: "";
if (address) {
QRCode.toCanvas($("receive-qr"), address, {
width: 200,
@@ -44,25 +51,28 @@ function show() {
warningEl.textContent =
"This is an ERC-20 token. Only send " +
symbol +
" on " +
currentNetwork().name +
" to this address. Sending tokens on other networks will result in permanent loss.";
warningEl.style.visibility = "visible";
" on the Ethereum network to this address. Sending tokens on other networks will result in permanent loss.";
warningEl.classList.remove("hidden");
} else {
warningEl.textContent = "";
warningEl.style.visibility = "hidden";
warningEl.classList.add("hidden");
}
showView("receive");
attachCopyHandlers("view-receive");
}
function init(ctx) {
$("receive-address-block").addEventListener("click", () => {
const addr = $("receive-address-block").dataset.full;
if (addr) {
navigator.clipboard.writeText(addr);
showFlash("Copied!");
}
});
$("btn-receive-copy").addEventListener("click", () => {
const addr = $("receive-address-block").dataset.full;
if (addr) {
navigator.clipboard.writeText(addr);
showFlash("Copied!");
flashCopyFeedback($("receive-address-block"));
}
});

View File

@@ -3,10 +3,9 @@
const {
$,
showFlash,
addressDotHtml,
addressTitle,
escapeHtml,
renderAddressHtml,
attachCopyHandlers,
} = require("./helpers");
const { state, currentAddress } = require("../../shared/state");
let ctx;
@@ -114,6 +113,13 @@ function updateToValidation() {
}
}
const EXT_ICON =
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
`<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5">` +
`<path d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5"/>` +
`<path d="M7 1.5h3.5V5M7 5.5L10.5 1.5"/>` +
`</svg></span>`;
function isSpoofedToken(t) {
const upper = (t.symbol || "").toUpperCase();
if (!KNOWN_SYMBOLS.has(upper)) return false;
@@ -142,12 +148,24 @@ function renderSendTokenSelect(addr) {
function updateSendBalance() {
const addr = currentAddress();
if (!addr) return;
const dot = addressDotHtml(addr.address);
const link = `https://etherscan.io/address/${addr.address}`;
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
const title = addressTitle(addr.address, state.wallets);
$("send-from").innerHTML = renderAddressHtml(addr.address, {
title,
ensName: addr.ensName,
});
attachCopyHandlers($("send-from"));
let fromHtml = "";
if (title) {
fromHtml += `<div class="flex items-center font-bold">${dot}${escapeHtml(title)}</div>`;
if (addr.ensName) {
fromHtml += `<div>${escapeHtml(addr.ensName)}</div>`;
}
fromHtml += `<div class="break-all">${escapeHtml(addr.address)}${extLink}</div>`;
} else if (addr.ensName) {
fromHtml += `<div class="flex items-center font-bold">${dot}${escapeHtml(addr.ensName)}</div>`;
fromHtml += `<div class="break-all">${escapeHtml(addr.address)}${extLink}</div>`;
} else {
fromHtml += `<div class="flex items-center">${dot}<span class="break-all">${escapeHtml(addr.address)}</span>${extLink}</div>`;
}
$("send-from").innerHTML = fromHtml;
const token = state.selectedToken || $("send-token").value;
if (token === "ETH") {
$("send-balance").textContent =

View File

@@ -1,8 +1,6 @@
const { $, showView, showFlash, escapeHtml } = require("./helpers");
const { applyTheme } = require("../theme");
const { state, saveState, currentNetwork } = require("../../shared/state");
const { NETWORKS, SUPPORTED_CHAIN_IDS } = require("../../shared/networks");
const { onChainSwitch } = require("../../shared/chainSwitch");
const { state, saveState } = require("../../shared/state");
const { ETHEREUM_MAINNET_CHAIN_ID } = require("../../shared/constants");
const { log, debugFetch } = require("../../shared/log");
const deleteWallet = require("./deleteWallet");
@@ -126,10 +124,6 @@ function renderWalletListSettings() {
function show() {
$("settings-rpc").value = state.rpcUrl;
$("settings-blockscout").value = state.blockscoutUrl;
const networkSelect = $("settings-network");
if (networkSelect) {
networkSelect.value = state.networkId;
}
renderTrackedTokens();
renderSiteLists();
renderWalletListSettings();
@@ -173,12 +167,9 @@ function init(ctx) {
showFlash("Endpoint returned error: " + json.error.message);
return;
}
const net = currentNetwork();
if (json.result !== net.chainId) {
if (json.result !== ETHEREUM_MAINNET_CHAIN_ID) {
showFlash(
"Wrong network (expected " +
net.name +
", got chain " +
"Wrong network (expected mainnet, got chain " +
json.result +
").",
);
@@ -217,30 +208,12 @@ function init(ctx) {
showFlash("Saved.");
});
const networkSelect = $("settings-network");
if (networkSelect) {
networkSelect.addEventListener("change", async () => {
const newId = networkSelect.value;
const net = await onChainSwitch(newId);
$("settings-rpc").value = state.rpcUrl;
$("settings-blockscout").value = state.blockscoutUrl;
showFlash("Switched to " + net.name + ".");
});
}
$("settings-show-zero-balances").checked = state.showZeroBalanceTokens;
$("settings-show-zero-balances").addEventListener("change", async () => {
state.showZeroBalanceTokens = $("settings-show-zero-balances").checked;
await saveState();
});
$("settings-theme").value = state.theme;
$("settings-theme").addEventListener("change", async () => {
state.theme = $("settings-theme").value;
await saveState();
applyTheme(state.theme);
});
$("settings-hide-low-holders").checked = state.hideLowHolderTokens;
$("settings-hide-low-holders").addEventListener("change", async () => {
state.hideLowHolderTokens = $("settings-hide-low-holders").checked;
@@ -268,12 +241,6 @@ function init(ctx) {
}
});
$("settings-utc-timestamps").checked = state.utcTimestamps;
$("settings-utc-timestamps").addEventListener("change", async () => {
state.utcTimestamps = $("settings-utc-timestamps").checked;
await saveState();
});
$("btn-main-add-wallet").addEventListener("click", ctx.showAddWalletView);
$("btn-settings-add-token").addEventListener(

View File

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

View File

@@ -5,41 +5,31 @@ const {
$,
showView,
showFlash,
flashCopyFeedback,
addressDotHtml,
addressTitle,
escapeHtml,
isoDate,
timeAgo,
renderAddressHtml,
attachCopyHandlers,
copyableHtml,
etherscanLinkHtml,
} = require("./helpers");
const { state, currentNetwork } = require("../../shared/state");
const { formatEther, formatUnits } = require("ethers");
const { state } = require("../../shared/state");
const makeBlockie = require("ethereum-blockies-base64");
const { log, debugFetch } = require("../../shared/log");
const { decodeCalldata } = require("./approval");
const EXT_ICON =
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
`<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5">` +
`<path d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5"/>` +
`<path d="M7 1.5h3.5V5M7 5.5L10.5 1.5"/>` +
`</svg></span>`;
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" +
(extraClass ? " " + extraClass : "");
return `<span class="${cls}" data-copy="${escapeHtml(text)}">${escapeHtml(text)}</span>`;
}
function blockieHtml(address) {
@@ -47,16 +37,44 @@ function blockieHtml(address) {
return `<img src="${src}" width="48" height="48" style="image-rendering:pixelated;border-radius:50%;display:inline-block">`;
}
function txAddressHtml(address, ensName, title) {
const blockie = blockieHtml(address);
function etherscanLinkHtml(url) {
return (
`<div class="mb-1">${blockie}</div>` +
renderAddressHtml(address, { title, ensName })
`<a href="${url}" target="_blank" rel="noopener" ` +
`class="inline-flex items-center"` +
`>${EXT_ICON}</a>`
);
}
function txAddressHtml(address, ensName, title) {
const blockie = blockieHtml(address);
const dot = addressDotHtml(address);
const link = `https://etherscan.io/address/${address}`;
const extLink = etherscanLinkHtml(link);
let html = `<div class="mb-1">${blockie}</div>`;
if (title) {
html += `<div class="font-bold">${escapeHtml(title)}</div>`;
}
if (ensName) {
html +=
`<div class="flex items-center">${dot}` +
copyableHtml(ensName, "") +
`</div>` +
`<div class="flex items-center">${dot}` +
copyableHtml(address, "break-all") +
extLink +
`</div>`;
} else {
html +=
`<div class="flex items-center">${dot}` +
copyableHtml(address, "break-all") +
extLink +
`</div>`;
}
return html;
}
function txHashHtml(hash) {
const link = `${currentNetwork().explorerUrl}/tx/${hash}`;
const link = `https://etherscan.io/tx/${hash}`;
const extLink = etherscanLinkHtml(link);
return copyableHtml(hash, "break-all") + extLink;
}
@@ -80,7 +98,6 @@ function show(tx) {
direction: tx.direction || null,
isContractCall: tx.isContractCall || false,
method: tx.method || null,
contractAddress: tx.contractAddress || null,
},
};
render();
@@ -117,162 +134,48 @@ function render() {
nativeEl.parentElement.classList.add("hidden");
}
// Always show transaction type as the first field
// Show type label for contract interactions (Swap, Execute, etc.)
const typeSection = $("tx-detail-type-section");
const typeEl = $("tx-detail-type");
const headingEl = $("tx-detail-heading");
if (typeSection && typeEl) {
typeEl.textContent = getTransactionType(tx);
typeSection.classList.remove("hidden");
if (tx.direction === "contract" && tx.directionLabel) {
if (typeSection) {
typeEl.textContent = tx.directionLabel;
typeSection.classList.remove("hidden");
}
} else {
if (typeSection) typeSection.classList.add("hidden");
}
if (headingEl) headingEl.textContent = "Transaction";
// Token contract address (for ERC-20 transfers)
const tokenContractSection = $("tx-detail-token-contract-section");
const tokenContractEl = $("tx-detail-token-contract");
if (tokenContractSection && tokenContractEl) {
if (tx.contractAddress) {
const dot = addressDotHtml(tx.contractAddress);
const link = `${currentNetwork().explorerUrl}/token/${tx.contractAddress}`;
tokenContractEl.innerHTML =
`<div class="flex items-center">${dot}` +
copyableHtml(tx.contractAddress, "break-all") +
etherscanLinkHtml(link) +
`</div>`;
tokenContractSection.classList.remove("hidden");
} else {
tokenContractSection.classList.add("hidden");
}
}
// Hide calldata and raw data sections; always fetch full tx details
// Hide calldata and raw data sections; re-fetch if this is a contract call
const calldataSection = $("tx-detail-calldata-section");
if (calldataSection) calldataSection.classList.add("hidden");
const rawDataSection = $("tx-detail-rawdata-section");
if (rawDataSection) rawDataSection.classList.add("hidden");
// 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");
if (tx.isContractCall || tx.direction === "contract") {
loadCalldata(tx.hash, tx.to);
}
loadFullTxDetails(tx.hash, tx.to, tx.isContractCall);
const isoStr = isoDate(tx.timestamp);
$("tx-detail-time").innerHTML =
copyableHtml(isoStr) + " (" + escapeHtml(timeAgo(tx.timestamp)) + ")";
$("tx-detail-status").textContent = tx.isError ? "Failed" : "Success";
showView("transaction");
attachCopyHandlers("view-transaction");
}
function showDetailField(sectionId, contentId, value) {
const section = $(sectionId);
const el = $(contentId);
if (!section || !el) return;
el.innerHTML = copyableHtml(value, "");
section.classList.remove("hidden");
}
function populateOnChainDetails(txData) {
// Block number
if (txData.block_number != null) {
const blockLink = `${currentNetwork().explorerUrl}/block/${txData.block_number}`;
const blockSection = $("tx-detail-block-section");
const blockEl = $("tx-detail-block");
if (blockSection && blockEl) {
blockEl.innerHTML =
copyableHtml(String(txData.block_number), "") +
etherscanLinkHtml(blockLink);
blockSection.classList.remove("hidden");
}
}
// Nonce
if (txData.nonce != null) {
showDetailField(
"tx-detail-nonce-section",
"tx-detail-nonce",
String(txData.nonce),
);
}
// Transaction fee
const feeWei = txData.fee?.value || txData.tx_fee;
if (feeWei) {
const feeEth = formatEther(String(feeWei));
showDetailField(
"tx-detail-fee-section",
"tx-detail-fee",
feeEth + " ETH",
);
}
// Gas price
const gasPrice = txData.gas_price;
if (gasPrice) {
const gwei = formatUnits(String(gasPrice), "gwei");
showDetailField(
"tx-detail-gasprice-section",
"tx-detail-gasprice",
gwei + " Gwei",
);
}
// Gas used
const gasUsed = txData.gas_used;
if (gasUsed) {
showDetailField(
"tx-detail-gasused-section",
"tx-detail-gasused",
String(gasUsed),
);
}
// Show the network details wrapper if any child section is visible
const networkWrapper = $("tx-detail-network-section");
if (networkWrapper) {
const hasVisible = [
"tx-detail-nonce-section",
"tx-detail-fee-section",
"tx-detail-gasprice-section",
"tx-detail-gasused-section",
].some((id) => {
const el = $(id);
return el && !el.classList.contains("hidden");
});
if (hasVisible) networkWrapper.classList.remove("hidden");
}
// Bind copy handlers for newly added elements
for (const id of [
"tx-detail-block-section",
"tx-detail-nonce-section",
"tx-detail-fee-section",
"tx-detail-gasprice-section",
"tx-detail-gasused-section",
]) {
const section = $(id);
if (!section) continue;
section.querySelectorAll("[data-copy]").forEach((el) => {
document
.getElementById("view-transaction")
.querySelectorAll("[data-copy]")
.forEach((el) => {
el.onclick = () => {
navigator.clipboard.writeText(el.dataset.copy);
showFlash("Copied!");
flashCopyFeedback(el);
};
});
}
}
async function loadFullTxDetails(txHash, toAddress, isContractCall) {
async function loadCalldata(txHash, toAddress) {
const section = $("tx-detail-calldata-section");
const actionEl = $("tx-detail-calldata-action");
const detailsEl = $("tx-detail-calldata-details");
@@ -287,10 +190,6 @@ async function loadFullTxDetails(txHash, toAddress, isContractCall) {
);
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;
@@ -306,14 +205,19 @@ async function loadFullTxDetails(txHash, toAddress, isContractCall) {
detailsHtml += `<div class="mb-2">`;
detailsHtml += `<div class="text-muted">${escapeHtml(d.label)}</div>`;
if (d.address && d.isToken) {
// Token entry: show symbol on its own line, then address via shared renderer
// Token entry: show symbol on its own line, then dot + address + Etherscan link
const dot = addressDotHtml(d.address);
const tokenSymbol = d.value.match(/^(\S+)\s*\(/)?.[1];
if (tokenSymbol) {
detailsHtml += `<div class="font-bold">${escapeHtml(tokenSymbol)}</div>`;
}
detailsHtml += renderAddressHtml(d.address);
const etherscanUrl = `https://etherscan.io/token/${d.address}`;
detailsHtml += `<div class="flex items-center">${dot}${copyableHtml(d.address, "break-all")}${etherscanLinkHtml(etherscanUrl)}</div>`;
} else if (d.address) {
detailsHtml += renderAddressHtml(d.address);
// Protocol/contract entry: show name + Etherscan link
const dot = addressDotHtml(d.address);
const etherscanUrl = `https://etherscan.io/address/${d.address}`;
detailsHtml += `<div class="flex items-center">${dot}${copyableHtml(d.value, "break-all")}${etherscanLinkHtml(etherscanUrl)}</div>`;
} else {
detailsHtml += `<div class="font-bold">${escapeHtml(d.value)}</div>`;
}
@@ -340,7 +244,12 @@ async function loadFullTxDetails(txHash, toAddress, isContractCall) {
// Bind copy handlers for new elements (including raw data now outside section)
const copyTargets = [section, rawSection].filter(Boolean);
for (const container of copyTargets) {
attachCopyHandlers(container);
container.querySelectorAll("[data-copy]").forEach((el) => {
el.onclick = () => {
navigator.clipboard.writeText(el.dataset.copy);
showFlash("Copied!");
};
});
}
} catch (e) {
log.errorf("loadCalldata failed:", e.message);

View File

@@ -3,18 +3,23 @@
const {
$,
showView,
showFlash,
addressDotHtml,
addressTitle,
escapeHtml,
renderAddressHtml,
attachCopyHandlers,
copyableHtml,
etherscanLinkHtml,
} = require("./helpers");
const { TOKEN_BY_ADDRESS } = require("../../shared/tokenList");
const { state, saveState, currentNetwork } = require("../../shared/state");
const { state, saveState } = require("../../shared/state");
const { getProvider } = require("../../shared/balances");
const { log } = require("../../shared/log");
const EXT_ICON =
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
`<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5">` +
`<path d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5"/>` +
`<path d="M7 1.5h3.5V5M7 5.5L10.5 1.5"/>` +
`</svg></span>`;
let ctx;
let elapsedTimer = null;
let pollTimer = null;
@@ -31,19 +36,49 @@ function clearTimers() {
}
function toAddressHtml(address) {
const dot = addressDotHtml(address);
const link = `https://etherscan.io/address/${address}`;
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
const title = addressTitle(address, state.wallets);
return renderAddressHtml(address, { title });
if (title) {
return (
`<div class="flex items-center font-bold">${dot}${escapeHtml(title)}</div>` +
`<div class="break-all underline decoration-dashed cursor-pointer" data-copy="${escapeHtml(address)}">${escapeHtml(address)}</div>` +
extLink
);
}
return `<div class="flex items-center">${dot}<span class="break-all underline decoration-dashed cursor-pointer" data-copy="${escapeHtml(address)}">${escapeHtml(address)}</span>${extLink}</div>`;
}
function txHashHtml(hash) {
const link = `${currentNetwork().explorerUrl}/tx/${hash}`;
return copyableHtml(hash, "break-all") + etherscanLinkHtml(link);
const link = `https://etherscan.io/tx/${hash}`;
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
return (
`<span class="underline decoration-dashed cursor-pointer break-all" data-copy="${escapeHtml(hash)}">${escapeHtml(hash)}</span>` +
extLink
);
}
function blockNumberHtml(blockNumber) {
const num = String(blockNumber);
const link = `${currentNetwork().explorerUrl}/block/${num}`;
return copyableHtml(num) + etherscanLinkHtml(link);
const link = `https://etherscan.io/block/${num}`;
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
return (
`<span class="underline decoration-dashed cursor-pointer" data-copy="${escapeHtml(num)}">${escapeHtml(num)}</span>` +
extLink
);
}
function attachCopyHandlers(viewId) {
document
.getElementById(viewId)
.querySelectorAll("[data-copy]")
.forEach((el) => {
el.onclick = () => {
navigator.clipboard.writeText(el.dataset.copy);
showFlash("Copied!");
};
});
}
function showWait(txInfo, txHash) {
@@ -110,7 +145,7 @@ function tokenLabel(address) {
}
function etherscanTokenLink(address) {
return `${currentNetwork().explorerUrl}/token/${address}`;
return `https://etherscan.io/token/${address}`;
}
function decodedDetailsHtml(decoded) {

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,6 @@ const DEBUG_MNEMONIC =
"cube evolve unfold result inch risk jealous skill hotel bulb night wreck";
const ETHEREUM_MAINNET_CHAIN_ID = "0x1";
const ETHEREUM_SEPOLIA_CHAIN_ID = "0xaa36a7";
const DEFAULT_RPC_URL = "https://ethereum-rpc.publicnode.com";
@@ -21,28 +20,12 @@ const ERC20_ABI = [
"function approve(address spender, uint256 amount) returns (bool)",
];
// Known null/burn addresses that permanently destroy funds.
const BURN_ADDRESSES = new Set([
"0x0000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000001",
"0x000000000000000000000000000000000000dead",
"0xdead000000000000000000000000000000000000",
"0x00000000000000000000000000000000deadbeef",
]);
function isBurnAddress(address) {
return BURN_ADDRESSES.has(address.toLowerCase());
}
module.exports = {
DEBUG,
DEBUG_MNEMONIC,
ETHEREUM_MAINNET_CHAIN_ID,
ETHEREUM_SEPOLIA_CHAIN_ID,
DEFAULT_RPC_URL,
DEFAULT_BLOCKSCOUT_URL,
BIP44_ETH_PATH,
ERC20_ABI,
BURN_ADDRESSES,
isBurnAddress,
};

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@@ -153,38 +153,24 @@ 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. 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.
// 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.
for (const tt of ttJson.items || []) {
const parsed = parseTokenTransfer(tt, addrLower);
const existing = txsByHash.get(parsed.hash);
if (existing && existing.direction === "contract") {
// 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;
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);
}
// Non-contract token transfers get their own entries.
// Use composite key so multiple token transfers per tx are kept.
const ttKey = parsed.hash + ":" + (parsed.contractAddress || "");
txsByHash.set(ttKey, parsed);
}

View File

@@ -359,12 +359,9 @@ 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;
// 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;
if (!minOutput) minOutput = s.amountOutMin;
}
}
@@ -372,9 +369,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;
outputToken = s.tokenOut;
minOutput = s.amountOutMin;
if (!minOutput) minOutput = s.amountOutMin;
}
}
@@ -391,11 +388,12 @@ 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;
// Always update output: last swap step wins
if (v4.tokenOut) outputToken = v4.tokenOut;
if (v4.amountOutMin) minOutput = v4.amountOutMin;
if (!minOutput && v4.amountOutMin)
minOutput = v4.amountOutMin;
}
}

View File

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

View File

@@ -1,205 +0,0 @@
// 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);
});
});
});