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

@@ -135,6 +135,7 @@ struct WalletBtreeStats {
int shieldedKeys = 0; ///< zkey + czkey + sapzkey + csapzkey (≈ shielded addresses)
int addressBook = 0; ///< name records (labeled/received addresses)
int txCount = 0; ///< tx records (wallet transactions)
long long createdEpoch = 0; ///< earliest keymeta nCreateTime (wallet birthday); 0 = unknown
bool encrypted = false; ///< saw an mkey record
bool hdSeed = false; ///< saw hdseed/chdseed/hdchain
std::size_t bytesRead = 0; ///< bytes actually read (for budgeting)
@@ -275,7 +276,8 @@ inline WalletBtreeStats parseWalletBtree(const std::string& path,
if (aborted) return st;
if (subRoots.empty()) subRoots.push_back(root); // single-database file
auto countKey = [&](const unsigned char* kp, uint32_t kl) {
auto countRecord = [&](const unsigned char* kp, uint32_t kl,
const unsigned char* dp, uint32_t dl, uint8_t dtype) {
const uint32_t nlen = kp[0]; // CompactSize length of the type string
if (nlen < 2 || nlen > 20 || 1u + nlen > kl) return;
const char* nm = reinterpret_cast<const char*>(kp + 1);
@@ -286,9 +288,21 @@ inline WalletBtreeStats parseWalletBtree(const std::string& path,
else if (is("tx")) st.txCount++;
else if (is("mkey")) st.encrypted = true;
else if (is("hdseed") || is("chdseed") || is("hdchain")) st.hdSeed = true;
else if (is("keymeta")) {
// CKeyMetadata data = nVersion(int32) + nCreateTime(int64) + …, all little-endian (Bitcoin
// serialization). The earliest non-zero nCreateTime is the wallet birthday (daemon's
// nTimeFirstKey). B_KEYDATA(1) only; overflow values aren't inline.
if (dtype == B_KEYDATA && dp && dl >= 12) {
long long t = 0;
for (int b = 0; b < 8; ++b) t |= static_cast<long long>(dp[4 + b]) << (8 * b);
if (t > 0 && (st.createdEpoch == 0 || t < st.createdEpoch)) st.createdEpoch = t;
}
}
};
for (const uint32_t sr : subRoots) {
traverse(sr, [&](const unsigned char* kp, uint32_t kl, const unsigned char*, uint32_t, uint8_t) { countKey(kp, kl); });
traverse(sr, [&](const unsigned char* kp, uint32_t kl, const unsigned char* dp, uint32_t dl, uint8_t dt) {
countRecord(kp, kl, dp, dl, dt);
});
if (aborted) return st;
}
st.parsed = true;