Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions doc/release-notes-7595.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
GUI changes
-----------

- Dash-Qt now labels masternode registration and update transactions in the
transaction history as **Masternode Registration** and **Masternode Update**
instead of generic "Payment to yourself" rows. A new **Masternode** filter
shows only these operations. The amount shown is the transaction's net effect
on your wallet (for example, the network fee on a self-funded registration).
(#7595)
3 changes: 3 additions & 0 deletions src/Makefile.qttest.include
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ TEST_QT_MOC_CPP = \
if ENABLE_WALLET
TEST_QT_MOC_CPP += \
qt/test/moc_addressbooktests.cpp \
qt/test/moc_providertransactiontests.cpp \
qt/test/moc_wallettests.cpp
endif # ENABLE_WALLET

TEST_QT_H = \
qt/test/addressbooktests.h \
qt/test/apptests.h \
qt/test/optiontests.h \
qt/test/providertransactiontests.h \
qt/test/rpcnestedtests.h \
qt/test/uritests.h \
qt/test/util.h \
Expand All @@ -45,6 +47,7 @@ qt_test_test_dash_qt_SOURCES = \
if ENABLE_WALLET
qt_test_test_dash_qt_SOURCES += \
qt/test/addressbooktests.cpp \
qt/test/providertransactiontests.cpp \
qt/test/wallettests.cpp \
wallet/test/wallet_test_fixture.cpp
endif # ENABLE_WALLET
Expand Down
383 changes: 383 additions & 0 deletions src/qt/test/providertransactiontests.cpp

Large diffs are not rendered by default.

33 changes: 33 additions & 0 deletions src/qt/test/providertransactiontests.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (c) 2026 The Dash Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#ifndef BITCOIN_QT_TEST_PROVIDERTRANSACTIONTESTS_H
#define BITCOIN_QT_TEST_PROVIDERTRANSACTIONTESTS_H

#include <QObject>

namespace interfaces {
class Node;
} // namespace interfaces

class ProviderTransactionTests : public QObject
{
Q_OBJECT

public:
explicit ProviderTransactionTests(interfaces::Node& node) :
m_node(node)
{
}

private Q_SLOTS:
void transactionTypeSettingCompatibility_data();
void transactionTypeSettingCompatibility();
void providerTransactionHistory();

private:
interfaces::Node& m_node;
};

#endif // BITCOIN_QT_TEST_PROVIDERTRANSACTIONTESTS_H
4 changes: 4 additions & 0 deletions src/qt/test/test_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

#ifdef ENABLE_WALLET
#include <qt/test/addressbooktests.h>
#include <qt/test/providertransactiontests.h>
#include <qt/test/wallettests.h>
#endif // ENABLE_WALLET

Expand Down Expand Up @@ -105,6 +106,9 @@ int main(int argc, char* argv[])

AddressBookTests test6(app.node());
num_test_failures += QTest::qExec(&test6);

ProviderTransactionTests provider_transaction_tests(app.node());
num_test_failures += QTest::qExec(&provider_transaction_tests);
#endif
TrafficGraphDataTests test7;
num_test_failures += QTest::qExec(&test7);
Expand Down
46 changes: 24 additions & 22 deletions src/qt/transactiondesc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -105,30 +105,36 @@ QString TransactionDesc::toHTML(interfaces::Node& node, interfaces::Wallet& wall
CAmount nCredit = wtx.credit;
CAmount nDebit = wtx.debit;
CAmount nNet = nCredit - nDebit;
const bool is_masternode_transaction{rec->type == TransactionRecord::MasternodeRegistration ||
Comment thread
PastaPastaPasta marked this conversation as resolved.
rec->type == TransactionRecord::MasternodeUpdate};

strHTML += "<b>" + tr("Status") + ":</b> " + FormatTxStatus(status, inMempool);
strHTML += "<br>";

strHTML += "<b>" + tr("Date") + ":</b> " + (nTime ? GUIUtil::dateTimeStr(nTime) : "") + "<br>";

switch (rec->type) {
case TransactionRecord::MasternodeRegistration:
strHTML += "<b>" + tr("Type") + ":</b> " + tr("Masternode Registration") + "<br>";
break;
case TransactionRecord::MasternodeUpdate:
strHTML += "<b>" + tr("Type") + ":</b> " + tr("Masternode Update") + "<br>";
break;
default:
break;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

//
// From
//
if (wtx.is_coinbase)
{
if (!is_masternode_transaction && wtx.is_coinbase) {
strHTML += "<b>" + tr("Source") + ":</b> " + tr("Generated") + "<br>";
}
else if (wtx.is_platform_transfer)
{
} else if (!is_masternode_transaction && wtx.is_platform_transfer) {
strHTML += "<b>" + tr("Source") + ":</b> " + tr("Platform Transfer") + "<br>";
}
else if (wtx.value_map.count("from") && !wtx.value_map["from"].empty())
{
} else if (!is_masternode_transaction && wtx.value_map.count("from") && !wtx.value_map["from"].empty()) {
// Online transaction
strHTML += "<b>" + tr("From") + ":</b> " + GUIUtil::HtmlEscape(wtx.value_map["from"]) + "<br>";
}
else
{
} else if (!is_masternode_transaction) {
// Offline transaction
if (nNet > 0)
{
Expand Down Expand Up @@ -156,8 +162,7 @@ QString TransactionDesc::toHTML(interfaces::Node& node, interfaces::Wallet& wall
//
// To
//
if (wtx.value_map.count("to") && !wtx.value_map["to"].empty())
{
if (!is_masternode_transaction && wtx.value_map.count("to") && !wtx.value_map["to"].empty()) {
// Online transaction
std::string strAddress = wtx.value_map["to"];
strHTML += "<b>" + tr("To") + ":</b> ";
Expand All @@ -172,8 +177,7 @@ QString TransactionDesc::toHTML(interfaces::Node& node, interfaces::Wallet& wall
//
// Amount
//
if (wtx.is_coinbase && nCredit == 0)
{
if (!is_masternode_transaction && wtx.is_coinbase && nCredit == 0) {
//
// Coinbase
//
Expand All @@ -186,16 +190,12 @@ QString TransactionDesc::toHTML(interfaces::Node& node, interfaces::Wallet& wall
else
strHTML += "(" + tr("not accepted") + ")";
strHTML += "<br>";
}
else if (nNet > 0)
{
} else if (!is_masternode_transaction && nNet > 0) {
//
// Credit
//
strHTML += "<b>" + tr("Credit") + ":</b> " + BitcoinUnits::formatHtmlWithUnit(unit, nNet) + "<br>";
}
else
{
} else if (!is_masternode_transaction) {
isminetype fAllFromMe = ISMINE_SPENDABLE;
for (const isminetype mine : wtx.txin_is_mine)
{
Expand Down Expand Up @@ -293,7 +293,9 @@ QString TransactionDesc::toHTML(interfaces::Node& node, interfaces::Wallet& wall
strHTML += "<br><b>" + tr("Comment") + ":</b><br>" + GUIUtil::HtmlEscape(wtx.value_map["comment"], true) + "<br>";

strHTML += "<b>" + tr("Transaction ID") + ":</b> " + rec->getTxHash() + "<br>";
strHTML += "<b>" + tr("Output index") + ":</b> " + QString::number(rec->getOutputIndex()) + "<br>";
if (!is_masternode_transaction) {
Comment thread
PastaPastaPasta marked this conversation as resolved.
strHTML += "<b>" + tr("Output index") + ":</b> " + QString::number(rec->getOutputIndex()) + "<br>";
}
strHTML += "<b>" + tr("Transaction total size") + ":</b> " + QString::number(wtx.tx->GetTotalSize()) + " bytes<br>";

// Show OP_RETURN payload for this specific output
Expand Down
5 changes: 4 additions & 1 deletion src/qt/transactionfilterproxy.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ class TransactionFilterProxy : public QSortFilterProxyModel
public:
explicit TransactionFilterProxy(QObject *parent = nullptr);

/** Types to exclude from common transaction lists (CoinJoin internal transactions and dust) */
/** Types to exclude from common transaction lists (CoinJoin internal transactions and dust).
* Masternode transactions remain visible in the default common-transactions view. */
static constexpr quint32 EXCLUDED_TYPES =
TransactionTypeToBit(TransactionRecord::CoinJoinCollateralPayment) |
TransactionTypeToBit(TransactionRecord::CoinJoinCreateDenominations) |
Expand All @@ -36,6 +37,8 @@ class TransactionFilterProxy : public QSortFilterProxyModel
static constexpr quint32 ALL_TYPES = 0xFFFFFFFF;
/** Type filter bit field (all types except excluded) */
static constexpr quint32 COMMON_TYPES = ALL_TYPES & ~EXCLUDED_TYPES;
static_assert(TransactionRecord::MasternodeUpdate < 32,
"TransactionRecord::Type no longer fits in the quint32 type filter");

static constexpr quint32 TYPE(int type) { return TransactionTypeToBit(type); }

Expand Down
33 changes: 32 additions & 1 deletion src/qt/transactionrecord.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@
#include <qt/transactionrecord.h>

#include <chain.h>
#include <interfaces/wallet.h>
#include <interfaces/node.h>
#include <interfaces/wallet.h>
#include <primitives/transaction.h>

#include <wallet/ismine.h>

#include <algorithm>
#include <cstdint>
#include <optional>

using wallet::ISMINE_SPENDABLE;
using wallet::ISMINE_WATCH_ONLY;
Expand All @@ -26,6 +29,22 @@ bool TransactionRecord::showTransaction()
return true;
}

static std::optional<TransactionRecord::Type> MasternodeRecordType(const CTransaction& tx)
{
if (!tx.IsSpecialTxVersion()) return std::nullopt;

switch (tx.nType) {
case TRANSACTION_PROVIDER_REGISTER:
return TransactionRecord::MasternodeRegistration;
case TRANSACTION_PROVIDER_UPDATE_SERVICE:
case TRANSACTION_PROVIDER_UPDATE_REGISTRAR:
case TRANSACTION_PROVIDER_UPDATE_REVOKE:
return TransactionRecord::MasternodeUpdate;
default:
return std::nullopt;
}
}

/*
* Decompose CWallet transaction to model transaction records.
*/
Expand All @@ -39,6 +58,18 @@ QList<TransactionRecord> TransactionRecord::decomposeTransaction(interfaces::Nod
CAmount nNet = nCredit - nDebit;
uint256 hash = wtx.tx->GetHash();
std::map<std::string, std::string> mapValue = wtx.value_map;

if (const auto mn_type = MasternodeRecordType(*wtx.tx)) {
TransactionRecord record(hash, nTime, *mn_type, /*_strAddress=*/"", std::min<CAmount>(nNet, 0),
std::max<CAmount>(nNet, 0));
record.involvesWatchAddress = std::any_of(wtx.txin_is_mine.begin(), wtx.txin_is_mine.end(),
[](const isminetype mine) { return mine & ISMINE_WATCH_ONLY; }) ||
std::any_of(wtx.txout_is_mine.begin(), wtx.txout_is_mine.end(),
[](const isminetype mine) { return mine & ISMINE_WATCH_ONLY; });
parts.append(record);
return parts;
}

auto& coinJoinOptions = node.coinJoinOptions();

// Check if any inputs belong to this wallet (for dust detection)
Expand Down
7 changes: 6 additions & 1 deletion src/qt/transactionrecord.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ struct TransactionStatus {
class TransactionRecord
{
public:
// Update EXCLUDED_TYPES in TransactionFilterProxy when adding a new type
// Update EXCLUDED_TYPES in TransactionFilterProxy when adding a new type.
// Append new types so existing values remain stable as type-filter bit positions.
enum Type
{
Other,
Expand All @@ -89,6 +90,10 @@ class TransactionRecord
PlatformTransfer,
DustReceive,
DataTransaction,
/// ProRegTx that registers a regular or Evo masternode
MasternodeRegistration,
/// ProUpServTx, ProUpRegTx, or ProUpRevTx for an existing masternode
MasternodeUpdate,
};

/** Number of confirmation recommended for accepting a transaction */
Expand Down
22 changes: 22 additions & 0 deletions src/qt/transactiontablemodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,10 @@ QString TransactionTableModel::formatTxType(const TransactionRecord *wtx) const
return tr("Data Transaction");
case TransactionRecord::DustReceive:
return tr("Dust Receive");
case TransactionRecord::MasternodeRegistration:
return tr("Masternode Registration");
case TransactionRecord::MasternodeUpdate:
return tr("Masternode Update");

case TransactionRecord::CoinJoinMixing:
return tr("%1 Mixing").arg(QString::fromStdString(gCoinJoinName));
Expand Down Expand Up @@ -492,6 +496,8 @@ QString TransactionTableModel::formatTxToAddress(const TransactionRecord *wtx, b
case TransactionRecord::CoinJoinMakeCollaterals:
case TransactionRecord::CoinJoinCreateDenominations:
case TransactionRecord::DataTransaction:
case TransactionRecord::MasternodeRegistration:
case TransactionRecord::MasternodeUpdate:
case TransactionRecord::Other:
break; // use fail-over here
} // no default case, so the compiler can warn about missing cases
Expand Down Expand Up @@ -521,6 +527,8 @@ QVariant TransactionTableModel::addressColor(const TransactionRecord *wtx) const
case TransactionRecord::CoinJoinMakeCollaterals:
case TransactionRecord::CoinJoinCollateralPayment:
case TransactionRecord::DataTransaction:
case TransactionRecord::MasternodeRegistration:
case TransactionRecord::MasternodeUpdate:
return GUIUtil::getThemedQColor(GUIUtil::ThemedColor::BAREADDRESS);
case TransactionRecord::SendToOther:
case TransactionRecord::RecvFromOther:
Expand Down Expand Up @@ -564,6 +572,8 @@ QVariant TransactionTableModel::amountColor(const TransactionRecord *rec) const
case TransactionRecord::CoinJoinMakeCollaterals:
case TransactionRecord::CoinJoinCreateDenominations:
case TransactionRecord::DustReceive:
case TransactionRecord::MasternodeRegistration:
case TransactionRecord::MasternodeUpdate:
return GUIUtil::getThemedQColor(GUIUtil::ThemedColor::ORANGE);
}
return GUIUtil::getThemedQColor(GUIUtil::ThemedColor::DEFAULT);
Expand Down Expand Up @@ -618,6 +628,18 @@ QString TransactionTableModel::formatTooltip(const TransactionRecord *rec) const
{
tooltip += QString(" ") + formatTxToAddress(rec, true);
}
switch (rec->type) {
case TransactionRecord::MasternodeRegistration:
tooltip += QString("\n") + tr("Registers a masternode. The amount is this wallet's net balance change and is "
"normally only the network fee when the collateral remains in this wallet.");
break;
case TransactionRecord::MasternodeUpdate:
tooltip += QString("\n") + tr("Updates an existing masternode registration. The amount is this wallet's net "
"balance change, normally only the network fee.");
break;
default:
break;
}
return tooltip;
}

Expand Down
14 changes: 10 additions & 4 deletions src/qt/transactionview.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ TransactionView::TransactionView(QWidget* parent) :
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(settings.value("transactionType").toInt());

hlayout->addWidget(typeWidget);
Expand Down Expand Up @@ -790,10 +792,14 @@ void TransactionView::updateCoinJoinVisibility()
int idx = fEnabled ? 0 : 1;
chooseType(idx);
typeWidget->setCurrentIndex(idx);
// Hide all CoinJoin related filters
// Hide all CoinJoin related filters by value so this stays correct when entries are reordered.
QListView* typeList = qobject_cast<QListView*>(typeWidget->view());
std::vector<int> vecRows{4, 5, 6, 7, 8};
for (auto nRow : vecRows) {
typeList->setRowHidden(nRow, !fEnabled);
for (const quint32 type_filter : {TransactionFilterProxy::TYPE(TransactionRecord::CoinJoinSend),
TransactionFilterProxy::TYPE(TransactionRecord::CoinJoinMakeCollaterals),
TransactionFilterProxy::TYPE(TransactionRecord::CoinJoinCreateDenominations),
TransactionFilterProxy::TYPE(TransactionRecord::CoinJoinMixing),
TransactionFilterProxy::TYPE(TransactionRecord::CoinJoinCollateralPayment)}) {
const int row = typeWidget->findData(type_filter);
if (row >= 0) typeList->setRowHidden(row, !fEnabled);
}
}
2 changes: 2 additions & 0 deletions test/util/data/non-backported.txt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ src/qt/masternodelist.*
src/qt/masternodemodel.*
src/qt/mnemonicverificationdialog.*
src/qt/networkwidget.*
src/qt/test/providertransactiontests.cpp
src/qt/test/providertransactiontests.h
src/qt/proposalcreate.*
src/qt/proposalinfo.*
src/qt/proposallist.*
Expand Down
Loading