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:
+13
-6
@@ -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.");
|
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.
|
// 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 {
|
auto balAt = [&](int minconf) -> double {
|
||||||
try {
|
nlohmann::json b = c.call("z_getbalance", {sourceAddr, minconf});
|
||||||
nlohmann::json b = c.call("z_getbalance", {sourceAddr, minconf});
|
return b.is_number() ? b.get<double>()
|
||||||
return b.is_number() ? b.get<double>()
|
: b.is_string() ? std::stod(b.get<std::string>()) : 0.0;
|
||||||
: b.is_string() ? std::stod(b.get<std::string>()) : 0.0;
|
|
||||||
} catch (...) { return 0.0; }
|
|
||||||
};
|
};
|
||||||
double bal = balAt(1);
|
double bal = balAt(1);
|
||||||
if (bal <= 0.0) {
|
if (bal <= 0.0) {
|
||||||
@@ -7118,7 +7118,14 @@ void App::sendTransaction(const std::string& from, const std::string& to,
|
|||||||
wallet::LiteSendRequest req;
|
wallet::LiteSendRequest req;
|
||||||
wallet::LiteSendRecipient recipient;
|
wallet::LiteSendRecipient recipient;
|
||||||
recipient.address = to;
|
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;
|
recipient.memo = memo;
|
||||||
req.recipients.push_back(std::move(recipient));
|
req.recipients.push_back(std::move(recipient));
|
||||||
if (!lite_wallet_->sendTransaction(req)) {
|
if (!lite_wallet_->sendTransaction(req)) {
|
||||||
|
|||||||
@@ -359,12 +359,15 @@ void applyLiteRefreshModelToWalletState(const LiteWalletAppRefreshModel& model,
|
|||||||
state.transparentBalance = static_cast<double>(model.balance.transparentZatoshis) / kZatoshisPerCoin;
|
state.transparentBalance = static_cast<double>(model.balance.transparentZatoshis) / kZatoshisPerCoin;
|
||||||
state.totalBalance = static_cast<double>(model.balance.totalZatoshis) / kZatoshisPerCoin;
|
state.totalBalance = static_cast<double>(model.balance.totalZatoshis) / kZatoshisPerCoin;
|
||||||
state.unconfirmedBalance = static_cast<double>(model.balance.unconfirmedZatoshis) / kZatoshisPerCoin;
|
state.unconfirmedBalance = static_cast<double>(model.balance.unconfirmedZatoshis) / kZatoshisPerCoin;
|
||||||
// Lite already tracks confirmed/unconfirmed itself and its per-address balances are confirmed
|
// Use the backend's SPENDABLE (verified + confirmed) shielded balance for the spend gate, not the
|
||||||
// (spendable outputs only). Mirror the aggregate spendable fields so full-node-shaped spend
|
// gross shielded total which includes unverified/unconfirmed notes you can't yet spend. The
|
||||||
// validation isn't zeroed on lite (the actual lite spend gate is per-address, set below).
|
// spendable_zbalance field is required by the parser, so it's always present when hasBalance is
|
||||||
state.spendablePrivateBalance = state.privateBalance;
|
// true; clamp to the shielded total defensively. Transparent is already confirmed on lite.
|
||||||
|
const double spendableShielded = static_cast<double>(
|
||||||
|
std::min(model.balance.spendableShieldedZatoshis, model.balance.shieldedZatoshis)) / kZatoshisPerCoin;
|
||||||
|
state.spendablePrivateBalance = spendableShielded;
|
||||||
state.spendableTransparentBalance = state.transparentBalance;
|
state.spendableTransparentBalance = state.transparentBalance;
|
||||||
state.spendableTotalBalance = state.totalBalance;
|
state.spendableTotalBalance = spendableShielded + state.transparentBalance;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model.hasAddresses) {
|
if (model.hasAddresses) {
|
||||||
|
|||||||
Reference in New Issue
Block a user