fix(aegis): refresh was a silent no-op, and reported success either way

Three separate reasons the Refresh button looked dead:

- wallet.js refresh() returned immediately when state.scanning was set, so a
  manual press during a background poll did nothing at all. A forced refresh
  now awaits the in-flight pass and then does real work; background polls
  still yield. scanning is only ever written inside doRefresh, which only
  refresh() calls, so scanning implies a pending inflight to wait on.
- The adapters catch their own fetch failures onto state.error instead of
  rejecting, so awaiting refresh() proved nothing and refreshChain reported
  ok:true for a wallet that had just failed against a dead server. It now
  reads the snapshot back.
- Success changed only a tooltip. Balances that were already current left the
  screen identical, which is indistinguishable from a broken button. It now
  flashes a result and says how many wallets were refreshed, or how many
  failed and why.

Version bumped once for this batch; not published yet.
This commit is contained in:
Local Dev 2026-10-02 20:34:01 +02:00
parent fce2331701
commit 57177a3439
4 changed files with 43 additions and 8 deletions

View file

@ -1,7 +1,7 @@
{
"id": "aegis",
"name": "Aegis Wallet",
"version": "0.25.2",
"version": "0.26.0",
"category": "plugin",
"description": "Multi-chain wallet (BCH, BTC, TRX, ETH, SOL, SC, DGB) derived from your Theseus vault. Dapps get window.bitcoincash on .x sites; window.tronWeb / window.tronLink / window.ethereum / window.solana on any https page.",
"author": "Silent Mode",

View file

@ -1348,8 +1348,15 @@ function registerPanelMessages(api) {
await Promise.all(targets.map(async (w) => {
const rt = ctx.runtimes.get(w.id);
if (!rt || !rt.adapter) { out.push({ id: w.id, ok: false, error: "adapter not mounted" }); return; }
try { await rt.adapter.refresh(true); out.push({ id: w.id, ok: true }); }
catch (e) { out.push({ id: w.id, ok: false, error: e?.message || String(e) }); }
try {
await rt.adapter.refresh(true);
// The adapters catch their own fetch failures onto state.error rather
// than rejecting, so awaiting refresh() proves nothing. Read the
// snapshot back, or a dead server reports a successful refresh.
let snapErr = null;
try { snapErr = rt.adapter.snapshot()?.error || null; } catch { snapErr = null; }
out.push(snapErr ? { id: w.id, ok: false, error: snapErr } : { id: w.id, ok: true });
} catch (e) { out.push({ id: w.id, ok: false, error: e?.message || String(e) }); }
}));
return { chain, network, results: out };
});

View file

@ -282,8 +282,22 @@ module.exports = function makeWallet({ client, keys, tx, cashaddr, sha256, stora
return null;
}
// A manual refresh used to return here the moment a background poll was
// in flight, and refreshChain reported ok:true for it — so the Refresh
// button no-opped and claimed success, which is exactly when a user is
// most likely to press it. A forced refresh now waits for the in-flight
// pass and then does real work; a background poll still yields.
let inflight = null;
async function refresh(full = false) {
if (state.scanning) return;
if (state.scanning) {
if (!full) return;
try { await inflight; } catch { /* its own error is already on state */ }
if (state.scanning) return; // another forced pass won the race
}
inflight = doRefresh(full);
return inflight;
}
async function doRefresh(full) {
state.scanning = true; state.error = null; onChange();
try {
if (full || !state.watched.size) await scan();

View file

@ -3145,16 +3145,30 @@ function renderInlineCoinList(el, groupKey, group) {
refreshBtn.dataset.spinning = "1";
const prev = refreshBtn.textContent;
refreshBtn.textContent = "…";
// Say what happened. A tooltip-only result is indistinguishable from the
// button doing nothing, which is how this one came to be reported as
// broken even when it worked: balances that were already current changed
// nothing on screen.
let mark = null;
try {
const r = await S.invoke("refreshChain", { chain: gw[0]?.chain, network: gw[0]?.network });
const failed = (r?.results || []).filter((x) => !x.ok);
if (failed.length) refreshBtn.title = "Refresh failed: " + failed.map((f) => f.error).join("; ");
else refreshBtn.title = "Refresh balances now";
if (failed.length) {
refreshBtn.title = `Refresh failed on ${failed.length} of ${r.results.length}: ` + failed.map((f) => f.error).join("; ");
mark = "⚠";
} else {
refreshBtn.title = `Refreshed ${r.results.length} wallet${r.results.length === 1 ? "" : "s"}`;
mark = "✓";
}
} catch (e) {
refreshBtn.title = "Refresh failed: " + (e?.message || e);
refreshBtn.title = "Refresh failed: " + cleanErr(e);
mark = "⚠";
} finally {
refreshBtn.textContent = prev;
delete refreshBtn.dataset.spinning;
if (mark) {
refreshBtn.textContent = mark;
setTimeout(() => { if (refreshBtn.isConnected) refreshBtn.textContent = prev; }, 1200);
} else refreshBtn.textContent = prev;
}
});
el.querySelectorAll("[data-listpick]").forEach((row) => row.addEventListener("click", async (e) => {