fix(send): stop a fee-tier change from re-sending Max; compare affordability in satoshis

Fund-correctness audit, send path:
- Flagship wrong-send: s_send_max was only cleared on a successful send, so after
  Max → typing a smaller amount → changing the fee tier in the confirm dialog,
  the amount was silently overwritten back to the maximum and Confirm sent far
  more than the user typed. Clear s_send_max on any manual amount edit (DRGX and
  USD inputs), so a later fee bump never rewrites a hand-entered amount.
- A valid exact-maximum send was blocked by 1 ulp because amount+fee<=available
  was computed in IEEE doubles. New util::toSats/canAfford compare in int64
  satoshis; wired into the Review gate, the confirm-time re-check, the Send
  tooltip and the amount-error banner. This only unblocks a correct send; it
  changes no sent amount.
- Added util::coinsToZatoshis (reject non-finite / <=0 / rounds-to-0 / over-supply)
  and regression tests for formatAmountFixed, toSats/canAfford and coinsToZatoshis
  (incl. the exact failing inputs the audit found).

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 c0d10e9664
commit 70d5c233c0
3 changed files with 57 additions and 8 deletions
+21
View File
@@ -1,11 +1,32 @@
#pragma once
#include <string>
#include <cstdint>
#include <cmath>
namespace dragonx {
namespace util {
std::string formatAmountFixed(double amount, int decimals = 8);
// DRGX amounts are 8-decimal. Converting to int64 satoshis and comparing there avoids the IEEE-double
// rounding that could block a valid exact-max send (amount+fee computed in doubles landed 1 ulp over).
inline std::int64_t toSats(double coins) { return static_cast<std::int64_t>(std::llround(coins * 1e8)); }
// True when amount + fee fits within available, compared in integer satoshis (not doubles).
inline bool canAfford(double amount, double fee, double available) {
return toSats(amount) + toSats(fee) <= toSats(available);
}
// Convert a coin amount to zatoshis for a send, rejecting non-finite / non-positive / rounds-to-zero /
// over-supply values. Returns false (and leaves out untouched) on any of those.
inline bool coinsToZatoshis(double coins, std::uint64_t& out) {
if (!std::isfinite(coins) || coins <= 0.0) return false;
const std::int64_t z = toSats(coins);
if (z <= 0 || z > static_cast<std::int64_t>(21000000) * 100000000LL) return false;
out = static_cast<std::uint64_t>(z);
return true;
}
} // namespace util
} // namespace dragonx