feat(wallets): show created date + sort the wallet list

Parse the earliest keymeta nCreateTime out of the wallet.dat btree (the daemon's
"wallet birthday") and surface it in each row's metadata line ("created Aug 2025").

Add a sort control above the list — Date created / Address count / Transaction
count / Wallet size, with an ascending/descending toggle (descending default:
newest / most / largest first). Sorting reorders a display-index array
(s_order) rather than s_rows, so the async, index-aligned probe batch is
untouched; sort keys read a single frame-consistent snapshot of the probe
results. Address count prefers the authoritative cached index value, else the
btree key count.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-11 16:37:36 -05:00
parent 8eefab99ec
commit 54a14485a5
4 changed files with 105 additions and 13 deletions

View File

@@ -14,6 +14,7 @@
#include <cctype>
#include <cstddef>
#include <cstdint>
#include <ctime>
#include <filesystem>
#include <memory>
#include <mutex>
@@ -86,7 +87,8 @@ public:
+ ctrlRow // footer buttons
+ 6.0f * style.ItemSpacing.y; // uncounted inter-item gaps
const float headH = Type().h6()->LegacySize + Layout::spacingXs() // framework h6 title
+ Type().caption()->LegacySize + Layout::spacingSm(); // intro + gap
+ Type().caption()->LegacySize + Layout::spacingSm() // intro + gap
+ ctrlRow + Layout::spacingSm(); // sort control row + gap
const float padV = 48.0f; // content-child padding (top+bottom) + margin
// Size to content, but cap at a viewport fraction: with many wallets on a small / HiDPI
// screen the content-sized card could exceed the window (the framework would clip a too-tall
@@ -110,6 +112,54 @@ public:
const std::string active = app->settings() ? app->settings()->getActiveWalletFile() : "wallet.dat";
// ---- Frame-consistent snapshot of the async probe results (badges / counts / sort all read
// the same view; one lock instead of one-per-row). ----------------------------------
std::vector<ProbeResult> probeSnap;
if (s_probe) { std::lock_guard<std::mutex> lk(s_probe->mtx); probeSnap = s_probe->results; }
auto probeAt = [&](std::size_t idx) -> ProbeResult {
return idx < probeSnap.size() ? probeSnap[idx] : ProbeResult{};
};
// ---- Sort controls: pick the key; the arrow flips ascending/descending -----------------
{
ImGui::AlignTextToFramePadding();
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("wallets_sort_by"));
ImGui::SameLine();
const char* items[] = { TR("wallets_sort_created"), TR("wallets_sort_addresses"),
TR("wallets_sort_txs"), TR("wallets_sort_size") };
ImGui::SetNextItemWidth(190.0f * dp);
ImGui::Combo("##walletSort", &s_sortMode, items, IM_ARRAYSIZE(items));
ImGui::SameLine();
const float dbH = ImGui::GetFrameHeight();
if (StyledButton(s_sortDesc ? ICON_MD_ARROW_DOWNWARD : ICON_MD_ARROW_UPWARD,
ImVec2(dbH, dbH), Type().iconSmall()))
s_sortDesc = !s_sortDesc;
if (ImGui::IsItemHovered()) Tooltip("%s", TR(s_sortDesc ? "wallets_sort_desc" : "wallets_sort_asc"));
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// ---- Compute the display order for the active sort key (stable, so ties keep scan order).
s_order.resize(s_rows.size());
for (std::size_t k = 0; k < s_rows.size(); ++k) s_order[k] = k;
{
std::vector<long long> sortVal(s_rows.size(), 0);
for (std::size_t k = 0; k < s_rows.size(); ++k) {
const WalletRow& r = s_rows[k];
const ProbeResult pr = probeAt(k);
switch (s_sortMode) {
case 1: { const auto* m = r.inDatadir ? app->walletIndex().find(r.fileName) : nullptr;
sortVal[k] = (m && m->cachedAddressCount >= 0) ? (long long)m->cachedAddressCount
: pr.keyCount; break; }
case 2: sortVal[k] = pr.txCount; break;
case 3: sortVal[k] = r.sizeBytes; break;
default: sortVal[k] = pr.createdEpoch; break; // 0 = date created
}
}
std::stable_sort(s_order.begin(), s_order.end(), [&](std::size_t a, std::size_t b) {
return s_sortDesc ? sortVal[a] > sortVal[b] : sortVal[a] < sortVal[b];
});
}
// ---- Wallet cards (Material-style rows; no table) -------------------------------------
const float listW = ImGui::GetContentRegionAvail().x;
// Flex only when the card was viewport-capped: give the list the space left above the
@@ -142,7 +192,8 @@ public:
return s;
};
for (std::size_t i = 0; i < s_rows.size(); ++i) {
for (std::size_t k = 0; k < s_order.size(); ++k) {
const std::size_t i = s_order[k]; // sorted display order → actual s_rows index
const WalletRow& r = s_rows[i];
// The wallet index is keyed by bare filename and is only ever written for the active
// DATADIR wallet — so an external row (esp. a same-named one surfaced by the recursive
@@ -187,9 +238,7 @@ public:
// file was scanned (probeComplete): seed iff hdSeed; "legacy" only when complete & no seed;
// "encrypted" iff mkey found; and an "unknown" row when a truncated scan couldn't confirm
// encryption — so absence of a lock never falsely reads as "unencrypted" on a huge wallet.
ProbeResult pres; // filled in asynchronously by the background probe thread
if (s_probe) { std::lock_guard<std::mutex> lk(s_probe->mtx);
if (i < s_probe->results.size()) pres = s_probe->results[i]; }
const ProbeResult pres = probeAt(i); // from the frame-consistent snapshot above
const bool bLock = pres.probed && pres.encrypted;
const bool bSeed = pres.probed && pres.hdSeed;
const bool bLegacy = pres.probed && pres.complete && !pres.hdSeed;
@@ -239,6 +288,11 @@ public:
if (meta && meta->cachedAddressCount >= 0) { snprintf(b, sizeof(b), "%s%lld %s", dot, (long long)meta->cachedAddressCount, TR("wallets_col_addresses")); ms += b; }
else if (pres.hasCounts && pres.keyCount > 0) { snprintf(b, sizeof(b), "%s%d %s", dot, pres.keyCount, TR("wallets_col_keys")); ms += b; }
if (pres.hasCounts) { snprintf(b, sizeof(b), "%s%d %s", dot, pres.txCount, TR("wallets_col_txs")); ms += b; }
if (pres.hasCounts && pres.createdEpoch > 0) { // wallet birthday, e.g. "created Aug 2025"
std::time_t ct = static_cast<std::time_t>(pres.createdEpoch);
char cd[24]; std::strftime(cd, sizeof(cd), "%b %Y", std::localtime(&ct));
snprintf(b, sizeof(b), "%s%s %s", dot, TR("wallets_created"), cd); ms += b;
}
if (meta && meta->cachedBalance >= 0.0) { snprintf(b, sizeof(b), "%s%.4f %s", dot, meta->cachedBalance, DRAGONX_TICKER); ms += b; }
ms += dot;
ms += (meta && meta->lastOpenedEpoch > 0) ? util::formatTimeAgo(meta->lastOpenedEpoch) : std::string(TR("wallets_never"));
@@ -361,6 +415,7 @@ private:
bool hasCounts = false; // an exact btree walk produced the counts below
int keyCount = 0; // transparent + shielded spendable keys (≈ addresses, incl. change)
int txCount = 0; // wallet transaction records
long long createdEpoch = 0; // wallet birthday (earliest keymeta nCreateTime); 0 = unknown
};
struct ProbeBatch {
std::mutex mtx;
@@ -518,7 +573,7 @@ private:
// byte-scan for badges only (no counts).
const auto bt = util::parseWalletBtree(t.first, std::min(budget, kPerFile));
if (bt.parsed && bt.complete) {
res = ProbeResult{ true, true, bt.encrypted, bt.hdSeed, true, bt.addresses(), bt.txCount };
res = ProbeResult{ true, true, bt.encrypted, bt.hdSeed, true, bt.addresses(), bt.txCount, bt.createdEpoch };
budget -= std::min(budget, bt.bytesRead);
} else {
const auto pr = util::probeWalletFile(t.first, std::min(budget, kPerFile));
@@ -545,6 +600,9 @@ private:
static inline char s_newName[128] = "";
static inline std::vector<WalletRow> s_rows;
static inline std::shared_ptr<ProbeBatch> s_probe; // current async probe batch (index-aligned w/ s_rows)
static inline std::vector<std::size_t> s_order; // display order (indices into s_rows) for the active sort
static inline int s_sortMode = 0; // 0 created · 1 addresses · 2 txs · 3 size
static inline bool s_sortDesc = true; // newest / most / largest first
};
} // namespace ui