Compare commits
2
Commits
c0d10e9664
...
f4460beddd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4460beddd | ||
|
|
70d5c233c0 |
+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)) {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
#include "../../util/address_validation.h"
|
#include "../../util/address_validation.h"
|
||||||
#include "../../util/i18n.h"
|
#include "../../util/i18n.h"
|
||||||
#include "../../util/text_format.h"
|
#include "../../util/text_format.h"
|
||||||
|
#include "../../util/amount_format.h" // toSats / canAfford — integer-satoshi affordability check
|
||||||
#include "../notifications.h"
|
#include "../notifications.h"
|
||||||
#include "../layout.h"
|
#include "../layout.h"
|
||||||
#include "contact_picker.h"
|
#include "contact_picker.h"
|
||||||
@@ -916,7 +917,7 @@ void RenderSendConfirmPopup(App* app) {
|
|||||||
const auto& liveState = app->getWalletState();
|
const auto& liveState = app->getWalletState();
|
||||||
double liveAvail = GetAvailableBalance(app);
|
double liveAvail = GetAvailableBalance(app);
|
||||||
if (!app->isConnected() || liveState.sync.syncing || s_amount <= 0 ||
|
if (!app->isConnected() || liveState.sync.syncing || s_amount <= 0 ||
|
||||||
(s_amount + s_fee) > liveAvail) {
|
!util::canAfford(s_amount, s_fee, liveAvail)) {
|
||||||
s_tx_status = TR("send_cannot_send_now");
|
s_tx_status = TR("send_cannot_send_now");
|
||||||
s_status_success = false;
|
s_status_success = false;
|
||||||
s_status_timestamp = ImGui::GetTime();
|
s_status_timestamp = ImGui::GetTime();
|
||||||
@@ -1037,7 +1038,7 @@ static void RenderActionButtons(App* app, float width, float vScale,
|
|||||||
is_valid_address &&
|
is_valid_address &&
|
||||||
s_amount > 0 &&
|
s_amount > 0 &&
|
||||||
s_from_address[0] != '\0' &&
|
s_from_address[0] != '\0' &&
|
||||||
total <= available &&
|
util::canAfford(s_amount, s_fee, available) &&
|
||||||
sourceSpendable &&
|
sourceSpendable &&
|
||||||
!s_sending;
|
!s_sending;
|
||||||
|
|
||||||
@@ -1095,7 +1096,7 @@ static void RenderActionButtons(App* app, float width, float vScale,
|
|||||||
material::Tooltip("%s", TR("send_tooltip_invalid_address"));
|
material::Tooltip("%s", TR("send_tooltip_invalid_address"));
|
||||||
else if (s_amount <= 0)
|
else if (s_amount <= 0)
|
||||||
material::Tooltip("%s", TR("send_tooltip_enter_amount"));
|
material::Tooltip("%s", TR("send_tooltip_enter_amount"));
|
||||||
else if (total > available)
|
else if (!util::canAfford(s_amount, s_fee, available))
|
||||||
material::Tooltip("%s", TR("send_tooltip_exceeds_balance"));
|
material::Tooltip("%s", TR("send_tooltip_exceeds_balance"));
|
||||||
else if (!sourceSpendable)
|
else if (!sourceSpendable)
|
||||||
material::Tooltip("%s", TR("send_tooltip_view_only"));
|
material::Tooltip("%s", TR("send_tooltip_view_only"));
|
||||||
@@ -1530,6 +1531,7 @@ void RenderSendTab(App* app)
|
|||||||
if (ImGui::InputDouble("##AmountUSD", &s_usd_amount, 0, 0, "$%.2f")) {
|
if (ImGui::InputDouble("##AmountUSD", &s_usd_amount, 0, 0, "$%.2f")) {
|
||||||
// Normalize to 8dp (satoshi precision) so the reviewed/sent DRGX matches the preview.
|
// Normalize to 8dp (satoshi precision) so the reviewed/sent DRGX matches the preview.
|
||||||
s_amount = std::round((s_usd_amount / market.price_usd) * 1e8) / 1e8;
|
s_amount = std::round((s_usd_amount / market.price_usd) * 1e8) / 1e8;
|
||||||
|
s_send_max = false; // a manual edit is no longer "max" — don't let a later fee bump overwrite it
|
||||||
}
|
}
|
||||||
// Draw DRGX equivalent inside the input field (right-aligned overlay)
|
// Draw DRGX equivalent inside the input field (right-aligned overlay)
|
||||||
{
|
{
|
||||||
@@ -1555,6 +1557,7 @@ void RenderSendTab(App* app)
|
|||||||
if (s_amount < 0) s_amount = 0;
|
if (s_amount < 0) s_amount = 0;
|
||||||
if (market.price_usd > 0)
|
if (market.price_usd > 0)
|
||||||
s_usd_amount = s_amount * market.price_usd;
|
s_usd_amount = s_amount * market.price_usd;
|
||||||
|
s_send_max = false; // a manual edit is no longer "max" — don't let a later fee bump overwrite it
|
||||||
}
|
}
|
||||||
// Draw USD equivalent inside the input field (right-aligned overlay)
|
// Draw USD equivalent inside the input field (right-aligned overlay)
|
||||||
if (market.price_usd > 0 && s_amount > 0) {
|
if (market.price_usd > 0 && s_amount > 0) {
|
||||||
@@ -1619,7 +1622,7 @@ void RenderSendTab(App* app)
|
|||||||
RenderAmountBar(dl, available, colW, capFont, market.price_usd, "##f");
|
RenderAmountBar(dl, available, colW, capFont, market.price_usd, "##f");
|
||||||
|
|
||||||
// Amount error
|
// Amount error
|
||||||
if (s_amount > 0 && s_amount + s_fee > available && available > 0) {
|
if (s_amount > 0 && !util::canAfford(s_amount, s_fee, available) && available > 0) {
|
||||||
snprintf(buf, sizeof(buf), TR("send_exceeds_available"), available - s_fee);
|
snprintf(buf, sizeof(buf), TR("send_exceeds_available"), available - s_fee);
|
||||||
Type().textColored(TypeStyle::Caption, Error(), buf);
|
Type().textColored(TypeStyle::Caption, Error(), buf);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,32 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
namespace util {
|
namespace util {
|
||||||
|
|
||||||
std::string formatAmountFixed(double amount, int decimals = 8);
|
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 util
|
||||||
} // namespace dragonx
|
} // namespace dragonx
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
+29
-4
@@ -1208,10 +1208,35 @@ void testWalletFileProbe()
|
|||||||
|
|
||||||
void testAmountFormatting()
|
void testAmountFormatting()
|
||||||
{
|
{
|
||||||
EXPECT_EQ(dragonx::util::formatAmountFixed(1.0), std::string("1.00000000"));
|
using namespace dragonx::util;
|
||||||
EXPECT_EQ(dragonx::util::formatAmountFixed(0.1), std::string("0.10000000"));
|
EXPECT_EQ(formatAmountFixed(1.0), std::string("1.00000000"));
|
||||||
EXPECT_EQ(dragonx::util::formatAmountFixed(0.000000019), std::string("0.00000002"));
|
EXPECT_EQ(formatAmountFixed(0.1), std::string("0.10000000"));
|
||||||
EXPECT_EQ(dragonx::util::formatAmountFixed(12.3456, 2), std::string("12.35"));
|
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()
|
void testSpendableFiltering()
|
||||||
|
|||||||
Reference in New Issue
Block a user