fix: resolve all RULES.md divergences
Some checks are pending
check / check (push) Waiting to run

- Update RULES.md to document Blockscout as third allowed external service
- Remove DEBUG-conditional log threshold (DEBUG now only enables banner + test phrase)
- Add   fallback in balanceLine() to prevent layout shift
- Show ISO datetime + relative age in all transaction list views
- Rename 'mnemonic' to 'recoveryPhrase' in all code identifiers and HTML IDs
- Deduplicate isoDate/timeAgo into helpers.js (single source of truth)

fixes #1
This commit is contained in:
2026-02-27 02:14:40 -08:00
parent 9365cd03a6
commit 02eefa8f80
11 changed files with 78 additions and 174 deletions

View File

@@ -17,8 +17,8 @@ contradicts either, the originals govern.
## External Communication ## External Communication
- [ ] Extension contacts exactly two external services: configured RPC endpoint - [ ] Extension contacts exactly three external services: configured RPC
and CoinDesk price API endpoint, CoinDesk price API, and configured Blockscout API
- [ ] No analytics, telemetry, or tracking - [ ] No analytics, telemetry, or tracking
- [ ] No user-specific data sent except to the configured RPC endpoint - [ ] No user-specific data sent except to the configured RPC endpoint
- [ ] No Infura/Alchemy hard dependency - [ ] No Infura/Alchemy hard dependency

View File

@@ -71,7 +71,7 @@
</div> </div>
<div class="mb-2"> <div class="mb-2">
<textarea <textarea
id="wallet-mnemonic" id="wallet-recovery-phrase"
rows="3" rows="3"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg resize-y" class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg resize-y"
placeholder="word word word ..." placeholder="word word word ..."

View File

@@ -1,15 +1,15 @@
const { $, showView, showFlash } = require("./helpers"); const { $, showView, showFlash } = require("./helpers");
const { const {
generateMnemonic, generateRecoveryPhrase,
hdWalletFromMnemonic, hdWalletFromRecoveryPhrase,
isValidMnemonic, isValidRecoveryPhrase,
} = require("../../shared/wallet"); } = require("../../shared/wallet");
const { encryptWithPassword } = require("../../shared/vault"); const { encryptWithPassword } = require("../../shared/vault");
const { state, saveState } = require("../../shared/state"); const { state, saveState } = require("../../shared/state");
const { scanForAddresses } = require("../../shared/balances"); const { scanForAddresses } = require("../../shared/balances");
function show() { function show() {
$("wallet-mnemonic").value = ""; $("wallet-recovery-phrase").value = "";
$("add-wallet-password").value = ""; $("add-wallet-password").value = "";
$("add-wallet-password-confirm").value = ""; $("add-wallet-password-confirm").value = "";
$("add-wallet-phrase-warning").classList.add("hidden"); $("add-wallet-phrase-warning").classList.add("hidden");
@@ -18,19 +18,19 @@ function show() {
function init(ctx) { function init(ctx) {
$("btn-generate-phrase").addEventListener("click", () => { $("btn-generate-phrase").addEventListener("click", () => {
$("wallet-mnemonic").value = generateMnemonic(); $("wallet-recovery-phrase").value = generateRecoveryPhrase();
$("add-wallet-phrase-warning").classList.remove("hidden"); $("add-wallet-phrase-warning").classList.remove("hidden");
}); });
$("btn-add-wallet-confirm").addEventListener("click", async () => { $("btn-add-wallet-confirm").addEventListener("click", async () => {
const mnemonic = $("wallet-mnemonic").value.trim(); const recoveryPhrase = $("wallet-recovery-phrase").value.trim();
if (!mnemonic) { if (!recoveryPhrase) {
showFlash( showFlash(
"Enter a recovery phrase or press the die to generate one.", "Enter a recovery phrase or press the die to generate one.",
); );
return; return;
} }
const words = mnemonic.split(/\s+/); const words = recoveryPhrase.split(/\s+/);
if (words.length !== 12 && words.length !== 24) { if (words.length !== 12 && words.length !== 24) {
showFlash( showFlash(
"Recovery phrase must be 12 or 24 words. You entered " + "Recovery phrase must be 12 or 24 words. You entered " +
@@ -39,7 +39,7 @@ function init(ctx) {
); );
return; return;
} }
if (!isValidMnemonic(mnemonic)) { if (!isValidRecoveryPhrase(recoveryPhrase)) {
showFlash("Invalid recovery phrase. Check for typos."); showFlash("Invalid recovery phrase. Check for typos.");
return; return;
} }
@@ -57,7 +57,8 @@ function init(ctx) {
showFlash("Passwords do not match."); showFlash("Passwords do not match.");
return; return;
} }
const { xpub, firstAddress } = hdWalletFromMnemonic(mnemonic); const { xpub, firstAddress } =
hdWalletFromRecoveryPhrase(recoveryPhrase);
const duplicate = state.wallets.find( const duplicate = state.wallets.find(
(w) => (w) =>
w.type === "hd" && w.type === "hd" &&
@@ -73,7 +74,7 @@ function init(ctx) {
); );
return; return;
} }
const encrypted = await encryptWithPassword(mnemonic, pw); const encrypted = await encryptWithPassword(recoveryPhrase, pw);
const walletNum = state.wallets.length + 1; const walletNum = state.wallets.length + 1;
const wallet = { const wallet = {
type: "hd", type: "hd",

View File

@@ -6,6 +6,8 @@ const {
addressDotHtml, addressDotHtml,
escapeHtml, escapeHtml,
truncateMiddle, truncateMiddle,
isoDate,
timeAgo,
} = require("./helpers"); } = require("./helpers");
const { state, currentAddress, saveState } = require("../../shared/state"); const { state, currentAddress, saveState } = require("../../shared/state");
const { formatUsd, getAddressValueUsd } = require("../../shared/prices"); const { formatUsd, getAddressValueUsd } = require("../../shared/prices");
@@ -84,41 +86,6 @@ function show() {
loadTransactions(addr.address); loadTransactions(addr.address);
} }
function isoDate(timestamp) {
const d = new Date(timestamp * 1000);
const pad = (n) => String(n).padStart(2, "0");
return (
d.getFullYear() +
"-" +
pad(d.getMonth() + 1) +
"-" +
pad(d.getDate()) +
" " +
pad(d.getHours()) +
":" +
pad(d.getMinutes()) +
":" +
pad(d.getSeconds())
);
}
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 loadedTxs = [];
let ensNameMap = new Map(); let ensNameMap = new Map();
@@ -200,7 +167,7 @@ function renderTransactions(txs) {
const ago = escapeHtml(timeAgo(tx.timestamp)); const ago = escapeHtml(timeAgo(tx.timestamp));
const iso = escapeHtml(isoDate(tx.timestamp)); const iso = escapeHtml(isoDate(tx.timestamp));
html += `<div class="tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover" data-tx="${i}" style="${opacity}">`; html += `<div class="tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover" data-tx="${i}" style="${opacity}">`;
html += `<div class="flex justify-between"><span class="text-muted" title="${iso}">${ago}</span><span>${dirLabel}${err}</span></div>`; html += `<div class="flex justify-between"><span class="text-muted">${iso} (${ago})</span><span>${dirLabel}${err}</span></div>`;
html += `<div class="flex justify-between"><span class="flex items-center">${dot}${addrStr}</span><span>${amountStr}</span></div>`; html += `<div class="flex justify-between"><span class="flex items-center">${dot}${addrStr}</span><span>${amountStr}</span></div>`;
html += `</div>`; html += `</div>`;
i++; i++;

View File

@@ -9,6 +9,8 @@ const {
escapeHtml, escapeHtml,
truncateMiddle, truncateMiddle,
balanceLine, balanceLine,
isoDate,
timeAgo,
} = require("./helpers"); } = require("./helpers");
const { state, currentAddress, saveState } = require("../../shared/state"); const { state, currentAddress, saveState } = require("../../shared/state");
const { const {
@@ -38,41 +40,6 @@ function etherscanAddressLink(address) {
return `https://etherscan.io/address/${address}`; return `https://etherscan.io/address/${address}`;
} }
function isoDate(timestamp) {
const d = new Date(timestamp * 1000);
const pad = (n) => String(n).padStart(2, "0");
return (
d.getFullYear() +
"-" +
pad(d.getMonth() + 1) +
"-" +
pad(d.getDate()) +
" " +
pad(d.getHours()) +
":" +
pad(d.getMinutes()) +
":" +
pad(d.getSeconds())
);
}
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 loadedTxs = [];
let ensNameMap = new Map(); let ensNameMap = new Map();
let currentSymbol = null; let currentSymbol = null;
@@ -225,7 +192,7 @@ function renderTransactions(txs) {
const ago = escapeHtml(timeAgo(tx.timestamp)); const ago = escapeHtml(timeAgo(tx.timestamp));
const iso = escapeHtml(isoDate(tx.timestamp)); const iso = escapeHtml(isoDate(tx.timestamp));
html += `<div class="tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover" data-tx="${i}" style="${opacity}">`; html += `<div class="tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover" data-tx="${i}" style="${opacity}">`;
html += `<div class="flex justify-between"><span class="text-muted" title="${iso}">${ago}</span><span>${dirLabel}${err}</span></div>`; html += `<div class="flex justify-between"><span class="text-muted">${iso} (${ago})</span><span>${dirLabel}${err}</span></div>`;
html += `<div class="flex justify-between"><span class="flex items-center">${dot}${addrStr}</span><span>${amountStr}</span></div>`; html += `<div class="flex justify-between"><span class="flex items-center">${dot}${addrStr}</span><span>${amountStr}</span></div>`;
html += `</div>`; html += `</div>`;
i++; i++;

View File

@@ -93,7 +93,7 @@ function balanceLine(symbol, amount, price, tokenId) {
`<span>${symbol}</span>` + `<span>${symbol}</span>` +
`<span>${qty}</span>` + `<span>${qty}</span>` +
`</span>` + `</span>` +
`<span class="text-right text-muted flex-1">${usd}</span>` + `<span class="text-right text-muted flex-1">${usd || "&nbsp;"}</span>` +
`</div>` `</div>`
); );
} }
@@ -147,6 +147,41 @@ function truncateMiddle(str, maxLen) {
return str.slice(0, half) + "\u2026" + str.slice(-(maxLen - 1 - half)); return str.slice(0, half) + "\u2026" + str.slice(-(maxLen - 1 - half));
} }
function isoDate(timestamp) {
const d = new Date(timestamp * 1000);
const pad = (n) => String(n).padStart(2, "0");
return (
d.getFullYear() +
"-" +
pad(d.getMonth() + 1) +
"-" +
pad(d.getDate()) +
" " +
pad(d.getHours()) +
":" +
pad(d.getMinutes()) +
":" +
pad(d.getSeconds())
);
}
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";
}
// 16 colors evenly spaced around the hue wheel (22.5° apart), // 16 colors evenly spaced around the hue wheel (22.5° apart),
// all at HSL saturation 70%, lightness 50% for uniform vibrancy. // all at HSL saturation 70%, lightness 50% for uniform vibrancy.
const ADDRESS_COLORS = [ const ADDRESS_COLORS = [
@@ -233,4 +268,6 @@ module.exports = {
addressTitle, addressTitle,
formatAddressHtml, formatAddressHtml,
truncateMiddle, truncateMiddle,
isoDate,
timeAgo,
}; };

View File

@@ -6,6 +6,8 @@ const {
addressDotHtml, addressDotHtml,
escapeHtml, escapeHtml,
truncateMiddle, truncateMiddle,
isoDate,
timeAgo,
} = require("./helpers"); } = require("./helpers");
const { state, saveState, currentAddress } = require("../../shared/state"); const { state, saveState, currentAddress } = require("../../shared/state");
const { updateSendBalance, renderSendTokenSelect } = require("./send"); const { updateSendBalance, renderSendTokenSelect } = require("./send");
@@ -87,41 +89,6 @@ function renderActiveAddress() {
} }
} }
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";
}
function isoDate(timestamp) {
const d = new Date(timestamp * 1000);
const pad = (n) => String(n).padStart(2, "0");
return (
d.getFullYear() +
"-" +
pad(d.getMonth() + 1) +
"-" +
pad(d.getDate()) +
" " +
pad(d.getHours()) +
":" +
pad(d.getMinutes()) +
":" +
pad(d.getSeconds())
);
}
let homeTxs = []; let homeTxs = [];
function renderHomeTxList(ctx) { function renderHomeTxList(ctx) {
@@ -149,7 +116,7 @@ function renderHomeTxList(ctx) {
const ago = escapeHtml(timeAgo(tx.timestamp)); const ago = escapeHtml(timeAgo(tx.timestamp));
const iso = escapeHtml(isoDate(tx.timestamp)); const iso = escapeHtml(isoDate(tx.timestamp));
html += `<div class="home-tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover" data-tx="${i}" style="${opacity}">`; html += `<div class="home-tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover" data-tx="${i}" style="${opacity}">`;
html += `<div class="flex justify-between"><span class="text-muted" title="${iso}">${ago}</span><span>${dirLabel}${err}</span></div>`; html += `<div class="flex justify-between"><span class="text-muted">${iso} (${ago})</span><span>${dirLabel}${err}</span></div>`;
html += `<div class="flex justify-between"><span class="flex items-center">${dot}${addrStr}</span><span>${amountStr}</span></div>`; html += `<div class="flex justify-between"><span class="flex items-center">${dot}${addrStr}</span><span>${amountStr}</span></div>`;
html += `</div>`; html += `</div>`;
i++; i++;

View File

@@ -8,6 +8,8 @@ const {
addressDotHtml, addressDotHtml,
addressTitle, addressTitle,
escapeHtml, escapeHtml,
isoDate,
timeAgo,
} = require("./helpers"); } = require("./helpers");
const { state } = require("../../shared/state"); const { state } = require("../../shared/state");
const makeBlockie = require("ethereum-blockies-base64"); const makeBlockie = require("ethereum-blockies-base64");
@@ -21,41 +23,6 @@ const EXT_ICON =
let ctx; let ctx;
function isoDate(timestamp) {
const d = new Date(timestamp * 1000);
const pad = (n) => String(n).padStart(2, "0");
return (
d.getFullYear() +
"-" +
pad(d.getMonth() + 1) +
"-" +
pad(d.getDate()) +
" " +
pad(d.getHours()) +
":" +
pad(d.getMinutes()) +
":" +
pad(d.getSeconds())
);
}
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";
}
function copyableHtml(text, extraClass) { function copyableHtml(text, extraClass) {
const cls = const cls =
"underline decoration-dashed cursor-pointer" + "underline decoration-dashed cursor-pointer" +

View File

@@ -1,5 +1,5 @@
const DEBUG = true; const DEBUG = true;
const DEBUG_MNEMONIC = const DEBUG_RECOVERY_PHRASE =
"cube evolve unfold result inch risk jealous skill hotel bulb night wreck"; "cube evolve unfold result inch risk jealous skill hotel bulb night wreck";
const ETHEREUM_MAINNET_CHAIN_ID = "0x1"; const ETHEREUM_MAINNET_CHAIN_ID = "0x1";
@@ -22,7 +22,7 @@ const ERC20_ABI = [
module.exports = { module.exports = {
DEBUG, DEBUG,
DEBUG_MNEMONIC, DEBUG_RECOVERY_PHRASE,
ETHEREUM_MAINNET_CHAIN_ID, ETHEREUM_MAINNET_CHAIN_ID,
DEFAULT_RPC_URL, DEFAULT_RPC_URL,
DEFAULT_BLOCKSCOUT_URL, DEFAULT_BLOCKSCOUT_URL,

View File

@@ -1,10 +1,8 @@
// Leveled logger. Outputs to console with [AutistMask] prefix. // Leveled logger. Outputs to console with [AutistMask] prefix.
// Level is DEBUG when the DEBUG constant is true, INFO otherwise. // Level is DEBUG when the DEBUG constant is true, INFO otherwise.
const { DEBUG } = require("./constants");
const LEVELS = { debug: 0, info: 1, warn: 2, error: 3 }; const LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
const threshold = DEBUG ? LEVELS.debug : LEVELS.info; const threshold = LEVELS.info;
function emit(level, method, args) { function emit(level, method, args) {
if (LEVELS[level] >= threshold) { if (LEVELS[level] >= threshold) {

View File

@@ -1,11 +1,11 @@
// Wallet operations: mnemonic generation, HD derivation, signing. // Wallet operations: recovery phrase generation, HD derivation, signing.
// All crypto delegated to ethers.js. // All crypto delegated to ethers.js.
const { Mnemonic, HDNodeWallet, Wallet } = require("ethers"); const { Mnemonic, HDNodeWallet, Wallet } = require("ethers");
const { DEBUG, DEBUG_MNEMONIC, BIP44_ETH_PATH } = require("./constants"); const { DEBUG, DEBUG_RECOVERY_PHRASE, BIP44_ETH_PATH } = require("./constants");
function generateMnemonic() { function generateRecoveryPhrase() {
if (DEBUG) return DEBUG_MNEMONIC; if (DEBUG) return DEBUG_RECOVERY_PHRASE;
const m = Mnemonic.fromEntropy( const m = Mnemonic.fromEntropy(
globalThis.crypto.getRandomValues(new Uint8Array(16)), globalThis.crypto.getRandomValues(new Uint8Array(16)),
); );
@@ -17,8 +17,8 @@ function deriveAddressFromXpub(xpub, index) {
return node.deriveChild(index).address; return node.deriveChild(index).address;
} }
function hdWalletFromMnemonic(mnemonic) { function hdWalletFromRecoveryPhrase(recoveryPhrase) {
const node = HDNodeWallet.fromPhrase(mnemonic, "", BIP44_ETH_PATH); const node = HDNodeWallet.fromPhrase(recoveryPhrase, "", BIP44_ETH_PATH);
const xpub = node.neuter().extendedKey; const xpub = node.neuter().extendedKey;
const firstAddress = node.deriveChild(0).address; const firstAddress = node.deriveChild(0).address;
return { xpub, firstAddress }; return { xpub, firstAddress };
@@ -41,15 +41,15 @@ function getSignerForAddress(walletData, addrIndex, decryptedSecret) {
return new Wallet(decryptedSecret); return new Wallet(decryptedSecret);
} }
function isValidMnemonic(mnemonic) { function isValidRecoveryPhrase(phrase) {
return Mnemonic.isValidMnemonic(mnemonic); return Mnemonic.isValidMnemonic(phrase);
} }
module.exports = { module.exports = {
generateMnemonic, generateRecoveryPhrase,
deriveAddressFromXpub, deriveAddressFromXpub,
hdWalletFromMnemonic, hdWalletFromRecoveryPhrase,
addressFromPrivateKey, addressFromPrivateKey,
getSignerForAddress, getSignerForAddress,
isValidMnemonic, isValidRecoveryPhrase,
}; };