Files
AutistMask/src/popup/views/addressDetail.js
sneak 2aebf3ddf9
Some checks failed
check / check (push) Has been cancelled
Show full address without truncation on address detail page
2026-02-26 12:40:57 +07:00

241 lines
8.3 KiB
JavaScript

const {
$,
showView,
showFlash,
balanceLinesForAddress,
addressDotHtml,
escapeHtml,
formatAddressHtml,
truncateMiddle,
} = 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, renderSendTokenSelect } = require("./send");
const { log } = require("../../shared/log");
const QRCode = require("qrcode");
function show() {
const wallet = state.wallets[state.selectedWallet];
const addr = wallet.addresses[state.selectedAddress];
const wi = state.selectedWallet;
const ai = state.selectedAddress;
$("address-title").textContent =
wallet.name + " \u2014 Address " + (wi + 1) + "." + (ai + 1);
$("address-dot").innerHTML = addressDotHtml(addr.address);
$("address-full").dataset.full = addr.address;
$("address-full").textContent = addr.address;
$("address-usd-total").textContent = formatUsd(getAddressValueUsd(addr));
const ensEl = $("address-ens");
if (addr.ensName) {
ensEl.innerHTML =
addressDotHtml(addr.address) + escapeHtml(addr.ensName);
ensEl.classList.remove("hidden");
} else {
ensEl.classList.add("hidden");
}
$("address-balances").innerHTML = balanceLinesForAddress(addr);
renderSendTokenSelect(addr);
$("tx-list").innerHTML =
'<div class="text-muted text-xs py-1">Loading...</div>';
showView("address");
loadTransactions(addr.address);
}
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";
}
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);
$("tx-list").innerHTML =
'<div class="text-muted text-xs py-1">Failed to load transactions.</div>';
}
}
function renderTransactions(txs) {
const list = $("tx-list");
if (txs.length === 0) {
list.innerHTML =
'<div class="text-muted text-xs py-1">No transactions found.</div>';
return;
}
let html = "";
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, 36 - Math.max(0, amountStr.length - 10));
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 class="flex items-center">${dot}${addrStr}</span><span>${amountStr}</span></div>`;
html += `</div>`;
i++;
}
list.innerHTML = html;
list.querySelectorAll(".tx-row").forEach((row) => {
row.addEventListener("click", () => {
const idx = parseInt(row.dataset.tx, 10);
showTxDetail(loadedTxs[idx]);
});
});
}
function etherscanAddressLink(address) {
return `https://etherscan.io/address/${address}`;
}
function etherscanTxLink(hash) {
return `https://etherscan.io/tx/${hash}`;
}
function txDetailAddressHtml(address) {
const ensName = ensNameMap.get(address) || null;
const dot = addressDotHtml(address);
const link = etherscanAddressLink(address);
if (ensName) {
return (
dot +
`<a href="${link}" target="_blank" class="underline decoration-dashed">${escapeHtml(ensName)}</a>` +
`<div class="break-all"><a href="${link}" target="_blank" class="underline decoration-dashed">${escapeHtml(address)}</a></div>`
);
}
return (
dot +
`<a href="${link}" target="_blank" class="underline decoration-dashed break-all">${escapeHtml(address)}</a>`
);
}
function showTxDetail(tx) {
const txLink = etherscanTxLink(tx.hash);
$("tx-detail-hash").innerHTML =
`<a href="${txLink}" target="_blank" class="underline decoration-dashed">${escapeHtml(tx.hash)}</a>`;
$("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) + ")";
$("tx-detail-status").textContent = tx.isError ? "Failed" : "Success";
showView("transaction");
}
function init(ctx) {
$("address-full").addEventListener("click", () => {
const addr = $("address-full").dataset.full;
if (addr) {
navigator.clipboard.writeText(addr);
showFlash("Copied!");
}
});
$("btn-address-back").addEventListener("click", () => {
ctx.renderWalletList();
showView("main");
});
$("btn-send").addEventListener("click", () => {
const addr =
state.wallets[state.selectedWallet].addresses[
state.selectedAddress
];
if (!addr.balance || parseFloat(addr.balance) === 0) {
showFlash("Cannot send \u2014 zero balance.");
return;
}
$("send-to").value = "";
$("send-amount").value = "";
updateSendBalance();
showView("send");
});
$("btn-receive").addEventListener("click", () => {
const addr = currentAddress();
const address = addr ? addr.address : "";
$("receive-address").textContent = address;
if (address) {
QRCode.toCanvas($("receive-qr"), address, {
width: 200,
margin: 2,
color: { dark: "#000000", light: "#ffffff" },
});
}
showView("receive");
});
$("btn-add-token").addEventListener("click", ctx.showAddTokenView);
$("btn-tx-back").addEventListener("click", () => {
show();
});
}
module.exports = { init, show };