fix(fullnode): reliable rescan completion + self-explaining shielded send errors

Two related fixes for the post-bootstrap "send fails / rescan stuck at 99%" trap:

1) Rescan completion now keys off warmup-end. A -rescan runs entirely inside daemon
   warmup (every RPC returns -28 until it finishes), so warmup completing IS the rescan
   completing. The old detectors relied on getrescaninfo (which some daemons answer with
   "Method not found") or a "Done rescanning"/bench log line the daemon may never print,
   leaving the status bar stuck at 99% — so users killed the rescan before it finished.
   When warmup ends and a rescan was confirmed active, clear the rescan state, flip to
   100%, refresh history/balance, and toast completion.

2) z_sendmany failures that mean stale shielded note data (shielded-requirements-not-met,
   missing sapling anchor, invalid sapling spend proof, bad-txns-sapling-*) now append a
   plain-language hint telling the user to run a full rescan, instead of surfacing only the
   raw daemon string.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-16 22:43:24 -05:00
parent f58d009703
commit 6ff80354df
3 changed files with 40 additions and 1 deletions

View File

@@ -431,6 +431,21 @@ wallet::LiteRolloutDecision resolveLiteRolloutDecision(config::Settings& setting
}
return decision;
}
// True when a z_sendmany failure means the wallet's shielded note data (witnesses/anchors) is
// out of sync with the chain — the symptom after a bootstrap/reindex without a full rescan. These
// are consensus/proof-build rejections, not user mistakes, and the only fix is a full rescan.
bool sendErrorNeedsRescan(const std::string& raw)
{
auto has = [&](const char* s) { return raw.find(s) != std::string::npos; };
return has("shielded-requirements-not-met") ||
has("shielded requirements not met") ||
has("missing sapling anchor") ||
has("Invalid sapling spend proof") ||
has("Invalid output proof") ||
has("bad-txns-sapling-") ||
has("anchor");
}
} // namespace
void App::rebuildLiteWallet(bool force)
@@ -849,7 +864,10 @@ void App::update()
// Failures: route to the originating send UI when there is one (it shows
// its own error toast); otherwise surface a generic notification (this is
// how shield/merge/auto-shield failures become visible).
for (const auto& [opid, msg] : parsed.failureByOpid) {
for (const auto& [opid, rawMsg] : parsed.failureByOpid) {
std::string msg = sendErrorNeedsRescan(rawMsg)
? rawMsg + "\n\n" + TR("send_err_needs_rescan")
: rawMsg;
if (!invokeSendResultCallback(opid, false, msg)) {
ui::Notifications::instance().error(msg);
}