fix(sweep,lite): don't misread a funded key as empty; guard lite send amount; use spendable balance

Fund-correctness audit, sweep + lite paths:
- Key-import sweep: balAt() swallowed every RPC error as 0.0, so a transient
  timeout/warmup after the import misclassified a funded key as "no funds". Let
  the failure propagate to the outer handler so the user sees a real error and
  can retry, instead of a false __NOFUNDS__.
- Lite send: the double→zatoshi conversion was an unchecked llround+cast — a
  negative/NaN amount would wrap to a huge uint64. Route it through
  util::coinsToZatoshis and reject invalid amounts before broadcast.
- Lite balance: the spend gate used the gross shielded total (including
  unverified/unconfirmed notes). Use the backend's spendable_zbalance (a required
  field) for spendablePrivateBalance/Total, clamped to the shielded total.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 13:10:55 -05:00
co-authored by Claude Opus 4.8
parent 70d5c233c0
commit f4460beddd
2 changed files with 21 additions and 11 deletions
+13 -6
View File
@@ -5727,12 +5727,12 @@ void App::sweepPrivateKey(const std::string& rawKey, int startHeight, int destMo
if (sourceAddr.empty()) throw std::runtime_error("Could not determine the key's address.");
// 3. Confirm the key holds spendable (confirmed) funds. z_getbalance covers t- and z-addrs.
// Let an RPC failure PROPAGATE to the outer catch (→ a real error) instead of swallowing it as
// 0.0, which would misclassify a funded key as "__NOFUNDS__" after a transient timeout/warmup.
auto balAt = [&](int minconf) -> double {
try {
nlohmann::json b = c.call("z_getbalance", {sourceAddr, minconf});
return b.is_number() ? b.get<double>()
: b.is_string() ? std::stod(b.get<std::string>()) : 0.0;
} catch (...) { return 0.0; }
nlohmann::json b = c.call("z_getbalance", {sourceAddr, minconf});
return b.is_number() ? b.get<double>()
: b.is_string() ? std::stod(b.get<std::string>()) : 0.0;
};
double bal = balAt(1);
if (bal <= 0.0) {
@@ -7118,7 +7118,14 @@ void App::sendTransaction(const std::string& from, const std::string& to,
wallet::LiteSendRequest req;
wallet::LiteSendRecipient recipient;
recipient.address = to;
recipient.amountZatoshis = static_cast<std::uint64_t>(std::llround(amount * 100000000.0));
// Guard the double→zatoshi conversion: a negative/NaN amount would wrap to a huge uint64, and a
// sub-zatoshi amount rounds to 0. Reject rather than broadcast a bogus value.
std::uint64_t zat = 0;
if (!util::coinsToZatoshis(amount, zat)) {
if (callback) callback(false, "Invalid send amount");
return;
}
recipient.amountZatoshis = zat;
recipient.memo = memo;
req.recipients.push_back(std::move(recipient));
if (!lite_wallet_->sendTransaction(req)) {