const { $, showView, showFlash, flashCopyFeedback, balanceLinesForAddress, isoDate, timeAgo, addressDotHtml, addressTitle, escapeHtml, truncateMiddle, } = require("./helpers"); const { state, saveState, currentAddress, currentNetwork, } = require("../../shared/state"); const { updateSendBalance, renderSendTokenSelect, resetSendValidation, } = require("./send"); const { deriveAddressFromXpub } = require("../../shared/wallet"); const { formatUsd, getPrice, getAddressValueUsd, } = require("../../shared/prices"); const { fetchRecentTransactions, filterTransactions, } = require("../../shared/transactions"); const { log } = require("../../shared/log"); function findActiveAddr() { for (const w of state.wallets) { for (const a of w.addresses) { if (a.address === state.activeAddress) return a; } } return null; } function renderTotalValue() { const el = $("total-value"); const subEl = $("total-value-sub"); const priceEl = $("eth-price-display"); if (!el) return; const ethPrice = getPrice("ETH"); if (priceEl) { priceEl.innerHTML = ethPrice ? formatUsd(ethPrice) + " USD/ETH" : " "; } const addr = findActiveAddr(); if (!addr) { el.innerHTML = " "; if (subEl) subEl.innerHTML = " "; return; } const ethBal = parseFloat(addr.balance || "0"); const ethStr = ethBal.toFixed(4) + " ETH"; const ethUsd = ethPrice ? " (" + formatUsd(ethBal * ethPrice) + ")" : ""; el.textContent = ethStr + ethUsd; if (subEl) { const totalUsd = getAddressValueUsd(addr); subEl.innerHTML = totalUsd !== null ? "Total: " + formatUsd(totalUsd) : " "; } } const EXT_ICON = `` + `` + `` + `` + ``; function renderActiveAddress() { const el = $("active-address-display"); if (!el) return; if (state.activeAddress) { const addr = state.activeAddress; const dot = addressDotHtml(addr); const link = `${currentNetwork().explorerUrl}/address/${addr}`; el.innerHTML = `${dot}${escapeHtml(addr)}` + `${EXT_ICON}`; $("active-addr-copy").addEventListener("click", (e) => { navigator.clipboard.writeText(addr); showFlash("Copied!"); flashCopyFeedback(e.currentTarget); }); } else { el.textContent = ""; } } let homeTxs = []; function renderHomeTxList(ctx) { const list = $("home-tx-list"); if (!list) return; if (homeTxs.length === 0) { list.innerHTML = '
No transactions found.
'; return; } let html = ""; let i = 0; for (const tx of homeTxs) { // For swap transactions, show the user's own labelled wallet // address (the one that initiated the swap) instead of the // contract address which is not useful in the list view. const counterparty = tx.direction === "contract" && tx.directionLabel === "Swap" ? tx.from : tx.direction === "sent" || tx.direction === "contract" ? tx.to : tx.from; const dirLabel = tx.directionLabel; const amountStr = tx.value ? escapeHtml(tx.value + " " + tx.symbol) : escapeHtml(tx.symbol); const title = addressTitle(counterparty, state.wallets); const maxAddr = Math.max(32, 36 - Math.max(0, amountStr.length - 10)); const displayAddr = title || 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(".home-tx-row").forEach((row) => { row.addEventListener("click", () => { const idx = parseInt(row.dataset.tx, 10); const tx = homeTxs[idx]; // Set selectedWallet/selectedAddress so back navigation works for (let wi = 0; wi < state.wallets.length; wi++) { for ( let ai = 0; ai < state.wallets[wi].addresses.length; ai++ ) { const addr = state.wallets[wi].addresses[ai].address; if ( addr.toLowerCase() === tx.from.toLowerCase() || addr.toLowerCase() === tx.to.toLowerCase() ) { state.selectedWallet = wi; state.selectedAddress = ai; state.selectedToken = null; ctx.showTransactionDetail(tx); return; } } } }); }); } async function loadHomeTxs(ctx) { const allAddresses = []; for (const w of state.wallets) { for (const a of w.addresses) { allAddresses.push(a.address); } } if (allAddresses.length === 0) return; const filters = { hideLowHolderTokens: state.hideLowHolderTokens, hideFraudContracts: state.hideFraudContracts, hideDustTransactions: state.hideDustTransactions, dustThresholdGwei: state.dustThresholdGwei, fraudContracts: state.fraudContracts, }; try { const fetches = allAddresses.map((addr) => fetchRecentTransactions(addr, state.blockscoutUrl), ); const results = await Promise.all(fetches); // Merge, deduplicate by hash, filter, sort, take 25 const seen = new Set(); let merged = []; for (const txs of results) { for (const tx of txs) { if (seen.has(tx.hash)) continue; seen.add(tx.hash); merged.push(tx); } } const filtered = filterTransactions(merged, filters); // Persist any newly discovered fraud contracts if (filtered.newFraudContracts.length > 0) { for (const addr of filtered.newFraudContracts) { if (!state.fraudContracts.includes(addr)) { state.fraudContracts.push(addr); } } await saveState(); } merged = filtered.transactions; merged.sort((a, b) => b.blockNumber - a.blockNumber); homeTxs = merged.slice(0, 25); renderHomeTxList(ctx); } catch (e) { log.errorf("loadHomeTxs failed:", e.message); const list = $("home-tx-list"); if (list) { list.innerHTML = '
Failed to load transactions.
'; } } } function render(ctx) { const container = $("wallet-list"); if (state.wallets.length === 0) { container.innerHTML = '

No wallets yet. Add one to get started.

'; renderTotalValue(); renderActiveAddress(); return; } let html = ""; state.wallets.forEach((wallet, wi) => { html += `
`; html += `
`; html += `${wallet.name}`; if (wallet.type === "hd" || wallet.type === "xprv") { html += ``; } html += `
`; wallet.addresses.forEach((addr, ai) => { html += `
`; const isActive = state.activeAddress === addr.address; const infoBtn = `[info]`; const dot = addressDotHtml(addr.address); const titleBold = isActive ? "font-bold" : ""; html += `
Address ${ai + 1}
`; if (addr.ensName) { html += `
${dot}${addr.ensName}
`; } html += `
`; html += `${addr.ensName ? "" : dot}${addr.address}`; html += `${infoBtn}`; html += `
`; const addrUsd = formatUsd(getAddressValueUsd(addr)); html += `
${addrUsd || " "}
`; html += balanceLinesForAddress( addr, state.trackedTokens, state.showZeroBalanceTokens, ); html += `
`; }); html += `
`; }); container.innerHTML = html; container.querySelectorAll(".address-row").forEach((row) => { row.addEventListener("click", async () => { const wi = parseInt(row.dataset.wallet, 10); const ai = parseInt(row.dataset.address, 10); const addr = state.wallets[wi].addresses[ai].address; if (state.activeAddress !== addr) { state.activeAddress = addr; await saveState(); render(ctx); const runtime = typeof browser !== "undefined" ? browser.runtime : chrome.runtime; runtime.sendMessage({ type: "AUTISTMASK_ACTIVE_CHANGED" }); } }); }); container.querySelectorAll(".btn-addr-info").forEach((btn) => { btn.addEventListener("click", (e) => { e.stopPropagation(); state.selectedWallet = parseInt(btn.dataset.wallet, 10); state.selectedAddress = parseInt(btn.dataset.address, 10); ctx.showAddressDetail(); }); }); container.querySelectorAll(".btn-add-address").forEach((btn) => { btn.addEventListener("click", async (e) => { e.stopPropagation(); const wi = parseInt(btn.dataset.wallet, 10); const wallet = state.wallets[wi]; const newAddr = deriveAddressFromXpub( wallet.xpub, wallet.nextIndex, ); wallet.addresses.push({ address: newAddr, balance: "0.0000", tokenBalances: [], }); wallet.nextIndex++; await saveState(); render(ctx); ctx.doRefreshAndRender(); }); }); container.querySelectorAll(".wallet-name").forEach((span) => { span.addEventListener("click", (e) => { e.stopPropagation(); const wi = parseInt(span.dataset.wallet, 10); const wallet = state.wallets[wi]; const input = document.createElement("input"); input.type = "text"; input.value = wallet.name; input.className = "font-bold border border-border p-0 bg-bg text-fg"; input.style.width = "100%"; const save = async () => { const val = input.value.trim(); if (val && val !== wallet.name) { wallet.name = val; await saveState(); } render(ctx); }; input.addEventListener("blur", save); input.addEventListener("keydown", (ev) => { if (ev.key === "Enter") input.blur(); if (ev.key === "Escape") { input.value = wallet.name; input.blur(); } }); span.replaceWith(input); input.focus(); input.select(); }); }); renderTotalValue(); renderActiveAddress(); loadHomeTxs(ctx); } function selectActiveAddress() { for (let wi = 0; wi < state.wallets.length; wi++) { for (let ai = 0; ai < state.wallets[wi].addresses.length; ai++) { if ( state.wallets[wi].addresses[ai].address === state.activeAddress ) { state.selectedWallet = wi; state.selectedAddress = ai; return true; } } } return false; } function init(ctx) { $("btn-add-wallet-bottom").addEventListener("click", ctx.showAddWalletView); $("btn-main-send").addEventListener("click", () => { if (!selectActiveAddress()) { showFlash("No active address selected."); return; } const addr = currentAddress(); 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"); renderSendTokenSelect(addr); updateSendBalance(); resetSendValidation(); showView("send"); }); $("btn-main-receive").addEventListener("click", () => { if (!selectActiveAddress()) { showFlash("No active address selected."); return; } ctx.showReceive(); }); } module.exports = { init, render };