Files
AutistMask/src/popup/views/helpers.js
sneak 21fe854fa4
All checks were successful
check / check (push) Successful in 17s
Add address-token detail view for per-token transaction filtering
Clicking a token balance on the address detail view navigates to a
focused view showing only that token's transactions. Send pre-selects
and locks the token dropdown, Receive shows an ERC-20 warning for
non-ETH tokens, and all back buttons return to the correct parent view.
2026-02-27 11:26:59 +07:00

221 lines
5.7 KiB
JavaScript

// Shared DOM helpers used by all views.
const { DEBUG } = require("../../shared/constants");
const {
formatUsd,
getPrice,
getAddressValueUsd,
} = require("../../shared/prices");
const VIEWS = [
"welcome",
"add-wallet",
"import-key",
"main",
"address",
"address-token",
"send",
"confirm-tx",
"receive",
"add-token",
"settings",
"transaction",
"approve-site",
"approve-tx",
];
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();
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;
}
function truncateMiddle(str, maxLen) {
if (str.length <= maxLen) return str;
if (maxLen < 5) return str.slice(0, maxLen);
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>`;
}
module.exports = {
$,
showError,
hideError,
showView,
showFlash,
balanceLine,
balanceLinesForAddress,
addressColor,
addressDotHtml,
escapeHtml,
addressTitle,
formatAddressHtml,
truncateMiddle,
};