Compare commits

..

3 Commits

Author SHA1 Message Date
clawbot
c37ffcc864 fix: consolidate chain-switching into onChainSwitch()
All checks were successful
check / check (push) Successful in 13s
Introduces src/shared/chainSwitch.js with a single onChainSwitch()
function that handles every state change required when switching
networks:

- Updates networkId, rpcUrl, blockscoutUrl from network config
- Clears price caches (testnet tokens are worthless)
- Resets lastBalanceRefresh to force immediate refresh
- Clears per-address balances and token balances
- Clears tokenHolderCache and fraudContracts (chain-specific)
- Persists state

Both callers (settings UI dropdown, background
wallet_switchEthereumChain) now go through this single code path.
Adding a new chain (e.g. ETC) requires only a new entry in
networks.js with no per-caller wiring.

Also adds defense-in-depth testnet checks in prices.js (getPrice,
getAddressValueUsd, etc.) and fixes a hardcoded etherscan.io link
in addressToken.js to use the network-aware explorer URL.

Closes #139
2026-03-01 11:24:16 -08:00
6b40fa8836 feat: show estimated USD value for ETH in approve-tx view (#141)
All checks were successful
check / check (push) Successful in 25s
The approve-tx view (dapp-initiated transaction approval) now shows the estimated USD value next to the ETH amount being transferred, using the existing `getPrice`/`formatUsd` from `shared/prices.js`.

This matches the behavior already present in the manual send confirmation view (`confirmTx.js`).

When ETH price is available, the value line shows e.g. `0.5000 ETH ($1,650.00)`. When price is unavailable, it falls back gracefully to just the ETH amount.

closes #138

Co-authored-by: Jeffrey Paul <sneak@noreply.example.org>
Reviewed-on: #141
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-01 20:16:00 +01:00
bc2aedaab6 fix: etherscan link on address-token page goes to token-specific URL (#136)
All checks were successful
check / check (push) Successful in 9s
When viewing the address-token page for our own address with an ERC-20 token, the etherscan link now navigates to the token-specific page (`etherscan.io/token/<contract>?a=<address>`) instead of the plain address page.

Closes #135

Co-authored-by: user <user@Mac.lan guest wan>
Co-authored-by: Jeffrey Paul <sneak@noreply.example.org>
Reviewed-on: #136
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-01 20:13:55 +01:00
6 changed files with 80 additions and 15 deletions

View File

@@ -4,6 +4,7 @@
const { DEFAULT_RPC_URL } = require("../shared/constants");
const { SUPPORTED_CHAIN_IDS, networkByChainId } = require("../shared/networks");
const { onChainSwitch } = require("../shared/chainSwitch");
const { getBytes } = require("ethers");
const {
state,
@@ -345,12 +346,8 @@ async function handleRpc(method, params, origin) {
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();
await onChainSwitch(target.id);
broadcastChainChanged(target.chainId);
return { result: null };
}

View File

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

View File

@@ -9,6 +9,7 @@ const {
} = require("./helpers");
const { state, saveState, currentNetwork } = require("../../shared/state");
const { formatEther, formatUnits, Interface, toUtf8String } = require("ethers");
const { getPrice, formatUsd } = require("../../shared/prices");
const { ERC20_ABI } = require("../../shared/constants");
const { TOKEN_BY_ADDRESS } = require("../../shared/tokenList");
const txStatus = require("./txStatus");
@@ -243,8 +244,14 @@ function showTxApproval(details) {
$("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 =
formatTxValue(formatEther(details.txParams.value || "0")) + " ETH";
ethValueFormatted + " ETH" + (usdStr ? " (" + usdStr + ")" : "");
// Decode calldata (reuse decoded from above)
const decodedEl = $("approve-tx-decoded");

View File

@@ -2,7 +2,7 @@ const { $, showView, showFlash, escapeHtml } = require("./helpers");
const { applyTheme } = require("../theme");
const { state, saveState, currentNetwork } = require("../../shared/state");
const { NETWORKS, SUPPORTED_CHAIN_IDS } = require("../../shared/networks");
const { clearPrices } = require("../../shared/prices");
const { onChainSwitch } = require("../../shared/chainSwitch");
const { log, debugFetch } = require("../../shared/log");
const deleteWallet = require("./deleteWallet");
@@ -221,15 +221,9 @@ function init(ctx) {
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;
const net = await onChainSwitch(newId);
$("settings-rpc").value = state.rpcUrl;
$("settings-blockscout").value = state.blockscoutUrl;
if (net.isTestnet) clearPrices();
await saveState();
showFlash("Switched to " + net.name + ".");
});
}

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

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