fetchTokenBalances() did parseInt(item.token.decimals || "18", 10) before writing to state.wallets[].addresses[].tokenBalances[].decimals, so a token whose decimals() reverts -- one the block explorer reports no scale for -- was stored with a fabricated 18 that no reader could tell from a real one.
That is upstream of a rule already merged. #306 made the ERC-20 approval amount line resolve the real scale or refuse to format, and #340 extended it to the swap lines; both read this stored value as an authoritative source, so the guess walked straight past refusals that were intact and simply never fired. A 1,000-unit approval of such a token rendered 0.000000001 on the one screen whose job is to state what is being authorized.
The stored value is now the explorer's own answer or null, never a default. Both approval paths reach unknownDecimalsAmount() on a null, using the refusal that was already there. The history list's token transfers carried the same || "18" and now state exact base units with the scale unknown rather than a quantity at a guessed one.
A holding whose scale nothing knows has no quantity either, so its balance is stored as null -- unknown, never zero -- and the balance list, the address USD total, the Send screen and the confirmation screen each say so rather than printing 0.0000 for money that is really there. The zero-balance filter moved onto the base-unit integer, where it needs no scale at all. The bundled token list and the user's tracked tokens already outrank the explorer, so a token either of them knows still displays its real quantity when the explorer's entry omits decimals; only what none of the three knows is unknown.
The uint8 check is one shared toDecimals() rather than three copies of it, and it answers 0 for a real scale of zero: || "18" collapsed that to eighteen, the falsy-collapse trap of #246.
Existing installs hold 18s that cannot be told apart retroactively -- that is the defect, and no migration can undo it. They display exactly as they do today until the next balance refresh, which rewrites tokenBalances wholesale and needs no user action. The schema version is not bumped: version 1 records stay valid and are read exactly as before.
The only 18s left in src/ are native ETH's real scale in uniswap.js and the fixed-point comparison scale in txValidation.js.
## Summary
Fixes USD prices still showing on the main view when connected to a testnet (e.g. Sepolia). The root cause was stale mainnet prices lingering in the in-memory price cache after switching networks.
### Root Cause
PR #137 correctly made `refreshPrices()` skip fetching on testnets, but the cached prices from a prior mainnet session remained in the `prices` object. All display functions (`getPrice()`, `getAddressValueUsd()`, etc.) used whatever was cached without checking which network was active.
### Changes
- **`src/shared/prices.js`**
- `refreshPrices()` now clears the price cache when on a testnet instead of silently returning
- New `clearPrices()` function empties the cache and resets the fetch timestamp
- `getPrice()` returns null on testnets (defense-in-depth)
- `getAddressValueUsd()`, `getWalletValueUsd()`, `getTotalValueUsd()` return null on testnets
- **`src/popup/views/settings.js`**
- Network switcher immediately clears prices when switching to a testnet, so the UI updates without waiting for the next refresh cycle
closes#139
Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Reviewed-on: #142
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
## 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>
Delete src/shared/tokens.js and migrate all consumers to
src/shared/tokenList.js which has 511 tokens (vs ~150) sourced
from CoinGecko with on-chain verified decimals.
- prices.js: getTopTokenPrices now from tokenList
- transactions.js: KNOWN_SYMBOLS now from tokenList (3.4x more
symbols for spoof detection)
- send.js: KNOWN_SYMBOLS for token dropdown filtering
- approval.js: uses pre-built TOKEN_BY_ADDRESS map instead of
constructing its own from TOKENS array
- addToken.js: uses getTopTokens(25) for quick-pick buttons
(only top 25 shown, not all 511)
Major changes:
- Fetch token balances and tx history from Blockscout API (configurable)
- Remove manual token discovery (discoverTokens) in favor of Blockscout
- HD address gap scanning on mnemonic import
- Duplicate mnemonic detection on wallet add
- EIP-6963 multi-wallet discovery + selectedAddress updates in inpage
- Two-tier balance refresh: 10s while popup open, 60s background
- Fix $0.00 flash before prices load (return null when no prices)
- No-layout-shift: min-height on total value element
- Aligned balance columns (42ch address width, consistent USD column)
- All errors use flash messages instead of off-screen error divs
- Settings gear in global title bar, add-wallet moved to settings pane
- Settings wells with light grey background, configurable Blockscout URL
- Consistent "< Back" buttons top-left on all views
- Address titles (Address 1.1, 1.2, etc.) on main and detail views
- Send view shows current balance of selected asset
- Clickable affordance policy added to README
- Shortened mnemonic backup warning
- Fix broken background script constant imports
Split popup/index.js (784 lines) into focused modules:
- shared/state.js: state management, storage persistence
- shared/wallet.js: mnemonic gen, HD derivation, signing
- shared/prices.js: price cache (5min TTL), USD formatting,
value aggregation (address → wallet → total)
- shared/balances.js: ETH + ERC-20 balance cache (60s TTL),
ENS lookup, token contract metadata lookup
- shared/vault.js: unchanged (libsodium encryption)
- shared/tokens.js: unchanged (token list + CoinDesk client)
- popup/index.js: view switching and event wiring only
Token tracking is now app-wide: trackedTokens stored in state,
balances fetched for all tracked tokens across all addresses.
Add Token now calls the real contract to read name/symbol/decimals.
Total portfolio value shown in 2x type on Home screen.