Compare commits

..

1 Commits

Author SHA1 Message Date
user
2244b52f5f fix: rules audit items 1,3,6 (closes #1)
All checks were successful
check / check (push) Successful in 22s
2026-02-27 02:18:45 -08:00
10 changed files with 141 additions and 74 deletions

View File

@@ -18,7 +18,7 @@ contradicts either, the originals govern.
## External Communication
- [ ] Extension contacts exactly three external services: configured RPC
endpoint, CoinDesk price API, and configured Blockscout API
endpoint, CoinDesk price API, and Blockscout block-explorer 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-recovery-phrase"
id="wallet-mnemonic"
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 {
generateRecoveryPhrase,
hdWalletFromRecoveryPhrase,
isValidRecoveryPhrase,
generateMnemonic,
hdWalletFromMnemonic,
isValidMnemonic,
} = require("../../shared/wallet");
const { encryptWithPassword } = require("../../shared/vault");
const { state, saveState } = require("../../shared/state");
const { scanForAddresses } = require("../../shared/balances");
function show() {
$("wallet-recovery-phrase").value = "";
$("wallet-mnemonic").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-recovery-phrase").value = generateRecoveryPhrase();
$("wallet-mnemonic").value = generateMnemonic();
$("add-wallet-phrase-warning").classList.remove("hidden");
});
$("btn-add-wallet-confirm").addEventListener("click", async () => {
const recoveryPhrase = $("wallet-recovery-phrase").value.trim();
if (!recoveryPhrase) {
const mnemonic = $("wallet-mnemonic").value.trim();
if (!mnemonic) {
showFlash(
"Enter a recovery phrase or press the die to generate one.",
);
return;
}
const words = recoveryPhrase.split(/\s+/);
const words = mnemonic.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 (!isValidRecoveryPhrase(recoveryPhrase)) {
if (!isValidMnemonic(mnemonic)) {
showFlash("Invalid recovery phrase. Check for typos.");
return;
}
@@ -57,8 +57,7 @@ function init(ctx) {
showFlash("Passwords do not match.");
return;
}
const { xpub, firstAddress } =
hdWalletFromRecoveryPhrase(recoveryPhrase);
const { xpub, firstAddress } = hdWalletFromMnemonic(mnemonic);
const duplicate = state.wallets.find(
(w) =>
w.type === "hd" &&
@@ -74,7 +73,7 @@ function init(ctx) {
);
return;
}
const encrypted = await encryptWithPassword(recoveryPhrase, pw);
const encrypted = await encryptWithPassword(mnemonic, pw);
const walletNum = state.wallets.length + 1;
const wallet = {
type: "hd",

View File

@@ -6,8 +6,6 @@ const {
addressDotHtml,
escapeHtml,
truncateMiddle,
isoDate,
timeAgo,
} = require("./helpers");
const { state, currentAddress, saveState } = require("../../shared/state");
const { formatUsd, getAddressValueUsd } = require("../../shared/prices");
@@ -86,6 +84,41 @@ 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();
@@ -167,7 +200,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">${iso} (${ago})</span><span>${dirLabel}${err}</span></div>`;
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="flex items-center">${dot}${addrStr}</span><span>${amountStr}</span></div>`;
html += `</div>`;
i++;

View File

@@ -9,8 +9,6 @@ const {
escapeHtml,
truncateMiddle,
balanceLine,
isoDate,
timeAgo,
} = require("./helpers");
const { state, currentAddress, saveState } = require("../../shared/state");
const {
@@ -40,6 +38,41 @@ 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;
@@ -192,7 +225,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">${iso} (${ago})</span><span>${dirLabel}${err}</span></div>`;
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="flex items-center">${dot}${addrStr}</span><span>${amountStr}</span></div>`;
html += `</div>`;
i++;

View File

@@ -82,7 +82,7 @@ function showFlash(msg, duration = 2000) {
function balanceLine(symbol, amount, price, tokenId) {
const qty = amount.toFixed(4);
const usd = price ? formatUsd(amount * price) : "";
const usd = price ? formatUsd(amount * price) || "&nbsp;" : "&nbsp;";
const tokenAttr = tokenId ? ` data-token="${tokenId}"` : "";
const clickClass = tokenId
? " cursor-pointer hover:bg-hover balance-row"
@@ -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 || "&nbsp;"}</span>` +
`<span class="text-right text-muted flex-1">${usd}</span>` +
`</div>`
);
}
@@ -147,41 +147,6 @@ 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 = [
@@ -254,6 +219,41 @@ function formatAddressHtml(address, ensName, maxLen, title) {
return `<div class="flex items-center">${dot}<span class="break-all">${escapeHtml(displayAddr)}</span></div>`;
}
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";
}
module.exports = {
$,
showError,

View File

@@ -3,11 +3,11 @@ const {
showView,
showFlash,
balanceLinesForAddress,
isoDate,
timeAgo,
addressDotHtml,
escapeHtml,
truncateMiddle,
isoDate,
timeAgo,
} = require("./helpers");
const { state, saveState, currentAddress } = require("../../shared/state");
const { updateSendBalance, renderSendTokenSelect } = require("./send");
@@ -116,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">${iso} (${ago})</span><span>${dirLabel}${err}</span></div>`;
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="flex items-center">${dot}${addrStr}</span><span>${amountStr}</span></div>`;
html += `</div>`;
i++;

View File

@@ -1,5 +1,5 @@
const DEBUG = true;
const DEBUG_RECOVERY_PHRASE =
const DEBUG_MNEMONIC =
"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_RECOVERY_PHRASE,
DEBUG_MNEMONIC,
ETHEREUM_MAINNET_CHAIN_ID,
DEFAULT_RPC_URL,
DEFAULT_BLOCKSCOUT_URL,

View File

@@ -1,8 +1,10 @@
// 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 = LEVELS.info;
const threshold = DEBUG ? LEVELS.debug : LEVELS.info;
function emit(level, method, args) {
if (LEVELS[level] >= threshold) {

View File

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