Skip to content
Merged
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
64 changes: 53 additions & 11 deletions src/qt/test/providertransactiontests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

#include <qt/test/providertransactiontests.h>

#include <evo/assetlocktx.h>
#include <evo/specialtx.h>
#include <interfaces/chain.h>
#include <interfaces/node.h>
#include <key.h>
Expand All @@ -22,6 +24,7 @@
#include <wallet/wallet.h>
#include <wallet/walletdb.h>

#include <algorithm>
#include <memory>
#include <optional>
#include <vector>
Expand Down Expand Up @@ -64,6 +67,19 @@ CTransactionRef MakeSpecialTransaction(uint16_t type, const COutPoint& input, CA
return MakeTransactionRef(std::move(tx));
}

CTransactionRef MakeAssetLockTransaction(const COutPoint& input, CAmount amount, const CScript& credit_script,
std::optional<CTxOut> core_output = std::nullopt)
{
CMutableTransaction tx;
tx.nVersion = CTransaction::SPECIAL_VERSION;
tx.nType = TRANSACTION_ASSET_LOCK;
tx.vin.emplace_back(input);
tx.vout.emplace_back(amount, CScript{} << OP_RETURN << std::vector<unsigned char>{});
if (core_output) tx.vout.emplace_back(std::move(*core_output));
SetTxPayload(tx, CAssetLockPayload{{CTxOut{amount, credit_script}}, CAssetLockPayload::INITIAL_VERSION});
return MakeTransactionRef(std::move(tx));
}

std::vector<int> FindTransactionRows(const QAbstractItemModel& model, const uint256& txid)
{
const QString hash{QString::fromStdString(txid.ToString())};
Expand Down Expand Up @@ -171,6 +187,8 @@ void ProviderTransactionTests::transactionTypeSettingPersistence_data()
QTest::newRow("masternode") << (TransactionFilterProxy::TYPE(TransactionRecord::MasternodeRegistration) |
TransactionFilterProxy::TYPE(TransactionRecord::MasternodeUpdate))
<< QString{"Masternode"};
QTest::newRow("asset-lock") << TransactionFilterProxy::TYPE(TransactionRecord::AssetLock)
<< QString{"Asset Lock"};
QTest::newRow("data") << TransactionFilterProxy::TYPE(TransactionRecord::DataTransaction)
<< QString{"Data Transaction"};
QTest::newRow("dust") << TransactionFilterProxy::TYPE(TransactionRecord::DustReceive)
Expand Down Expand Up @@ -257,8 +275,12 @@ void ProviderTransactionTests::providerTransactionHistory()
external_script)};
const CTransactionRef update_revoke{MakeSpecialTransaction(TRANSACTION_PROVIDER_UPDATE_REVOKE, make_owned_input(3),
input_amount(3) - revoke_fee, own_script)};
const CTransactionRef other_special_tx{
MakeSpecialTransaction(TRANSACTION_ASSET_LOCK, make_owned_input(4), input_amount(4) - 5000, own_script)};
const CAmount asset_lock_fee{5000};
const CTransactionRef asset_lock{MakeAssetLockTransaction(make_owned_input(4),
input_amount(4) - asset_lock_fee, external_script)};
const CAmount received_core_amount{COIN};
const CTransactionRef received_from_asset_lock{MakeAssetLockTransaction(
COutPoint{uint256::TWO, 7}, 2 * COIN, external_script, CTxOut{received_core_amount, own_script})};

std::vector<ExpectedRecord> expected{
{registration->GetHash(), TransactionRecord::MasternodeRegistration, -registration_fee,
Expand All @@ -271,9 +293,12 @@ void ProviderTransactionTests::providerTransactionHistory()
QString{"Masternode Update"}, QString{"Updates an existing masternode"}},
{update_revoke->GetHash(), TransactionRecord::MasternodeUpdate, -revoke_fee, QString{"Masternode Update"},
QString{"Updates an existing masternode"}},
{asset_lock->GetHash(), TransactionRecord::AssetLock, -input_amount(4), QString{"Asset Lock"},
QString{"Locks funds for use on Dash Platform"}},
};

for (const CTransactionRef& tx : {registration, update_service, update_registrar, update_revoke, other_special_tx}) {
for (const CTransactionRef& tx :
{registration, update_service, update_registrar, update_revoke, asset_lock, received_from_asset_lock}) {
QVERIFY(wallet->AddToWallet(tx, TxStateInactive{}) != nullptr);
}

Expand All @@ -289,12 +314,17 @@ void ProviderTransactionTests::providerTransactionHistory()
// Transactions loaded before the model is constructed exercise the wallet-restart path.
CheckProviderRecords(*model, expected);

const std::vector<int> other_rows{FindTransactionRows(*model, other_special_tx->GetHash())};
QCOMPARE(other_rows.size(), size_t{1});
QCOMPARE(model->index(other_rows.front(), 0).data(TransactionTableModel::TypeRole).toInt(),
static_cast<int>(TransactionRecord::SendToSelf));
QCOMPARE(model->index(other_rows.front(), TransactionTableModel::Type).data(Qt::DisplayRole).toString(),
QString{"Payment to yourself"});
const std::vector<int> asset_lock_rows{FindTransactionRows(*model, asset_lock->GetHash())};
QCOMPARE(asset_lock_rows.size(), size_t{1});
const QModelIndex asset_lock_index{model->index(asset_lock_rows.front(), 0)};
QVERIFY(!asset_lock_index.data(TransactionTableModel::LongDescriptionRole).toString().contains("Output index"));

const std::vector<int> received_rows{FindTransactionRows(*model, received_from_asset_lock->GetHash())};
QCOMPARE(received_rows.size(), size_t{1});
const QModelIndex received_index{model->index(received_rows.front(), 0)};
QCOMPARE(received_index.data(TransactionTableModel::TypeRole).toInt(),
static_cast<int>(TransactionRecord::RecvWithAddress));
QCOMPARE(received_index.data(TransactionTableModel::AmountRole).toLongLong(), received_core_amount);

// Add an externally funded registration after model construction to exercise live wallet
// notification. Its owned output is the wallet's positive net change and still yields one
Expand All @@ -311,10 +341,17 @@ void ProviderTransactionTests::providerTransactionHistory()

const quint32 masternode_filter{TransactionFilterProxy::TYPE(TransactionRecord::MasternodeRegistration) |
TransactionFilterProxy::TYPE(TransactionRecord::MasternodeUpdate)};
const int masternode_count{static_cast<int>(std::count_if(expected.cbegin(), expected.cend(),
[](const ExpectedRecord& record) {
return record.type == TransactionRecord::MasternodeRegistration ||
record.type == TransactionRecord::MasternodeUpdate;
}))};
TransactionFilterProxy filter;
filter.setSourceModel(model);
filter.setTypeFilter(masternode_filter);
QCOMPARE(filter.rowCount(), static_cast<int>(expected.size()));
QCOMPARE(filter.rowCount(), masternode_count);
filter.setTypeFilter(TransactionFilterProxy::TYPE(TransactionRecord::AssetLock));
QCOMPARE(filter.rowCount(), 1);

#if defined(Q_OS_MACOS)
if (QApplication::platformName() == "minimal") {
Expand All @@ -332,6 +369,8 @@ void ProviderTransactionTests::providerTransactionHistory()
QVERIFY(type_widget != nullptr);
const int masternode_row{type_widget->findText("Masternode")};
QCOMPARE(type_widget->itemData(masternode_row).toUInt(), masternode_filter);
QCOMPARE(type_widget->itemData(type_widget->findText("Asset Lock")).toUInt(),
TransactionFilterProxy::TYPE(TransactionRecord::AssetLock));

QListView* const type_list{qobject_cast<QListView*>(type_widget->view())};
QVERIFY(type_list != nullptr);
Expand All @@ -353,7 +392,7 @@ void ProviderTransactionTests::providerTransactionHistory()
QCOMPARE(QSettings{}.value("transactionTypeFilter").toUInt(), masternode_filter);
QTableView* const table{transaction_view.findChild<QTableView*>("transactionView")};
QVERIFY(table != nullptr);
QCOMPARE(table->model()->rowCount(), static_cast<int>(expected.size()));
QCOMPARE(table->model()->rowCount(), masternode_count);

struct RestoreCase {
quint32 saved_filter;
Expand All @@ -364,6 +403,9 @@ void ProviderTransactionTests::providerTransactionHistory()
{TransactionFilterProxy::TYPE(TransactionRecord::DataTransaction),
TransactionFilterProxy::TYPE(TransactionRecord::DataTransaction),
QString{"Data Transaction"}},
{TransactionFilterProxy::TYPE(TransactionRecord::AssetLock),
TransactionFilterProxy::TYPE(TransactionRecord::AssetLock),
QString{"Asset Lock"}},
{TransactionFilterProxy::TYPE(TransactionRecord::DustReceive),
TransactionFilterProxy::TYPE(TransactionRecord::DustReceive),
QString{"Dust Receive"}},
Expand Down
26 changes: 15 additions & 11 deletions src/qt/transactiondesc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,9 @@ 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 ||
rec->type == TransactionRecord::MasternodeUpdate};
const bool is_transaction_level_record{rec->type == TransactionRecord::MasternodeRegistration ||
rec->type == TransactionRecord::MasternodeUpdate ||
rec->type == TransactionRecord::AssetLock};

strHTML += "<b>" + tr("Status") + ":</b> " + FormatTxStatus(status, inMempool);
strHTML += "<br>";
Expand All @@ -120,21 +121,24 @@ QString TransactionDesc::toHTML(interfaces::Node& node, interfaces::Wallet& wall
case TransactionRecord::MasternodeUpdate:
strHTML += "<b>" + tr("Type") + ":</b> " + tr("Masternode Update") + "<br>";
break;
case TransactionRecord::AssetLock:
strHTML += "<b>" + tr("Type") + ":</b> " + tr("Asset Lock") + "<br>";
break;
Comment thread
PastaPastaPasta marked this conversation as resolved.
Resolved
default:
break;
}

//
// From
//
if (!is_masternode_transaction && wtx.is_coinbase) {
if (!is_transaction_level_record && wtx.is_coinbase) {
strHTML += "<b>" + tr("Source") + ":</b> " + tr("Generated") + "<br>";
} else if (!is_masternode_transaction && wtx.is_platform_transfer) {
} else if (!is_transaction_level_record && wtx.is_platform_transfer) {
strHTML += "<b>" + tr("Source") + ":</b> " + tr("Platform Transfer") + "<br>";
} else if (!is_masternode_transaction && wtx.value_map.count("from") && !wtx.value_map["from"].empty()) {
} else if (!is_transaction_level_record && 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 if (!is_masternode_transaction) {
} else if (!is_transaction_level_record) {
// Offline transaction
if (nNet > 0)
{
Expand Down Expand Up @@ -162,7 +166,7 @@ QString TransactionDesc::toHTML(interfaces::Node& node, interfaces::Wallet& wall
//
// To
//
if (!is_masternode_transaction && wtx.value_map.count("to") && !wtx.value_map["to"].empty()) {
if (!is_transaction_level_record && 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 @@ -177,7 +181,7 @@ QString TransactionDesc::toHTML(interfaces::Node& node, interfaces::Wallet& wall
//
// Amount
//
if (!is_masternode_transaction && wtx.is_coinbase && nCredit == 0) {
if (!is_transaction_level_record && wtx.is_coinbase && nCredit == 0) {
//
// Coinbase
//
Expand All @@ -190,12 +194,12 @@ QString TransactionDesc::toHTML(interfaces::Node& node, interfaces::Wallet& wall
else
strHTML += "(" + tr("not accepted") + ")";
strHTML += "<br>";
} else if (!is_masternode_transaction && nNet > 0) {
} else if (!is_transaction_level_record && nNet > 0) {
//
// Credit
//
strHTML += "<b>" + tr("Credit") + ":</b> " + BitcoinUnits::formatHtmlWithUnit(unit, nNet) + "<br>";
} else if (!is_masternode_transaction) {
} else if (!is_transaction_level_record) {
isminetype fAllFromMe = ISMINE_SPENDABLE;
for (const isminetype mine : wtx.txin_is_mine)
{
Expand Down Expand Up @@ -293,7 +297,7 @@ 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>";
if (!is_masternode_transaction) {
if (!is_transaction_level_record) {
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>";
Expand Down
2 changes: 1 addition & 1 deletion src/qt/transactionfilterproxy.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ 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,
static_assert(TransactionRecord::AssetLock < 32,
"TransactionRecord::Type no longer fits in the quint32 type filter");

static constexpr quint32 TYPE(int type) { return TransactionTypeToBit(type); }
Expand Down
25 changes: 12 additions & 13 deletions src/qt/transactionrecord.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ bool TransactionRecord::showTransaction()
return true;
}

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

Expand All @@ -40,6 +40,8 @@ static std::optional<TransactionRecord::Type> MasternodeRecordType(const CTransa
case TRANSACTION_PROVIDER_UPDATE_REGISTRAR:
case TRANSACTION_PROVIDER_UPDATE_REVOKE:
return TransactionRecord::MasternodeUpdate;
case TRANSACTION_ASSET_LOCK:
return TransactionRecord::AssetLock;
default:
return std::nullopt;
}
Expand All @@ -59,8 +61,14 @@ QList<TransactionRecord> TransactionRecord::decomposeTransaction(interfaces::Nod
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),
// Check if any inputs belong to this wallet (for special transaction handling and dust detection)
const bool isFromMe{std::any_of(wtx.txin_is_mine.cbegin(), wtx.txin_is_mine.cend(),
[](const isminetype mine) { return mine; })};
const auto special_type{SpecialTransactionRecordType(*wtx.tx)};
const bool is_asset_lock{special_type == TransactionRecord::AssetLock};

if (special_type && (!is_asset_lock || isFromMe)) {
TransactionRecord record(hash, nTime, *special_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; }) ||
Expand All @@ -72,15 +80,6 @@ QList<TransactionRecord> TransactionRecord::decomposeTransaction(interfaces::Nod

auto& coinJoinOptions = node.coinJoinOptions();

// Check if any inputs belong to this wallet (for dust detection)
bool isFromMe = false;
for (const isminetype mine : wtx.txin_is_mine) {
if (mine) {
isFromMe = true;
break;
}
}

if (nNet > 0 || wtx.is_coinbase || wtx.is_platform_transfer)
{
//
Expand Down Expand Up @@ -133,7 +132,7 @@ QList<TransactionRecord> TransactionRecord::decomposeTransaction(interfaces::Nod

parts.append(sub);
}
else if (!wtx.is_coinbase && IsDataScript(txout.scriptPubKey))
else if (!wtx.is_coinbase && !is_asset_lock && IsDataScript(txout.scriptPubKey))
{
TransactionRecord sub(hash, nTime);
sub.credit = txout.nValue;
Expand Down
2 changes: 2 additions & 0 deletions src/qt/transactionrecord.h
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ class TransactionRecord
MasternodeRegistration,
/// ProUpServTx, ProUpRegTx, or ProUpRevTx for an existing masternode
MasternodeUpdate,
/// AssetLockTx that moves funds from Core into Platform
AssetLock,
};

/** Number of confirmation recommended for accepting a transaction */
Expand Down
9 changes: 9 additions & 0 deletions src/qt/transactiontablemodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,8 @@ QString TransactionTableModel::formatTxType(const TransactionRecord *wtx) const
return tr("Masternode Registration");
case TransactionRecord::MasternodeUpdate:
return tr("Masternode Update");
case TransactionRecord::AssetLock:
return tr("Asset Lock");

case TransactionRecord::CoinJoinMixing:
return tr("%1 Mixing").arg(QString::fromStdString(gCoinJoinName));
Expand Down Expand Up @@ -498,6 +500,7 @@ QString TransactionTableModel::formatTxToAddress(const TransactionRecord *wtx, b
case TransactionRecord::DataTransaction:
case TransactionRecord::MasternodeRegistration:
case TransactionRecord::MasternodeUpdate:
case TransactionRecord::AssetLock:
case TransactionRecord::Other:
break; // use fail-over here
} // no default case, so the compiler can warn about missing cases
Expand Down Expand Up @@ -529,6 +532,7 @@ QVariant TransactionTableModel::addressColor(const TransactionRecord *wtx) const
case TransactionRecord::DataTransaction:
case TransactionRecord::MasternodeRegistration:
case TransactionRecord::MasternodeUpdate:
case TransactionRecord::AssetLock:
return GUIUtil::getThemedQColor(GUIUtil::ThemedColor::BAREADDRESS);
case TransactionRecord::SendToOther:
case TransactionRecord::RecvFromOther:
Expand Down Expand Up @@ -564,6 +568,7 @@ QVariant TransactionTableModel::amountColor(const TransactionRecord *rec) const
case TransactionRecord::SendToAddress:
case TransactionRecord::SendToOther:
case TransactionRecord::DataTransaction:
case TransactionRecord::AssetLock:
case TransactionRecord::Other:
return GUIUtil::getThemedQColor(GUIUtil::ThemedColor::RED);
case TransactionRecord::SendToSelf:
Expand Down Expand Up @@ -637,6 +642,10 @@ QString TransactionTableModel::formatTooltip(const TransactionRecord *rec) const
tooltip += QString("\n") + tr("Updates an existing masternode registration. The amount is this wallet's net "
"balance change, normally only the network fee.");
break;
case TransactionRecord::AssetLock:
tooltip += QString("\n") + tr("Locks funds for use on Dash Platform. The amount is this wallet's net balance "
"change, including the network fee.");
break;
default:
break;
}
Expand Down
1 change: 1 addition & 0 deletions src/qt/transactionview.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ TransactionView::TransactionView(QWidget* parent) :
typeWidget->addItem(tr("Masternode"), TransactionFilterProxy::TYPE(TransactionRecord::MasternodeRegistration) |
TransactionFilterProxy::TYPE(TransactionRecord::MasternodeUpdate));
typeWidget->addItem(tr("Platform Transfer"), TransactionFilterProxy::TYPE(TransactionRecord::PlatformTransfer));
typeWidget->addItem(tr("Asset Lock"), TransactionFilterProxy::TYPE(TransactionRecord::AssetLock));
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));
Expand Down
Loading