Token auto-discovery, tx history, balance polling, EIP-6963, UI overhaul
All checks were successful
check / check (push) Successful in 14s

Major changes:
- Fetch token balances and tx history from Blockscout API (configurable)
- Remove manual token discovery (discoverTokens) in favor of Blockscout
- HD address gap scanning on mnemonic import
- Duplicate mnemonic detection on wallet add
- EIP-6963 multi-wallet discovery + selectedAddress updates in inpage
- Two-tier balance refresh: 10s while popup open, 60s background
- Fix $0.00 flash before prices load (return null when no prices)
- No-layout-shift: min-height on total value element
- Aligned balance columns (42ch address width, consistent USD column)
- All errors use flash messages instead of off-screen error divs
- Settings gear in global title bar, add-wallet moved to settings pane
- Settings wells with light grey background, configurable Blockscout URL
- Consistent "< Back" buttons top-left on all views
- Address titles (Address 1.1, 1.2, etc.) on main and detail views
- Send view shows current balance of selected asset
- Clickable affordance policy added to README
- Shortened mnemonic backup warning
- Fix broken background script constant imports
This commit is contained in:
2026-02-26 02:13:39 +07:00
parent 2b2137716c
commit 3bd2b58543
27 changed files with 978 additions and 420 deletions

View File

@@ -1,16 +1,20 @@
const { $, showView } = require("./helpers");
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];
$("address-title").textContent = wallet.name;
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-copied-msg").textContent = "";
$("address-eth-balance").textContent = addr.balance;
$("address-usd-value").textContent = formatUsd(getAddressValueUsd(addr));
$("address-usd-total").textContent = formatUsd(getAddressValueUsd(addr));
const ensEl = $("address-ens");
if (addr.ensName) {
ensEl.textContent = addr.ensName;
@@ -18,28 +22,71 @@ function show() {
} else {
ensEl.classList.add("hidden");
}
renderTokenList(addr);
$("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 renderTokenList(addr) {
const list = $("token-list");
const balances = addr.tokenBalances || [];
if (balances.length === 0 && state.trackedTokens.length === 0) {
function formatDate(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())
);
}
function escapeHtml(s) {
const div = document.createElement("div");
div.textContent = s;
return div.innerHTML;
}
async function loadTransactions(address) {
try {
const txs = await fetchRecentTransactions(address, state.blockscoutUrl);
renderTransactions(txs, address);
} 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, address) {
const list = $("tx-list");
if (txs.length === 0) {
list.innerHTML =
'<div class="text-muted text-xs py-1">No tokens added yet. Use "+ Add" to track a token.</div>';
'<div class="text-muted text-xs py-1">No transactions found.</div>';
return;
}
list.innerHTML = balances
.map(
(t) =>
`<div class="py-1 border-b border-border-light flex justify-between">` +
`<span>${t.symbol}</span>` +
`<span>${t.balance || "0"}</span>` +
`</div>`,
)
.join("");
const addrLower = address.toLowerCase();
let html = "";
for (const tx of txs) {
const arrow = tx.direction === "sent" ? "\u2192" : "\u2190";
const counterparty = tx.direction === "sent" ? tx.to : tx.from;
const label = tx.direction === "sent" ? "to" : "from";
const errorClass = tx.isError ? ' style="opacity:0.5"' : "";
const errorTag = tx.isError
? ' <span class="text-muted">[failed]</span>'
: "";
html += `<div class="py-1 border-b border-border-light text-xs"${errorClass}>`;
html += `<div>${formatDate(tx.timestamp)} ${arrow} ${escapeHtml(tx.value)} ${escapeHtml(tx.symbol)}${errorTag}</div>`;
html += `<div class="text-muted break-all">${label}: ${escapeHtml(counterparty)}</div>`;
html += `<div class="break-all"><a href="https://etherscan.io/tx/${escapeHtml(tx.hash)}" target="_blank" class="underline decoration-dashed">${escapeHtml(tx.hash)}</a></div>`;
html += `</div>`;
}
list.innerHTML = html;
}
function renderSendTokenSelect(addr) {
@@ -58,10 +105,7 @@ function init(ctx) {
const addr = $("address-full").textContent;
if (addr) {
navigator.clipboard.writeText(addr);
$("address-copied-msg").textContent = "Copied!";
setTimeout(() => {
$("address-copied-msg").textContent = "";
}, 2000);
showFlash("Copied!");
}
});
@@ -71,11 +115,17 @@ function init(ctx) {
});
$("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 = "";
$("send-password").value = "";
$("send-fee-estimate").classList.add("hidden");
$("send-status").classList.add("hidden");
updateSendBalance();
showView("send");
});