Add address color dots and cached ENS reverse lookups
Some checks failed
check / check (push) Has been cancelled
Some checks failed
check / check (push) Has been cancelled
Deterministic colored dots derived from address bytes (16-color palette) displayed before every address. ENS reverse resolution for transaction counterparties with 12-hour localStorage cache.
This commit is contained in:
@@ -196,11 +196,15 @@
|
||||
<h2 class="font-bold" id="address-title">Address</h2>
|
||||
</div>
|
||||
|
||||
<div id="address-ens" class="font-bold mb-1 hidden"></div>
|
||||
<div
|
||||
class="flex text-xs mb-3 cursor-pointer"
|
||||
id="address-ens"
|
||||
class="font-bold mb-1 hidden flex items-center"
|
||||
></div>
|
||||
<div
|
||||
class="flex text-xs mb-3 cursor-pointer items-center"
|
||||
title="Click to copy"
|
||||
>
|
||||
<span id="address-dot"></span>
|
||||
<span
|
||||
id="address-full"
|
||||
style="width: 42ch; max-width: 100%"
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
const { $, showView, showFlash, balanceLinesForAddress } = require("./helpers");
|
||||
const {
|
||||
$,
|
||||
showView,
|
||||
showFlash,
|
||||
balanceLinesForAddress,
|
||||
addressDotHtml,
|
||||
} = require("./helpers");
|
||||
const { state, currentAddress } = require("../../shared/state");
|
||||
const { formatUsd, getAddressValueUsd } = require("../../shared/prices");
|
||||
const { fetchRecentTransactions } = require("../../shared/transactions");
|
||||
const { resolveEnsNames } = require("../../shared/ens");
|
||||
const { updateSendBalance } = require("./send");
|
||||
const { log } = require("../../shared/log");
|
||||
const QRCode = require("qrcode");
|
||||
@@ -13,11 +20,13 @@ function show() {
|
||||
const ai = state.selectedAddress;
|
||||
$("address-title").textContent =
|
||||
wallet.name + " \u2014 Address " + (wi + 1) + "." + (ai + 1);
|
||||
$("address-dot").innerHTML = addressDotHtml(addr.address);
|
||||
$("address-full").textContent = addr.address;
|
||||
$("address-usd-total").textContent = formatUsd(getAddressValueUsd(addr));
|
||||
const ensEl = $("address-ens");
|
||||
if (addr.ensName) {
|
||||
ensEl.textContent = addr.ensName;
|
||||
ensEl.innerHTML =
|
||||
addressDotHtml(addr.address) + escapeHtml(addr.ensName);
|
||||
ensEl.classList.remove("hidden");
|
||||
} else {
|
||||
ensEl.classList.add("hidden");
|
||||
@@ -80,10 +89,30 @@ function escapeHtml(s) {
|
||||
|
||||
let loadedTxs = [];
|
||||
|
||||
let ensNameMap = new Map();
|
||||
|
||||
async function loadTransactions(address) {
|
||||
try {
|
||||
const txs = await fetchRecentTransactions(address, state.blockscoutUrl);
|
||||
loadedTxs = txs;
|
||||
|
||||
// Collect unique counterparty addresses for ENS resolution.
|
||||
const counterparties = [
|
||||
...new Set(
|
||||
txs.map((tx) => (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 += `<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" title="${iso}">${ago}</span><span>${dirLabel}${err}</span></div>`;
|
||||
html += `<div class="flex justify-between"><span>${addrStr}</span><span>${amountStr}</span></div>`;
|
||||
html += `<div class="flex justify-between"><span class="flex items-center">${dot}${addrStr}</span><span>${amountStr}</span></div>`;
|
||||
html += `</div>`;
|
||||
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) +
|
||||
`<div class="break-all">${escapeHtml(address)}</div>`
|
||||
);
|
||||
}
|
||||
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) + ")";
|
||||
|
||||
@@ -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 `<span style="width:8px;height:8px;border-radius:50%;display:inline-block;background:${color};margin-right:4px;vertical-align:middle;flex-shrink:0;"></span>`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
$,
|
||||
showError,
|
||||
@@ -106,4 +135,6 @@ module.exports = {
|
||||
showView,
|
||||
showFlash,
|
||||
balanceLinesForAddress,
|
||||
addressColor,
|
||||
addressDotHtml,
|
||||
};
|
||||
|
||||
@@ -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 += `<div class="address-row py-1 border-b border-border-light cursor-pointer hover:bg-hover" data-wallet="${wi}" data-address="${ai}">`;
|
||||
html += `<div class="text-xs font-bold">Address ${wi + 1}.${ai + 1}</div>`;
|
||||
const dot = addressDotHtml(addr.address);
|
||||
if (addr.ensName) {
|
||||
html += `<div class="text-xs font-bold">${addr.ensName}</div>`;
|
||||
html += `<div class="text-xs font-bold flex items-center">${dot}${addr.ensName}</div>`;
|
||||
}
|
||||
const addrUsd = formatUsd(getAddressValueUsd(addr));
|
||||
html += `<div class="flex text-xs">`;
|
||||
html += `<span style="width:42ch;max-width:100%">${addr.address}</span>`;
|
||||
html += `<span class="flex items-center" style="width:42ch;max-width:100%">${addr.ensName ? "" : dot}${addr.address}</span>`;
|
||||
html += `<span class="text-right text-muted flex-1">${addrUsd}</span>`;
|
||||
html += `</div>`;
|
||||
html += balanceLinesForAddress(addr);
|
||||
|
||||
57
src/shared/ens.js
Normal file
57
src/shared/ens.js
Normal file
@@ -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 };
|
||||
Reference in New Issue
Block a user