From 0d543288b2bbe4a4f3885c94e22bd9a2ba633244 Mon Sep 17 00:00:00 2001 From: sneak Date: Thu, 26 Feb 2026 03:46:25 +0700 Subject: [PATCH] Parallelize address scanning and unify address display formatting 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. --- src/popup/views/addressDetail.js | 8 +--- src/popup/views/approval.js | 9 +++-- src/popup/views/confirmTx.js | 25 +++++++----- src/popup/views/helpers.js | 23 +++++++++++ src/shared/balances.js | 66 ++++++++++++++++++-------------- 5 files changed, 84 insertions(+), 47 deletions(-) diff --git a/src/popup/views/addressDetail.js b/src/popup/views/addressDetail.js index c9f794f..eb046f9 100644 --- a/src/popup/views/addressDetail.js +++ b/src/popup/views/addressDetail.js @@ -4,6 +4,8 @@ const { showFlash, balanceLinesForAddress, addressDotHtml, + escapeHtml, + formatAddressHtml, truncateMiddle, } = require("./helpers"); const { state, currentAddress } = require("../../shared/state"); @@ -76,12 +78,6 @@ function timeAgo(timestamp) { return years + " year" + (years !== 1 ? "s" : "") + " ago"; } -function escapeHtml(s) { - const div = document.createElement("div"); - div.textContent = s; - return div.innerHTML; -} - let loadedTxs = []; let ensNameMap = new Map(); diff --git a/src/popup/views/approval.js b/src/popup/views/approval.js index 311f7e2..47eb361 100644 --- a/src/popup/views/approval.js +++ b/src/popup/views/approval.js @@ -1,4 +1,4 @@ -const { $, addressDotHtml } = require("./helpers"); +const { $, formatAddressHtml } = require("./helpers"); const { state, saveState } = require("../../shared/state"); const runtime = @@ -14,8 +14,11 @@ function show(id) { return; } $("approve-hostname").textContent = details.hostname; - const dot = addressDotHtml(state.activeAddress); - $("approve-address").innerHTML = dot + state.activeAddress; + $("approve-address").innerHTML = formatAddressHtml( + state.activeAddress, + null, + null, + ); $("approve-remember").checked = state.rememberSiteChoice; }); } diff --git a/src/popup/views/confirmTx.js b/src/popup/views/confirmTx.js index 7c5048d..7d4f55f 100644 --- a/src/popup/views/confirmTx.js +++ b/src/popup/views/confirmTx.js @@ -3,7 +3,13 @@ // password modal, decrypts secret, signs and broadcasts. const { parseEther } = require("ethers"); -const { $, showError, hideError, showView } = require("./helpers"); +const { + $, + showError, + hideError, + showView, + formatAddressHtml, +} = require("./helpers"); const { state } = require("../../shared/state"); const { getSignerForAddress } = require("../../shared/wallet"); const { decryptWithPassword } = require("../../shared/vault"); @@ -16,16 +22,15 @@ let pendingTx = null; function show(txInfo) { pendingTx = txInfo; - $("confirm-from").textContent = txInfo.from; - $("confirm-to").textContent = txInfo.to; + $("confirm-from").innerHTML = formatAddressHtml(txInfo.from, null, null); + $("confirm-to").innerHTML = formatAddressHtml( + txInfo.to, + txInfo.ensName, + null, + ); - const ensEl = $("confirm-to-ens"); - if (txInfo.ensName) { - ensEl.textContent = "(" + txInfo.ensName + ")"; - ensEl.classList.remove("hidden"); - } else { - ensEl.classList.add("hidden"); - } + // Hide the separate ENS element — it's now inline in the address display + $("confirm-to-ens").classList.add("hidden"); $("confirm-amount").textContent = txInfo.amount + " " + txInfo.token; diff --git a/src/popup/views/helpers.js b/src/popup/views/helpers.js index a6db37c..f26a6bb 100644 --- a/src/popup/views/helpers.js +++ b/src/popup/views/helpers.js @@ -137,6 +137,27 @@ function addressDotHtml(address) { return ``; } +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 ( + `
${dot}${escapeHtml(ensName)}
` + + `
${escapeHtml(displayAddr)}
` + ); + } + return `
${dot}${escapeHtml(displayAddr)}
`; +} + module.exports = { $, showError, @@ -146,5 +167,7 @@ module.exports = { balanceLinesForAddress, addressColor, addressDotHtml, + escapeHtml, + formatAddressHtml, truncateMiddle, }; diff --git a/src/shared/balances.js b/src/shared/balances.js index f9ffb6b..f51d4e3 100644 --- a/src/shared/balances.js +++ b/src/shared/balances.js @@ -166,44 +166,54 @@ async function lookupTokenInfo(contractAddress, rpcUrl) { } // 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 }. async function scanForAddresses(xpub, rpcUrl, gapLimit = 5) { log.debugf("scanForAddresses start, gapLimit:", gapLimit); const provider = getProvider(rpcUrl); const used = []; - let gap = 0; - let index = 0; + let checked = 0; + let checkUpTo = gapLimit; - while (gap < gapLimit) { - const addr = deriveAddressFromXpub(xpub, index); - let balance, txCount; - try { - [balance, txCount] = await Promise.all([ - provider.getBalance(addr), - provider.getTransactionCount(addr), - ]); - } catch (e) { - log.errorf( - "scanForAddresses check failed", - addr, - e.shortMessage || e.message, - ); - // Treat RPC failure as empty to avoid infinite loop - gap++; - index++; - continue; + while (checked < checkUpTo) { + const batch = []; + for (let i = checked; i < checkUpTo; i++) { + const addr = deriveAddressFromXpub(xpub, i); + batch.push({ addr, index: i }); } - if (balance > 0n || txCount > 0) { - used.push({ address: addr, index }); - gap = 0; - log.debugf("scanForAddresses used", addr, "index:", index); - } else { - gap++; + + const results = await Promise.all( + batch.map(async ({ addr, index }) => { + try { + const [balance, txCount] = await Promise.all([ + provider.getBalance(addr), + provider.getTransactionCount(addr), + ]); + return { addr, index, isUsed: balance > 0n || txCount > 0 }; + } catch (e) { + log.errorf( + "scanForAddresses check failed", + addr, + e.shortMessage || e.message, + ); + return { addr, index, isUsed: false }; + } + }), + ); + + 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); + } } - index++; } + used.sort((a, b) => a.index - b.index); const nextIndex = used.length > 0 ? used[used.length - 1].index + 1 : 1; log.infof( "scanForAddresses done, found:",