// Shared DOM helpers used by all views. const { DEBUG } = require("../../shared/constants"); const { formatUsd, getPrice, getAddressValueUsd, } = require("../../shared/prices"); const { state, saveState } = require("../../shared/state"); // When views are added, removed, or transitions between them change, // update the view-navigation documentation in README.md to match. const VIEWS = [ "welcome", "add-wallet", "import-key", "main", "address", "address-token", "send", "confirm-tx", "wait-tx", "success-tx", "error-tx", "receive", "add-token", "settings", "delete-wallet-confirm", "settings-addtoken", "transaction", "approve-site", "approve-tx", "approve-sign", "export-privkey", ]; function $(id) { return document.getElementById(id); } function showError(id, msg) { const el = $(id); el.textContent = msg; el.classList.remove("hidden"); } function hideError(id) { $(id).classList.add("hidden"); } function showView(name) { for (const v of VIEWS) { const el = document.getElementById(`view-${v}`); if (el) { el.classList.toggle("hidden", v !== name); } } clearFlash(); state.currentView = name; saveState(); if (DEBUG) { const banner = document.getElementById("debug-banner"); if (banner) { banner.textContent = "DEBUG / INSECURE (" + name + ")"; } } } let flashTimer = null; function clearFlash() { if (flashTimer) { clearTimeout(flashTimer); flashTimer = null; } $("flash-msg").textContent = ""; } function showFlash(msg, duration = 2000) { clearFlash(); $("flash-msg").textContent = msg; flashTimer = setTimeout(() => { $("flash-msg").textContent = ""; flashTimer = null; }, duration); } function balanceLine(symbol, amount, price, tokenId) { const qty = amount.toFixed(4); const usd = price ? formatUsd(amount * price) || " " : " "; const tokenAttr = tokenId ? ` data-token="${tokenId}"` : ""; const clickClass = tokenId ? " cursor-pointer hover:bg-hover balance-row" : ""; return ( `
` + `` + `${symbol}` + `${qty}` + `` + `${usd}` + `
` ); } function balanceLinesForAddress(addr, trackedTokens, showZero) { let html = balanceLine( "ETH", parseFloat(addr.balance || "0"), getPrice("ETH"), "ETH", ); const seen = new Set(); for (const t of addr.tokenBalances || []) { const bal = parseFloat(t.balance || "0"); if (bal === 0 && !showZero) continue; html += balanceLine( t.symbol, bal, getPrice(t.symbol), t.address.toLowerCase(), ); seen.add(t.address.toLowerCase()); } if (showZero && trackedTokens) { for (const t of trackedTokens) { if (seen.has(t.address.toLowerCase())) continue; html += balanceLine( t.symbol, 0, getPrice(t.symbol), t.address.toLowerCase(), ); } } return html; } // Truncate the middle of a string, replacing removed characters with "…". // Safety: refuses to truncate more than 10 characters, which is the maximum // that still prevents address spoofing attacks (see Display Consistency in // README). Callers that need to display less should use a different UI // approach rather than silently making addresses insecure. function truncateMiddle(str, maxLen) { if (str.length <= maxLen) return str; const removed = str.length - maxLen + 1; // +1 for the ellipsis char if (removed > 10) { maxLen = str.length - 10 + 1; } if (maxLen >= str.length) return str; const half = Math.floor((maxLen - 1) / 2); return str.slice(0, half) + "\u2026" + str.slice(-(maxLen - 1 - half)); } // 16 colors evenly spaced around the hue wheel (22.5° apart), // all at HSL saturation 70%, lightness 50% for uniform vibrancy. const ADDRESS_COLORS = [ "#d92626", "#d96926", "#d9ac26", "#c2d926", "#80d926", "#3dd926", "#26d953", "#26d996", "#26d9d9", "#2696d9", "#2653d9", "#3d26d9", "#8026d9", "#c226d9", "#d926ac", "#d92669", ]; function addressColor(address) { const idx = parseInt(address.slice(2, 6), 16) % 16; return ADDRESS_COLORS[idx]; } function addressDotHtml(address) { const color = addressColor(address); return ``; } function escapeHtml(s) { const div = document.createElement("div"); div.textContent = s; return div.innerHTML; } // 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) { const lower = address.toLowerCase(); for (let wi = 0; wi < wallets.length; wi++) { const addrs = wallets[wi].addresses; for (let ai = 0; ai < addrs.length; ai++) { if (addrs[ai].address.toLowerCase() === lower) { return wallets[wi].name + " \u2014 Address " + (ai + 1); } } } return null; } // Render an address with color dot, optional ENS name, optional title, // and optional truncation. Title and ENS are shown as bold labels above // the full address. function formatAddressHtml(address, ensName, maxLen, title) { const dot = addressDotHtml(address); const displayAddr = maxLen ? truncateMiddle(address, maxLen) : address; if (title || ensName) { let html = ""; if (title) { html += `
${dot}${escapeHtml(title)}
`; } if (ensName) { html += `
${title ? "" : dot}${escapeHtml(ensName)}
`; } html += `
${escapeHtml(displayAddr)}
`; return html; } return `
${dot}${escapeHtml(displayAddr)}
`; } 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, hideError, showView, showFlash, balanceLine, balanceLinesForAddress, addressColor, addressDotHtml, escapeHtml, addressTitle, formatAddressHtml, truncateMiddle, isoDate, timeAgo, };