From 184cad14f9ab228a6ccb86c7a37ad21643cceb8d Mon Sep 17 00:00:00 2001 From: Keeqler <33733651+Keeqler@users.noreply.github.com> Date: Fri, 31 Jul 2026 15:46:23 -0300 Subject: [PATCH 1/3] Implement estimateTransactionFee --- src/wallet.cpp | 83 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/src/wallet.cpp b/src/wallet.cpp index daa6631..2d4b549 100644 --- a/src/wallet.cpp +++ b/src/wallet.cpp @@ -2017,7 +2017,88 @@ namespace lwsf { namespace internal uint64_t wallet::estimateTransactionFee(const std::vector> &destinations, Monero::PendingTransaction::Priority priority) const { - throw std::runtime_error{"Not Implemented yet"}; + // Weight-based fee estimate. Local only — no tx build, no network. Gathers + // the (cached) unspent outputs and greedily counts how many inputs cover + // the amount + fee, so the estimate tracks fragmented wallets, then models + // tx_extra to account for subaddress destinations. + const unsigned priority_int = priority <= 0 ? 1 : unsigned(priority) - 1; + const std::size_t n_outputs = destinations.size() + 1; // + change + + std::uint64_t per_byte_fee = 0; + std::uint64_t fee_mask = 0; + cryptonote::network_type ctype{}; + std::vector unspent_amounts; + { + const boost::lock_guard lock{data_.wallet->sync}; + fee_mask = data_.wallet->fee_mask; + if (data_.wallet->per_byte_fee.empty() || !fee_mask) + return 0; // no fee info yet (not logged in / not synced) + per_byte_fee = (data_.wallet->per_byte_fee.size() <= priority_int) + ? data_.wallet->per_byte_fee.back() + : data_.wallet->per_byte_fee[priority_int]; + ctype = data_.wallet->get_net_type(); + + // Gather spendable outputs for account 0, + // mirroring createTransactionMultDest: unlocked, minus already-spent. + const std::uint64_t height = data_.wallet->blockchain_height; + const Monero::NetworkType mtype = data_.wallet->primary.type; + std::unordered_map unspent; + for (const auto& tx : data_.wallet->primary.txes) + { + if (!tx.second->is_unlocked(height, mtype)) + continue; + for (const auto& receive : tx.second->receives) + { + const std::uint64_t amt = receive.second.amount; + if (amt && receive.second.recipient.maj_i == 0) + unspent[receive.first] = amt; + } + } + for (const auto& tx : data_.wallet->primary.txes) + for (const auto& spend : tx.second->spends) + unspent.erase(spend.second.output_pub); + + unspent_amounts.reserve(unspent.size()); + for (const auto& u : unspent) + unspent_amounts.push_back(u.second); + } + + // Model tx_extra realistically: the tx public key (33) + an 8-byte + // payment-id nonce (real for integrated addresses, the dummy Monero adds + // otherwise) + additional tx public keys (one per output) when any + // destination is a subaddress. A flat 44 misses the subaddress case. + std::size_t extra_size = 33 + 11; + bool any_subaddress = false; + for (const auto& dest : destinations) + { + cryptonote::address_parse_info info{}; + if (cryptonote::get_account_address_from_str(info, ctype, dest.first) && info.is_subaddress) + any_subaddress = true; + } + if (any_subaddress) + extra_size += 2 + 32 * n_outputs; // additional pub keys + + std::uint64_t total = 0; + for (const auto& dest : destinations) + total += dest.second; + + // Greedily take the largest outputs until they cover amount + fee, + // recomputing the fee as the input count grows (fee depends on size). + std::sort(unspent_amounts.begin(), unspent_amounts.end(), std::greater()); + std::uint64_t selected = 0; + std::size_t n_inputs = 0; + for (const std::uint64_t amt : unspent_amounts) + { + selected += amt; + ++n_inputs; + const std::uint64_t fee = estimate_fee(per_byte_fee, n_inputs, n_outputs, mixin_, extra_size, fee_mask); + if (selected >= total + fee) + break; + } + if (n_inputs == 0) + n_inputs = 1; // no unspent info cached; fall back to a single input + + return estimate_fee(per_byte_fee, n_inputs, n_outputs, mixin_, extra_size, fee_mask); } bool wallet::exportKeyImages(const std::string &filename, bool all) From 87aa1f61027284feeed044a3b886a93051844184 Mon Sep 17 00:00:00 2001 From: Keeqler <33733651+Keeqler@users.noreply.github.com> Date: Mon, 3 Aug 2026 13:30:34 -0300 Subject: [PATCH 2/3] Mirror real algo and select 2 inputs when 1 is necessary --- src/wallet.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/wallet.cpp b/src/wallet.cpp index 2d4b549..87ea690 100644 --- a/src/wallet.cpp +++ b/src/wallet.cpp @@ -2098,6 +2098,10 @@ namespace lwsf { namespace internal if (n_inputs == 0) n_inputs = 1; // no unspent info cached; fall back to a single input + // Real txs merge dust in as a 2nd input when one would suffice; mirror it. + if (n_inputs < 2 && unspent_amounts.size() >= 2) + n_inputs = 2; + return estimate_fee(per_byte_fee, n_inputs, n_outputs, mixin_, extra_size, fee_mask); } From 42130b80ea1b4e003b566feadace390d19998785 Mon Sep 17 00:00:00 2001 From: Keeqler <33733651+Keeqler@users.noreply.github.com> Date: Wed, 5 Aug 2026 16:26:41 -0300 Subject: [PATCH 3/3] Handle not enough unspent coins --- src/wallet.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/wallet.cpp b/src/wallet.cpp index 87ea690..9236122 100644 --- a/src/wallet.cpp +++ b/src/wallet.cpp @@ -2087,14 +2087,22 @@ namespace lwsf { namespace internal std::sort(unspent_amounts.begin(), unspent_amounts.end(), std::greater()); std::uint64_t selected = 0; std::size_t n_inputs = 0; + bool covered = false; for (const std::uint64_t amt : unspent_amounts) { selected += amt; ++n_inputs; const std::uint64_t fee = estimate_fee(per_byte_fee, n_inputs, n_outputs, mixin_, extra_size, fee_mask); if (selected >= total + fee) + { + covered = true; break; + } } + + if (!unspent_amounts.empty() && !covered) + return 0; + if (n_inputs == 0) n_inputs = 1; // no unspent info cached; fall back to a single input