const { $, showView, showFlash, flashCopyFeedback, balanceLinesForAddress, addressDotHtml, addressTitle, escapeHtml, truncateMiddle, } = 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; const EXT_ICON = `` + `` + `` + `` + ``; function etherscanAddressLink(address) { return `https://etherscan.io/address/${address}`; } 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); $("address-dot").innerHTML = addressDotHtml(addr.address); $("address-full").dataset.full = addr.address; $("address-full").textContent = addr.address; const addrLink = etherscanAddressLink(addr.address); $("address-etherscan-link").innerHTML = `${EXT_ICON}`; const usdTotal = formatUsd(getAddressValueUsd(addr)); $("address-usd-total").innerHTML = usdTotal || " "; 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, 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"); 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 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; $("address-full").addEventListener("click", () => { const addr = $("address-full").dataset.full; if (addr) { navigator.clipboard.writeText(addr); showFlash("Copied!"); flashCopyFeedback($("address-full")); } }); $("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 = ""; $("send-token").classList.remove("hidden"); $("send-token-static").classList.add("hidden"); updateSendBalance(); resetSendValidation(); 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"); 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); $("export-privkey-dot").innerHTML = addressDotHtml(addr.address); $("export-privkey-address").textContent = addr.address; $("export-privkey-address").dataset.full = addr.address; $("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")); } }); $("export-privkey-address").addEventListener("click", () => { const full = $("export-privkey-address").dataset.full; if (full) { navigator.clipboard.writeText(full); showFlash("Copied!"); flashCopyFeedback($("export-privkey-address")); } }); $("btn-export-privkey-back").addEventListener("click", () => { $("export-privkey-value").textContent = ""; $("export-privkey-password").value = ""; show(); }); } module.exports = { init, show };