All checks were successful
check / check (push) Successful in 22s
- Moved 'Export private key' from prominent button row to a small muted text link at the bottom of the address detail view - Added 'export-privkey' to the VIEWS array in helpers.js — this was the cause of the blank view (showView toggled all known views but didn't know about export-privkey, so it was never unhidden)
279 lines
7.8 KiB
JavaScript
279 lines
7.8 KiB
JavaScript
// 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 (
|
|
`<div class="flex text-xs${clickClass}"${tokenAttr}>` +
|
|
`<span class="flex justify-between" style="width:42ch;max-width:100%">` +
|
|
`<span>${symbol}</span>` +
|
|
`<span>${qty}</span>` +
|
|
`</span>` +
|
|
`<span class="text-right text-muted flex-1">${usd}</span>` +
|
|
`</div>`
|
|
);
|
|
}
|
|
|
|
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 `<span style="width:8px;height:8px;border-radius:50%;display:inline-block;background:${color};margin-right:4px;vertical-align:middle;flex-shrink:0;"></span>`;
|
|
}
|
|
|
|
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 += `<div class="flex items-center font-bold">${dot}${escapeHtml(title)}</div>`;
|
|
}
|
|
if (ensName) {
|
|
html += `<div class="flex items-center font-bold">${title ? "" : dot}${escapeHtml(ensName)}</div>`;
|
|
}
|
|
html += `<div class="break-all">${escapeHtml(displayAddr)}</div>`;
|
|
return html;
|
|
}
|
|
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,
|
|
hideError,
|
|
showView,
|
|
showFlash,
|
|
balanceLine,
|
|
balanceLinesForAddress,
|
|
addressColor,
|
|
addressDotHtml,
|
|
escapeHtml,
|
|
addressTitle,
|
|
formatAddressHtml,
|
|
truncateMiddle,
|
|
isoDate,
|
|
timeAgo,
|
|
};
|