From e53420f2e2b1d634e9fb15989f44e8e77f57bb32 Mon Sep 17 00:00:00 2001 From: clawbot Date: Sun, 1 Mar 2026 20:11:22 +0100 Subject: [PATCH] feat: add Sepolia testnet support (#137) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Adds Sepolia testnet support to AutistMask. ### Changes - **New `src/shared/networks.js`** — centralized network definitions (mainnet + Sepolia) with chain IDs, default RPC/Blockscout endpoints, and block explorer URLs - **State management** — `networkId` added to persisted state; defaults to mainnet for backward compatibility - **Settings UI** — network selector dropdown lets users switch between Ethereum Mainnet and Sepolia Testnet - **Dynamic explorer links** — all hardcoded `etherscan.io` URLs replaced with dynamic links from the current network config (`sepolia.etherscan.io` for Sepolia) - **Background service** — `wallet_switchEthereumChain` now accepts both mainnet (0x1) and Sepolia (0xaa36a7); broadcasts `chainChanged` to connected dApps - **Inpage provider** — fetches chain ID on init and updates dynamically via `chainChanged` events (no more hardcoded `0x1`) - **Blockscout API** — uses `eth-sepolia.blockscout.com/api/v2` for Sepolia - **Etherscan labels** — phishing/scam checks use the correct explorer per network - **Price fetching** — skipped on testnets (testnet tokens have no real market value) - **RPC validation** — checks against the selected network's chain ID, not hardcoded mainnet - **ethers provider** — `getProvider()` uses the correct ethers `Network` for Sepolia ### API Endpoints Verified | Service | Mainnet | Sepolia | |---------|---------|--------| | Etherscan | etherscan.io | sepolia.etherscan.io | | Blockscout | eth.blockscout.com/api/v2 | eth-sepolia.blockscout.com/api/v2 | | RPC | ethereum-rpc.publicnode.com | ethereum-sepolia-rpc.publicnode.com | | CoinDesk (prices) | ✅ | N/A (skipped on testnet) | closes #110 Reviewed-on: https://git.eeqj.de/sneak/AutistMask/pulls/137 THIS WAS ONESHOTTED USING OPUS 4. WTAF Co-authored-by: clawbot Co-committed-by: clawbot --- src/background/index.js | 60 +++++++++++++++++++++++----- src/content/inpage.js | 36 +++++++++++++++-- src/popup/index.html | 18 +++++++++ src/popup/views/addressDetail.js | 9 ++++- src/popup/views/addressToken.js | 13 ++++-- src/popup/views/approval.js | 6 +-- src/popup/views/confirmTx.js | 6 +-- src/popup/views/home.js | 9 ++++- src/popup/views/receive.js | 9 +++-- src/popup/views/send.js | 4 +- src/popup/views/settings.js | 31 ++++++++++++-- src/popup/views/transactionDetail.js | 14 +++---- src/popup/views/txStatus.js | 10 ++--- src/shared/balances.js | 13 ++++-- src/shared/constants.js | 2 + src/shared/etherscanLabels.js | 11 +++-- src/shared/networks.js | 57 ++++++++++++++++++++++++++ src/shared/prices.js | 3 ++ src/shared/state.js | 17 +++++++- 19 files changed, 272 insertions(+), 56 deletions(-) create mode 100644 src/shared/networks.js diff --git a/src/background/index.js b/src/background/index.js index 3508e28..cb925f3 100644 --- a/src/background/index.js +++ b/src/background/index.js @@ -2,12 +2,15 @@ // Handles EIP-1193 RPC requests from content scripts and proxies // non-sensitive calls to the configured Ethereum JSON-RPC endpoint. -const { - ETHEREUM_MAINNET_CHAIN_ID, - DEFAULT_RPC_URL, -} = require("../shared/constants"); +const { DEFAULT_RPC_URL } = require("../shared/constants"); +const { SUPPORTED_CHAIN_IDS, networkByChainId } = require("../shared/networks"); const { getBytes } = require("ethers"); -const { state, loadState, saveState } = require("../shared/state"); +const { + state, + loadState, + saveState, + currentNetwork, +} = require("../shared/state"); const { refreshBalances, getProvider } = require("../shared/balances"); const { debugFetch } = require("../shared/log"); const { decryptWithPassword } = require("../shared/vault"); @@ -329,31 +332,47 @@ async function handleRpc(method, params, origin) { } if (method === "eth_chainId") { - return { result: ETHEREUM_MAINNET_CHAIN_ID }; + return { result: currentNetwork().chainId }; } if (method === "net_version") { - return { result: "1" }; + return { result: currentNetwork().networkVersion }; } if (method === "wallet_switchEthereumChain") { const chainId = params?.[0]?.chainId; - if (chainId === ETHEREUM_MAINNET_CHAIN_ID) { + if (chainId === currentNetwork().chainId) { + return { result: null }; + } + if (SUPPORTED_CHAIN_IDS.has(chainId)) { + // Switch to the requested network + const target = networkByChainId(chainId); + state.networkId = target.id; + state.rpcUrl = target.defaultRpcUrl; + state.blockscoutUrl = target.defaultBlockscoutUrl; + await saveState(); + broadcastChainChanged(target.chainId); return { result: null }; } return { error: { code: 4902, - message: "AutistMask only supports Ethereum mainnet.", + message: + "AutistMask supports Ethereum Mainnet and Sepolia Testnet only.", }, }; } if (method === "wallet_addEthereumChain") { + const chainId = params?.[0]?.chainId; + if (SUPPORTED_CHAIN_IDS.has(chainId)) { + return { result: null }; + } return { error: { code: 4902, - message: "AutistMask only supports Ethereum mainnet.", + message: + "AutistMask supports Ethereum Mainnet and Sepolia Testnet only.", }, }; } @@ -499,6 +518,27 @@ async function handleRpc(method, params, origin) { return { error: { message: "Unsupported method: " + method } }; } +// Broadcast chainChanged to all tabs when the network is switched. +function broadcastChainChanged(chainId) { + tabsApi.query({}, (tabs) => { + for (const tab of tabs) { + tabsApi.sendMessage( + tab.id, + { + type: "AUTISTMASK_EVENT", + eventName: "chainChanged", + data: chainId, + }, + () => { + if (runtime.lastError) { + // expected for tabs without our content script + } + }, + ); + } + }); +} + // Broadcast accountsChanged to all tabs, respecting per-address permissions async function broadcastAccountsChanged() { // Clear non-remembered approvals on address switch diff --git a/src/content/inpage.js b/src/content/inpage.js index 9a95012..8147f49 100644 --- a/src/content/inpage.js +++ b/src/content/inpage.js @@ -2,7 +2,10 @@ // Creates window.ethereum (EIP-1193 provider) and announces via EIP-6963. (function () { - const CHAIN_ID = "0x1"; // Ethereum mainnet + // Defaults to mainnet; updated dynamically via eth_chainId on init and + // chainChanged events from the extension. + let currentChainId = "0x1"; + let currentNetworkVersion = "1"; const listeners = {}; let nextId = 1; @@ -28,6 +31,12 @@ if (event.source !== window) return; if (event.data?.type !== "AUTISTMASK_EVENT") return; const { eventName, data } = event.data; + if (eventName === "chainChanged") { + currentChainId = data; + currentNetworkVersion = String(parseInt(data, 16)); + provider.chainId = currentChainId; + provider.networkVersion = currentNetworkVersion; + } emit(eventName, data); }); @@ -57,8 +66,8 @@ const provider = { isAutistMask: true, isMetaMask: true, // compatibility — many dApps check this - chainId: CHAIN_ID, - networkVersion: "1", + chainId: currentChainId, + networkVersion: currentNetworkVersion, selectedAddress: null, async request(args) { @@ -75,6 +84,12 @@ ? result[0] : null; } + if (args.method === "eth_chainId" && result) { + currentChainId = result; + currentNetworkVersion = String(parseInt(result, 16)); + provider.chainId = currentChainId; + provider.networkVersion = currentNetworkVersion; + } return result; }, @@ -189,4 +204,19 @@ window.addEventListener("eip6963:requestProvider", announceProvider); announceProvider(); + + // Fetch the current chain ID from the extension on load so the provider + // reflects the selected network immediately (covers Sepolia etc.). + sendRequest({ method: "eth_chainId", params: [] }) + .then((chainId) => { + if (chainId) { + currentChainId = chainId; + currentNetworkVersion = String(parseInt(chainId, 16)); + provider.chainId = currentChainId; + provider.networkVersion = currentNetworkVersion; + } + }) + .catch(() => { + // Best-effort — keep defaults. + }); })(); diff --git a/src/popup/index.html b/src/popup/index.html index 821571d..90fb615 100644 --- a/src/popup/index.html +++ b/src/popup/index.html @@ -882,6 +882,24 @@ +
+

Network

+

+ Select the Ethereum network. Switching networks will + update the RPC and Blockscout endpoints to their + defaults. +

+
+ +
+
+

Ethereum RPC

diff --git a/src/popup/views/addressDetail.js b/src/popup/views/addressDetail.js index 3ef4951..169ab10 100644 --- a/src/popup/views/addressDetail.js +++ b/src/popup/views/addressDetail.js @@ -9,7 +9,12 @@ const { escapeHtml, truncateMiddle, } = require("./helpers"); -const { state, currentAddress, saveState } = require("../../shared/state"); +const { + state, + currentAddress, + saveState, + currentNetwork, +} = require("../../shared/state"); const { formatUsd, getAddressValueUsd } = require("../../shared/prices"); const { fetchRecentTransactions, @@ -36,7 +41,7 @@ const EXT_ICON = ``; function etherscanAddressLink(address) { - return `https://etherscan.io/address/${address}`; + return `${currentNetwork().explorerUrl}/address/${address}`; } function show() { diff --git a/src/popup/views/addressToken.js b/src/popup/views/addressToken.js index 78b03c9..8b31b60 100644 --- a/src/popup/views/addressToken.js +++ b/src/popup/views/addressToken.js @@ -12,7 +12,12 @@ const { truncateMiddle, balanceLine, } = require("./helpers"); -const { state, currentAddress, saveState } = require("../../shared/state"); +const { + state, + currentAddress, + saveState, + currentNetwork, +} = require("../../shared/state"); const { TOKEN_BY_ADDRESS, resolveSymbol } = require("../../shared/tokenList"); const { formatUsd, @@ -42,7 +47,7 @@ const EXT_ICON = ``; function etherscanAddressLink(address) { - return `https://etherscan.io/address/${address}`; + return `${currentNetwork().explorerUrl}/address/${address}`; } function isoDate(timestamp) { @@ -194,7 +199,7 @@ function show() { : null; const tokenHolders = tb && tb.holders != null ? tb.holders : null; const dot = addressDotHtml(tokenId); - const tokenLink = `https://etherscan.io/token/${escapeHtml(tokenId)}`; + const tokenLink = `${currentNetwork().explorerUrl}/token/${escapeHtml(tokenId)}`; const projectUrl = knownToken && knownToken.url ? knownToken.url : null; let infoHtml = `

Contract Address
`; infoHtml += @@ -381,7 +386,7 @@ function init(_ctx) { let staticHtml = `
${escapeHtml(currentSymbol)}
`; if (tokenId !== "ETH") { const dot = addressDotHtml(tokenId); - const link = `https://etherscan.io/token/${tokenId}`; + const link = `${currentNetwork().explorerUrl}/token/${tokenId}`; const extLink = `${EXT_ICON}`; staticHtml += `
${dot}` + diff --git a/src/popup/views/approval.js b/src/popup/views/approval.js index 10de99b..fb95e1b 100644 --- a/src/popup/views/approval.js +++ b/src/popup/views/approval.js @@ -7,7 +7,7 @@ const { showError, hideError, } = require("./helpers"); -const { state, saveState } = require("../../shared/state"); +const { state, saveState, currentNetwork } = require("../../shared/state"); const { formatEther, formatUnits, Interface, toUtf8String } = require("ethers"); const { ERC20_ABI } = require("../../shared/constants"); const { TOKEN_BY_ADDRESS } = require("../../shared/tokenList"); @@ -27,7 +27,7 @@ const erc20Iface = new Interface(ERC20_ABI); function approvalAddressHtml(address) { const dot = addressDotHtml(address); - const link = `https://etherscan.io/address/${address}`; + const link = `${currentNetwork().explorerUrl}/address/${address}`; const extLink = `${EXT_ICON}`; const title = addressTitle(address, state.wallets); let html = ""; @@ -53,7 +53,7 @@ function tokenLabel(address) { } function etherscanTokenLink(address) { - return `https://etherscan.io/token/${address}`; + return `${currentNetwork().explorerUrl}/token/${address}`; } // Try to decode calldata using known ABIs. diff --git a/src/popup/views/confirmTx.js b/src/popup/views/confirmTx.js index 42d984d..be61e71 100644 --- a/src/popup/views/confirmTx.js +++ b/src/popup/views/confirmTx.js @@ -20,7 +20,7 @@ const { addressDotHtml, escapeHtml, } = require("./helpers"); -const { state } = require("../../shared/state"); +const { state, currentNetwork } = require("../../shared/state"); const { getSignerForAddress } = require("../../shared/wallet"); const { decryptWithPassword } = require("../../shared/vault"); const { formatUsd, getPrice } = require("../../shared/prices"); @@ -51,11 +51,11 @@ function restore() { } function etherscanTokenLink(address) { - return `https://etherscan.io/token/${address}`; + return `${currentNetwork().explorerUrl}/token/${address}`; } function etherscanAddressLink(address) { - return `https://etherscan.io/address/${address}`; + return `${currentNetwork().explorerUrl}/address/${address}`; } function blockieHtml(address) { diff --git a/src/popup/views/home.js b/src/popup/views/home.js index d6c2f73..68171d3 100644 --- a/src/popup/views/home.js +++ b/src/popup/views/home.js @@ -11,7 +11,12 @@ const { escapeHtml, truncateMiddle, } = require("./helpers"); -const { state, saveState, currentAddress } = require("../../shared/state"); +const { + state, + saveState, + currentAddress, + currentNetwork, +} = require("../../shared/state"); const { updateSendBalance, renderSendTokenSelect, @@ -82,7 +87,7 @@ function renderActiveAddress() { if (state.activeAddress) { const addr = state.activeAddress; const dot = addressDotHtml(addr); - const link = `https://etherscan.io/address/${addr}`; + const link = `${currentNetwork().explorerUrl}/address/${addr}`; el.innerHTML = `${dot}${escapeHtml(addr)}` + `${EXT_ICON}`; diff --git a/src/popup/views/receive.js b/src/popup/views/receive.js index 17103e3..59bac3e 100644 --- a/src/popup/views/receive.js +++ b/src/popup/views/receive.js @@ -6,7 +6,7 @@ const { formatAddressHtml, addressTitle, } = require("./helpers"); -const { state, currentAddress } = require("../../shared/state"); +const { state, currentAddress, currentNetwork } = require("../../shared/state"); const QRCode = require("qrcode"); const EXT_ICON = @@ -25,7 +25,8 @@ function show() { ? formatAddressHtml(address, ensName, null, title) : ""; $("receive-address-block").dataset.full = address; - const link = address ? `https://etherscan.io/address/${address}` : ""; + const net = currentNetwork(); + const link = address ? `${net.explorerUrl}/address/${address}` : ""; $("receive-etherscan-link").innerHTML = link ? `${EXT_ICON}` : ""; @@ -52,7 +53,9 @@ function show() { warningEl.textContent = "This is an ERC-20 token. Only send " + symbol + - " on the Ethereum network to this address. Sending tokens on other networks will result in permanent loss."; + " on " + + currentNetwork().name + + " to this address. Sending tokens on other networks will result in permanent loss."; warningEl.style.visibility = "visible"; } else { warningEl.textContent = ""; diff --git a/src/popup/views/send.js b/src/popup/views/send.js index 6778405..ada1370 100644 --- a/src/popup/views/send.js +++ b/src/popup/views/send.js @@ -7,7 +7,7 @@ const { addressTitle, escapeHtml, } = require("./helpers"); -const { state, currentAddress } = require("../../shared/state"); +const { state, currentAddress, currentNetwork } = require("../../shared/state"); let ctx; const { getProvider } = require("../../shared/balances"); const { KNOWN_SYMBOLS, resolveSymbol } = require("../../shared/tokenList"); @@ -149,7 +149,7 @@ function updateSendBalance() { const addr = currentAddress(); if (!addr) return; const dot = addressDotHtml(addr.address); - const link = `https://etherscan.io/address/${addr.address}`; + const link = `${currentNetwork().explorerUrl}/address/${addr.address}`; const extLink = `${EXT_ICON}`; const title = addressTitle(addr.address, state.wallets); let fromHtml = ""; diff --git a/src/popup/views/settings.js b/src/popup/views/settings.js index 9263c46..9bae55a 100644 --- a/src/popup/views/settings.js +++ b/src/popup/views/settings.js @@ -1,7 +1,7 @@ const { $, showView, showFlash, escapeHtml } = require("./helpers"); const { applyTheme } = require("../theme"); -const { state, saveState } = require("../../shared/state"); -const { ETHEREUM_MAINNET_CHAIN_ID } = require("../../shared/constants"); +const { state, saveState, currentNetwork } = require("../../shared/state"); +const { NETWORKS, SUPPORTED_CHAIN_IDS } = require("../../shared/networks"); const { log, debugFetch } = require("../../shared/log"); const deleteWallet = require("./deleteWallet"); @@ -125,6 +125,10 @@ function renderWalletListSettings() { function show() { $("settings-rpc").value = state.rpcUrl; $("settings-blockscout").value = state.blockscoutUrl; + const networkSelect = $("settings-network"); + if (networkSelect) { + networkSelect.value = state.networkId; + } renderTrackedTokens(); renderSiteLists(); renderWalletListSettings(); @@ -168,9 +172,12 @@ function init(ctx) { showFlash("Endpoint returned error: " + json.error.message); return; } - if (json.result !== ETHEREUM_MAINNET_CHAIN_ID) { + const net = currentNetwork(); + if (json.result !== net.chainId) { showFlash( - "Wrong network (expected mainnet, got chain " + + "Wrong network (expected " + + net.name + + ", got chain " + json.result + ").", ); @@ -209,6 +216,22 @@ function init(ctx) { showFlash("Saved."); }); + const networkSelect = $("settings-network"); + if (networkSelect) { + networkSelect.addEventListener("change", async () => { + const newId = networkSelect.value; + const net = NETWORKS[newId]; + if (!net) return; + state.networkId = newId; + state.rpcUrl = net.defaultRpcUrl; + state.blockscoutUrl = net.defaultBlockscoutUrl; + $("settings-rpc").value = state.rpcUrl; + $("settings-blockscout").value = state.blockscoutUrl; + await saveState(); + 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; diff --git a/src/popup/views/transactionDetail.js b/src/popup/views/transactionDetail.js index 7af9d50..306a9ac 100644 --- a/src/popup/views/transactionDetail.js +++ b/src/popup/views/transactionDetail.js @@ -12,7 +12,7 @@ const { isoDate, timeAgo, } = require("./helpers"); -const { state } = require("../../shared/state"); +const { state, currentNetwork } = require("../../shared/state"); const { formatEther, formatUnits } = require("ethers"); const makeBlockie = require("ethereum-blockies-base64"); const { log, debugFetch } = require("../../shared/log"); @@ -69,7 +69,7 @@ function etherscanLinkHtml(url) { function txAddressHtml(address, ensName, title) { const blockie = blockieHtml(address); const dot = addressDotHtml(address); - const link = `https://etherscan.io/address/${address}`; + const link = `${currentNetwork().explorerUrl}/address/${address}`; const extLink = etherscanLinkHtml(link); let html = `
${blockie}
`; if (title) { @@ -95,7 +95,7 @@ function txAddressHtml(address, ensName, title) { } function txHashHtml(hash) { - const link = `https://etherscan.io/tx/${hash}`; + const link = `${currentNetwork().explorerUrl}/tx/${hash}`; const extLink = etherscanLinkHtml(link); return copyableHtml(hash, "break-all") + extLink; } @@ -172,7 +172,7 @@ function render() { if (tokenContractSection && tokenContractEl) { if (tx.contractAddress) { const dot = addressDotHtml(tx.contractAddress); - const link = `https://etherscan.io/token/${tx.contractAddress}`; + const link = `${currentNetwork().explorerUrl}/token/${tx.contractAddress}`; tokenContractEl.innerHTML = `
${dot}` + copyableHtml(tx.contractAddress, "break-all") + @@ -234,7 +234,7 @@ function showDetailField(sectionId, contentId, value) { function populateOnChainDetails(txData) { // Block number if (txData.block_number != null) { - const blockLink = `https://etherscan.io/block/${txData.block_number}`; + const blockLink = `${currentNetwork().explorerUrl}/block/${txData.block_number}`; const blockSection = $("tx-detail-block-section"); const blockEl = $("tx-detail-block"); if (blockSection && blockEl) { @@ -361,12 +361,12 @@ async function loadFullTxDetails(txHash, toAddress, isContractCall) { if (tokenSymbol) { detailsHtml += `
${escapeHtml(tokenSymbol)}
`; } - const etherscanUrl = `https://etherscan.io/token/${d.address}`; + const etherscanUrl = `${currentNetwork().explorerUrl}/token/${d.address}`; detailsHtml += `
${dot}${copyableHtml(d.address, "break-all")}${etherscanLinkHtml(etherscanUrl)}
`; } else if (d.address) { // Protocol/contract entry: show name + Etherscan link const dot = addressDotHtml(d.address); - const etherscanUrl = `https://etherscan.io/address/${d.address}`; + const etherscanUrl = `${currentNetwork().explorerUrl}/address/${d.address}`; detailsHtml += `
${dot}${copyableHtml(d.value, "break-all")}${etherscanLinkHtml(etherscanUrl)}
`; } else { detailsHtml += `
${escapeHtml(d.value)}
`; diff --git a/src/popup/views/txStatus.js b/src/popup/views/txStatus.js index a406dca..ae155fc 100644 --- a/src/popup/views/txStatus.js +++ b/src/popup/views/txStatus.js @@ -10,7 +10,7 @@ const { escapeHtml, } = require("./helpers"); const { TOKEN_BY_ADDRESS } = require("../../shared/tokenList"); -const { state, saveState } = require("../../shared/state"); +const { state, saveState, currentNetwork } = require("../../shared/state"); const { getProvider } = require("../../shared/balances"); const { log } = require("../../shared/log"); @@ -38,7 +38,7 @@ function clearTimers() { function toAddressHtml(address) { const dot = addressDotHtml(address); - const link = `https://etherscan.io/address/${address}`; + const link = `${currentNetwork().explorerUrl}/address/${address}`; const extLink = `${EXT_ICON}`; const title = addressTitle(address, state.wallets); if (title) { @@ -52,7 +52,7 @@ function toAddressHtml(address) { } function txHashHtml(hash) { - const link = `https://etherscan.io/tx/${hash}`; + const link = `${currentNetwork().explorerUrl}/tx/${hash}`; const extLink = `${EXT_ICON}`; return ( `${escapeHtml(hash)}` + @@ -62,7 +62,7 @@ function txHashHtml(hash) { function blockNumberHtml(blockNumber) { const num = String(blockNumber); - const link = `https://etherscan.io/block/${num}`; + const link = `${currentNetwork().explorerUrl}/block/${num}`; const extLink = `${EXT_ICON}`; return ( `${escapeHtml(num)}` + @@ -147,7 +147,7 @@ function tokenLabel(address) { } function etherscanTokenLink(address) { - return `https://etherscan.io/token/${address}`; + return `${currentNetwork().explorerUrl}/token/${address}`; } function decodedDetailsHtml(decoded) { diff --git a/src/shared/balances.js b/src/shared/balances.js index 73f047b..46d5c5e 100644 --- a/src/shared/balances.js +++ b/src/shared/balances.js @@ -15,10 +15,15 @@ const { KNOWN_SYMBOLS, TOKEN_BY_ADDRESS } = require("./tokenList"); // Use a static network to skip auto-detection (which can fail and cause // "could not coalesce error" on some RPC endpoints like Cloudflare). -const mainnet = Network.from("mainnet"); - -function getProvider(rpcUrl) { - return new JsonRpcProvider(rpcUrl, mainnet, { staticNetwork: mainnet }); +// Accepts an optional networkName ("mainnet" or "sepolia") for the static +// network hint so ethers picks the right chain parameters. When omitted, +// reads the currently selected network from extension state. +function getProvider(rpcUrl, networkName) { + // Lazy require to avoid circular dependency issues at module scope. + const { currentNetwork } = require("./state"); + const name = networkName || currentNetwork().id; + const net = Network.from(name); + return new JsonRpcProvider(rpcUrl, net, { staticNetwork: net }); } function formatBalance(wei) { diff --git a/src/shared/constants.js b/src/shared/constants.js index cf9a661..1280296 100644 --- a/src/shared/constants.js +++ b/src/shared/constants.js @@ -3,6 +3,7 @@ const DEBUG_MNEMONIC = "cube evolve unfold result inch risk jealous skill hotel bulb night wreck"; const ETHEREUM_MAINNET_CHAIN_ID = "0x1"; +const ETHEREUM_SEPOLIA_CHAIN_ID = "0xaa36a7"; const DEFAULT_RPC_URL = "https://ethereum-rpc.publicnode.com"; @@ -37,6 +38,7 @@ module.exports = { DEBUG, DEBUG_MNEMONIC, ETHEREUM_MAINNET_CHAIN_ID, + ETHEREUM_SEPOLIA_CHAIN_ID, DEFAULT_RPC_URL, DEFAULT_BLOCKSCOUT_URL, BIP44_ETH_PATH, diff --git a/src/shared/etherscanLabels.js b/src/shared/etherscanLabels.js index 9c8c658..e74e884 100644 --- a/src/shared/etherscanLabels.js +++ b/src/shared/etherscanLabels.js @@ -2,8 +2,6 @@ // Extension users make the requests directly to Etherscan — no proxy needed. // This is a best-effort enrichment: network failures return null silently. -const ETHERSCAN_BASE = "https://etherscan.io/address/"; - // Patterns in the page title that indicate a flagged address. // Title format: "Fake_Phishing184810 | Address: 0x... | Etherscan" const PHISHING_LABEL_PATTERNS = [/^Fake_Phishing/i, /^Phish:/i, /^Exploiter/i]; @@ -74,12 +72,19 @@ function parseEtherscanPage(html) { * 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 { - const resp = await fetch(ETHERSCAN_BASE + address, { + // 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; diff --git a/src/shared/networks.js b/src/shared/networks.js new file mode 100644 index 0000000..4550590 --- /dev/null +++ b/src/shared/networks.js @@ -0,0 +1,57 @@ +// Network definitions for supported Ethereum networks. +// Each network specifies its chain ID, default RPC and Blockscout endpoints, +// and the block explorer base URL used for address/tx/token/block links. + +const NETWORKS = { + mainnet: { + id: "mainnet", + name: "Ethereum Mainnet", + chainId: "0x1", + networkVersion: "1", + nativeCurrency: "ETH", + defaultRpcUrl: "https://ethereum-rpc.publicnode.com", + defaultBlockscoutUrl: "https://eth.blockscout.com/api/v2", + explorerUrl: "https://etherscan.io", + isTestnet: false, + }, + sepolia: { + id: "sepolia", + name: "Sepolia Testnet", + chainId: "0xaa36a7", + networkVersion: "11155111", + nativeCurrency: "SepoliaETH", + defaultRpcUrl: "https://ethereum-sepolia-rpc.publicnode.com", + defaultBlockscoutUrl: "https://eth-sepolia.blockscout.com/api/v2", + explorerUrl: "https://sepolia.etherscan.io", + isTestnet: true, + }, +}; + +const SUPPORTED_CHAIN_IDS = new Set( + Object.values(NETWORKS).map((n) => n.chainId), +); + +function networkById(id) { + return NETWORKS[id] || NETWORKS.mainnet; +} + +function networkByChainId(chainId) { + for (const net of Object.values(NETWORKS)) { + if (net.chainId === chainId) return net; + } + return null; +} + +// Build a block explorer link for the given path type and value. +// type: "address" | "tx" | "token" | "block" +function explorerLink(network, type, value) { + return `${network.explorerUrl}/${type}/${value}`; +} + +module.exports = { + NETWORKS, + SUPPORTED_CHAIN_IDS, + networkById, + networkByChainId, + explorerLink, +}; diff --git a/src/shared/prices.js b/src/shared/prices.js index 61584ff..363f40c 100644 --- a/src/shared/prices.js +++ b/src/shared/prices.js @@ -8,6 +8,9 @@ const prices = {}; let lastFetchedAt = 0; async function refreshPrices() { + // Testnet tokens have no real market value — skip price fetching. + const { currentNetwork } = require("./state"); + if (currentNetwork().isTestnet) return; const now = Date.now(); if (now - lastFetchedAt < PRICE_CACHE_TTL) return; try { diff --git a/src/shared/state.js b/src/shared/state.js index 307a339..b39faed 100644 --- a/src/shared/state.js +++ b/src/shared/state.js @@ -1,6 +1,7 @@ // State management and extension storage persistence. const { DEFAULT_RPC_URL, DEFAULT_BLOCKSCOUT_URL } = require("./constants"); +const { networkById } = require("./networks"); const storageApi = typeof browser !== "undefined" @@ -11,6 +12,7 @@ const DEFAULT_STATE = { hasWallet: false, wallets: [], trackedTokens: [], + networkId: "mainnet", rpcUrl: DEFAULT_RPC_URL, blockscoutUrl: DEFAULT_BLOCKSCOUT_URL, lastBalanceRefresh: 0, @@ -38,11 +40,17 @@ const state = { viewData: {}, }; +// Return the network configuration for the currently selected network. +function currentNetwork() { + return networkById(state.networkId); +} + async function saveState() { const persisted = { hasWallet: state.hasWallet, wallets: state.wallets, trackedTokens: state.trackedTokens, + networkId: state.networkId, rpcUrl: state.rpcUrl, blockscoutUrl: state.blockscoutUrl, lastBalanceRefresh: state.lastBalanceRefresh, @@ -75,6 +83,7 @@ async function loadState() { state.hasWallet = saved.hasWallet; state.wallets = saved.wallets || []; state.trackedTokens = saved.trackedTokens || []; + state.networkId = saved.networkId || DEFAULT_STATE.networkId; state.rpcUrl = saved.rpcUrl || DEFAULT_STATE.rpcUrl; state.blockscoutUrl = saved.blockscoutUrl || DEFAULT_STATE.blockscoutUrl; @@ -134,4 +143,10 @@ function currentAddress() { return state.wallets[state.selectedWallet].addresses[state.selectedAddress]; } -module.exports = { state, saveState, loadState, currentAddress }; +module.exports = { + state, + saveState, + loadState, + currentAddress, + currentNetwork, +};