diff --git a/src/popup/index.html b/src/popup/index.html index 8c5ec69..4e423f1 100644 --- a/src/popup/index.html +++ b/src/popup/index.html @@ -196,11 +196,15 @@

Address

-
+
+ (tx.direction === "sent" ? tx.to : tx.from)), + ), + ]; + if (counterparties.length > 0) { + try { + ensNameMap = await resolveEnsNames( + counterparties, + state.rpcUrl, + ); + } catch { + ensNameMap = new Map(); + } + } + renderTransactions(txs); } catch (e) { log.errorf("loadTransactions failed:", e.message); @@ -103,17 +132,20 @@ function renderTransactions(txs) { let i = 0; for (const tx of txs) { const counterparty = tx.direction === "sent" ? tx.to : tx.from; + const ensName = ensNameMap.get(counterparty) || null; const dirLabel = tx.direction === "sent" ? "Sent" : "Received"; const amountStr = escapeHtml(tx.value + " " + tx.symbol); const maxAddr = Math.max(10, 38 - Math.max(0, amountStr.length - 10)); - const addrStr = escapeHtml(truncateMiddle(counterparty, maxAddr)); + const displayAddr = ensName || truncateMiddle(counterparty, maxAddr); + const addrStr = escapeHtml(displayAddr); + const dot = addressDotHtml(counterparty); const err = tx.isError ? " (failed)" : ""; const opacity = tx.isError ? " opacity:0.5;" : ""; const ago = escapeHtml(timeAgo(tx.timestamp)); const iso = escapeHtml(isoDate(tx.timestamp)); html += `
`; html += `
${ago}${dirLabel}${err}
`; - html += `
${addrStr}${amountStr}
`; + html += `
${dot}${addrStr}${amountStr}
`; html += `
`; i++; } @@ -126,10 +158,23 @@ function renderTransactions(txs) { }); } +function txDetailAddressHtml(address) { + const ensName = ensNameMap.get(address) || null; + const dot = addressDotHtml(address); + if (ensName) { + return ( + dot + + escapeHtml(ensName) + + `
${escapeHtml(address)}
` + ); + } + return dot + escapeHtml(address); +} + function showTxDetail(tx) { $("tx-detail-hash").textContent = tx.hash; - $("tx-detail-from").textContent = tx.from; - $("tx-detail-to").textContent = tx.to; + $("tx-detail-from").innerHTML = txDetailAddressHtml(tx.from); + $("tx-detail-to").innerHTML = txDetailAddressHtml(tx.to); $("tx-detail-value").textContent = tx.value + " " + tx.symbol; $("tx-detail-time").textContent = isoDate(tx.timestamp) + " (" + timeAgo(tx.timestamp) + ")"; diff --git a/src/popup/views/helpers.js b/src/popup/views/helpers.js index 501eb2c..fb48ca8 100644 --- a/src/popup/views/helpers.js +++ b/src/popup/views/helpers.js @@ -99,6 +99,35 @@ function balanceLinesForAddress(addr) { return html; } +const ADDRESS_COLORS = [ + "#e6194b", + "#3cb44b", + "#4363d8", + "#f58231", + "#911eb4", + "#42d4f4", + "#f032e6", + "#bfef45", + "#fabed4", + "#469990", + "#dcbeff", + "#9a6324", + "#800000", + "#aaffc3", + "#808000", + "#000075", +]; + +function addressColor(address) { + const idx = parseInt(address.slice(2, 6), 16) % 16; + return ADDRESS_COLORS[idx]; +} + +function addressDotHtml(address) { + const color = addressColor(address); + return ``; +} + module.exports = { $, showError, @@ -106,4 +135,6 @@ module.exports = { showView, showFlash, balanceLinesForAddress, + addressColor, + addressDotHtml, }; diff --git a/src/popup/views/home.js b/src/popup/views/home.js index 8a59061..e36f69e 100644 --- a/src/popup/views/home.js +++ b/src/popup/views/home.js @@ -1,4 +1,10 @@ -const { $, showView, showFlash, balanceLinesForAddress } = require("./helpers"); +const { + $, + showView, + showFlash, + balanceLinesForAddress, + addressDotHtml, +} = require("./helpers"); const { state, saveState } = require("../../shared/state"); const { deriveAddressFromXpub } = require("../../shared/wallet"); const { @@ -35,12 +41,13 @@ function render(ctx) { wallet.addresses.forEach((addr, ai) => { html += `
`; html += `
Address ${wi + 1}.${ai + 1}
`; + const dot = addressDotHtml(addr.address); if (addr.ensName) { - html += `
${addr.ensName}
`; + html += `
${dot}${addr.ensName}
`; } const addrUsd = formatUsd(getAddressValueUsd(addr)); html += `
`; - html += `${addr.address}`; + html += `${addr.ensName ? "" : dot}${addr.address}`; html += `${addrUsd}`; html += `
`; html += balanceLinesForAddress(addr); diff --git a/src/shared/ens.js b/src/shared/ens.js new file mode 100644 index 0000000..9b06aa6 --- /dev/null +++ b/src/shared/ens.js @@ -0,0 +1,57 @@ +// Cached ENS reverse resolution. +// Resolves addresses to ENS names via ethers provider.lookupAddress(), +// caching results in localStorage with a 12-hour TTL. + +const { getProvider } = require("./balances"); +const { log } = require("./log"); + +const CACHE_TTL_MS = 43200000; // 12 hours +const CACHE_PREFIX = "ens:"; + +function getCached(address) { + const key = CACHE_PREFIX + address.toLowerCase(); + try { + const raw = localStorage.getItem(key); + if (!raw) return undefined; + const entry = JSON.parse(raw); + if (Date.now() - entry.ts < CACHE_TTL_MS) { + return entry.name; + } + } catch { + // Corrupt cache entry — treat as miss. + } + return undefined; +} + +function setCache(address, name) { + const key = CACHE_PREFIX + address.toLowerCase(); + localStorage.setItem(key, JSON.stringify({ name, ts: Date.now() })); +} + +async function resolveEnsName(address, rpcUrl) { + const cached = getCached(address); + if (cached !== undefined) return cached; + + const provider = getProvider(rpcUrl); + try { + const name = (await provider.lookupAddress(address)) || null; + setCache(address, name); + return name; + } catch (e) { + log.errorf("ENS reverse lookup failed", address, e.message); + setCache(address, null); + return null; + } +} + +async function resolveEnsNames(addresses, rpcUrl) { + const results = new Map(); + await Promise.all( + addresses.map(async (addr) => { + results.set(addr, await resolveEnsName(addr, rpcUrl)); + }), + ); + return results; +} + +module.exports = { resolveEnsName, resolveEnsNames };