Files
AutistMask/src/popup/views/home.js
sneak b64f9b56cc
All checks were successful
check / check (push) Successful in 17s
Show contract calls as "Approve USDT" instead of "0.0000 ETH"
Contract interactions (approve, swap, etc.) now display the method
name and token symbol instead of the meaningless 0 ETH value.
Blockscout provides the method name and whether the target is a
contract — parseTx uses these plus TOKEN_BY_ADDRESS to produce
labels like "Approve USDT" or "Swap LINK".

Added directionLabel field to parsed transactions so renderers
don't need to know about the sent/received/contract distinction.

Also: clicking a transaction on the home screen now opens the
transaction detail view instead of navigating to the address
detail view.
2026-02-27 12:54:42 +07:00

425 lines
15 KiB
JavaScript

const {
$,
showView,
showFlash,
balanceLinesForAddress,
addressDotHtml,
escapeHtml,
truncateMiddle,
} = require("./helpers");
const { state, saveState, currentAddress } = require("../../shared/state");
const { updateSendBalance, renderSendTokenSelect } = require("./send");
const { deriveAddressFromXpub } = require("../../shared/wallet");
const {
formatUsd,
getPrice,
getAddressValueUsd,
} = require("../../shared/prices");
const {
fetchRecentTransactions,
filterTransactions,
} = require("../../shared/transactions");
const { log } = require("../../shared/log");
function findActiveAddr() {
for (const w of state.wallets) {
for (const a of w.addresses) {
if (a.address === state.activeAddress) return a;
}
}
return null;
}
function renderTotalValue() {
const el = $("total-value");
const subEl = $("total-value-sub");
const priceEl = $("eth-price-display");
if (!el) return;
const ethPrice = getPrice("ETH");
if (priceEl) {
priceEl.textContent = ethPrice ? formatUsd(ethPrice) + " USD/ETH" : "";
}
const addr = findActiveAddr();
if (!addr) {
el.textContent = "";
if (subEl) subEl.textContent = "";
return;
}
const ethBal = parseFloat(addr.balance || "0");
const ethStr = ethBal.toFixed(4) + " ETH";
const ethUsd = ethPrice ? " (" + formatUsd(ethBal * ethPrice) + ")" : "";
el.textContent = ethStr + ethUsd;
if (subEl) {
const totalUsd = getAddressValueUsd(addr);
subEl.textContent =
totalUsd !== null ? "Total: " + formatUsd(totalUsd) : "";
}
}
const EXT_ICON =
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
`<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5">` +
`<path d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5"/>` +
`<path d="M7 1.5h3.5V5M7 5.5L10.5 1.5"/>` +
`</svg></span>`;
function renderActiveAddress() {
const el = $("active-address-display");
if (!el) return;
if (state.activeAddress) {
const addr = state.activeAddress;
const dot = addressDotHtml(addr);
const link = `https://etherscan.io/address/${addr}`;
el.innerHTML =
`<span class="underline decoration-dashed cursor-pointer" id="active-addr-copy">${dot}${escapeHtml(addr)}</span>` +
`<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
$("active-addr-copy").addEventListener("click", () => {
navigator.clipboard.writeText(addr);
showFlash("Copied!");
});
} else {
el.textContent = "";
}
}
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.directionLabel;
const amountStr = tx.value
? escapeHtml(tx.value + " " + tx.symbol)
: escapeHtml(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];
// Set selectedWallet/selectedAddress so back navigation works
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;
state.selectedToken = null;
ctx.showTransactionDetail(tx);
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) {
container.innerHTML =
'<p class="text-muted py-2">No wallets yet. Add one to get started.</p>';
renderTotalValue();
renderActiveAddress();
return;
}
let html = "";
state.wallets.forEach((wallet, wi) => {
html += `<div>`;
html += `<div class="flex justify-between items-center bg-section py-1 px-2" style="margin:0 -0.5rem">`;
html += `<span class="font-bold cursor-pointer wallet-name underline decoration-dashed" data-wallet="${wi}">${wallet.name}</span>`;
if (wallet.type === "hd") {
html += `<button class="btn-add-address border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer text-xs" data-wallet="${wi}" title="Add another address to this wallet">+</button>`;
}
html += `</div>`;
wallet.addresses.forEach((addr, ai) => {
html += `<div class="address-row py-1 border-b border-border-light cursor-pointer hover:bg-hover" data-wallet="${wi}" data-address="${ai}">`;
const isActive = state.activeAddress === addr.address;
const infoBtn = `<span class="btn-addr-info text-xs cursor-pointer border border-border hover:bg-fg hover:text-bg" style="padding:0" data-wallet="${wi}" data-address="${ai}">[info]</span>`;
const dot = addressDotHtml(addr.address);
const titleBold = isActive ? "font-bold" : "";
html += `<div class="text-xs ${titleBold}">Address ${ai + 1}</div>`;
if (addr.ensName) {
html += `<div class="text-xs font-bold flex items-center">${dot}${addr.ensName}</div>`;
}
html += `<div class="flex text-xs items-center justify-between">`;
html += `<span class="flex items-center break-all">${addr.ensName ? "" : dot}${addr.address}</span>`;
html += `<span class="flex-shrink-0 ml-1">${infoBtn}</span>`;
html += `</div>`;
const addrUsd = formatUsd(getAddressValueUsd(addr));
html += `<div class="text-xs text-muted text-right">${addrUsd}</div>`;
html += balanceLinesForAddress(
addr,
state.trackedTokens,
state.showZeroBalanceTokens,
);
html += `</div>`;
});
html += `</div>`;
});
container.innerHTML = html;
container.querySelectorAll(".address-row").forEach((row) => {
row.addEventListener("click", async () => {
const wi = parseInt(row.dataset.wallet, 10);
const ai = parseInt(row.dataset.address, 10);
const addr = state.wallets[wi].addresses[ai].address;
if (state.activeAddress !== addr) {
state.activeAddress = addr;
await saveState();
render(ctx);
const runtime =
typeof browser !== "undefined"
? browser.runtime
: chrome.runtime;
runtime.sendMessage({ type: "AUTISTMASK_ACTIVE_CHANGED" });
}
});
});
container.querySelectorAll(".btn-addr-info").forEach((btn) => {
btn.addEventListener("click", (e) => {
e.stopPropagation();
state.selectedWallet = parseInt(btn.dataset.wallet, 10);
state.selectedAddress = parseInt(btn.dataset.address, 10);
ctx.showAddressDetail();
});
});
container.querySelectorAll(".btn-add-address").forEach((btn) => {
btn.addEventListener("click", async (e) => {
e.stopPropagation();
const wi = parseInt(btn.dataset.wallet, 10);
const wallet = state.wallets[wi];
const newAddr = deriveAddressFromXpub(
wallet.xpub,
wallet.nextIndex,
);
wallet.addresses.push({
address: newAddr,
balance: "0.0000",
tokenBalances: [],
});
wallet.nextIndex++;
await saveState();
render(ctx);
ctx.doRefreshAndRender();
});
});
container.querySelectorAll(".wallet-name").forEach((span) => {
span.addEventListener("click", (e) => {
e.stopPropagation();
const wi = parseInt(span.dataset.wallet, 10);
const wallet = state.wallets[wi];
const input = document.createElement("input");
input.type = "text";
input.value = wallet.name;
input.className =
"font-bold border border-border p-0 bg-bg text-fg";
input.style.width = "100%";
const save = async () => {
const val = input.value.trim();
if (val && val !== wallet.name) {
wallet.name = val;
await saveState();
}
render(ctx);
};
input.addEventListener("blur", save);
input.addEventListener("keydown", (ev) => {
if (ev.key === "Enter") input.blur();
if (ev.key === "Escape") {
input.value = wallet.name;
input.blur();
}
});
span.replaceWith(input);
input.focus();
input.select();
});
});
renderTotalValue();
renderActiveAddress();
loadHomeTxs(ctx);
}
function selectActiveAddress() {
for (let wi = 0; wi < state.wallets.length; wi++) {
for (let ai = 0; ai < state.wallets[wi].addresses.length; ai++) {
if (
state.wallets[wi].addresses[ai].address === state.activeAddress
) {
state.selectedWallet = wi;
state.selectedAddress = ai;
return true;
}
}
}
return false;
}
function init(ctx) {
$("btn-add-wallet-bottom").addEventListener("click", ctx.showAddWalletView);
$("btn-main-send").addEventListener("click", () => {
if (!selectActiveAddress()) {
showFlash("No active address selected.");
return;
}
const addr = currentAddress();
if (!addr.balance || parseFloat(addr.balance) === 0) {
showFlash("Cannot send \u2014 zero balance.");
return;
}
$("send-to").value = "";
$("send-amount").value = "";
$("send-token").classList.remove("hidden");
$("send-token-static").classList.add("hidden");
renderSendTokenSelect(addr);
updateSendBalance();
showView("send");
});
$("btn-main-receive").addEventListener("click", () => {
if (!selectActiveAddress()) {
showFlash("No active address selected.");
return;
}
ctx.showReceive();
});
}
module.exports = { init, render };