241 lines
8.3 KiB
JavaScript
241 lines
8.3 KiB
JavaScript
const {
|
|
$,
|
|
showView,
|
|
showFlash,
|
|
balanceLinesForAddress,
|
|
addressDotHtml,
|
|
escapeHtml,
|
|
formatAddressHtml,
|
|
truncateMiddle,
|
|
} = require("./helpers");
|
|
const { state, currentAddress } = require("../../shared/state");
|
|
const { formatUsd, getAddressValueUsd } = require("../../shared/prices");
|
|
const { fetchRecentTransactions } = require("../../shared/transactions");
|
|
const { resolveEnsNames } = require("../../shared/ens");
|
|
const { updateSendBalance, renderSendTokenSelect } = 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-dot").innerHTML = addressDotHtml(addr.address);
|
|
$("address-full").dataset.full = addr.address;
|
|
$("address-full").textContent = addr.address;
|
|
$("address-usd-total").textContent = formatUsd(getAddressValueUsd(addr));
|
|
const ensEl = $("address-ens");
|
|
if (addr.ensName) {
|
|
ensEl.innerHTML =
|
|
addressDotHtml(addr.address) + escapeHtml(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";
|
|
}
|
|
|
|
let loadedTxs = [];
|
|
|
|
let ensNameMap = new Map();
|
|
|
|
async function loadTransactions(address) {
|
|
try {
|
|
const txs = await fetchRecentTransactions(address, state.blockscoutUrl);
|
|
loadedTxs = txs;
|
|
|
|
// Collect unique counterparty addresses for ENS resolution.
|
|
const counterparties = [
|
|
...new Set(
|
|
txs.map((tx) => (tx.direction === "sent" ? tx.to : tx.from)),
|
|
),
|
|
];
|
|
if (counterparties.length > 0) {
|
|
try {
|
|
ensNameMap = await resolveEnsNames(
|
|
counterparties,
|
|
state.rpcUrl,
|
|
);
|
|
} catch {
|
|
ensNameMap = new Map();
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
let html = "";
|
|
let i = 0;
|
|
for (const tx of txs) {
|
|
const counterparty = tx.direction === "sent" ? tx.to : tx.from;
|
|
const ensName = ensNameMap.get(counterparty) || null;
|
|
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 = ensName || 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="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(".tx-row").forEach((row) => {
|
|
row.addEventListener("click", () => {
|
|
const idx = parseInt(row.dataset.tx, 10);
|
|
showTxDetail(loadedTxs[idx]);
|
|
});
|
|
});
|
|
}
|
|
|
|
function etherscanAddressLink(address) {
|
|
return `https://etherscan.io/address/${address}`;
|
|
}
|
|
|
|
function etherscanTxLink(hash) {
|
|
return `https://etherscan.io/tx/${hash}`;
|
|
}
|
|
|
|
function txDetailAddressHtml(address) {
|
|
const ensName = ensNameMap.get(address) || null;
|
|
const dot = addressDotHtml(address);
|
|
const link = etherscanAddressLink(address);
|
|
if (ensName) {
|
|
return (
|
|
dot +
|
|
`<a href="${link}" target="_blank" class="underline decoration-dashed">${escapeHtml(ensName)}</a>` +
|
|
`<div class="break-all"><a href="${link}" target="_blank" class="underline decoration-dashed">${escapeHtml(address)}</a></div>`
|
|
);
|
|
}
|
|
return (
|
|
dot +
|
|
`<a href="${link}" target="_blank" class="underline decoration-dashed break-all">${escapeHtml(address)}</a>`
|
|
);
|
|
}
|
|
|
|
function showTxDetail(tx) {
|
|
const txLink = etherscanTxLink(tx.hash);
|
|
$("tx-detail-hash").innerHTML =
|
|
`<a href="${txLink}" target="_blank" class="underline decoration-dashed">${escapeHtml(tx.hash)}</a>`;
|
|
$("tx-detail-from").innerHTML = txDetailAddressHtml(tx.from);
|
|
$("tx-detail-to").innerHTML = txDetailAddressHtml(tx.to);
|
|
$("tx-detail-value").textContent = tx.value + " " + tx.symbol;
|
|
$("tx-detail-time").textContent =
|
|
isoDate(tx.timestamp) + " (" + timeAgo(tx.timestamp) + ")";
|
|
$("tx-detail-status").textContent = tx.isError ? "Failed" : "Success";
|
|
showView("transaction");
|
|
}
|
|
|
|
function init(ctx) {
|
|
$("address-full").addEventListener("click", () => {
|
|
const addr = $("address-full").dataset.full;
|
|
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 };
|