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
+29 -4
View File
@@ -1208,10 +1208,35 @@ void testWalletFileProbe()
void testAmountFormatting()
{
EXPECT_EQ(dragonx::util::formatAmountFixed(1.0), std::string("1.00000000"));
EXPECT_EQ(dragonx::util::formatAmountFixed(0.1), std::string("0.10000000"));
EXPECT_EQ(dragonx::util::formatAmountFixed(0.000000019), std::string("0.00000002"));
EXPECT_EQ(dragonx::util::formatAmountFixed(12.3456, 2), std::string("12.35"));
using namespace dragonx::util;
EXPECT_EQ(formatAmountFixed(1.0), std::string("1.00000000"));
EXPECT_EQ(formatAmountFixed(0.1), std::string("0.10000000"));
EXPECT_EQ(formatAmountFixed(0.000000019), std::string("0.00000002"));
EXPECT_EQ(formatAmountFixed(12.3456, 2), std::string("12.35"));
// No scientific notation / comma decimals for tiny or large amounts (the z_sendmany string).
EXPECT_EQ(formatAmountFixed(1e-8), std::string("0.00000001"));
EXPECT_EQ(formatAmountFixed(21000000.0), std::string("21000000.00000000"));
EXPECT_TRUE(formatAmountFixed(0.00000001).find('e') == std::string::npos);
// toSats / canAfford — the exact-max send that double addition wrongly blocked (fund-correctness audit).
EXPECT_EQ(toSats(63.0410039), 6304100390LL);
EXPECT_TRUE(canAfford(63.0409039, 0.0001, 63.0410039)); // true total == available; was false in doubles
EXPECT_TRUE(canAfford(44.83756352, 0.0001, 44.83766352));
EXPECT_TRUE(canAfford(0.4999, 0.0001, 0.5));
EXPECT_TRUE(canAfford(0.5, 0.0, 0.5)); // exact, no fee
EXPECT_TRUE(!canAfford(0.5, 0.0001, 0.5)); // amount+fee over available
EXPECT_TRUE(!canAfford(0.50001, 0.0, 0.5));
// coinsToZatoshis — rejects the values that would wrap/round wrong on the lite send path.
std::uint64_t z = 0;
EXPECT_TRUE(coinsToZatoshis(1.0, z) && z == 100000000ULL);
EXPECT_TRUE(coinsToZatoshis(0.00000001, z) && z == 1ULL);
EXPECT_TRUE(!coinsToZatoshis(0.0, z));
EXPECT_TRUE(!coinsToZatoshis(-1.0, z));
EXPECT_TRUE(!coinsToZatoshis(0.000000004, z)); // sub-zatoshi rounds to 0 → rejected
EXPECT_TRUE(!coinsToZatoshis(std::numeric_limits<double>::quiet_NaN(), z));
EXPECT_TRUE(!coinsToZatoshis(std::numeric_limits<double>::infinity(), z));
EXPECT_TRUE(!coinsToZatoshis(21000001.0, z)); // over supply cap
}
void testSpendableFiltering()