Parallelize address scanning and unify address display formatting
Some checks failed
check / check (push) Has been cancelled
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:
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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++) {
|
||||||
try {
|
const addr = deriveAddressFromXpub(xpub, i);
|
||||||
[balance, txCount] = await Promise.all([
|
batch.push({ addr, index: i });
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
if (balance > 0n || txCount > 0) {
|
|
||||||
used.push({ address: addr, index });
|
const results = await Promise.all(
|
||||||
gap = 0;
|
batch.map(async ({ addr, index }) => {
|
||||||
log.debugf("scanForAddresses used", addr, "index:", index);
|
try {
|
||||||
} else {
|
const [balance, txCount] = await Promise.all([
|
||||||
gap++;
|
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;
|
const nextIndex = used.length > 0 ? used[used.length - 1].index + 1 : 1;
|
||||||
log.infof(
|
log.infof(
|
||||||
"scanForAddresses done, found:",
|
"scanForAddresses done, found:",
|
||||||
|
|||||||
Reference in New Issue
Block a user