From 5ae5e5213b4b3ec06e4535ddfb5fb6225412e358 Mon Sep 17 00:00:00 2001 From: DanS Date: Mon, 12 Oct 2026 21:42:22 -0500 Subject: [PATCH] fix(lite): sort lite transactions newest-first to match the UI panels The lite backend lists transactions oldest-first, but every consumer of the transaction list assumes newest-first and takes a prefix of it, so the Recent Transactions / Recent Sends / Recent Received panels and the CSV export showed the oldest records instead of the latest. - The UI copy is now sorted pending (0-conf) first, then newest-first by timestamp, the same order as the History tab's default. - The sort is stable, so several records that share a txid (a send entry plus its per-note entries, or same-block transactions) keep a deterministic order across refreshes. That keeps the order-sensitive change check reporting "unchanged" and avoids bumping the transaction revision every couple of seconds. Tests: a new case covers the pending-first, newest-first ordering, a stable order within one txid, and the revision staying unchanged when the identical model is re-applied. Co-Authored-By: Claude Opus 4.8 --- src/wallet/lite_wallet_controller.cpp | 11 ++++++ tests/test_phase4.cpp | 57 +++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/src/wallet/lite_wallet_controller.cpp b/src/wallet/lite_wallet_controller.cpp index cb36597..f006a17 100644 --- a/src/wallet/lite_wallet_controller.cpp +++ b/src/wallet/lite_wallet_controller.cpp @@ -402,6 +402,17 @@ void applyLiteRefreshModelToWalletState(const LiteWalletAppRefreshModel& model, } nextTransactions.push_back(std::move(tx)); } + // The backend lists oldest-first; every consumer of state.transactions (Recent Transactions / Recent Sends / + // Recent Received, CSV export) assumes newest-first. Same key as the History tab's default: pending (0-conf) + // on top, then newest. STABLE: one txid yields several records with the same datetime (send entry + per-note + // entries, same-block txs), and stability keeps their order deterministic across refreshes, so the + // order-sensitive sameTransactionList() still reports "unchanged" and tx_revision is not bumped every 2 s. + std::stable_sort(nextTransactions.begin(), nextTransactions.end(), + [](const TransactionInfo& a, const TransactionInfo& b) { + const bool ap = a.confirmations == 0, bp = b.confirmations == 0; + if (ap != bp) return ap; + return a.timestamp > b.timestamp; + }); state.setTransactionsIfChanged(std::move(nextTransactions)); } diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index 46f10f6..6a7211f 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -10493,6 +10493,62 @@ void testLiteRefreshModelAppliesToWalletState() EXPECT_EQ(state.transactions[0].confirmations, 12); // chain 1001 - block 990 + 1 } +// The backend lists transactions oldest-first; the UI copy must be pending-first then newest-first +// (Recent panels take a prefix), stable for same-txid rows, and re-applying must not bump tx_revision. +void testLiteTransactionsAreNewestFirst() +{ + using namespace dragonx::wallet; + + LiteWalletRefreshBundle bundle; + bundle.complete = true; + bundle.successfulCommandCount = 2; + bundle.hasTransactions = true; + auto add = [&](const char* txid, std::int64_t height, std::int64_t dt, bool unconfirmed, const char* addr) { + LiteTransactionRecord r; + r.txid = txid; + r.datetime = dt; + if (!unconfirmed) r.blockHeight = height; + r.unconfirmed = unconfirmed; + r.direction = LiteTransactionDirection::Receive; + r.address = addr; + r.amount = 100000000; + bundle.transactions.transactions.push_back(r); + }; + add("A", 100, 1000, false, "zs1x"); + add("B", 200, 2000, false, "zs1x"); + add("C", 300, 3000, false, "zs1x"); + add("D", 0, 500, true, "zs1x"); + add("dup", 250, 2500, false, "zs1a"); + add("dup", 250, 2500, false, "zs1b"); + bundle.hasSyncStatus = true; + bundle.syncStatus.syncedBlocks = 1000; + bundle.syncStatus.totalBlocks = 1000; + bundle.syncStatus.progress = 1.0; + bundle.syncStatus.complete = true; + + const auto mapped = mapLiteWalletRefreshBundle(bundle); + EXPECT_TRUE(mapped.ok); + dragonx::WalletState state; + applyLiteRefreshModelToWalletState(mapped.model, state); + + EXPECT_EQ(static_cast(state.transactions.size()), 6); + if (state.transactions.size() == 6) { + EXPECT_EQ(state.transactions[0].txid, std::string("D")); + EXPECT_EQ(state.transactions[1].txid, std::string("C")); + EXPECT_EQ(state.transactions[2].txid, std::string("dup")); + EXPECT_EQ(state.transactions[2].address, std::string("zs1a")); + EXPECT_EQ(state.transactions[3].txid, std::string("dup")); + EXPECT_EQ(state.transactions[3].address, std::string("zs1b")); + EXPECT_EQ(state.transactions[4].txid, std::string("B")); + EXPECT_EQ(state.transactions[5].txid, std::string("A")); + } + + const std::uint64_t rev = state.tx_revision; + applyLiteRefreshModelToWalletState(mapped.model, state); + applyLiteRefreshModelToWalletState(mapped.model, state); + EXPECT_EQ(state.tx_revision, rev); +} + // A Send record carries its recipient in outgoing_metadata, not the top-level address/memo — // the transactions list must surface the destination + memo instead of blanks. void testLiteSendShowsRecipientFromOutgoing() @@ -17404,6 +17460,7 @@ int main() testLiteNewPassphraseRules(); testLiteChainNameMigration(); testLiteRefreshModelAppliesToWalletState(); + testLiteTransactionsAreNewestFirst(); testSameTransactionList(); testWalletStateTxRevision(); testTransactionRefreshRevision();