All checks were successful
check / check (push) Successful in 13s
Transaction list entries are now two lines with more spacing: - Line 1: humanized age (hover for ISO datetime) + direction (Sent/Received) - Line 2: counterparty address + amount with symbol - Clickable rows navigate to transaction detail view Transaction detail view (placeholder) shows: - Status, time, amount, from, to, transaction hash - Back button returns to address detail Also added "transaction" to VIEWS list in helpers.
211 lines
7.0 KiB
JavaScript
211 lines
7.0 KiB
JavaScript
const { $, showView, showFlash, balanceLinesForAddress } = require("./helpers");
|
|
const { state, currentAddress } = require("../../shared/state");
|
|
const { formatUsd, getAddressValueUsd } = require("../../shared/prices");
|
|
const { fetchRecentTransactions } = require("../../shared/transactions");
|
|
const { updateSendBalance } = require("./send");
|
|
const { log } = require("../../shared/log");
|
|
const QRCode = require("qrcode");
|
|
|
|
function show() {
|
|
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 " + (wi + 1) + "." + (ai + 1);
|
|
$("address-full").textContent = addr.address;
|
|
$("address-usd-total").textContent = formatUsd(getAddressValueUsd(addr));
|
|
const ensEl = $("address-ens");
|
|
if (addr.ensName) {
|
|
ensEl.textContent = addr.ensName;
|
|
ensEl.classList.remove("hidden");
|
|
} else {
|
|
ensEl.classList.add("hidden");
|
|
}
|
|
$("address-balances").innerHTML = balanceLinesForAddress(addr);
|
|
renderSendTokenSelect(addr);
|
|
$("tx-list").innerHTML =
|
|
'<div class="text-muted text-xs py-1">Loading...</div>';
|
|
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";
|
|
}
|
|
|
|
function escapeHtml(s) {
|
|
const div = document.createElement("div");
|
|
div.textContent = s;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
let loadedTxs = [];
|
|
|
|
async function loadTransactions(address) {
|
|
try {
|
|
const txs = await fetchRecentTransactions(address, state.blockscoutUrl);
|
|
loadedTxs = txs;
|
|
renderTransactions(txs);
|
|
} catch (e) {
|
|
log.errorf("loadTransactions failed:", e.message);
|
|
$("tx-list").innerHTML =
|
|
'<div class="text-muted text-xs py-1">Failed to load transactions.</div>';
|
|
}
|
|
}
|
|
|
|
function renderTransactions(txs) {
|
|
const list = $("tx-list");
|
|
if (txs.length === 0) {
|
|
list.innerHTML =
|
|
'<div class="text-muted text-xs py-1">No transactions found.</div>';
|
|
return;
|
|
}
|
|
list.innerHTML = "";
|
|
txs.forEach((tx, i) => {
|
|
const counterparty = tx.direction === "sent" ? tx.to : tx.from;
|
|
const dirLabel = tx.direction === "sent" ? "Sent" : "Received";
|
|
const errorStyle = tx.isError ? " opacity:0.5" : "";
|
|
|
|
const row = document.createElement("div");
|
|
row.className =
|
|
"py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover";
|
|
if (errorStyle) row.style.cssText = errorStyle;
|
|
|
|
const line1 = document.createElement("div");
|
|
line1.className = "flex justify-between";
|
|
const age = document.createElement("span");
|
|
age.className = "text-muted";
|
|
age.textContent = timeAgo(tx.timestamp);
|
|
age.title = isoDate(tx.timestamp);
|
|
const dir = document.createElement("span");
|
|
dir.className = tx.isError ? "text-muted" : "";
|
|
dir.textContent = dirLabel + (tx.isError ? " (failed)" : "");
|
|
line1.appendChild(age);
|
|
line1.appendChild(dir);
|
|
|
|
const line2 = document.createElement("div");
|
|
line2.className = "flex justify-between";
|
|
const addr = document.createElement("span");
|
|
addr.className = "break-all pr-2";
|
|
addr.textContent = counterparty;
|
|
const amount = document.createElement("span");
|
|
amount.className = "shrink-0";
|
|
amount.textContent = tx.value + " " + tx.symbol;
|
|
line2.appendChild(addr);
|
|
line2.appendChild(amount);
|
|
|
|
row.appendChild(line1);
|
|
row.appendChild(line2);
|
|
row.addEventListener("click", () => {
|
|
state.selectedTx = i;
|
|
showTxDetail(tx);
|
|
});
|
|
list.appendChild(row);
|
|
});
|
|
}
|
|
|
|
function showTxDetail(tx) {
|
|
$("tx-detail-hash").textContent = tx.hash;
|
|
$("tx-detail-from").textContent = tx.from;
|
|
$("tx-detail-to").textContent = tx.to;
|
|
$("tx-detail-value").textContent = tx.value + " " + tx.symbol;
|
|
$("tx-detail-time").textContent = isoDate(tx.timestamp);
|
|
$("tx-detail-status").textContent = tx.isError ? "Failed" : "Success";
|
|
showView("transaction");
|
|
}
|
|
|
|
function renderSendTokenSelect(addr) {
|
|
const sel = $("send-token");
|
|
sel.innerHTML = '<option value="ETH">ETH</option>';
|
|
for (const t of addr.tokenBalances || []) {
|
|
const opt = document.createElement("option");
|
|
opt.value = t.address;
|
|
opt.textContent = t.symbol;
|
|
sel.appendChild(opt);
|
|
}
|
|
}
|
|
|
|
function init(ctx) {
|
|
$("address-full").addEventListener("click", () => {
|
|
const addr = $("address-full").textContent;
|
|
if (addr) {
|
|
navigator.clipboard.writeText(addr);
|
|
showFlash("Copied!");
|
|
}
|
|
});
|
|
|
|
$("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 = "";
|
|
updateSendBalance();
|
|
showView("send");
|
|
});
|
|
|
|
$("btn-receive").addEventListener("click", () => {
|
|
const addr = currentAddress();
|
|
const address = addr ? addr.address : "";
|
|
$("receive-address").textContent = address;
|
|
if (address) {
|
|
QRCode.toCanvas($("receive-qr"), address, {
|
|
width: 200,
|
|
margin: 2,
|
|
color: { dark: "#000000", light: "#ffffff" },
|
|
});
|
|
}
|
|
showView("receive");
|
|
});
|
|
|
|
$("btn-add-token").addEventListener("click", ctx.showAddTokenView);
|
|
|
|
$("btn-tx-back").addEventListener("click", () => {
|
|
show();
|
|
});
|
|
}
|
|
|
|
module.exports = { init, show };
|