The Send tab auto-selected the highest-balance address on first load. When another tab had already pre-filled the source with "Send from this address", that first-load pick overwrote the user's choice, and the dropdown's selected index could drift out of step with the address that would actually be debited when the address list changed. - The send-from string is now the single source of truth. The one-shot auto-pick only fills an empty source and never replaces one the user or another tab set, and the list index is re-derived from the string each frame so it cannot desync. - An explicit "Send from this address" choice latches the auto-pick off, so the first visit cannot undo it. - A chosen source the dropdown does not list (zero balance, view-only, or the list still loading) now previews the address that will be debited instead of the "Select a source" placeholder. User-visible: a send-from address chosen from another tab survives the first load and later refresh ticks. Tests: the source resolver keeps a pre-fill on first visit, fills an empty source with the best spendable address, keeps a source missing from a short list with index -1, latches nothing while disconnected or with an empty list, and re-derives a stale index from the string. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
106 lines
3.5 KiB
C++
106 lines
3.5 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
|
|
#include "wallet_state.h"
|
|
#include "../util/text_format.h" // util::formatClockDateTime (app-wide 24h/12h clock)
|
|
#include <algorithm>
|
|
#include <ctime>
|
|
#include <sstream>
|
|
#include <iomanip>
|
|
|
|
namespace dragonx {
|
|
|
|
std::vector<size_t> sortedSpendableAddressIndices(const std::vector<AddressInfo>& addresses,
|
|
bool requirePositiveBalance)
|
|
{
|
|
std::vector<size_t> indices;
|
|
indices.reserve(addresses.size());
|
|
for (size_t i = 0; i < addresses.size(); ++i) {
|
|
const auto& address = addresses[i];
|
|
if (!address.isSpendable()) continue;
|
|
// Rank/filter by the CONFIRMED balance — an address holding only 0-conf change can't be sent from.
|
|
if (requirePositiveBalance && address.spendableBalance <= 0.0) continue;
|
|
indices.push_back(i);
|
|
}
|
|
|
|
std::sort(indices.begin(), indices.end(), [&](size_t lhs, size_t rhs) {
|
|
return addresses[lhs].spendableBalance > addresses[rhs].spendableBalance;
|
|
});
|
|
return indices;
|
|
}
|
|
|
|
int bestSpendableAddressIndex(const std::vector<AddressInfo>& addresses)
|
|
{
|
|
int bestIndex = -1;
|
|
double bestBalance = 0.0;
|
|
for (size_t i = 0; i < addresses.size(); ++i) {
|
|
if (addresses[i].isSpendable() && addresses[i].spendableBalance > bestBalance) {
|
|
bestBalance = addresses[i].spendableBalance;
|
|
bestIndex = static_cast<int>(i);
|
|
}
|
|
}
|
|
return bestIndex;
|
|
}
|
|
|
|
int addressIndexByString(const std::vector<AddressInfo>& addresses, const std::string& address)
|
|
{
|
|
if (address.empty()) return -1;
|
|
for (size_t i = 0; i < addresses.size(); ++i) {
|
|
if (addresses[i].address == address) return static_cast<int>(i);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
SourceSelection resolveSourceSelection(const std::vector<AddressInfo>& addresses, const std::string& current,
|
|
bool autoLatched, bool connected)
|
|
{
|
|
SourceSelection r;
|
|
r.address = current;
|
|
r.autoLatched = autoLatched;
|
|
r.index = addressIndexByString(addresses, current);
|
|
if (!autoLatched && connected && !addresses.empty()) {
|
|
if (current.empty()) { // only ever fill an empty source
|
|
const int best = bestSpendableAddressIndex(addresses);
|
|
if (best >= 0) { r.address = addresses[best].address; r.index = best; }
|
|
}
|
|
r.autoLatched = true; // one shot, whether or not it picked
|
|
}
|
|
return r;
|
|
}
|
|
|
|
std::string TransactionInfo::getTimeString() const
|
|
{
|
|
if (timestamp == 0) return "Unknown";
|
|
return util::formatClockDateTime(timestamp);
|
|
}
|
|
|
|
std::string TransactionInfo::getTypeDisplay() const
|
|
{
|
|
if (type == "send") return "Sent";
|
|
if (type == "receive") return "Received";
|
|
if (type == "mined" || type == "generate" || type == "immature") return "Mined";
|
|
return type;
|
|
}
|
|
|
|
std::string PeerInfo::getConnectionTime() const
|
|
{
|
|
if (conntime == 0) return "Unknown";
|
|
|
|
int64_t now = std::time(nullptr);
|
|
int64_t diff = now - conntime;
|
|
|
|
if (diff < 60) return std::to_string(diff) + "s";
|
|
if (diff < 3600) return std::to_string(diff / 60) + "m";
|
|
if (diff < 86400) return std::to_string(diff / 3600) + "h";
|
|
return std::to_string(diff / 86400) + "d";
|
|
}
|
|
|
|
std::string BannedPeer::getBannedUntilString() const
|
|
{
|
|
if (banned_until == 0) return "Never";
|
|
return util::formatClockDateTime(banned_until);
|
|
}
|
|
|
|
} // namespace dragonx
|