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>
|
<h2 class="font-bold" id="address-title">Address</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="address-ens" class="font-bold mb-1 hidden"></div>
|
|
||||||
<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"
|
title="Click to copy"
|
||||||
>
|
>
|
||||||
|
<span id="address-dot"></span>
|
||||||
<span
|
<span
|
||||||
id="address-full"
|
id="address-full"
|
||||||
style="width: 42ch; max-width: 100%"
|
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 { state, currentAddress } = require("../../shared/state");
|
||||||
const { formatUsd, getAddressValueUsd } = require("../../shared/prices");
|
const { formatUsd, getAddressValueUsd } = require("../../shared/prices");
|
||||||
const { fetchRecentTransactions } = require("../../shared/transactions");
|
const { fetchRecentTransactions } = require("../../shared/transactions");
|
||||||
|
const { resolveEnsNames } = require("../../shared/ens");
|
||||||
const { updateSendBalance } = require("./send");
|
const { updateSendBalance } = require("./send");
|
||||||
const { log } = require("../../shared/log");
|
const { log } = require("../../shared/log");
|
||||||
const QRCode = require("qrcode");
|
const QRCode = require("qrcode");
|
||||||
@@ -13,11 +20,13 @@ function show() {
|
|||||||
const ai = state.selectedAddress;
|
const ai = state.selectedAddress;
|
||||||
$("address-title").textContent =
|
$("address-title").textContent =
|
||||||
wallet.name + " \u2014 Address " + (wi + 1) + "." + (ai + 1);
|
wallet.name + " \u2014 Address " + (wi + 1) + "." + (ai + 1);
|
||||||
|
$("address-dot").innerHTML = addressDotHtml(addr.address);
|
||||||
$("address-full").textContent = addr.address;
|
$("address-full").textContent = addr.address;
|
||||||
$("address-usd-total").textContent = formatUsd(getAddressValueUsd(addr));
|
$("address-usd-total").textContent = formatUsd(getAddressValueUsd(addr));
|
||||||
const ensEl = $("address-ens");
|
const ensEl = $("address-ens");
|
||||||
if (addr.ensName) {
|
if (addr.ensName) {
|
||||||
ensEl.textContent = addr.ensName;
|
ensEl.innerHTML =
|
||||||
|
addressDotHtml(addr.address) + escapeHtml(addr.ensName);
|
||||||
ensEl.classList.remove("hidden");
|
ensEl.classList.remove("hidden");
|
||||||
} else {
|
} else {
|
||||||
ensEl.classList.add("hidden");
|
ensEl.classList.add("hidden");
|
||||||
@@ -80,10 +89,30 @@ function escapeHtml(s) {
|
|||||||
|
|
||||||
let loadedTxs = [];
|
let loadedTxs = [];
|
||||||
|
|
||||||
|
let ensNameMap = new Map();
|
||||||
|
|
||||||
async function loadTransactions(address) {
|
async function loadTransactions(address) {
|
||||||
try {
|
try {
|
||||||
const txs = await fetchRecentTransactions(address, state.blockscoutUrl);
|
const txs = await fetchRecentTransactions(address, state.blockscoutUrl);
|
||||||
loadedTxs = txs;
|
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);
|
renderTransactions(txs);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.errorf("loadTransactions failed:", e.message);
|
log.errorf("loadTransactions failed:", e.message);
|
||||||
@@ -103,17 +132,20 @@ function renderTransactions(txs) {
|
|||||||
let i = 0;
|
let i = 0;
|
||||||
for (const tx of txs) {
|
for (const tx of txs) {
|
||||||
const counterparty = tx.direction === "sent" ? tx.to : tx.from;
|
const counterparty = tx.direction === "sent" ? tx.to : tx.from;
|
||||||
|
const ensName = ensNameMap.get(counterparty) || null;
|
||||||
const dirLabel = tx.direction === "sent" ? "Sent" : "Received";
|
const dirLabel = tx.direction === "sent" ? "Sent" : "Received";
|
||||||
const amountStr = escapeHtml(tx.value + " " + tx.symbol);
|
const amountStr = escapeHtml(tx.value + " " + tx.symbol);
|
||||||
const maxAddr = Math.max(10, 38 - Math.max(0, amountStr.length - 10));
|
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 err = tx.isError ? " (failed)" : "";
|
||||||
const opacity = tx.isError ? " opacity:0.5;" : "";
|
const opacity = tx.isError ? " opacity:0.5;" : "";
|
||||||
const ago = escapeHtml(timeAgo(tx.timestamp));
|
const ago = escapeHtml(timeAgo(tx.timestamp));
|
||||||
const iso = escapeHtml(isoDate(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="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 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>`;
|
html += `</div>`;
|
||||||
i++;
|
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) {
|
function showTxDetail(tx) {
|
||||||
$("tx-detail-hash").textContent = tx.hash;
|
$("tx-detail-hash").textContent = tx.hash;
|
||||||
$("tx-detail-from").textContent = tx.from;
|
$("tx-detail-from").innerHTML = txDetailAddressHtml(tx.from);
|
||||||
$("tx-detail-to").textContent = tx.to;
|
$("tx-detail-to").innerHTML = txDetailAddressHtml(tx.to);
|
||||||
$("tx-detail-value").textContent = tx.value + " " + tx.symbol;
|
$("tx-detail-value").textContent = tx.value + " " + tx.symbol;
|
||||||
$("tx-detail-time").textContent =
|
$("tx-detail-time").textContent =
|
||||||
isoDate(tx.timestamp) + " (" + timeAgo(tx.timestamp) + ")";
|
isoDate(tx.timestamp) + " (" + timeAgo(tx.timestamp) + ")";
|
||||||
|
|||||||
@@ -99,6 +99,35 @@ function balanceLinesForAddress(addr) {
|
|||||||
return html;
|
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 = {
|
module.exports = {
|
||||||
$,
|
$,
|
||||||
showError,
|
showError,
|
||||||
@@ -106,4 +135,6 @@ module.exports = {
|
|||||||
showView,
|
showView,
|
||||||
showFlash,
|
showFlash,
|
||||||
balanceLinesForAddress,
|
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 { state, saveState } = require("../../shared/state");
|
||||||
const { deriveAddressFromXpub } = require("../../shared/wallet");
|
const { deriveAddressFromXpub } = require("../../shared/wallet");
|
||||||
const {
|
const {
|
||||||
@@ -35,12 +41,13 @@ function render(ctx) {
|
|||||||
wallet.addresses.forEach((addr, ai) => {
|
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="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>`;
|
html += `<div class="text-xs font-bold">Address ${wi + 1}.${ai + 1}</div>`;
|
||||||
|
const dot = addressDotHtml(addr.address);
|
||||||
if (addr.ensName) {
|
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));
|
const addrUsd = formatUsd(getAddressValueUsd(addr));
|
||||||
html += `<div class="flex text-xs">`;
|
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 += `<span class="text-right text-muted flex-1">${addrUsd}</span>`;
|
||||||
html += `</div>`;
|
html += `</div>`;
|
||||||
html += balanceLinesForAddress(addr);
|
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