feat: add Sepolia testnet support
All checks were successful
check / check (push) Successful in 25s

Add support for the Sepolia testnet alongside Ethereum mainnet:

- New src/shared/networks.js with network definitions (mainnet + sepolia)
  including chain IDs, default RPC/Blockscout endpoints, and explorer URLs
- State now tracks networkId; defaults to mainnet for backward compatibility
- Network selector in Settings lets users switch between mainnet and Sepolia
- Switching networks updates RPC URL, Blockscout URL, and chain ID
- All hardcoded etherscan.io URLs replaced with dynamic explorer links
  from the current network config (sepolia.etherscan.io for Sepolia)
- Background handles wallet_switchEthereumChain for both supported chains
  and broadcasts chainChanged events 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 for Sepolia
- Etherscan label/phishing checks use the correct explorer per network
- Price fetching skipped on testnets (tokens have no real value)
- RPC validation checks against the selected network's chain ID
- getProvider() uses the correct ethers Network for Sepolia

API endpoints verified:
- Etherscan: sepolia.etherscan.io
- Blockscout: eth-sepolia.blockscout.com/api/v2
- RPC: ethereum-sepolia-rpc.publicnode.com

closes #110
This commit is contained in:
user
2026-03-01 10:45:21 -08:00
parent d35bfb7d23
commit d4f2f34c89
19 changed files with 272 additions and 56 deletions

View File

@@ -2,12 +2,15 @@
// Handles EIP-1193 RPC requests from content scripts and proxies // Handles EIP-1193 RPC requests from content scripts and proxies
// non-sensitive calls to the configured Ethereum JSON-RPC endpoint. // non-sensitive calls to the configured Ethereum JSON-RPC endpoint.
const { const { DEFAULT_RPC_URL } = require("../shared/constants");
ETHEREUM_MAINNET_CHAIN_ID, const { SUPPORTED_CHAIN_IDS, networkByChainId } = require("../shared/networks");
DEFAULT_RPC_URL,
} = require("../shared/constants");
const { getBytes } = require("ethers"); 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 { refreshBalances, getProvider } = require("../shared/balances");
const { debugFetch } = require("../shared/log"); const { debugFetch } = require("../shared/log");
const { decryptWithPassword } = require("../shared/vault"); const { decryptWithPassword } = require("../shared/vault");
@@ -329,31 +332,47 @@ async function handleRpc(method, params, origin) {
} }
if (method === "eth_chainId") { if (method === "eth_chainId") {
return { result: ETHEREUM_MAINNET_CHAIN_ID }; return { result: currentNetwork().chainId };
} }
if (method === "net_version") { if (method === "net_version") {
return { result: "1" }; return { result: currentNetwork().networkVersion };
} }
if (method === "wallet_switchEthereumChain") { if (method === "wallet_switchEthereumChain") {
const chainId = params?.[0]?.chainId; 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 { result: null };
} }
return { return {
error: { error: {
code: 4902, code: 4902,
message: "AutistMask only supports Ethereum mainnet.", message:
"AutistMask supports Ethereum Mainnet and Sepolia Testnet only.",
}, },
}; };
} }
if (method === "wallet_addEthereumChain") { if (method === "wallet_addEthereumChain") {
const chainId = params?.[0]?.chainId;
if (SUPPORTED_CHAIN_IDS.has(chainId)) {
return { result: null };
}
return { return {
error: { error: {
code: 4902, 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 } }; 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 // Broadcast accountsChanged to all tabs, respecting per-address permissions
async function broadcastAccountsChanged() { async function broadcastAccountsChanged() {
// Clear non-remembered approvals on address switch // Clear non-remembered approvals on address switch

View File

@@ -2,7 +2,10 @@
// Creates window.ethereum (EIP-1193 provider) and announces via EIP-6963. // Creates window.ethereum (EIP-1193 provider) and announces via EIP-6963.
(function () { (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 = {}; const listeners = {};
let nextId = 1; let nextId = 1;
@@ -28,6 +31,12 @@
if (event.source !== window) return; if (event.source !== window) return;
if (event.data?.type !== "AUTISTMASK_EVENT") return; if (event.data?.type !== "AUTISTMASK_EVENT") return;
const { eventName, data } = event.data; const { eventName, data } = event.data;
if (eventName === "chainChanged") {
currentChainId = data;
currentNetworkVersion = String(parseInt(data, 16));
provider.chainId = currentChainId;
provider.networkVersion = currentNetworkVersion;
}
emit(eventName, data); emit(eventName, data);
}); });
@@ -57,8 +66,8 @@
const provider = { const provider = {
isAutistMask: true, isAutistMask: true,
isMetaMask: true, // compatibility — many dApps check this isMetaMask: true, // compatibility — many dApps check this
chainId: CHAIN_ID, chainId: currentChainId,
networkVersion: "1", networkVersion: currentNetworkVersion,
selectedAddress: null, selectedAddress: null,
async request(args) { async request(args) {
@@ -75,6 +84,12 @@
? result[0] ? result[0]
: null; : null;
} }
if (args.method === "eth_chainId" && result) {
currentChainId = result;
currentNetworkVersion = String(parseInt(result, 16));
provider.chainId = currentChainId;
provider.networkVersion = currentNetworkVersion;
}
return result; return result;
}, },
@@ -189,4 +204,19 @@
window.addEventListener("eip6963:requestProvider", announceProvider); window.addEventListener("eip6963:requestProvider", announceProvider);
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

@@ -882,6 +882,24 @@
</div> </div>
</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"> <div class="bg-well p-3 mx-1 mb-3">
<h3 class="font-bold mb-1">Ethereum RPC</h3> <h3 class="font-bold mb-1">Ethereum RPC</h3>
<p class="text-xs text-muted mb-1"> <p class="text-xs text-muted mb-1">

View File

@@ -9,7 +9,12 @@ const {
escapeHtml, escapeHtml,
truncateMiddle, truncateMiddle,
} = require("./helpers"); } = require("./helpers");
const { state, currentAddress, saveState } = require("../../shared/state"); const {
state,
currentAddress,
saveState,
currentNetwork,
} = require("../../shared/state");
const { formatUsd, getAddressValueUsd } = require("../../shared/prices"); const { formatUsd, getAddressValueUsd } = require("../../shared/prices");
const { const {
fetchRecentTransactions, fetchRecentTransactions,
@@ -36,7 +41,7 @@ const EXT_ICON =
`</svg></span>`; `</svg></span>`;
function etherscanAddressLink(address) { function etherscanAddressLink(address) {
return `https://etherscan.io/address/${address}`; return `${currentNetwork().explorerUrl}/address/${address}`;
} }
function show() { function show() {

View File

@@ -12,7 +12,12 @@ const {
truncateMiddle, truncateMiddle,
balanceLine, balanceLine,
} = require("./helpers"); } = 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 { TOKEN_BY_ADDRESS, resolveSymbol } = require("../../shared/tokenList");
const { const {
formatUsd, formatUsd,
@@ -42,7 +47,7 @@ const EXT_ICON =
`</svg></span>`; `</svg></span>`;
function etherscanAddressLink(address) { function etherscanAddressLink(address) {
return `https://etherscan.io/address/${address}`; return `${currentNetwork().explorerUrl}/address/${address}`;
} }
function isoDate(timestamp) { function isoDate(timestamp) {
@@ -194,7 +199,7 @@ function show() {
: null; : null;
const tokenHolders = tb && tb.holders != null ? tb.holders : null; const tokenHolders = tb && tb.holders != null ? tb.holders : null;
const dot = addressDotHtml(tokenId); 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; const projectUrl = knownToken && knownToken.url ? knownToken.url : null;
let infoHtml = `<div class="font-bold mb-2">Contract Address</div>`; let infoHtml = `<div class="font-bold mb-2">Contract Address</div>`;
infoHtml += infoHtml +=
@@ -381,7 +386,7 @@ function init(_ctx) {
let staticHtml = `<div class="font-bold">${escapeHtml(currentSymbol)}</div>`; let staticHtml = `<div class="font-bold">${escapeHtml(currentSymbol)}</div>`;
if (tokenId !== "ETH") { if (tokenId !== "ETH") {
const dot = addressDotHtml(tokenId); const dot = addressDotHtml(tokenId);
const link = `https://etherscan.io/token/${tokenId}`; const link = `${currentNetwork().explorerUrl}/token/${tokenId}`;
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`; const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
staticHtml += staticHtml +=
`<div class="flex items-center text-xs">${dot}` + `<div class="flex items-center text-xs">${dot}` +

View File

@@ -7,7 +7,7 @@ const {
showError, showError,
hideError, hideError,
} = require("./helpers"); } = require("./helpers");
const { state, saveState } = require("../../shared/state"); const { state, saveState, currentNetwork } = require("../../shared/state");
const { formatEther, formatUnits, Interface, toUtf8String } = require("ethers"); const { formatEther, formatUnits, Interface, toUtf8String } = require("ethers");
const { ERC20_ABI } = require("../../shared/constants"); const { ERC20_ABI } = require("../../shared/constants");
const { TOKEN_BY_ADDRESS } = require("../../shared/tokenList"); const { TOKEN_BY_ADDRESS } = require("../../shared/tokenList");
@@ -27,7 +27,7 @@ const erc20Iface = new Interface(ERC20_ABI);
function approvalAddressHtml(address) { function approvalAddressHtml(address) {
const dot = addressDotHtml(address); const dot = addressDotHtml(address);
const link = `https://etherscan.io/address/${address}`; const link = `${currentNetwork().explorerUrl}/address/${address}`;
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`; const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
const title = addressTitle(address, state.wallets); const title = addressTitle(address, state.wallets);
let html = ""; let html = "";
@@ -53,7 +53,7 @@ function tokenLabel(address) {
} }
function etherscanTokenLink(address) { function etherscanTokenLink(address) {
return `https://etherscan.io/token/${address}`; return `${currentNetwork().explorerUrl}/token/${address}`;
} }
// Try to decode calldata using known ABIs. // Try to decode calldata using known ABIs.

View File

@@ -20,7 +20,7 @@ const {
addressDotHtml, addressDotHtml,
escapeHtml, escapeHtml,
} = require("./helpers"); } = require("./helpers");
const { state } = require("../../shared/state"); const { state, currentNetwork } = require("../../shared/state");
const { getSignerForAddress } = require("../../shared/wallet"); const { getSignerForAddress } = require("../../shared/wallet");
const { decryptWithPassword } = require("../../shared/vault"); const { decryptWithPassword } = require("../../shared/vault");
const { formatUsd, getPrice } = require("../../shared/prices"); const { formatUsd, getPrice } = require("../../shared/prices");
@@ -51,11 +51,11 @@ function restore() {
} }
function etherscanTokenLink(address) { function etherscanTokenLink(address) {
return `https://etherscan.io/token/${address}`; return `${currentNetwork().explorerUrl}/token/${address}`;
} }
function etherscanAddressLink(address) { function etherscanAddressLink(address) {
return `https://etherscan.io/address/${address}`; return `${currentNetwork().explorerUrl}/address/${address}`;
} }
function blockieHtml(address) { function blockieHtml(address) {

View File

@@ -11,7 +11,12 @@ const {
escapeHtml, escapeHtml,
truncateMiddle, truncateMiddle,
} = require("./helpers"); } = require("./helpers");
const { state, saveState, currentAddress } = require("../../shared/state"); const {
state,
saveState,
currentAddress,
currentNetwork,
} = require("../../shared/state");
const { const {
updateSendBalance, updateSendBalance,
renderSendTokenSelect, renderSendTokenSelect,
@@ -82,7 +87,7 @@ function renderActiveAddress() {
if (state.activeAddress) { if (state.activeAddress) {
const addr = state.activeAddress; const addr = state.activeAddress;
const dot = addressDotHtml(addr); const dot = addressDotHtml(addr);
const link = `https://etherscan.io/address/${addr}`; const link = `${currentNetwork().explorerUrl}/address/${addr}`;
el.innerHTML = el.innerHTML =
`<span class="underline decoration-dashed cursor-pointer" id="active-addr-copy">${dot}${escapeHtml(addr)}</span>` + `<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>`; `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;

View File

@@ -6,7 +6,7 @@ const {
formatAddressHtml, formatAddressHtml,
addressTitle, addressTitle,
} = require("./helpers"); } = require("./helpers");
const { state, currentAddress } = require("../../shared/state"); const { state, currentAddress, currentNetwork } = require("../../shared/state");
const QRCode = require("qrcode"); const QRCode = require("qrcode");
const EXT_ICON = const EXT_ICON =
@@ -25,7 +25,8 @@ function show() {
? formatAddressHtml(address, ensName, null, title) ? formatAddressHtml(address, ensName, null, title)
: ""; : "";
$("receive-address-block").dataset.full = address; $("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 $("receive-etherscan-link").innerHTML = link
? `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>` ? `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`
: ""; : "";
@@ -52,7 +53,9 @@ function show() {
warningEl.textContent = warningEl.textContent =
"This is an ERC-20 token. Only send " + "This is an ERC-20 token. Only send " +
symbol + 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"; warningEl.style.visibility = "visible";
} else { } else {
warningEl.textContent = ""; warningEl.textContent = "";

View File

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

View File

@@ -1,7 +1,7 @@
const { $, showView, showFlash, escapeHtml } = require("./helpers"); const { $, showView, showFlash, escapeHtml } = require("./helpers");
const { applyTheme } = require("../theme"); const { applyTheme } = require("../theme");
const { state, saveState } = require("../../shared/state"); const { state, saveState, currentNetwork } = require("../../shared/state");
const { ETHEREUM_MAINNET_CHAIN_ID } = require("../../shared/constants"); const { NETWORKS, SUPPORTED_CHAIN_IDS } = require("../../shared/networks");
const { log, debugFetch } = require("../../shared/log"); const { log, debugFetch } = require("../../shared/log");
const deleteWallet = require("./deleteWallet"); const deleteWallet = require("./deleteWallet");
@@ -125,6 +125,10 @@ function renderWalletListSettings() {
function show() { function show() {
$("settings-rpc").value = state.rpcUrl; $("settings-rpc").value = state.rpcUrl;
$("settings-blockscout").value = state.blockscoutUrl; $("settings-blockscout").value = state.blockscoutUrl;
const networkSelect = $("settings-network");
if (networkSelect) {
networkSelect.value = state.networkId;
}
renderTrackedTokens(); renderTrackedTokens();
renderSiteLists(); renderSiteLists();
renderWalletListSettings(); renderWalletListSettings();
@@ -168,9 +172,12 @@ function init(ctx) {
showFlash("Endpoint returned error: " + json.error.message); showFlash("Endpoint returned error: " + json.error.message);
return; return;
} }
if (json.result !== ETHEREUM_MAINNET_CHAIN_ID) { const net = currentNetwork();
if (json.result !== net.chainId) {
showFlash( showFlash(
"Wrong network (expected mainnet, got chain " + "Wrong network (expected " +
net.name +
", got chain " +
json.result + json.result +
").", ").",
); );
@@ -209,6 +216,22 @@ function init(ctx) {
showFlash("Saved."); 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").checked = state.showZeroBalanceTokens;
$("settings-show-zero-balances").addEventListener("change", async () => { $("settings-show-zero-balances").addEventListener("change", async () => {
state.showZeroBalanceTokens = $("settings-show-zero-balances").checked; state.showZeroBalanceTokens = $("settings-show-zero-balances").checked;

View File

@@ -12,7 +12,7 @@ const {
isoDate, isoDate,
timeAgo, timeAgo,
} = require("./helpers"); } = require("./helpers");
const { state } = require("../../shared/state"); const { state, currentNetwork } = require("../../shared/state");
const { formatEther, formatUnits } = require("ethers"); const { formatEther, formatUnits } = require("ethers");
const makeBlockie = require("ethereum-blockies-base64"); const makeBlockie = require("ethereum-blockies-base64");
const { log, debugFetch } = require("../../shared/log"); const { log, debugFetch } = require("../../shared/log");
@@ -69,7 +69,7 @@ function etherscanLinkHtml(url) {
function txAddressHtml(address, ensName, title) { function txAddressHtml(address, ensName, title) {
const blockie = blockieHtml(address); const blockie = blockieHtml(address);
const dot = addressDotHtml(address); const dot = addressDotHtml(address);
const link = `https://etherscan.io/address/${address}`; const link = `${currentNetwork().explorerUrl}/address/${address}`;
const extLink = etherscanLinkHtml(link); const extLink = etherscanLinkHtml(link);
let html = `<div class="mb-1">${blockie}</div>`; let html = `<div class="mb-1">${blockie}</div>`;
if (title) { if (title) {
@@ -95,7 +95,7 @@ function txAddressHtml(address, ensName, title) {
} }
function txHashHtml(hash) { function txHashHtml(hash) {
const link = `https://etherscan.io/tx/${hash}`; const link = `${currentNetwork().explorerUrl}/tx/${hash}`;
const extLink = etherscanLinkHtml(link); const extLink = etherscanLinkHtml(link);
return copyableHtml(hash, "break-all") + extLink; return copyableHtml(hash, "break-all") + extLink;
} }
@@ -172,7 +172,7 @@ function render() {
if (tokenContractSection && tokenContractEl) { if (tokenContractSection && tokenContractEl) {
if (tx.contractAddress) { if (tx.contractAddress) {
const dot = addressDotHtml(tx.contractAddress); const dot = addressDotHtml(tx.contractAddress);
const link = `https://etherscan.io/token/${tx.contractAddress}`; const link = `${currentNetwork().explorerUrl}/token/${tx.contractAddress}`;
tokenContractEl.innerHTML = tokenContractEl.innerHTML =
`<div class="flex items-center">${dot}` + `<div class="flex items-center">${dot}` +
copyableHtml(tx.contractAddress, "break-all") + copyableHtml(tx.contractAddress, "break-all") +
@@ -234,7 +234,7 @@ function showDetailField(sectionId, contentId, value) {
function populateOnChainDetails(txData) { function populateOnChainDetails(txData) {
// Block number // Block number
if (txData.block_number != null) { 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 blockSection = $("tx-detail-block-section");
const blockEl = $("tx-detail-block"); const blockEl = $("tx-detail-block");
if (blockSection && blockEl) { if (blockSection && blockEl) {
@@ -361,12 +361,12 @@ async function loadFullTxDetails(txHash, toAddress, isContractCall) {
if (tokenSymbol) { if (tokenSymbol) {
detailsHtml += `<div class="font-bold">${escapeHtml(tokenSymbol)}</div>`; detailsHtml += `<div class="font-bold">${escapeHtml(tokenSymbol)}</div>`;
} }
const etherscanUrl = `https://etherscan.io/token/${d.address}`; const etherscanUrl = `${currentNetwork().explorerUrl}/token/${d.address}`;
detailsHtml += `<div class="flex items-center">${dot}${copyableHtml(d.address, "break-all")}${etherscanLinkHtml(etherscanUrl)}</div>`; detailsHtml += `<div class="flex items-center">${dot}${copyableHtml(d.address, "break-all")}${etherscanLinkHtml(etherscanUrl)}</div>`;
} else if (d.address) { } else if (d.address) {
// Protocol/contract entry: show name + Etherscan link // Protocol/contract entry: show name + Etherscan link
const dot = addressDotHtml(d.address); const dot = addressDotHtml(d.address);
const etherscanUrl = `https://etherscan.io/address/${d.address}`; const etherscanUrl = `${currentNetwork().explorerUrl}/address/${d.address}`;
detailsHtml += `<div class="flex items-center">${dot}${copyableHtml(d.value, "break-all")}${etherscanLinkHtml(etherscanUrl)}</div>`; detailsHtml += `<div class="flex items-center">${dot}${copyableHtml(d.value, "break-all")}${etherscanLinkHtml(etherscanUrl)}</div>`;
} else { } else {
detailsHtml += `<div class="font-bold">${escapeHtml(d.value)}</div>`; detailsHtml += `<div class="font-bold">${escapeHtml(d.value)}</div>`;

View File

@@ -10,7 +10,7 @@ const {
escapeHtml, escapeHtml,
} = require("./helpers"); } = require("./helpers");
const { TOKEN_BY_ADDRESS } = require("../../shared/tokenList"); 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 { getProvider } = require("../../shared/balances");
const { log } = require("../../shared/log"); const { log } = require("../../shared/log");
@@ -38,7 +38,7 @@ function clearTimers() {
function toAddressHtml(address) { function toAddressHtml(address) {
const dot = addressDotHtml(address); const dot = addressDotHtml(address);
const link = `https://etherscan.io/address/${address}`; const link = `${currentNetwork().explorerUrl}/address/${address}`;
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`; const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
const title = addressTitle(address, state.wallets); const title = addressTitle(address, state.wallets);
if (title) { if (title) {
@@ -52,7 +52,7 @@ function toAddressHtml(address) {
} }
function txHashHtml(hash) { function txHashHtml(hash) {
const link = `https://etherscan.io/tx/${hash}`; const link = `${currentNetwork().explorerUrl}/tx/${hash}`;
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`; const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
return ( return (
`<span class="underline decoration-dashed cursor-pointer break-all" data-copy="${escapeHtml(hash)}">${escapeHtml(hash)}</span>` + `<span class="underline decoration-dashed cursor-pointer break-all" data-copy="${escapeHtml(hash)}">${escapeHtml(hash)}</span>` +
@@ -62,7 +62,7 @@ function txHashHtml(hash) {
function blockNumberHtml(blockNumber) { function blockNumberHtml(blockNumber) {
const num = String(blockNumber); const num = String(blockNumber);
const link = `https://etherscan.io/block/${num}`; const link = `${currentNetwork().explorerUrl}/block/${num}`;
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`; const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
return ( return (
`<span class="underline decoration-dashed cursor-pointer" data-copy="${escapeHtml(num)}">${escapeHtml(num)}</span>` + `<span class="underline decoration-dashed cursor-pointer" data-copy="${escapeHtml(num)}">${escapeHtml(num)}</span>` +
@@ -147,7 +147,7 @@ function tokenLabel(address) {
} }
function etherscanTokenLink(address) { function etherscanTokenLink(address) {
return `https://etherscan.io/token/${address}`; return `${currentNetwork().explorerUrl}/token/${address}`;
} }
function decodedDetailsHtml(decoded) { function decodedDetailsHtml(decoded) {

View File

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

View File

@@ -3,6 +3,7 @@ const DEBUG_MNEMONIC =
"cube evolve unfold result inch risk jealous skill hotel bulb night wreck"; "cube evolve unfold result inch risk jealous skill hotel bulb night wreck";
const ETHEREUM_MAINNET_CHAIN_ID = "0x1"; const ETHEREUM_MAINNET_CHAIN_ID = "0x1";
const ETHEREUM_SEPOLIA_CHAIN_ID = "0xaa36a7";
const DEFAULT_RPC_URL = "https://ethereum-rpc.publicnode.com"; const DEFAULT_RPC_URL = "https://ethereum-rpc.publicnode.com";
@@ -37,6 +38,7 @@ module.exports = {
DEBUG, DEBUG,
DEBUG_MNEMONIC, DEBUG_MNEMONIC,
ETHEREUM_MAINNET_CHAIN_ID, ETHEREUM_MAINNET_CHAIN_ID,
ETHEREUM_SEPOLIA_CHAIN_ID,
DEFAULT_RPC_URL, DEFAULT_RPC_URL,
DEFAULT_BLOCKSCOUT_URL, DEFAULT_BLOCKSCOUT_URL,
BIP44_ETH_PATH, BIP44_ETH_PATH,

View File

@@ -2,8 +2,6 @@
// Extension users make the requests directly to Etherscan — no proxy needed. // Extension users make the requests directly to Etherscan — no proxy needed.
// This is a best-effort enrichment: network failures return null silently. // 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. // Patterns in the page title that indicate a flagged address.
// Title format: "Fake_Phishing184810 | Address: 0x... | Etherscan" // Title format: "Fake_Phishing184810 | Address: 0x... | Etherscan"
const PHISHING_LABEL_PATTERNS = [/^Fake_Phishing/i, /^Phish:/i, /^Exploiter/i]; 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. * Returns a warning object if the address is flagged, or null.
* Network failures return null silently (best-effort check). * 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. * @param {string} address - Ethereum address to check.
* @returns {Promise<{type: string, message: string, severity: string}|null>} * @returns {Promise<{type: string, message: string, severity: string}|null>}
*/ */
async function checkEtherscanLabel(address) { async function checkEtherscanLabel(address) {
try { 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" }, headers: { Accept: "text/html" },
}); });
if (!resp.ok) return null; if (!resp.ok) return null;

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

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

View File

@@ -8,6 +8,9 @@ const prices = {};
let lastFetchedAt = 0; let lastFetchedAt = 0;
async function refreshPrices() { 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(); const now = Date.now();
if (now - lastFetchedAt < PRICE_CACHE_TTL) return; if (now - lastFetchedAt < PRICE_CACHE_TTL) return;
try { try {

View File

@@ -1,6 +1,7 @@
// State management and extension storage persistence. // State management and extension storage persistence.
const { DEFAULT_RPC_URL, DEFAULT_BLOCKSCOUT_URL } = require("./constants"); const { DEFAULT_RPC_URL, DEFAULT_BLOCKSCOUT_URL } = require("./constants");
const { networkById } = require("./networks");
const storageApi = const storageApi =
typeof browser !== "undefined" typeof browser !== "undefined"
@@ -11,6 +12,7 @@ const DEFAULT_STATE = {
hasWallet: false, hasWallet: false,
wallets: [], wallets: [],
trackedTokens: [], trackedTokens: [],
networkId: "mainnet",
rpcUrl: DEFAULT_RPC_URL, rpcUrl: DEFAULT_RPC_URL,
blockscoutUrl: DEFAULT_BLOCKSCOUT_URL, blockscoutUrl: DEFAULT_BLOCKSCOUT_URL,
lastBalanceRefresh: 0, lastBalanceRefresh: 0,
@@ -38,11 +40,17 @@ const state = {
viewData: {}, viewData: {},
}; };
// Return the network configuration for the currently selected network.
function currentNetwork() {
return networkById(state.networkId);
}
async function saveState() { async function saveState() {
const persisted = { const persisted = {
hasWallet: state.hasWallet, hasWallet: state.hasWallet,
wallets: state.wallets, wallets: state.wallets,
trackedTokens: state.trackedTokens, trackedTokens: state.trackedTokens,
networkId: state.networkId,
rpcUrl: state.rpcUrl, rpcUrl: state.rpcUrl,
blockscoutUrl: state.blockscoutUrl, blockscoutUrl: state.blockscoutUrl,
lastBalanceRefresh: state.lastBalanceRefresh, lastBalanceRefresh: state.lastBalanceRefresh,
@@ -75,6 +83,7 @@ async function loadState() {
state.hasWallet = saved.hasWallet; state.hasWallet = saved.hasWallet;
state.wallets = saved.wallets || []; state.wallets = saved.wallets || [];
state.trackedTokens = saved.trackedTokens || []; state.trackedTokens = saved.trackedTokens || [];
state.networkId = saved.networkId || DEFAULT_STATE.networkId;
state.rpcUrl = saved.rpcUrl || DEFAULT_STATE.rpcUrl; state.rpcUrl = saved.rpcUrl || DEFAULT_STATE.rpcUrl;
state.blockscoutUrl = state.blockscoutUrl =
saved.blockscoutUrl || DEFAULT_STATE.blockscoutUrl; saved.blockscoutUrl || DEFAULT_STATE.blockscoutUrl;
@@ -134,4 +143,10 @@ function currentAddress() {
return state.wallets[state.selectedWallet].addresses[state.selectedAddress]; return state.wallets[state.selectedWallet].addresses[state.selectedAddress];
} }
module.exports = { state, saveState, loadState, currentAddress }; module.exports = {
state,
saveState,
loadState,
currentAddress,
currentNetwork,
};