diff --git a/README.md b/README.md
index 1d19c85..08456a8 100644
--- a/README.md
+++ b/README.md
@@ -842,9 +842,11 @@ The same data must be formatted identically everywhere it appears. Token and ETH
amounts are displayed with exactly 4 decimal places (e.g. "1.0500 ETH", "17.1900
USDT") in balance lists, transaction lists, send confirmations, and approval
screens. Timestamps include both an ISO datetime and a humanized relative age
-wherever shown. If a formatting rule applies in one place, it applies in every
-place. Users should never see the same value rendered differently on two
-screens.
+wherever shown, written by `isoDate()` and `timeAgo()` in
+`src/popup/views/helpers.js` on every screen; the ISO datetime is in UTC when
+the UTC Timestamps setting is on. If a formatting rule applies in one place, it
+applies in every place. Users should never see the same value rendered
+differently on two screens.
The native token's label is a network's `nativeCurrency` in
`src/shared/networks.js`: `ETH` on mainnet, `SepoliaETH` on Sepolia. The
diff --git a/TODO.md b/TODO.md
index e354a4a..1ddac09 100644
--- a/TODO.md
+++ b/TODO.md
@@ -45,6 +45,19 @@ but the review is broader than any of them.
# Completed Steps
+- 2026-10-05: Dead code removed and copied view helpers shared
+ ([#168](https://git.eeqj.de/sneak/AutistMask/issues/168)). AddressDetail and
+ AddressToken each defined their own `isoDate()` and `timeAgo()` in place of
+ the ones in `src/popup/views/helpers.js`, so a fix to the shared pair would
+ not have reached them. The copies were identical; every screen now uses the
+ shared pair. `blockieHtml()` and `tokenLabel()`, each defined twice, live in
+ `helpers.js` too. Removed as never called: `explorerLink()` (the views build
+ explorer links with `explorerUrl()`), `ETHEREUM_SEPOLIA_CHAIN_ID` (the chain
+ id lives in `src/shared/networks.js`), and `getWalletValue()` and
+ `getTotalValue()`: Home's "Total:" is the active address's total, as
+ `README.md` says. `addressColor()` and `etherscanAddressUrl()` are no longer
+ exported. Nothing the user sees changed.
+
- 2026-10-05: The Send screen has a "Max" button
([#198](https://git.eeqj.de/sneak/AutistMask/issues/198)). Emptying an ETH
address took guessing an amount and being refused by the confirmation screen's
diff --git a/src/popup/views/addressDetail.js b/src/popup/views/addressDetail.js
index 5ea6225..39f1f6e 100644
--- a/src/popup/views/addressDetail.js
+++ b/src/popup/views/addressDetail.js
@@ -11,6 +11,8 @@ const {
attachCopyHandlers,
goBack,
pushCurrentView,
+ isoDate,
+ timeAgo,
} = require("./helpers");
const { state, saveState, currentNetwork } = require("../../shared/state");
const { formatAddressTotal, getAddressValue } = require("../../shared/prices");
@@ -88,62 +90,6 @@ function show() {
loadTransactions(addr.address);
}
-function isoDate(timestamp) {
- const d = new Date(timestamp * 1000);
- const pad = (n) => String(n).padStart(2, "0");
- if (state.utcTimestamps) {
- return (
- d.getUTCFullYear() +
- "-" +
- pad(d.getUTCMonth() + 1) +
- "-" +
- pad(d.getUTCDate()) +
- "T" +
- pad(d.getUTCHours()) +
- ":" +
- pad(d.getUTCMinutes()) +
- ":" +
- pad(d.getUTCSeconds()) +
- "Z"
- );
- }
- const offsetMin = -d.getTimezoneOffset();
- const sign = offsetMin >= 0 ? "+" : "-";
- const absOff = Math.abs(offsetMin);
- const tzStr = sign + pad(Math.floor(absOff / 60)) + ":" + pad(absOff % 60);
- return (
- d.getFullYear() +
- "-" +
- pad(d.getMonth() + 1) +
- "-" +
- pad(d.getDate()) +
- "T" +
- pad(d.getHours()) +
- ":" +
- pad(d.getMinutes()) +
- ":" +
- pad(d.getSeconds()) +
- tzStr
- );
-}
-
-function timeAgo(timestamp) {
- const seconds = Math.floor(Date.now() / 1000 - timestamp);
- if (seconds < 60) return seconds + " seconds ago";
- const minutes = Math.floor(seconds / 60);
- if (minutes < 60)
- return minutes + " minute" + (minutes !== 1 ? "s" : "") + " ago";
- const hours = Math.floor(minutes / 60);
- if (hours < 24) return hours + " hour" + (hours !== 1 ? "s" : "") + " ago";
- const days = Math.floor(hours / 24);
- if (days < 30) return days + " day" + (days !== 1 ? "s" : "") + " ago";
- const months = Math.floor(days / 30);
- if (months < 12)
- return months + " month" + (months !== 1 ? "s" : "") + " ago";
- const years = Math.floor(days / 365);
- return years + " year" + (years !== 1 ? "s" : "") + " ago";
-}
-
let loadedTxs = [];
let ensNameMap = new Map();
diff --git a/src/popup/views/addressToken.js b/src/popup/views/addressToken.js
index 1ba8907..451e58b 100644
--- a/src/popup/views/addressToken.js
+++ b/src/popup/views/addressToken.js
@@ -17,6 +17,8 @@ const {
attachCopyHandlers,
goBack,
pushCurrentView,
+ isoDate,
+ timeAgo,
} = require("./helpers");
const { state, saveState, currentNetwork } = require("../../shared/state");
const { TOKEN_BY_ADDRESS, resolveSymbol } = require("../../shared/tokenList");
@@ -37,62 +39,6 @@ const { walletDefect } = require("../../shared/walletDefects");
let ctx;
-function isoDate(timestamp) {
- const d = new Date(timestamp * 1000);
- const pad = (n) => String(n).padStart(2, "0");
- if (state.utcTimestamps) {
- return (
- d.getUTCFullYear() +
- "-" +
- pad(d.getUTCMonth() + 1) +
- "-" +
- pad(d.getUTCDate()) +
- "T" +
- pad(d.getUTCHours()) +
- ":" +
- pad(d.getUTCMinutes()) +
- ":" +
- pad(d.getUTCSeconds()) +
- "Z"
- );
- }
- const offsetMin = -d.getTimezoneOffset();
- const sign = offsetMin >= 0 ? "+" : "-";
- const absOff = Math.abs(offsetMin);
- const tzStr = sign + pad(Math.floor(absOff / 60)) + ":" + pad(absOff % 60);
- return (
- d.getFullYear() +
- "-" +
- pad(d.getMonth() + 1) +
- "-" +
- pad(d.getDate()) +
- "T" +
- pad(d.getHours()) +
- ":" +
- pad(d.getMinutes()) +
- ":" +
- pad(d.getSeconds()) +
- tzStr
- );
-}
-
-function timeAgo(timestamp) {
- const seconds = Math.floor(Date.now() / 1000 - timestamp);
- if (seconds < 60) return seconds + " seconds ago";
- const minutes = Math.floor(seconds / 60);
- if (minutes < 60)
- return minutes + " minute" + (minutes !== 1 ? "s" : "") + " ago";
- const hours = Math.floor(minutes / 60);
- if (hours < 24) return hours + " hour" + (hours !== 1 ? "s" : "") + " ago";
- const days = Math.floor(hours / 24);
- if (days < 30) return days + " day" + (days !== 1 ? "s" : "") + " ago";
- const months = Math.floor(days / 30);
- if (months < 12)
- return months + " month" + (months !== 1 ? "s" : "") + " ago";
- const years = Math.floor(days / 365);
- return years + " year" + (years !== 1 ? "s" : "") + " ago";
-}
-
let loadedTxs = [];
let ensNameMap = new Map();
let currentSymbol = null;
diff --git a/src/popup/views/approval.js b/src/popup/views/approval.js
index d4e9e5d..3f8dddf 100644
--- a/src/popup/views/approval.js
+++ b/src/popup/views/approval.js
@@ -10,6 +10,7 @@ const {
attachCopyHandlers,
onViewLeave,
formatFee,
+ tokenLabel,
} = require("./helpers");
const { state, saveState } = require("../../shared/state");
const {
@@ -73,17 +74,6 @@ function tokenAmountText(rawAmount, decimals, symbol) {
};
}
-// The symbol shown for a token line, resolved from the bundled list, the
-// tokens the user tracks, and the explorer's report — the same chain the
-// amount line's scale comes from. Null when no source names one, so the token
-// lines keep saying `Unknown token` for a token nothing knows.
-function tokenLabel(address) {
- return resolveTokenSymbol(address, {
- trackedTokens: state.trackedTokens,
- wallets: state.wallets,
- });
-}
-
// Try to decode calldata using known ABIs.
// Returns { name, description, details } or null.
function decodeCalldata(data, toAddress) {
diff --git a/src/popup/views/confirmTx.js b/src/popup/views/confirmTx.js
index 4752419..942e951 100644
--- a/src/popup/views/confirmTx.js
+++ b/src/popup/views/confirmTx.js
@@ -13,6 +13,7 @@ const {
displaySymbol,
nativeCurrency,
renderAddressHtml,
+ blockieHtml,
attachCopyHandlers,
goBack,
onViewLeave,
@@ -47,7 +48,6 @@ const {
validateTransfer,
} = require("../../shared/txValidation");
const { log } = require("../../shared/log");
-const makeBlockie = require("ethereum-blockies-base64");
const txStatus = require("./txStatus");
let pendingTx = null;
@@ -67,11 +67,6 @@ function restore() {
}
}
-function blockieHtml(address) {
- const src = makeBlockie(address);
- return ``;
-}
-
function confirmAddressHtml(address, ensName, title) {
const blockie = blockieHtml(address);
return (
diff --git a/src/popup/views/helpers.js b/src/popup/views/helpers.js
index a285d24..bf44d49 100644
--- a/src/popup/views/helpers.js
+++ b/src/popup/views/helpers.js
@@ -13,10 +13,12 @@
// reasoning behind it are; it is re-exported below so views keep importing
// it from here.
const { formatEther } = require("ethers");
+const makeBlockie = require("ethereum-blockies-base64");
const {
truncateAmountNeverZero,
isBelowOneMillionth,
} = require("../../shared/amountDisplay");
+const { resolveTokenSymbol } = require("../../shared/approvalAmount");
const { DEBUG } = require("../../shared/constants");
const { escapeHtml } = require("../../shared/html");
const { isDebug } = require("../../shared/log");
@@ -278,6 +280,17 @@ function nativeCurrency() {
return currentNetwork().nativeCurrency;
}
+// The symbol shown for a token line, resolved from the bundled list, the
+// tokens the user tracks, and the explorer's report — the same chain the
+// amount line's scale comes from. Null when no source names one, so the token
+// lines keep saying `Unknown token` for a token nothing knows.
+function tokenLabel(address) {
+ return resolveTokenSymbol(address, {
+ trackedTokens: state.trackedTokens,
+ wallets: state.wallets,
+ });
+}
+
// A network fee in wei as the confirmation and approval screens both show it:
// the ETH figure through truncateAmountNeverZero() and labelled `symbol`, the
// native currency of the network the fee is paid on, then its USD value when
@@ -446,6 +459,11 @@ function addressDotHtml(address) {
return ``;
}
+function blockieHtml(address) {
+ const src = makeBlockie(address);
+ return `
`;
+}
+
// Look up an address across all wallets and return its title
// (e.g. "Address 1.2") or null if it's not one of ours.
function addressTitle(address, wallets) {
@@ -492,6 +510,9 @@ function formatAddressHtml(address, ensName, maxLen, title) {
return renderAddressHtml(address, { title, ensName, maxLen });
}
+// A transaction's time as every screen shows it (README, Display
+// Consistency): the ISO datetime, in UTC when the UTC Timestamps setting is
+// on, and the relative age. Views import these two; they keep no copies.
function isoDate(timestamp) {
const d = new Date(timestamp * 1000);
const pad = (n) => String(n).padStart(2, "0");
@@ -691,9 +712,10 @@ module.exports = {
addressHoldsFunds,
unknownableAmount,
nativeCurrency,
+ tokenLabel,
formatFee,
- addressColor,
addressDotHtml,
+ blockieHtml,
escapeHtml,
displaySymbol,
addressTitle,
@@ -703,7 +725,6 @@ module.exports = {
renderAddressHtml,
copyableHtml,
attachCopyHandlers,
- etherscanAddressUrl,
etherscanLinkHtml,
explorerUrl,
EXT_ICON,
diff --git a/src/popup/views/transactionDetail.js b/src/popup/views/transactionDetail.js
index 871aeff..11c800b 100644
--- a/src/popup/views/transactionDetail.js
+++ b/src/popup/views/transactionDetail.js
@@ -13,6 +13,7 @@ const {
isoDate,
timeAgo,
renderAddressHtml,
+ blockieHtml,
attachCopyHandlers,
copyableHtml,
etherscanLinkHtml,
@@ -23,7 +24,6 @@ const {
const { state } = require("../../shared/state");
const { nativeCurrencyByChainId } = require("../../shared/networks");
const { formatEther, formatUnits } = require("ethers");
-const makeBlockie = require("ethereum-blockies-base64");
const { log, debugFetch } = require("../../shared/log");
const { decodeCalldata } = require("./approval");
@@ -48,11 +48,6 @@ function getTransactionType(tx) {
return "Native " + nativeCurrencyByChainId(tx.chainId) + " Transfer";
}
-function blockieHtml(address) {
- const src = makeBlockie(address);
- return `
`;
-}
-
function txAddressHtml(address, ensName, title) {
const blockie = blockieHtml(address);
return (
diff --git a/src/popup/views/txStatus.js b/src/popup/views/txStatus.js
index 0ee254d..cc65a87 100644
--- a/src/popup/views/txStatus.js
+++ b/src/popup/views/txStatus.js
@@ -13,8 +13,8 @@ const {
explorerUrl,
displaySymbol,
clearViewStack,
+ tokenLabel,
} = require("./helpers");
-const { resolveTokenSymbol } = require("../../shared/approvalAmount");
const { state } = require("../../shared/state");
const { nativeCurrencyByChainId } = require("../../shared/networks");
const { getProvider } = require("../../shared/balances");
@@ -243,17 +243,6 @@ function showSuccess(txInfo, txHash, blockNumber) {
ctx.doRefreshAndRender();
}
-// The symbol shown for a decoded token line, resolved from the bundled list,
-// the tokens the user tracks, and the explorer's report — the same chain the
-// approval screen uses. Null when no source names one, so the line keeps
-// saying `Unknown token`.
-function tokenLabel(address) {
- return resolveTokenSymbol(address, {
- trackedTokens: state.trackedTokens,
- wallets: state.wallets,
- });
-}
-
function decodedDetailsHtml(decoded) {
if (!decoded || !decoded.details) return "";
let html = `