Show 25 most recent transactions from all addresses on home screen
All checks were successful
check / check (push) Successful in 15s
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:
@@ -217,6 +217,14 @@
|
||||
<!-- wallet list -->
|
||||
<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">
|
||||
<span
|
||||
id="btn-add-wallet-bottom"
|
||||
|
||||
@@ -5,6 +5,7 @@ const {
|
||||
balanceLinesForAddress,
|
||||
addressDotHtml,
|
||||
escapeHtml,
|
||||
truncateMiddle,
|
||||
} = require("./helpers");
|
||||
const { state, saveState, currentAddress } = require("../../shared/state");
|
||||
const { updateSendBalance, renderSendTokenSelect } = require("./send");
|
||||
@@ -15,6 +16,11 @@ const {
|
||||
getPrice,
|
||||
getAddressValueUsd,
|
||||
} = require("../../shared/prices");
|
||||
const {
|
||||
fetchRecentTransactions,
|
||||
filterTransactions,
|
||||
} = require("../../shared/transactions");
|
||||
const { log } = require("../../shared/log");
|
||||
|
||||
function findActiveAddr() {
|
||||
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) {
|
||||
const container = $("wallet-list");
|
||||
if (state.wallets.length === 0) {
|
||||
@@ -211,6 +370,7 @@ function render(ctx) {
|
||||
|
||||
renderTotalValue();
|
||||
renderActiveAddress();
|
||||
loadHomeTxs(ctx);
|
||||
}
|
||||
|
||||
function selectActiveAddress() {
|
||||
|
||||
Reference in New Issue
Block a user