diff --git a/src/qt/test/providertransactiontests.cpp b/src/qt/test/providertransactiontests.cpp index 7e6192775d28..940044e32b42 100644 --- a/src/qt/test/providertransactiontests.cpp +++ b/src/qt/test/providertransactiontests.cpp @@ -4,6 +4,8 @@ #include +#include +#include #include #include #include @@ -22,6 +24,7 @@ #include #include +#include #include #include #include @@ -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 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{}); + 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 FindTransactionRows(const QAbstractItemModel& model, const uint256& txid) { const QString hash{QString::fromStdString(txid.ToString())}; @@ -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) @@ -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 expected{ {registration->GetHash(), TransactionRecord::MasternodeRegistration, -registration_fee, @@ -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); } @@ -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 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(TransactionRecord::SendToSelf)); - QCOMPARE(model->index(other_rows.front(), TransactionTableModel::Type).data(Qt::DisplayRole).toString(), - QString{"Payment to yourself"}); + const std::vector 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 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(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 @@ -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(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(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") { @@ -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(type_widget->view())}; QVERIFY(type_list != nullptr); @@ -353,7 +392,7 @@ void ProviderTransactionTests::providerTransactionHistory() 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())); + QCOMPARE(table->model()->rowCount(), masternode_count); struct RestoreCase { quint32 saved_filter; @@ -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"}}, diff --git a/src/qt/transactiondesc.cpp b/src/qt/transactiondesc.cpp index 7809c4255619..cf0937357ab2 100644 --- a/src/qt/transactiondesc.cpp +++ b/src/qt/transactiondesc.cpp @@ -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 += "" + tr("Status") + ": " + FormatTxStatus(status, inMempool); strHTML += "
"; @@ -120,6 +121,9 @@ QString TransactionDesc::toHTML(interfaces::Node& node, interfaces::Wallet& wall case TransactionRecord::MasternodeUpdate: strHTML += "" + tr("Type") + ": " + tr("Masternode Update") + "
"; break; + case TransactionRecord::AssetLock: + strHTML += "" + tr("Type") + ": " + tr("Asset Lock") + "
"; + break; default: break; } @@ -127,14 +131,14 @@ QString TransactionDesc::toHTML(interfaces::Node& node, interfaces::Wallet& wall // // From // - if (!is_masternode_transaction && wtx.is_coinbase) { + if (!is_transaction_level_record && wtx.is_coinbase) { strHTML += "" + tr("Source") + ": " + tr("Generated") + "
"; - } else if (!is_masternode_transaction && wtx.is_platform_transfer) { + } else if (!is_transaction_level_record && wtx.is_platform_transfer) { strHTML += "" + tr("Source") + ": " + tr("Platform Transfer") + "
"; - } 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 += "" + tr("From") + ": " + GUIUtil::HtmlEscape(wtx.value_map["from"]) + "
"; - } else if (!is_masternode_transaction) { + } else if (!is_transaction_level_record) { // Offline transaction if (nNet > 0) { @@ -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 += "" + tr("To") + ": "; @@ -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 // @@ -190,12 +194,12 @@ QString TransactionDesc::toHTML(interfaces::Node& node, interfaces::Wallet& wall else strHTML += "(" + tr("not accepted") + ")"; strHTML += "
"; - } else if (!is_masternode_transaction && nNet > 0) { + } else if (!is_transaction_level_record && nNet > 0) { // // Credit // strHTML += "" + tr("Credit") + ": " + BitcoinUnits::formatHtmlWithUnit(unit, nNet) + "
"; - } else if (!is_masternode_transaction) { + } else if (!is_transaction_level_record) { isminetype fAllFromMe = ISMINE_SPENDABLE; for (const isminetype mine : wtx.txin_is_mine) { @@ -293,7 +297,7 @@ QString TransactionDesc::toHTML(interfaces::Node& node, interfaces::Wallet& wall strHTML += "
" + tr("Comment") + ":
" + GUIUtil::HtmlEscape(wtx.value_map["comment"], true) + "
"; strHTML += "" + tr("Transaction ID") + ": " + rec->getTxHash() + "
"; - if (!is_masternode_transaction) { + if (!is_transaction_level_record) { strHTML += "" + tr("Output index") + ": " + QString::number(rec->getOutputIndex()) + "
"; } strHTML += "" + tr("Transaction total size") + ": " + QString::number(wtx.tx->GetTotalSize()) + " bytes
"; diff --git a/src/qt/transactionfilterproxy.h b/src/qt/transactionfilterproxy.h index c96914ec78c2..5d81ec13a75f 100644 --- a/src/qt/transactionfilterproxy.h +++ b/src/qt/transactionfilterproxy.h @@ -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); } diff --git a/src/qt/transactionrecord.cpp b/src/qt/transactionrecord.cpp index 6de7beaf0287..73ed9cc2f744 100644 --- a/src/qt/transactionrecord.cpp +++ b/src/qt/transactionrecord.cpp @@ -29,7 +29,7 @@ bool TransactionRecord::showTransaction() return true; } -static std::optional MasternodeRecordType(const CTransaction& tx) +static std::optional SpecialTransactionRecordType(const CTransaction& tx) { if (!tx.IsSpecialTxVersion()) return std::nullopt; @@ -40,6 +40,8 @@ static std::optional 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; } @@ -59,8 +61,14 @@ QList TransactionRecord::decomposeTransaction(interfaces::Nod uint256 hash = wtx.tx->GetHash(); std::map mapValue = wtx.value_map; - if (const auto mn_type = MasternodeRecordType(*wtx.tx)) { - TransactionRecord record(hash, nTime, *mn_type, /*_strAddress=*/"", std::min(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(nNet, 0), std::max(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; }) || @@ -72,15 +80,6 @@ QList 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) { // @@ -133,7 +132,7 @@ QList 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; diff --git a/src/qt/transactionrecord.h b/src/qt/transactionrecord.h index c526b1ca2b3b..1efd91c5d1ce 100644 --- a/src/qt/transactionrecord.h +++ b/src/qt/transactionrecord.h @@ -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 */ diff --git a/src/qt/transactiontablemodel.cpp b/src/qt/transactiontablemodel.cpp index d0b67efa4aac..49a3b1e602c4 100644 --- a/src/qt/transactiontablemodel.cpp +++ b/src/qt/transactiontablemodel.cpp @@ -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)); @@ -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 @@ -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: @@ -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: @@ -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; } diff --git a/src/qt/transactionview.cpp b/src/qt/transactionview.cpp index a9421e3793c7..d3a5518174b8 100644 --- a/src/qt/transactionview.cpp +++ b/src/qt/transactionview.cpp @@ -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));