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 = `
`;
infoHtml +=
@@ -381,7 +386,7 @@ function init(_ctx) {
let staticHtml = `
`;
if (tokenId !== "ETH") {
const dot = addressDotHtml(tokenId);
- const link = `https://etherscan.io/token/${tokenId}`;
+ const link = `${currentNetwork().explorerUrl}/token/${tokenId}`;
const extLink = `
${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,
+};