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
- [ ] Extension contacts exactly two external services: configured RPC endpoint
and CoinDesk price API
- [ ] Extension contacts exactly three external services: configured RPC
endpoint, CoinDesk price API, and configured Blockscout API
- [ ] No analytics, telemetry, or tracking
- [ ] No user-specific data sent except to the configured RPC endpoint
- [ ] No Infura/Alchemy hard dependency

View File

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

View File

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

View File

@@ -6,6 +6,8 @@ const {
addressDotHtml,
escapeHtml,
truncateMiddle,
isoDate,
timeAgo,
} = require("./helpers");
const { state, currentAddress, saveState } = require("../../shared/state");
const { formatUsd, getAddressValueUsd } = require("../../shared/prices");
@@ -84,41 +86,6 @@ function show() {
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 ensNameMap = new Map();
@@ -200,7 +167,7 @@ function renderTransactions(txs) {
const ago = escapeHtml(timeAgo(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="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>`;
i++;

View File

@@ -9,6 +9,8 @@ const {
escapeHtml,
truncateMiddle,
balanceLine,
isoDate,
timeAgo,
} = require("./helpers");
const { state, currentAddress, saveState } = require("../../shared/state");
const {
@@ -38,41 +40,6 @@ function etherscanAddressLink(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 ensNameMap = new Map();
let currentSymbol = null;
@@ -225,7 +192,7 @@ function renderTransactions(txs) {
const ago = escapeHtml(timeAgo(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="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>`;
i++;

View File

@@ -93,7 +93,7 @@ function balanceLine(symbol, amount, price, tokenId) {
`<span>${symbol}</span>` +
`<span>${qty}</span>` +
`</span>` +
`<span class="text-right text-muted flex-1">${usd}</span>` +
`<span class="text-right text-muted flex-1">${usd || "&nbsp;"}</span>` +
`</div>`
);
}
@@ -147,6 +147,41 @@ function truncateMiddle(str, maxLen) {
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),
// all at HSL saturation 70%, lightness 50% for uniform vibrancy.
const ADDRESS_COLORS = [
@@ -233,4 +268,6 @@ module.exports = {
addressTitle,
formatAddressHtml,
truncateMiddle,
isoDate,
timeAgo,
};

View File

@@ -6,6 +6,8 @@ const {
addressDotHtml,
escapeHtml,
truncateMiddle,
isoDate,
timeAgo,
} = require("./helpers");
const { state, saveState, currentAddress } = require("../../shared/state");
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 = [];
function renderHomeTxList(ctx) {
@@ -149,7 +116,7 @@ function renderHomeTxList(ctx) {
const ago = escapeHtml(timeAgo(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="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>`;
i++;

View File

@@ -8,6 +8,8 @@ const {
addressDotHtml,
addressTitle,
escapeHtml,
isoDate,
timeAgo,
} = require("./helpers");
const { state } = require("../../shared/state");
const makeBlockie = require("ethereum-blockies-base64");
@@ -21,41 +23,6 @@ const EXT_ICON =
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) {
const cls =
"underline decoration-dashed cursor-pointer" +

View File

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

View File

@@ -1,10 +1,8 @@
// Leveled logger. Outputs to console with [AutistMask] prefix.
// 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 threshold = DEBUG ? LEVELS.debug : LEVELS.info;
const threshold = LEVELS.info;
function emit(level, method, args) {
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.
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() {
if (DEBUG) return DEBUG_MNEMONIC;
function generateRecoveryPhrase() {
if (DEBUG) return DEBUG_RECOVERY_PHRASE;
const m = Mnemonic.fromEntropy(
globalThis.crypto.getRandomValues(new Uint8Array(16)),
);
@@ -17,8 +17,8 @@ function deriveAddressFromXpub(xpub, index) {
return node.deriveChild(index).address;
}
function hdWalletFromMnemonic(mnemonic) {
const node = HDNodeWallet.fromPhrase(mnemonic, "", BIP44_ETH_PATH);
function hdWalletFromRecoveryPhrase(recoveryPhrase) {
const node = HDNodeWallet.fromPhrase(recoveryPhrase, "", BIP44_ETH_PATH);
const xpub = node.neuter().extendedKey;
const firstAddress = node.deriveChild(0).address;
return { xpub, firstAddress };
@@ -41,15 +41,15 @@ function getSignerForAddress(walletData, addrIndex, decryptedSecret) {
return new Wallet(decryptedSecret);
}
function isValidMnemonic(mnemonic) {
return Mnemonic.isValidMnemonic(mnemonic);
function isValidRecoveryPhrase(phrase) {
return Mnemonic.isValidMnemonic(phrase);
}
module.exports = {
generateMnemonic,
generateRecoveryPhrase,
deriveAddressFromXpub,
hdWalletFromMnemonic,
hdWalletFromRecoveryPhrase,
addressFromPrivateKey,
getSignerForAddress,
isValidMnemonic,
isValidRecoveryPhrase,
};