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
// 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