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

@@ -260,6 +260,14 @@ void I18n::loadBuiltinEnglish()
strings_["wallets_col_addresses"] = "Addresses";
strings_["wallets_col_txs"] = "txs";
strings_["wallets_col_keys"] = "keys";
strings_["wallets_created"] = "created";
strings_["wallets_sort_by"] = "Sort:";
strings_["wallets_sort_created"] = "Date created";
strings_["wallets_sort_addresses"] = "Address count";
strings_["wallets_sort_txs"] = "Transaction count";
strings_["wallets_sort_size"] = "Wallet size";
strings_["wallets_sort_asc"] = "Ascending (oldest / fewest / smallest first)";
strings_["wallets_sort_desc"] = "Descending (newest / most / largest first)";
strings_["wallets_col_balance"] = "Balance";
strings_["wallets_col_opened"] = "Last opened";
strings_["wallets_current"] = "current";

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;