Show 25 most recent transactions from all addresses on home screen
All checks were successful
check / check (push) Successful in 15s

Fetch transactions for every address across all wallets in parallel,
merge and deduplicate by hash, apply anti-poisoning filters, sort by
block number, and display the top 25 below the wallet list. Clicking
a transaction navigates to the address detail page for the relevant
address. Shows "Loading..." placeholder to prevent layout shift.
This commit is contained in:
2026-02-26 16:41:15 +07:00
parent 4c04dc4878
commit e5ffbb4634
2 changed files with 168 additions and 0 deletions

View File

@@ -217,6 +217,14 @@
<!-- wallet list --> <!-- wallet list -->
<div id="wallet-list"></div> <div id="wallet-list"></div>
<!-- recent transactions across all addresses -->
<div class="border-t border-border mt-3 pt-2">
<h3 class="font-bold text-xs mb-1">Recent Transactions</h3>
<div id="home-tx-list">
<div class="text-muted text-xs py-1">Loading...</div>
</div>
</div>
<div class="text-xs text-muted mt-2"> <div class="text-xs text-muted mt-2">
<span <span
id="btn-add-wallet-bottom" id="btn-add-wallet-bottom"

View File

@@ -5,6 +5,7 @@ const {
balanceLinesForAddress, balanceLinesForAddress,
addressDotHtml, addressDotHtml,
escapeHtml, escapeHtml,
truncateMiddle,
} = require("./helpers"); } = require("./helpers");
const { state, saveState, currentAddress } = require("../../shared/state"); const { state, saveState, currentAddress } = require("../../shared/state");
const { updateSendBalance, renderSendTokenSelect } = require("./send"); const { updateSendBalance, renderSendTokenSelect } = require("./send");
@@ -15,6 +16,11 @@ const {
getPrice, getPrice,
getAddressValueUsd, getAddressValueUsd,
} = require("../../shared/prices"); } = require("../../shared/prices");
const {
fetchRecentTransactions,
filterTransactions,
} = require("../../shared/transactions");
const { log } = require("../../shared/log");
function findActiveAddr() { function findActiveAddr() {
for (const w of state.wallets) { for (const w of state.wallets) {
@@ -80,6 +86,159 @@ function renderActiveAddress() {
} }
} }
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";
}
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())
);
}
let homeTxs = [];
function renderHomeTxList(ctx) {
const list = $("home-tx-list");
if (!list) return;
if (homeTxs.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 homeTxs) {
const counterparty = tx.direction === "sent" ? tx.to : tx.from;
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 = 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="home-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(".home-tx-row").forEach((row) => {
row.addEventListener("click", () => {
const idx = parseInt(row.dataset.tx, 10);
const tx = homeTxs[idx];
// Find which wallet/address this tx belongs to and navigate
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;
ctx.showAddressDetail();
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 =
'<div class="text-muted text-xs py-1">Failed to load transactions.</div>';
}
}
}
function render(ctx) { function render(ctx) {
const container = $("wallet-list"); const container = $("wallet-list");
if (state.wallets.length === 0) { if (state.wallets.length === 0) {
@@ -211,6 +370,7 @@ function render(ctx) {
renderTotalValue(); renderTotalValue();
renderActiveAddress(); renderActiveAddress();
loadHomeTxs(ctx);
} }
function selectActiveAddress() { function selectActiveAddress() {