fix(fund): Tier-B parser/aggregation hardening — range-check amounts, int-satoshi sums, strict balance parse
Fund-correctness audit, Tier-B (parse/validation/display only — no send/sweep/encrypt call touched; each change can only reject bad input or remove drift, never over-report): - lite_result_parsers: cap every backend-reported amount at the 21,000,000 DRGX supply (note value, outgoing output value + running sum, received amount, the net-amount fallback magnitude), and replace the two bare `return false` paths in the outgoing-metadata loop — which left ok=false with an EMPTY error and silently rejected the whole tx list — with fail() carrying a reason. - network_refresh_service: accumulate per-address balances in integer satoshis (util::toSats) instead of summing doubles, so many notes can't drift by an ulp per addition (ten 0.1 notes now sum to exactly 1.0); skip non-finite amounts. - readBalanceString: strict decimal parse (digits / one '.' / optional leading '-') instead of std::stod, which silently accepted "1.5abc", whitespace, hex, exponents and nan/inf. Tests: testLiteParserAmountRangeChecks + integer-satoshi drift + strict-parse cases. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
#include "rpc/rpc_busy_gate.h"
|
||||
#include "../util/logger.h"
|
||||
#include "../util/i18n.h" // util::tr for the price-fetch-failed default
|
||||
#include "../util/amount_format.h" // util::toSats: integer-satoshi accumulation
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
@@ -34,7 +35,20 @@ std::optional<T> readOptional(const json& source, const char* key)
|
||||
std::optional<double> readBalanceString(const json& source, const char* key)
|
||||
{
|
||||
try {
|
||||
if (source.contains(key)) return std::stod(source[key].get<std::string>());
|
||||
if (source.contains(key)) {
|
||||
const std::string text = source[key].get<std::string>();
|
||||
// Strict: plain decimal only (digits, one '.', optional leading '-'). std::stod would accept
|
||||
// '1.5abc', whitespace, '+', hex, exponents and nan/inf, silently.
|
||||
if (text.empty()) return std::nullopt;
|
||||
for (std::size_t i = 0; i < text.size(); ++i) {
|
||||
const char c = text[i];
|
||||
if (!((c >= '0' && c <= '9') || c == '.' || (c == '-' && i == 0))) return std::nullopt;
|
||||
}
|
||||
char* end = nullptr;
|
||||
const double value = std::strtod(text.c_str(), &end);
|
||||
if (end != text.c_str() + text.size() || !std::isfinite(value)) return std::nullopt;
|
||||
return value;
|
||||
}
|
||||
} catch (...) {}
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -52,15 +66,17 @@ void applyBalancesFromUnspent(std::vector<AddressInfo>& addresses, const json& u
|
||||
// Partition each note/utxo by its per-entry "confirmations": `total` (minconf=0 — DISPLAY, includes
|
||||
// the user's own pending 0-conf change) vs `spendable` (confirmations>=1 — what z_sendmany, run at
|
||||
// minconf=1, can actually spend). This lets a single z_listunspent(0)/listunspent(0) feed both.
|
||||
std::map<std::string, double> total;
|
||||
std::map<std::string, double> spendable;
|
||||
// Accumulated in integer satoshis (not doubles) so many notes can't drift by an ulp per addition.
|
||||
std::map<std::string, std::int64_t> total;
|
||||
std::map<std::string, std::int64_t> spendable;
|
||||
for (const auto& output : unspent) {
|
||||
auto address = readOptional<std::string>(output, "address");
|
||||
auto amount = readOptional<double>(output, "amount");
|
||||
if (!address || !amount) continue;
|
||||
total[*address] += *amount;
|
||||
if (!address || !amount || !std::isfinite(*amount)) continue;
|
||||
const std::int64_t sats = util::toSats(*amount);
|
||||
total[*address] += sats;
|
||||
auto conf = readOptional<int>(output, "confirmations");
|
||||
if (conf && *conf >= 1) spendable[*address] += *amount;
|
||||
if (conf && *conf >= 1) spendable[*address] += sats;
|
||||
}
|
||||
|
||||
// The address lists are rebuilt fresh (default 0) each refresh, so hard-set both — an address with no
|
||||
@@ -68,8 +84,8 @@ void applyBalancesFromUnspent(std::vector<AddressInfo>& addresses, const json& u
|
||||
for (auto& info : addresses) {
|
||||
auto t = total.find(info.address);
|
||||
auto s = spendable.find(info.address);
|
||||
info.balance = (t != total.end()) ? t->second : 0.0;
|
||||
info.spendableBalance = (s != spendable.end()) ? s->second : 0.0;
|
||||
info.balance = (t != total.end()) ? static_cast<double>(t->second) / 1e8 : 0.0;
|
||||
info.spendableBalance = (s != spendable.end()) ? static_cast<double>(s->second) / 1e8 : 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user