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

## 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: #137

THIS WAS ONESHOTTED USING OPUS 4.  WTAF
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
This commit was merged in pull request #137.
This commit is contained in:
2026-03-01 20:11:22 +01:00
committed by Jeffrey Paul
parent d35bfb7d23
commit e53420f2e2
19 changed files with 272 additions and 56 deletions

View File

@@ -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 =
`</svg></span>`;
function etherscanAddressLink(address) {
return `https://etherscan.io/address/${address}`;
return `${currentNetwork().explorerUrl}/address/${address}`;
}
function show() {

View File

@@ -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 =
`</svg></span>`;
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 = `<div class="font-bold mb-2">Contract Address</div>`;
infoHtml +=
@@ -381,7 +386,7 @@ function init(_ctx) {
let staticHtml = `<div class="font-bold">${escapeHtml(currentSymbol)}</div>`;
if (tokenId !== "ETH") {
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>`;
staticHtml +=
`<div class="flex items-center text-xs">${dot}` +

View File

@@ -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 = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
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.

View File

@@ -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) {

View File

@@ -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 =
`<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>`;

View File

@@ -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
? `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`
: "";
@@ -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 = "";

View File

@@ -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 = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
const title = addressTitle(addr.address, state.wallets);
let fromHtml = "";

View File

@@ -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;

View File

@@ -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 = `<div class="mb-1">${blockie}</div>`;
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 =
`<div class="flex items-center">${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 += `<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>`;
} 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 += `<div class="flex items-center">${dot}${copyableHtml(d.value, "break-all")}${etherscanLinkHtml(etherscanUrl)}</div>`;
} else {
detailsHtml += `<div class="font-bold">${escapeHtml(d.value)}</div>`;

View File

@@ -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 = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
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 = `<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>` +
@@ -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 = `<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>` +
@@ -147,7 +147,7 @@ function tokenLabel(address) {
}
function etherscanTokenLink(address) {
return `https://etherscan.io/token/${address}`;
return `${currentNetwork().explorerUrl}/token/${address}`;
}
function decodedDetailsHtml(decoded) {