- 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:
4
RULES.md
4
RULES.md
@@ -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
|
||||
|
||||
@@ -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 ..."
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -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 || " "}</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,
|
||||
};
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -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" +
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user