Compare commits

..

2 Commits

Author SHA1 Message Date
user
8c805537c0 fix: use null instead of 0 for testnet token USD fallback
All checks were successful
check / check (push) Successful in 12s
When price is null (testnet), the fallback value 0 caused formatUsd()
to display "$0.00" instead of hiding the USD value. Using null makes
formatUsd() return an empty string, so the UI correctly shows no USD
on the token detail view.
2026-03-01 11:18:04 -08:00
user
57de01b546 fix: suppress USD display on testnet networks
All checks were successful
check / check (push) Successful in 13s
When connected to a testnet (e.g. Sepolia), stale mainnet prices from
the in-memory cache caused USD values to display even though
refreshPrices() correctly skipped fetching. Three fixes applied:

- refreshPrices() now clears the price cache when on a testnet instead
  of silently returning, removing any stale mainnet prices
- getPrice(), getAddressValueUsd(), getWalletValueUsd(), and
  getTotalValueUsd() all return null when the current network is a
  testnet, as defense-in-depth
- The settings network switcher immediately clears prices when
  switching to a testnet, so the UI updates without waiting for the
  next refresh cycle

closes #139
2026-03-01 11:14:39 -08:00
6 changed files with 15 additions and 80 deletions

View File

@@ -4,7 +4,6 @@
const { DEFAULT_RPC_URL } = require("../shared/constants"); const { DEFAULT_RPC_URL } = require("../shared/constants");
const { SUPPORTED_CHAIN_IDS, networkByChainId } = require("../shared/networks"); const { SUPPORTED_CHAIN_IDS, networkByChainId } = require("../shared/networks");
const { onChainSwitch } = require("../shared/chainSwitch");
const { getBytes } = require("ethers"); const { getBytes } = require("ethers");
const { const {
state, state,
@@ -346,8 +345,12 @@ async function handleRpc(method, params, origin) {
return { result: null }; return { result: null };
} }
if (SUPPORTED_CHAIN_IDS.has(chainId)) { if (SUPPORTED_CHAIN_IDS.has(chainId)) {
// Switch to the requested network
const target = networkByChainId(chainId); const target = networkByChainId(chainId);
await onChainSwitch(target.id); state.networkId = target.id;
state.rpcUrl = target.defaultRpcUrl;
state.blockscoutUrl = target.defaultBlockscoutUrl;
await saveState();
broadcastChainChanged(target.chainId); broadcastChainChanged(target.chainId);
return { result: null }; return { result: null };
} }

View File

@@ -50,10 +50,6 @@ function etherscanAddressLink(address) {
return `${currentNetwork().explorerUrl}/address/${address}`; return `${currentNetwork().explorerUrl}/address/${address}`;
} }
function etherscanTokenLink(tokenContract, holderAddress) {
return `${currentNetwork().explorerUrl}/token/${tokenContract}?a=${holderAddress}`;
}
function isoDate(timestamp) { function isoDate(timestamp) {
const d = new Date(timestamp * 1000); const d = new Date(timestamp * 1000);
const pad = (n) => String(n).padStart(2, "0"); const pad = (n) => String(n).padStart(2, "0");
@@ -160,10 +156,7 @@ function show() {
$("address-token-dot").innerHTML = addressDotHtml(addr.address); $("address-token-dot").innerHTML = addressDotHtml(addr.address);
$("address-token-full").dataset.full = addr.address; $("address-token-full").dataset.full = addr.address;
$("address-token-full").textContent = addr.address; $("address-token-full").textContent = addr.address;
const addrLink = const addrLink = etherscanAddressLink(addr.address);
tokenId !== "ETH"
? etherscanTokenLink(tokenId, addr.address)
: etherscanAddressLink(addr.address);
$("address-token-etherscan-link").innerHTML = $("address-token-etherscan-link").innerHTML =
`<a href="${addrLink}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`; `<a href="${addrLink}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;

View File

@@ -9,7 +9,6 @@ const {
} = require("./helpers"); } = require("./helpers");
const { state, saveState, currentNetwork } = 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 { getPrice, formatUsd } = require("../../shared/prices");
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");
const txStatus = require("./txStatus"); const txStatus = require("./txStatus");
@@ -244,14 +243,8 @@ function showTxApproval(details) {
$("approve-tx-to").innerHTML = escapeHtml("(contract creation)"); $("approve-tx-to").innerHTML = escapeHtml("(contract creation)");
} }
const ethValueFormatted = formatTxValue(
formatEther(details.txParams.value || "0"),
);
const ethPrice = getPrice("ETH");
const ethUsd = ethPrice ? parseFloat(ethValueFormatted) * ethPrice : null;
const usdStr = formatUsd(ethUsd);
$("approve-tx-value").textContent = $("approve-tx-value").textContent =
ethValueFormatted + " ETH" + (usdStr ? " (" + usdStr + ")" : ""); formatTxValue(formatEther(details.txParams.value || "0")) + " ETH";
// Decode calldata (reuse decoded from above) // Decode calldata (reuse decoded from above)
const decodedEl = $("approve-tx-decoded"); const decodedEl = $("approve-tx-decoded");

View File

@@ -2,7 +2,7 @@ const { $, showView, showFlash, escapeHtml } = require("./helpers");
const { applyTheme } = require("../theme"); const { applyTheme } = require("../theme");
const { state, saveState, currentNetwork } = require("../../shared/state"); const { state, saveState, currentNetwork } = require("../../shared/state");
const { NETWORKS, SUPPORTED_CHAIN_IDS } = require("../../shared/networks"); const { NETWORKS, SUPPORTED_CHAIN_IDS } = require("../../shared/networks");
const { onChainSwitch } = require("../../shared/chainSwitch"); const { clearPrices } = require("../../shared/prices");
const { log, debugFetch } = require("../../shared/log"); const { log, debugFetch } = require("../../shared/log");
const deleteWallet = require("./deleteWallet"); const deleteWallet = require("./deleteWallet");
@@ -221,9 +221,15 @@ function init(ctx) {
if (networkSelect) { if (networkSelect) {
networkSelect.addEventListener("change", async () => { networkSelect.addEventListener("change", async () => {
const newId = networkSelect.value; const newId = networkSelect.value;
const net = await onChainSwitch(newId); const net = NETWORKS[newId];
if (!net) return;
state.networkId = newId;
state.rpcUrl = net.defaultRpcUrl;
state.blockscoutUrl = net.defaultBlockscoutUrl;
$("settings-rpc").value = state.rpcUrl; $("settings-rpc").value = state.rpcUrl;
$("settings-blockscout").value = state.blockscoutUrl; $("settings-blockscout").value = state.blockscoutUrl;
if (net.isTestnet) clearPrices();
await saveState();
showFlash("Switched to " + net.name + "."); showFlash("Switched to " + net.name + ".");
}); });
} }

View File

@@ -1,57 +0,0 @@
// Consolidated chain-switch handler.
//
// Every state change required when the active network changes is
// performed here so that callers (settings UI, background
// wallet_switchEthereumChain, future chain additions) all go
// through a single code path.
//
// Adding a new chain (e.g. ETC) requires only a new entry in
// networks.js — no per-caller wiring is needed.
const { networkById } = require("./networks");
const { clearPrices } = require("./prices");
// Switch the active chain and reset all chain-specific cached state.
// Returns the network configuration object for the new chain.
async function onChainSwitch(newNetworkId) {
const { state, saveState } = require("./state");
const net = networkById(newNetworkId);
// --- core identity ---
state.networkId = net.id;
state.rpcUrl = net.defaultRpcUrl;
state.blockscoutUrl = net.defaultBlockscoutUrl;
// --- price cache ---
// Prices are chain-specific (testnet tokens are worthless,
// ETC has different pricing, etc.).
clearPrices();
// --- balance / refresh state ---
// Reset last-refresh timestamp so the next polling cycle
// triggers an immediate balance refresh on the new chain.
state.lastBalanceRefresh = 0;
// Clear per-address balances and token balances so stale data
// from the previous chain is never displayed while the first
// refresh on the new chain is in flight.
for (const wallet of state.wallets) {
for (const addr of wallet.addresses) {
addr.balance = "0";
addr.tokenBalances = [];
}
}
// --- chain-specific caches ---
// Token holder counts and fraud contract lists are
// chain-specific and must not carry over.
state.tokenHolderCache = {};
state.fraudContracts = [];
await saveState();
return net;
}
module.exports = { onChainSwitch };

View File

@@ -26,8 +26,6 @@ async function refreshPrices() {
} }
} }
// Clear all cached prices and reset the fetch timestamp so the
// next refreshPrices() call will fetch fresh data.
function clearPrices() { function clearPrices() {
for (const key of Object.keys(prices)) { for (const key of Object.keys(prices)) {
delete prices[key]; delete prices[key];
@@ -35,7 +33,6 @@ function clearPrices() {
lastFetchedAt = 0; lastFetchedAt = 0;
} }
// Return the USD price for a symbol, or null on testnet / unknown.
function getPrice(symbol) { function getPrice(symbol) {
const { currentNetwork } = require("./state"); const { currentNetwork } = require("./state");
if (currentNetwork().isTestnet) return null; if (currentNetwork().isTestnet) return null;