Parallelize address scanning and unify address display formatting
Some checks failed
check / check (push) Has been cancelled

Scanning: check all gap-limit addresses in parallel per batch instead
of sequentially. For a wallet with 1 used address this reduces from
12 sequential RPC round-trips to 1 parallel batch + 1 small follow-up.

Display: add shared formatAddressHtml(address, ensName, maxLen) and
escapeHtml() to helpers.js. Use them in confirm-tx (was missing color
dot entirely) and approval view. Remove duplicate escapeHtml from
addressDetail.js.
This commit is contained in:
2026-02-26 03:46:25 +07:00
parent 1dfc006cb9
commit 0d543288b2
5 changed files with 84 additions and 47 deletions

View File

@@ -4,6 +4,8 @@ const {
showFlash, showFlash,
balanceLinesForAddress, balanceLinesForAddress,
addressDotHtml, addressDotHtml,
escapeHtml,
formatAddressHtml,
truncateMiddle, truncateMiddle,
} = require("./helpers"); } = require("./helpers");
const { state, currentAddress } = require("../../shared/state"); const { state, currentAddress } = require("../../shared/state");
@@ -76,12 +78,6 @@ function timeAgo(timestamp) {
return years + " year" + (years !== 1 ? "s" : "") + " ago"; return years + " year" + (years !== 1 ? "s" : "") + " ago";
} }
function escapeHtml(s) {
const div = document.createElement("div");
div.textContent = s;
return div.innerHTML;
}
let loadedTxs = []; let loadedTxs = [];
let ensNameMap = new Map(); let ensNameMap = new Map();

View File

@@ -1,4 +1,4 @@
const { $, addressDotHtml } = require("./helpers"); const { $, formatAddressHtml } = require("./helpers");
const { state, saveState } = require("../../shared/state"); const { state, saveState } = require("../../shared/state");
const runtime = const runtime =
@@ -14,8 +14,11 @@ function show(id) {
return; return;
} }
$("approve-hostname").textContent = details.hostname; $("approve-hostname").textContent = details.hostname;
const dot = addressDotHtml(state.activeAddress); $("approve-address").innerHTML = formatAddressHtml(
$("approve-address").innerHTML = dot + state.activeAddress; state.activeAddress,
null,
null,
);
$("approve-remember").checked = state.rememberSiteChoice; $("approve-remember").checked = state.rememberSiteChoice;
}); });
} }

View File

@@ -3,7 +3,13 @@
// password modal, decrypts secret, signs and broadcasts. // password modal, decrypts secret, signs and broadcasts.
const { parseEther } = require("ethers"); const { parseEther } = require("ethers");
const { $, showError, hideError, showView } = require("./helpers"); const {
$,
showError,
hideError,
showView,
formatAddressHtml,
} = require("./helpers");
const { state } = require("../../shared/state"); const { state } = require("../../shared/state");
const { getSignerForAddress } = require("../../shared/wallet"); const { getSignerForAddress } = require("../../shared/wallet");
const { decryptWithPassword } = require("../../shared/vault"); const { decryptWithPassword } = require("../../shared/vault");
@@ -16,16 +22,15 @@ let pendingTx = null;
function show(txInfo) { function show(txInfo) {
pendingTx = txInfo; pendingTx = txInfo;
$("confirm-from").textContent = txInfo.from; $("confirm-from").innerHTML = formatAddressHtml(txInfo.from, null, null);
$("confirm-to").textContent = txInfo.to; $("confirm-to").innerHTML = formatAddressHtml(
txInfo.to,
txInfo.ensName,
null,
);
const ensEl = $("confirm-to-ens"); // Hide the separate ENS element — it's now inline in the address display
if (txInfo.ensName) { $("confirm-to-ens").classList.add("hidden");
ensEl.textContent = "(" + txInfo.ensName + ")";
ensEl.classList.remove("hidden");
} else {
ensEl.classList.add("hidden");
}
$("confirm-amount").textContent = txInfo.amount + " " + txInfo.token; $("confirm-amount").textContent = txInfo.amount + " " + txInfo.token;

View File

@@ -137,6 +137,27 @@ function addressDotHtml(address) {
return `<span style="width:8px;height:8px;border-radius:50%;display:inline-block;background:${color};margin-right:4px;vertical-align:middle;flex-shrink:0;"></span>`; return `<span style="width:8px;height:8px;border-radius:50%;display:inline-block;background:${color};margin-right:4px;vertical-align:middle;flex-shrink:0;"></span>`;
} }
function escapeHtml(s) {
const div = document.createElement("div");
div.textContent = s;
return div.innerHTML;
}
// Render an address with color dot, optional ENS name, optional truncation.
// When ensName is provided, shows ENS name (bold) on one line and
// the address below it. Otherwise shows just the dotted address.
function formatAddressHtml(address, ensName, maxLen) {
const dot = addressDotHtml(address);
const displayAddr = maxLen ? truncateMiddle(address, maxLen) : address;
if (ensName) {
return (
`<div class="flex items-center font-bold">${dot}${escapeHtml(ensName)}</div>` +
`<div class="break-all">${escapeHtml(displayAddr)}</div>`
);
}
return `<div class="flex items-center">${dot}<span class="break-all">${escapeHtml(displayAddr)}</span></div>`;
}
module.exports = { module.exports = {
$, $,
showError, showError,
@@ -146,5 +167,7 @@ module.exports = {
balanceLinesForAddress, balanceLinesForAddress,
addressColor, addressColor,
addressDotHtml, addressDotHtml,
escapeHtml,
formatAddressHtml,
truncateMiddle, truncateMiddle,
}; };

View File

@@ -166,44 +166,54 @@ async function lookupTokenInfo(contractAddress, rpcUrl) {
} }
// Derive HD addresses starting from index 0 and check for on-chain activity. // Derive HD addresses starting from index 0 and check for on-chain activity.
// Stops after gapLimit consecutive addresses with zero balance and zero tx count. // Checks gapLimit addresses in parallel per batch. Stops when an entire
// batch has no used addresses (i.e. gapLimit consecutive empty addresses).
// Returns { addresses: [{ address, index }], nextIndex }. // Returns { addresses: [{ address, index }], nextIndex }.
async function scanForAddresses(xpub, rpcUrl, gapLimit = 5) { async function scanForAddresses(xpub, rpcUrl, gapLimit = 5) {
log.debugf("scanForAddresses start, gapLimit:", gapLimit); log.debugf("scanForAddresses start, gapLimit:", gapLimit);
const provider = getProvider(rpcUrl); const provider = getProvider(rpcUrl);
const used = []; const used = [];
let gap = 0; let checked = 0;
let index = 0; let checkUpTo = gapLimit;
while (gap < gapLimit) { while (checked < checkUpTo) {
const addr = deriveAddressFromXpub(xpub, index); const batch = [];
let balance, txCount; for (let i = checked; i < checkUpTo; i++) {
const addr = deriveAddressFromXpub(xpub, i);
batch.push({ addr, index: i });
}
const results = await Promise.all(
batch.map(async ({ addr, index }) => {
try { try {
[balance, txCount] = await Promise.all([ const [balance, txCount] = await Promise.all([
provider.getBalance(addr), provider.getBalance(addr),
provider.getTransactionCount(addr), provider.getTransactionCount(addr),
]); ]);
return { addr, index, isUsed: balance > 0n || txCount > 0 };
} catch (e) { } catch (e) {
log.errorf( log.errorf(
"scanForAddresses check failed", "scanForAddresses check failed",
addr, addr,
e.shortMessage || e.message, e.shortMessage || e.message,
); );
// Treat RPC failure as empty to avoid infinite loop return { addr, index, isUsed: false };
gap++; }
index++; }),
continue; );
checked = checkUpTo;
for (const r of results) {
if (r.isUsed) {
used.push({ address: r.addr, index: r.index });
log.debugf("scanForAddresses used", r.addr, "index:", r.index);
checkUpTo = Math.max(checkUpTo, r.index + 1 + gapLimit);
} }
if (balance > 0n || txCount > 0) {
used.push({ address: addr, index });
gap = 0;
log.debugf("scanForAddresses used", addr, "index:", index);
} else {
gap++;
} }
index++;
} }
used.sort((a, b) => a.index - b.index);
const nextIndex = used.length > 0 ? used[used.length - 1].index + 1 : 1; const nextIndex = used.length > 0 ? used[used.length - 1].index + 1 : 1;
log.infof( log.infof(
"scanForAddresses done, found:", "scanForAddresses done, found:",