From dc08f2f0a6083c37a330266e20eb280c19e42dc0 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Mon, 17 Aug 2026 00:30:39 +0700 Subject: [PATCH 1/6] fix(qt): remove regression test that depends on filter entry order transactionTypeSettingCompatibility pinned the combo box index of every checked type filter entry ("Data Transaction" at 12, "Masternode" at 15, ...), so any re-ordering or insertion of entries broke it. The order of entries in the type filter is presentation, not behavior worth a regression test. --- src/qt/test/providertransactiontests.cpp | 30 ------------------------ src/qt/test/providertransactiontests.h | 2 -- 2 files changed, 32 deletions(-) diff --git a/src/qt/test/providertransactiontests.cpp b/src/qt/test/providertransactiontests.cpp index 5defac08423c..4afd79d6d273 100644 --- a/src/qt/test/providertransactiontests.cpp +++ b/src/qt/test/providertransactiontests.cpp @@ -179,36 +179,6 @@ void CheckProviderRecords(const TransactionTableModel& model, const std::vector< } // namespace -void ProviderTransactionTests::transactionTypeSettingCompatibility_data() -{ - QTest::addColumn("saved_index"); - QTest::addColumn("expected_text"); - QTest::addColumn("expected_filter"); - - QTest::newRow("data") << 12 << QString{"Data Transaction"} - << TransactionFilterProxy::TYPE(TransactionRecord::DataTransaction); - QTest::newRow("dust") << 13 << QString{"Dust Receive"} << TransactionFilterProxy::TYPE(TransactionRecord::DustReceive); - QTest::newRow("other") << 14 << QString{"Other"} << TransactionFilterProxy::TYPE(TransactionRecord::Other); -} - -void ProviderTransactionTests::transactionTypeSettingCompatibility() -{ - QFETCH(int, saved_index); - QFETCH(QString, expected_text); - QFETCH(quint32, expected_filter); - - TransactionTypeSettingRestorer setting_restorer; - QSettings{}.setValue("transactionType", saved_index); - - TransactionView transaction_view; - QComboBox* const type_widget{FindTransactionTypeWidget(transaction_view)}; - QVERIFY(type_widget != nullptr); - QCOMPARE(type_widget->currentIndex(), saved_index); - QCOMPARE(type_widget->currentText(), expected_text); - QCOMPARE(type_widget->currentData().toUInt(), expected_filter); - QCOMPARE(type_widget->findText("Masternode"), 15); -} - void ProviderTransactionTests::providerTransactionHistory() { TestChain100Setup test; diff --git a/src/qt/test/providertransactiontests.h b/src/qt/test/providertransactiontests.h index 8551c5d34dd3..81d65a6fba3c 100644 --- a/src/qt/test/providertransactiontests.h +++ b/src/qt/test/providertransactiontests.h @@ -22,8 +22,6 @@ class ProviderTransactionTests : public QObject } private Q_SLOTS: - void transactionTypeSettingCompatibility_data(); - void transactionTypeSettingCompatibility(); void providerTransactionHistory(); private: From 75df77a8020a777c49877707adf4841e97faf5ce Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Mon, 17 Aug 2026 00:30:53 +0700 Subject: [PATCH 2/6] fix(qt): persist transaction type filter by value instead of combo index The selected type filter was saved as the combo box index, so re-ordering or inserting filter entries silently changed what a saved selection restores to. updateCoinJoinVisibility() additionally reset the selection to "All"/"Most Common" on every wallet model attach, wiping the saved choice anyway. Save the TransactionFilterProxy bit mask under a new "transactionTypeFilter" key and restore it with findData(), which does not care about entry order. A new key is used deliberately: almost every existing "transactionType" value is a forced index 0 or 1 (rewritten on each start by updateCoinJoinVisibility()) and re-interpreting an old index as a bit mask would restore an arbitrary filter. Unknown values select nothing and fall back to "All"/"Most Common" once a wallet model attaches. updateCoinJoinVisibility() now keeps the current selection and only falls back when nothing is selected or the selected entry is a CoinJoin filter that just got hidden. Cover the new behavior with an order-independent test: a saved filter value restores the matching entry, an unknown value selects nothing, and chooseType() saves the bit mask rather than the row. --- src/qt/test/providertransactiontests.cpp | 38 +++++++++++++++++++++--- src/qt/test/providertransactiontests.h | 2 ++ src/qt/transactionview.cpp | 16 +++++----- 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/src/qt/test/providertransactiontests.cpp b/src/qt/test/providertransactiontests.cpp index 4afd79d6d273..19b2482a162a 100644 --- a/src/qt/test/providertransactiontests.cpp +++ b/src/qt/test/providertransactiontests.cpp @@ -118,16 +118,16 @@ class TransactionTypeSettingRestorer { public: TransactionTypeSettingRestorer() : - m_had_value(m_settings.contains("transactionType")), - m_value(m_settings.value("transactionType")) + m_had_value(m_settings.contains("transactionTypeFilter")), + m_value(m_settings.value("transactionTypeFilter")) { } ~TransactionTypeSettingRestorer() { if (m_had_value) { - m_settings.setValue("transactionType", m_value); + m_settings.setValue("transactionTypeFilter", m_value); } else { - m_settings.remove("transactionType"); + m_settings.remove("transactionTypeFilter"); } } @@ -179,6 +179,35 @@ void CheckProviderRecords(const TransactionTableModel& model, const std::vector< } // namespace +void ProviderTransactionTests::transactionTypeSettingPersistence_data() +{ + QTest::addColumn("saved_filter"); + QTest::addColumn("expected_text"); + + QTest::newRow("masternode") << (TransactionFilterProxy::TYPE(TransactionRecord::MasternodeRegistration) | + TransactionFilterProxy::TYPE(TransactionRecord::MasternodeUpdate)) + << QString{"Masternode"}; + QTest::newRow("data") << TransactionFilterProxy::TYPE(TransactionRecord::DataTransaction) + << QString{"Data Transaction"}; + // An unknown stored filter selects nothing instead of an arbitrary entry. + QTest::newRow("unknown") << quint32{0} << QString{}; +} + +void ProviderTransactionTests::transactionTypeSettingPersistence() +{ + QFETCH(quint32, saved_filter); + QFETCH(QString, expected_text); + + TransactionTypeSettingRestorer setting_restorer; + QSettings{}.setValue("transactionTypeFilter", saved_filter); + + TransactionView transaction_view; + QComboBox* const type_widget{FindTransactionTypeWidget(transaction_view)}; + QVERIFY(type_widget != nullptr); + QCOMPARE(type_widget->currentText(), expected_text); + QCOMPARE(type_widget->currentData().toUInt(), saved_filter); +} + void ProviderTransactionTests::providerTransactionHistory() { TestChain100Setup test; @@ -339,6 +368,7 @@ void ProviderTransactionTests::providerTransactionHistory() type_widget->setCurrentIndex(masternode_row); transaction_view.chooseType(masternode_row); + QCOMPARE(QSettings{}.value("transactionTypeFilter").toUInt(), masternode_filter); QTableView* const table{transaction_view.findChild("transactionView")}; QVERIFY(table != nullptr); QCOMPARE(table->model()->rowCount(), static_cast(expected.size())); diff --git a/src/qt/test/providertransactiontests.h b/src/qt/test/providertransactiontests.h index 81d65a6fba3c..125ac695763f 100644 --- a/src/qt/test/providertransactiontests.h +++ b/src/qt/test/providertransactiontests.h @@ -22,6 +22,8 @@ class ProviderTransactionTests : public QObject } private Q_SLOTS: + void transactionTypeSettingPersistence_data(); + void transactionTypeSettingPersistence(); void providerTransactionHistory(); private: diff --git a/src/qt/transactionview.cpp b/src/qt/transactionview.cpp index bc1d7fe4a8da..3ab1806efe6e 100644 --- a/src/qt/transactionview.cpp +++ b/src/qt/transactionview.cpp @@ -101,7 +101,7 @@ TransactionView::TransactionView(QWidget* parent) : typeWidget->addItem(tr("Other"), TransactionFilterProxy::TYPE(TransactionRecord::Other)); typeWidget->addItem(tr("Masternode"), TransactionFilterProxy::TYPE(TransactionRecord::MasternodeRegistration) | TransactionFilterProxy::TYPE(TransactionRecord::MasternodeUpdate)); - typeWidget->setCurrentIndex(settings.value("transactionType").toInt()); + typeWidget->setCurrentIndex(typeWidget->findData(settings.value("transactionTypeFilter").toUInt())); hlayout->addWidget(typeWidget); @@ -261,7 +261,6 @@ void TransactionView::setModel(WalletModel *_model) connect(_model, &WalletModel::notifyWatchonlyChanged, this, &TransactionView::updateWatchOnlyColumn); // Update transaction list with persisted settings - chooseType(settings.value("transactionType").toInt()); chooseDate(settings.value("transactionDate").toInt()); updateCoinJoinVisibility(); @@ -331,7 +330,7 @@ void TransactionView::chooseType(int idx) typeWidget->itemData(idx).toUInt()); // Persist settings QSettings settings; - settings.setValue("transactionType", idx); + settings.setValue("transactionTypeFilter", typeWidget->itemData(idx).toUInt()); } void TransactionView::chooseWatchonly(int idx) @@ -788,10 +787,6 @@ void TransactionView::updateCoinJoinVisibility() return; } bool fEnabled = model->node().coinJoinOptions().isEnabled(); - // If CoinJoin gets enabled use "All" else "Most common" - int idx = fEnabled ? 0 : 1; - chooseType(idx); - typeWidget->setCurrentIndex(idx); // Hide all CoinJoin related filters by value so this stays correct when entries are reordered. QListView* typeList = qobject_cast(typeWidget->view()); for (const quint32 type_filter : {TransactionFilterProxy::TYPE(TransactionRecord::CoinJoinSend), @@ -802,4 +797,11 @@ void TransactionView::updateCoinJoinVisibility() const int row = typeWidget->findData(type_filter); if (row >= 0) typeList->setRowHidden(row, !fEnabled); } + + int idx = typeWidget->currentIndex(); + if (idx < 0 || typeList->isRowHidden(idx)) { + idx = typeWidget->findData(fEnabled ? TransactionFilterProxy::ALL_TYPES : TransactionFilterProxy::COMMON_TYPES); + typeWidget->setCurrentIndex(idx); + } + chooseType(idx); } From 2bcb6c4fe6bcbfdfbf98e53a440f521f3a0a3667 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Mon, 17 Aug 2026 00:40:30 +0700 Subject: [PATCH 3/6] test(qt): drop transaction long-description rendering checks The provider-transaction test pinned TransactionDesc internals: the literal "Net amount" and "Transaction total size" strings, the summary being whatever precedes the first
, and a list of strings the summary must not contain. Any unrelated rewording or markup change in TransactionDesc breaks the test, while the regression it guards - one record per transaction with correct type, amount and label - is already covered by the role and display checks. --- src/qt/test/providertransactiontests.cpp | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/qt/test/providertransactiontests.cpp b/src/qt/test/providertransactiontests.cpp index 19b2482a162a..515a5c7ef58c 100644 --- a/src/qt/test/providertransactiontests.cpp +++ b/src/qt/test/providertransactiontests.cpp @@ -162,18 +162,6 @@ void CheckProviderRecords(const TransactionTableModel& model, const std::vector< const QString plain_text{base.data(TransactionTableModel::TxPlainTextRole).toString()}; QVERIFY(plain_text.contains(record.label)); QVERIFY(!plain_text.contains("Payment to yourself")); - - const QString description{base.data(TransactionTableModel::LongDescriptionRole).toString()}; - QVERIFY(description.contains(record.label)); - QVERIFY(description.contains(QString::fromStdString(record.txid.ToString()))); - QVERIFY(description.contains("Net amount")); - QVERIFY(description.contains("Transaction total size")); - const QString summary{description.section("
", 0, 0)}; - QVERIFY(!summary.contains("From:")); - QVERIFY(!summary.contains("To:")); - QVERIFY(!summary.contains("Debit:")); - QVERIFY(!summary.contains("Credit:")); - QVERIFY(!summary.contains("Output index")); } } @@ -300,13 +288,6 @@ void ProviderTransactionTests::providerTransactionHistory() // Transactions loaded before the model is constructed exercise the wallet-restart path. CheckProviderRecords(*model, expected); - const std::vector registrar_rows{FindTransactionRows(*model, update_registrar->GetHash())}; - QCOMPARE(registrar_rows.size(), size_t{1}); - const QString registrar_description{ - model->index(registrar_rows.front(), 0).data(TransactionTableModel::LongDescriptionRole).toString()}; - const QString registrar_summary{registrar_description.section("
", 0, 0)}; - const QString external_address{QString::fromStdString(EncodeDestination(PKHash(external_key.GetPubKey())))}; - QVERIFY(!registrar_summary.contains(external_address)); const std::vector other_rows{FindTransactionRows(*model, other_special_tx->GetHash())}; QCOMPARE(other_rows.size(), size_t{1}); From e5d1c571182eae55ce1475852fd981ebe2f3a275 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Mon, 17 Aug 2026 00:40:40 +0700 Subject: [PATCH 4/6] test(qt): drop redundant TxPlainTextRole checks The plain text is assembled from fields the test already compares exactly (type label via the Display role), so these contains-checks cannot fail without an earlier exact comparison failing first; what they additionally pin is plain-text formatting, which is not the behavior under test. --- src/qt/test/providertransactiontests.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/qt/test/providertransactiontests.cpp b/src/qt/test/providertransactiontests.cpp index 515a5c7ef58c..12efed98aa56 100644 --- a/src/qt/test/providertransactiontests.cpp +++ b/src/qt/test/providertransactiontests.cpp @@ -158,10 +158,6 @@ void CheckProviderRecords(const TransactionTableModel& model, const std::vector< QVERIFY(tooltip.contains(record.label)); QVERIFY(tooltip.contains(record.tooltip_text)); QVERIFY(!tooltip.contains("Payment to yourself")); - - const QString plain_text{base.data(TransactionTableModel::TxPlainTextRole).toString()}; - QVERIFY(plain_text.contains(record.label)); - QVERIFY(!plain_text.contains("Payment to yourself")); } } From 580982179b5335ab5c6c0f2695aef1aff452eaa9 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Sun, 16 Aug 2026 17:13:49 +0700 Subject: [PATCH 5/6] fix: re-ordering list of filters to put Masternode between "Mined" and "Platform transfer" Order here is important for showing user and it should not be after "Other". Anywhere after "Mining" and before "Dust received" seems fine for me. --- src/qt/transactionview.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/qt/transactionview.cpp b/src/qt/transactionview.cpp index 3ab1806efe6e..a9421e3793c7 100644 --- a/src/qt/transactionview.cpp +++ b/src/qt/transactionview.cpp @@ -95,12 +95,12 @@ TransactionView::TransactionView(QWidget* parent) : typeWidget->addItem(tr("%1 Collateral Payment").arg(strCoinJoinName), TransactionFilterProxy::TYPE(TransactionRecord::CoinJoinCollateralPayment)); typeWidget->addItem(tr("To yourself"), TransactionFilterProxy::TYPE(TransactionRecord::SendToSelf)); typeWidget->addItem(tr("Mined"), TransactionFilterProxy::TYPE(TransactionRecord::Generated)); + typeWidget->addItem(tr("Masternode"), TransactionFilterProxy::TYPE(TransactionRecord::MasternodeRegistration) | + TransactionFilterProxy::TYPE(TransactionRecord::MasternodeUpdate)); typeWidget->addItem(tr("Platform Transfer"), TransactionFilterProxy::TYPE(TransactionRecord::PlatformTransfer)); typeWidget->addItem(tr("Data Transaction"), TransactionFilterProxy::TYPE(TransactionRecord::DataTransaction)); typeWidget->addItem(tr("Dust Receive"), TransactionFilterProxy::TYPE(TransactionRecord::DustReceive)); typeWidget->addItem(tr("Other"), TransactionFilterProxy::TYPE(TransactionRecord::Other)); - typeWidget->addItem(tr("Masternode"), TransactionFilterProxy::TYPE(TransactionRecord::MasternodeRegistration) | - TransactionFilterProxy::TYPE(TransactionRecord::MasternodeUpdate)); typeWidget->setCurrentIndex(typeWidget->findData(settings.value("transactionTypeFilter").toUInt())); hlayout->addWidget(typeWidget); From bb24dc8e04120256cc89d26ca194ffa11947db7a Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Tue, 18 Aug 2026 00:56:53 +0700 Subject: [PATCH 6/6] test(qt): match CoinJoin type-filter entries by title, not a copied list The row-hiding check enumerated the same five TransactionRecord filter values as the implementation, so a CoinJoin filter added to the combo box but missed by updateCoinJoinVisibility() would never fail the test. Detect CoinJoin entries by their title instead: any entry containing "coinjoin" or "coin join" (case-insensitive) must be hidden while CoinJoin is disabled, and at least one such entry must exist. --- src/qt/test/providertransactiontests.cpp | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/qt/test/providertransactiontests.cpp b/src/qt/test/providertransactiontests.cpp index 12efed98aa56..ea4a9b81eb56 100644 --- a/src/qt/test/providertransactiontests.cpp +++ b/src/qt/test/providertransactiontests.cpp @@ -331,16 +331,17 @@ void ProviderTransactionTests::providerTransactionHistory() QListView* const type_list{qobject_cast(type_widget->view())}; QVERIFY(type_list != nullptr); - for (const quint32 coinjoin_filter : - {TransactionFilterProxy::TYPE(TransactionRecord::CoinJoinSend), - TransactionFilterProxy::TYPE(TransactionRecord::CoinJoinMakeCollaterals), - TransactionFilterProxy::TYPE(TransactionRecord::CoinJoinCreateDenominations), - TransactionFilterProxy::TYPE(TransactionRecord::CoinJoinMixing), - TransactionFilterProxy::TYPE(TransactionRecord::CoinJoinCollateralPayment)}) { - const int row{type_widget->findData(coinjoin_filter)}; - QVERIFY(row >= 0); - QVERIFY(type_list->isRowHidden(row)); + // Match CoinJoin entries by title so filters added later are covered without + // duplicating the implementation's filter list. + int hidden_coinjoin_rows{0}; + for (int row{0}; row < type_widget->count(); ++row) { + const QString title{type_widget->itemText(row)}; + if (title.contains("coinjoin", Qt::CaseInsensitive) || title.contains("coin join", Qt::CaseInsensitive)) { + QVERIFY(type_list->isRowHidden(row)); + ++hidden_coinjoin_rows; + } } + QVERIFY(hidden_coinjoin_rows > 0); QVERIFY(!type_list->isRowHidden(masternode_row)); type_widget->setCurrentIndex(masternode_row);