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 -->
|
<!-- 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"
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user