const { $, showView, showFlash, flashCopyFeedback, balanceLinesForAddress, addressDotHtml, addressTitle, escapeHtml, truncateMiddle, renderAddressHtml, attachCopyHandlers, goBack, pushCurrentView, } = require("./helpers"); const { state, currentAddress, saveState } = require("../../shared/state"); const { formatUsd, getAddressValueUsd } = require("../../shared/prices"); const { fetchRecentTransactions, filterTransactions, } = require("../../shared/transactions"); const { resolveEnsNames } = require("../../shared/ens"); const { updateSendBalance, renderSendTokenSelect, resetSendValidation, } = require("./send"); const { log } = require("../../shared/log"); const makeBlockie = require("ethereum-blockies-base64"); const { decryptWithPassword } = require("../../shared/vault"); const { getSignerForAddress } = require("../../shared/wallet"); let ctx; function show() { state.selectedToken = null; 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 " + (ai + 1); const blockieEl = $("address-jazzicon"); blockieEl.innerHTML = ""; const img = document.createElement("img"); img.src = makeBlockie(addr.address); img.width = 48; img.height = 48; img.style.imageRendering = "pixelated"; img.style.borderRadius = "50%"; blockieEl.appendChild(img); const addrTitle = addressTitle(addr.address, state.wallets); $("address-line").innerHTML = renderAddressHtml(addr.address, { title: addrTitle, ensName: addr.ensName, }); $("address-line").dataset.full = addr.address; attachCopyHandlers($("address-line")); const usdTotal = formatUsd(getAddressValueUsd(addr)); $("address-usd-total").innerHTML = usdTotal || " "; const ensEl = $("address-ens"); // ENS is now shown inside renderAddressHtml, hide the separate element ensEl.classList.add("hidden"); $("address-balances").innerHTML = balanceLinesForAddress( addr, state.trackedTokens, state.showZeroBalanceTokens, ); $("address-balances") .querySelectorAll(".balance-row") .forEach((row) => { row.addEventListener("click", () => { state.selectedToken = row.dataset.token; ctx.showAddressToken(); }); }); renderSendTokenSelect(addr); $("tx-list").innerHTML = '
Loading...
'; showView("address"); loadTransactions(addr.address); } function isoDate(timestamp) { const d = new Date(timestamp * 1000); const pad = (n) => String(n).padStart(2, "0"); if (state.utcTimestamps) { return ( d.getUTCFullYear() + "-" + pad(d.getUTCMonth() + 1) + "-" + pad(d.getUTCDate()) + "T" + pad(d.getUTCHours()) + ":" + pad(d.getUTCMinutes()) + ":" + pad(d.getUTCSeconds()) + "Z" ); } const offsetMin = -d.getTimezoneOffset(); const sign = offsetMin >= 0 ? "+" : "-"; const absOff = Math.abs(offsetMin); const tzStr = sign + pad(Math.floor(absOff / 60)) + ":" + pad(absOff % 60); return ( d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate()) + "T" + pad(d.getHours()) + ":" + pad(d.getMinutes()) + ":" + pad(d.getSeconds()) + tzStr ); } 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 rawTxs = await fetchRecentTransactions( address, state.blockscoutUrl, ); const result = filterTransactions(rawTxs, { hideLowHolderTokens: state.hideLowHolderTokens, hideFraudContracts: state.hideFraudContracts, hideDustTransactions: state.hideDustTransactions, dustThresholdGwei: state.dustThresholdGwei, fraudContracts: state.fraudContracts, }); const txs = result.transactions; // Persist any newly discovered fraud contracts if (result.newFraudContracts.length > 0) { for (const addr of result.newFraudContracts) { if (!state.fraudContracts.includes(addr)) { state.fraudContracts.push(addr); } } await saveState(); } loadedTxs = txs; // Collect ALL unique addresses (from + to) for ENS resolution so // that reverse lookups work for every displayed address, not just // the ones that were originally entered as ENS names. const counterparties = [ ...new Set(txs.flatMap((tx) => [tx.from, tx.to].filter(Boolean))), ]; 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 = '
Failed to load transactions.
'; } } function renderTransactions(txs) { const list = $("tx-list"); if (txs.length === 0) { list.innerHTML = '
No transactions found.
'; return; } let html = ""; let i = 0; for (const tx of txs) { // For swap transactions, show the user's own labelled wallet // address instead of the contract address (see issue #55). const counterparty = tx.direction === "contract" && tx.directionLabel === "Swap" ? tx.from : tx.direction === "sent" || tx.direction === "contract" ? tx.to : tx.from; const ensName = ensNameMap.get(counterparty) || null; const title = addressTitle(counterparty, state.wallets); const dirLabel = tx.directionLabel; const amountStr = tx.value ? escapeHtml(tx.value + " " + tx.symbol) : escapeHtml(tx.symbol); const maxAddr = Math.max(32, 36 - Math.max(0, amountStr.length - 10)); const displayAddr = title || 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 += `
${dot}${addrStr}${amountStr}
`; html += `
`; i++; } list.innerHTML = html; list.querySelectorAll(".tx-row").forEach((row) => { row.addEventListener("click", () => { const idx = parseInt(row.dataset.tx, 10); const tx = loadedTxs[idx]; const counterparty = tx.direction === "sent" ? tx.to : tx.from; tx.fromEns = ensNameMap.get(tx.from) || null; tx.toEns = ensNameMap.get(tx.to) || null; ctx.showTransactionDetail(tx); }); }); } function init(_ctx) { ctx = _ctx; $("btn-address-back").addEventListener("click", () => { goBack(); }); $("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 = ""; $("send-token").classList.remove("hidden"); $("send-token-static").classList.add("hidden"); updateSendBalance(); resetSendValidation(); pushCurrentView(); showView("send"); }); $("btn-receive").addEventListener("click", () => { ctx.showReceive(); }); $("btn-add-token").addEventListener("click", ctx.showAddTokenView); // More menu dropdown const moreBtn = $("btn-more-menu"); const moreDropdown = $("more-menu-dropdown"); moreBtn.addEventListener("click", (e) => { e.stopPropagation(); const isOpen = !moreDropdown.classList.toggle("hidden"); moreBtn.classList.toggle("bg-fg", isOpen); moreBtn.classList.toggle("text-bg", isOpen); }); document.addEventListener("click", () => { moreDropdown.classList.add("hidden"); moreBtn.classList.remove("bg-fg", "text-bg"); }); moreDropdown.addEventListener("click", (e) => { e.stopPropagation(); }); $("btn-export-privkey").addEventListener("click", () => { moreDropdown.classList.add("hidden"); moreBtn.classList.remove("bg-fg", "text-bg"); pushCurrentView(); const wallet = state.wallets[state.selectedWallet]; const addr = wallet.addresses[state.selectedAddress]; const blockieEl = $("export-privkey-jazzicon"); blockieEl.innerHTML = ""; const bImg = document.createElement("img"); bImg.src = makeBlockie(addr.address); bImg.width = 48; bImg.height = 48; bImg.style.imageRendering = "pixelated"; bImg.style.borderRadius = "50%"; blockieEl.appendChild(bImg); $("export-privkey-title").textContent = wallet.name + " \u2014 Address " + (state.selectedAddress + 1); const exportAddrContainer = $("export-privkey-dot").parentElement; exportAddrContainer.innerHTML = renderAddressHtml(addr.address); attachCopyHandlers(exportAddrContainer); $("export-privkey-password").value = ""; $("export-privkey-flash").textContent = ""; $("export-privkey-flash").style.visibility = "hidden"; $("export-privkey-password-section").classList.remove("hidden"); $("export-privkey-result").classList.add("hidden"); $("export-privkey-value").textContent = ""; showView("export-privkey"); }); $("btn-export-privkey-confirm").addEventListener("click", async () => { const password = $("export-privkey-password").value; if (!password) { $("export-privkey-flash").textContent = "Password is required."; $("export-privkey-flash").style.visibility = "visible"; return; } const btn = $("btn-export-privkey-confirm"); btn.disabled = true; btn.classList.add("text-muted"); const wallet = state.wallets[state.selectedWallet]; try { const secret = await decryptWithPassword( wallet.encryptedSecret, password, ); const signer = getSignerForAddress( wallet, state.selectedAddress, secret, ); const privateKey = signer.privateKey; $("export-privkey-password-section").classList.add("hidden"); $("export-privkey-value").textContent = privateKey; $("export-privkey-result").classList.remove("hidden"); $("export-privkey-flash").style.visibility = "hidden"; } catch { $("export-privkey-flash").textContent = "Wrong password."; $("export-privkey-flash").style.visibility = "visible"; } finally { btn.disabled = false; btn.classList.remove("text-muted"); } }); $("export-privkey-value").addEventListener("click", () => { const key = $("export-privkey-value").textContent; if (key) { navigator.clipboard.writeText(key); showFlash("Copied!"); flashCopyFeedback($("export-privkey-value")); } }); $("btn-export-privkey-back").addEventListener("click", () => { $("export-privkey-value").textContent = ""; $("export-privkey-password").value = ""; goBack(); }); } module.exports = { init, show };