diff --git a/src/qt/forms/optionsdialog.ui b/src/qt/forms/optionsdialog.ui
index eda0ce5cdd69..8e10cfb21bb7 100644
--- a/src/qt/forms/optionsdialog.ui
+++ b/src/qt/forms/optionsdialog.ui
@@ -407,6 +407,66 @@
+ -
+
+
+ Automatically lock small incoming transactions from external sources that may be dust attacks. Locked UTXOs will be excluded from coin selection.
+
+
+ Enable &dust attack protection
+
+
+
+ -
+
+
-
+
+
+ Dust threshold:
+
+
+
+ -
+
+
+ Transactions with outputs at or below this amount will be considered dust when received from external sources.
+
+
+ 1
+
+
+ 1000000
+
+
+ 10000
+
+
+
+ -
+
+
+ duffs
+
+
+ Qt::PlainText
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
-
diff --git a/src/qt/optionsdialog.cpp b/src/qt/optionsdialog.cpp
index 034ae4f908ef..c1b9bde9c07c 100644
--- a/src/qt/optionsdialog.cpp
+++ b/src/qt/optionsdialog.cpp
@@ -73,6 +73,11 @@ OptionsDialog::OptionsDialog(QWidget *parent, bool enableWallet) :
ui->pruneSize->setEnabled(false);
connect(ui->prune, &QPushButton::toggled, ui->pruneSize, &QWidget::setEnabled);
+ /* Dust protection */
+ ui->dustProtectionThreshold->setEnabled(false);
+ ui->dustProtectionThresholdUnitLabel->setText(BitcoinUnits::name(BitcoinUnits::Unit::duffs));
+ connect(ui->dustProtection, &QCheckBox::toggled, ui->dustProtectionThreshold, &QWidget::setEnabled);
+
/* Wallet */
ui->coinJoinEnabled->setText(tr("Enable %1 features").arg(QString::fromStdString(gCoinJoinName)));
@@ -344,6 +349,8 @@ void OptionsDialog::setMapper()
mapper->addMapping(ui->subFeeFromAmount, OptionsModel::SubFeeFromAmount);
mapper->addMapping(ui->m_enable_psbt_controls, OptionsModel::EnablePSBTControls);
mapper->addMapping(ui->keepChangeAddress, OptionsModel::KeepChangeAddress);
+ mapper->addMapping(ui->dustProtection, OptionsModel::DustProtection);
+ mapper->addMapping(ui->dustProtectionThreshold, OptionsModel::DustProtectionThreshold);
mapper->addMapping(ui->showMasternodesTab, OptionsModel::ShowMasternodesTab);
mapper->addMapping(ui->showGovernanceTab, OptionsModel::ShowGovernanceTab);
mapper->addMapping(ui->showAdvancedCJUI, OptionsModel::ShowAdvancedCJUI);
diff --git a/src/qt/optionsmodel.cpp b/src/qt/optionsmodel.cpp
index ce4579ca5814..497978ee67bd 100644
--- a/src/qt/optionsmodel.cpp
+++ b/src/qt/optionsmodel.cpp
@@ -357,6 +357,15 @@ bool OptionsModel::Init(bilingual_str& error)
if (!settings.contains("fLowKeysWarning"))
settings.setValue("fLowKeysWarning", true);
+
+ // Dust protection
+ if (!settings.contains("fDustProtection"))
+ settings.setValue("fDustProtection", false);
+ fDustProtection = settings.value("fDustProtection", false).toBool();
+
+ if (!settings.contains("nDustProtectionThreshold"))
+ settings.setValue("nDustProtectionThreshold", (qlonglong)DEFAULT_DUST_PROTECTION_THRESHOLD);
+ nDustProtectionThreshold = settings.value("nDustProtectionThreshold", (qlonglong)DEFAULT_DUST_PROTECTION_THRESHOLD).toLongLong();
#endif // ENABLE_WALLET
// These are shared with the core or have a command-line parameter
@@ -705,6 +714,10 @@ QVariant OptionsModel::getOption(OptionID option, const std::string& suffix) con
return settings.value("enable_psbt_controls");
case KeepChangeAddress:
return fKeepChangeAddress;
+ case DustProtection:
+ return fDustProtection;
+ case DustProtectionThreshold:
+ return qlonglong(nDustProtectionThreshold);
#endif // ENABLE_WALLET
case Prune:
return PruneEnabled(setting());
@@ -987,6 +1000,16 @@ bool OptionsModel::setOption(OptionID option, const QVariant& value, const std::
settings.setValue("fKeepChangeAddress", fKeepChangeAddress);
Q_EMIT keepChangeAddressChanged(fKeepChangeAddress);
break;
+ case DustProtection:
+ fDustProtection = value.toBool();
+ settings.setValue("fDustProtection", fDustProtection);
+ Q_EMIT dustProtectionChanged();
+ break;
+ case DustProtectionThreshold:
+ nDustProtectionThreshold = value.toLongLong();
+ settings.setValue("nDustProtectionThreshold", qlonglong(nDustProtectionThreshold));
+ Q_EMIT dustProtectionChanged();
+ break;
#endif // ENABLE_WALLET
case Prune:
if (changed()) {
diff --git a/src/qt/optionsmodel.h b/src/qt/optionsmodel.h
index 1bf20c7b0efc..e598b12d2128 100644
--- a/src/qt/optionsmodel.h
+++ b/src/qt/optionsmodel.h
@@ -24,6 +24,9 @@ class Node;
extern const char *DEFAULT_GUI_PROXY_HOST;
static constexpr uint16_t DEFAULT_GUI_PROXY_PORT = 9050;
+/** Default threshold for dust attack protection (in duffs) */
+static constexpr qint64 DEFAULT_DUST_PROTECTION_THRESHOLD = 10000;
+
/**
* Convert configured prune target MiB to displayed GB. Round up to avoid underestimating max disk usage.
*/
@@ -95,6 +98,8 @@ class OptionsModel : public QAbstractListModel
Server, // bool
EnablePSBTControls, // bool
MaskValues, // bool
+ DustProtection, // bool
+ DustProtectionThreshold, // CAmount (in duffs)
OptionIDRowCount,
};
@@ -130,6 +135,8 @@ class OptionsModel : public QAbstractListModel
bool getEnablePSBTControls() const { return m_enable_psbt_controls; }
bool getKeepChangeAddress() const { return fKeepChangeAddress; }
bool getShowAdvancedCJUI() { return fShowAdvancedCJUI; }
+ bool getDustProtection() const { return fDustProtection; }
+ qint64 getDustProtectionThreshold() const { return nDustProtectionThreshold; }
const QString& getOverriddenByCommandLine() { return strOverriddenByCommandLine; }
bool isOptionOverridden(const QString& option) const { return strOverriddenByCommandLine.contains(option); }
void emitCoinJoinEnabledChanged();
@@ -160,6 +167,8 @@ class OptionsModel : public QAbstractListModel
bool m_mask_values;
bool fKeepChangeAddress;
bool fShowAdvancedCJUI;
+ bool fDustProtection{false};
+ qint64 nDustProtectionThreshold{DEFAULT_DUST_PROTECTION_THRESHOLD};
/* settings that were overridden by command-line */
QString strOverriddenByCommandLine;
@@ -183,6 +192,7 @@ class OptionsModel : public QAbstractListModel
void keepChangeAddressChanged(bool);
void showTrayIconChanged(bool);
void fontForMoneyChanged(const QFont&);
+ void dustProtectionChanged();
};
Q_DECLARE_METATYPE(OptionsModel::FontChoice)
diff --git a/src/qt/overviewpage.cpp b/src/qt/overviewpage.cpp
index 8ed12617a19f..91fc3f1aafd1 100644
--- a/src/qt/overviewpage.cpp
+++ b/src/qt/overviewpage.cpp
@@ -13,6 +13,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -759,6 +760,8 @@ void OverviewPage::SetupTransactionList(int nNumItems)
filter->setDynamicSortFilter(true);
filter->setSortRole(Qt::EditRole);
filter->setShowInactive(false);
+ // Exclude dust receive transactions from overview
+ filter->setTypeFilter(TransactionFilterProxy::ALL_TYPES & ~TransactionFilterProxy::TYPE(TransactionRecord::DustReceive));
filter->sort(TransactionTableModel::Date, Qt::DescendingOrder);
ui->listTransactions->setModel(filter.get());
}
diff --git a/src/qt/transactionrecord.cpp b/src/qt/transactionrecord.cpp
index 3814cd9c1999..d80fe24ad661 100644
--- a/src/qt/transactionrecord.cpp
+++ b/src/qt/transactionrecord.cpp
@@ -29,7 +29,8 @@ bool TransactionRecord::showTransaction()
/*
* Decompose CWallet transaction to model transaction records.
*/
-QList TransactionRecord::decomposeTransaction(interfaces::Node& node, interfaces::Wallet& wallet, const interfaces::WalletTx& wtx)
+QList TransactionRecord::decomposeTransaction(interfaces::Node& node, interfaces::Wallet& wallet, const interfaces::WalletTx& wtx,
+ bool dustProtectionEnabled, CAmount dustThreshold)
{
QList parts;
int64_t nTime = wtx.time;
@@ -40,6 +41,15 @@ QList TransactionRecord::decomposeTransaction(interfaces::Nod
std::map mapValue = wtx.value_map;
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)
{
//
@@ -81,6 +91,15 @@ QList TransactionRecord::decomposeTransaction(interfaces::Nod
sub.type = TransactionRecord::PlatformTransfer;
}
+ // Check for dust attack: external receive with small amount
+ // Only override if not already a special type (coinbase, platform transfer)
+ if (dustProtectionEnabled && !isFromMe && !wtx.is_coinbase && !wtx.is_platform_transfer &&
+ sub.credit > 0 && sub.credit <= dustThreshold &&
+ (sub.type == TransactionRecord::RecvWithAddress || sub.type == TransactionRecord::RecvFromOther))
+ {
+ sub.type = TransactionRecord::DustReceive;
+ }
+
parts.append(sub);
}
}
diff --git a/src/qt/transactionrecord.h b/src/qt/transactionrecord.h
index 1c1a5bcdfdf4..6180d8967b6d 100644
--- a/src/qt/transactionrecord.h
+++ b/src/qt/transactionrecord.h
@@ -86,6 +86,7 @@ class TransactionRecord
CoinJoinCreateDenominations,
CoinJoinSend,
PlatformTransfer,
+ DustReceive,
};
/** Number of confirmation recommended for accepting a transaction */
@@ -116,7 +117,8 @@ class TransactionRecord
/** Decompose CWallet transaction to model transaction records.
*/
static bool showTransaction();
- static QList decomposeTransaction(interfaces::Node& node, interfaces::Wallet& wallet, const interfaces::WalletTx& wtx);
+ static QList decomposeTransaction(interfaces::Node& node, interfaces::Wallet& wallet, const interfaces::WalletTx& wtx,
+ bool dustProtectionEnabled = false, CAmount dustThreshold = 0);
/** @name Immutable transaction attributes
@{*/
diff --git a/src/qt/transactiontablemodel.cpp b/src/qt/transactiontablemodel.cpp
index 296a9e88622d..f8fefcf4a8b9 100644
--- a/src/qt/transactiontablemodel.cpp
+++ b/src/qt/transactiontablemodel.cpp
@@ -113,9 +113,11 @@ class TransactionTablePriv
assert(!m_loaded || force);
cachedWallet.clear();
try {
+ bool dustProtection = parent->walletModel->getOptionsModel()->getDustProtection();
+ qint64 dustThreshold = parent->walletModel->getOptionsModel()->getDustProtectionThreshold();
for (const auto& wtx : wallet.getWalletTxs()) {
if (TransactionRecord::showTransaction()) {
- cachedWallet.append(TransactionRecord::decomposeTransaction(parent->walletModel->node(), wallet, wtx));
+ cachedWallet.append(TransactionRecord::decomposeTransaction(parent->walletModel->node(), wallet, wtx, dustProtection, dustThreshold));
}
}
} catch(const std::exception& e) {
@@ -174,8 +176,10 @@ class TransactionTablePriv
break;
}
// Added -- insert at the right position
+ bool dustProtection = parent->walletModel->getOptionsModel()->getDustProtection();
+ qint64 dustThreshold = parent->walletModel->getOptionsModel()->getDustProtectionThreshold();
QList toInsert =
- TransactionRecord::decomposeTransaction(parent->walletModel->node(), wallet, wtx);
+ TransactionRecord::decomposeTransaction(parent->walletModel->node(), wallet, wtx, dustProtection, dustThreshold);
if(!toInsert.isEmpty()) /* only if something to insert */
{
parent->beginInsertRows(QModelIndex(), lowerIndex, lowerIndex+toInsert.size()-1);
@@ -278,6 +282,8 @@ TransactionTableModel::TransactionTableModel(WalletModel *parent):
priv->refreshWallet(walletModel->wallet());
connect(walletModel->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &TransactionTableModel::updateDisplayUnit);
+ // Refresh wallet when dust protection settings change to re-evaluate transaction types
+ connect(walletModel->getOptionsModel(), &OptionsModel::dustProtectionChanged, this, [this]() { refreshWallet(true); });
}
TransactionTableModel::~TransactionTableModel()
@@ -429,6 +435,8 @@ QString TransactionTableModel::formatTxType(const TransactionRecord *wtx) const
return tr("Mined");
case TransactionRecord::PlatformTransfer:
return tr("Platform Transfer");
+ case TransactionRecord::DustReceive:
+ return tr("Dust Receive");
case TransactionRecord::CoinJoinMixing:
return tr("%1 Mixing").arg(QString::fromStdString(gCoinJoinName));
@@ -473,6 +481,7 @@ QString TransactionTableModel::formatTxToAddress(const TransactionRecord *wtx, b
case TransactionRecord::Generated:
case TransactionRecord::CoinJoinSend:
case TransactionRecord::PlatformTransfer:
+ case TransactionRecord::DustReceive:
return formatAddressLabel(wtx->strAddress, wtx->label, tooltip) + watchAddress;
case TransactionRecord::SendToOther:
return QString::fromStdString(wtx->strAddress) + watchAddress;
@@ -499,6 +508,7 @@ QVariant TransactionTableModel::addressColor(const TransactionRecord *wtx) const
case TransactionRecord::PlatformTransfer:
case TransactionRecord::CoinJoinSend:
case TransactionRecord::RecvWithCoinJoin:
+ case TransactionRecord::DustReceive:
{
if (wtx->label.isEmpty()) {
return GUIUtil::getThemedQColor(GUIUtil::ThemedColor::BAREADDRESS);
@@ -550,6 +560,7 @@ QVariant TransactionTableModel::amountColor(const TransactionRecord *rec) const
case TransactionRecord::CoinJoinCollateralPayment:
case TransactionRecord::CoinJoinMakeCollaterals:
case TransactionRecord::CoinJoinCreateDenominations:
+ case TransactionRecord::DustReceive:
return GUIUtil::getThemedQColor(GUIUtil::ThemedColor::ORANGE);
}
return GUIUtil::getThemedQColor(GUIUtil::ThemedColor::DEFAULT);
@@ -737,6 +748,8 @@ QVariant TransactionTableModel::data(const QModelIndex &index, int role) const
return formatTxAmount(rec, false, BitcoinUnits::SeparatorStyle::NEVER);
case StatusRole:
return rec->status.status;
+ case OutputIndexRole:
+ return rec->idx;
}
return QVariant();
}
diff --git a/src/qt/transactiontablemodel.h b/src/qt/transactiontablemodel.h
index 630fec039f73..fdd06de91833 100644
--- a/src/qt/transactiontablemodel.h
+++ b/src/qt/transactiontablemodel.h
@@ -75,6 +75,8 @@ class TransactionTableModel : public QAbstractTableModel
StatusRole,
/** Unprocessed icon */
RawDecorationRole,
+ /** Output index within transaction (for UTXO identification) */
+ OutputIndexRole,
};
int rowCount(const QModelIndex &parent) const override;
diff --git a/src/qt/transactionview.cpp b/src/qt/transactionview.cpp
index b8054e4522f4..2c3a240f7a0c 100644
--- a/src/qt/transactionview.cpp
+++ b/src/qt/transactionview.cpp
@@ -19,6 +19,7 @@
#include
#include
+#include
#include
#include
@@ -95,6 +96,7 @@ TransactionView::TransactionView(QWidget* parent) :
typeWidget->addItem(tr("To yourself"), TransactionFilterProxy::TYPE(TransactionRecord::SendToSelf));
typeWidget->addItem(tr("Mined"), TransactionFilterProxy::TYPE(TransactionRecord::Generated));
typeWidget->addItem(tr("Platform Transfer"), TransactionFilterProxy::TYPE(TransactionRecord::PlatformTransfer));
+ typeWidget->addItem(tr("Dust Receive"), TransactionFilterProxy::TYPE(TransactionRecord::DustReceive));
typeWidget->addItem(tr("Other"), TransactionFilterProxy::TYPE(TransactionRecord::Other));
typeWidget->setCurrentIndex(settings.value("transactionType").toInt());
@@ -162,6 +164,7 @@ TransactionView::TransactionView(QWidget* parent) :
contextMenu->addSeparator();
abandonAction = contextMenu->addAction(tr("A&bandon transaction"), this, &TransactionView::abandonTx);
resendAction = contextMenu->addAction(tr("Rese&nd transaction"), this, &TransactionView::resendTx);
+ unlockDustAction = contextMenu->addAction(tr("&Unlock dust UTXO"), this, &TransactionView::unlockDust);
contextMenu->addAction(tr("&Edit address label"), this, &TransactionView::editLabel);
[[maybe_unused]] QAction* showAddressQRCodeAction = contextMenu->addAction(tr("Show address &QR code"), this, &TransactionView::showAddressQRCode);
#ifndef USE_QRCODE
@@ -419,6 +422,10 @@ void TransactionView::contextualMenu(const QPoint &point)
copyAddressAction->setEnabled(GUIUtil::hasEntryData(transactionView, 0, TransactionTableModel::AddressRole));
copyLabelAction->setEnabled(GUIUtil::hasEntryData(transactionView, 0, TransactionTableModel::LabelRole));
+ // Show unlock dust action only for dust receive transactions
+ int txType = selection.at(0).data(TransactionTableModel::TypeRole).toInt();
+ unlockDustAction->setVisible(txType == TransactionRecord::DustReceive);
+
if (index.isValid()) {
GUIUtil::PopupMenu(contextMenu, transactionView->viewport()->mapToGlobal(point));
}
@@ -455,6 +462,39 @@ void TransactionView::resendTx()
model->wallet().resendTransaction(hash);
}
+void TransactionView::unlockDust()
+{
+ if(!transactionView || !transactionView->selectionModel() || !model) {
+ return;
+ }
+ QModelIndexList selection = transactionView->selectionModel()->selectedRows(0);
+ if (selection.isEmpty()) {
+ return;
+ }
+
+ // Get the transaction hash
+ QVariant hashVar = selection.at(0).data(TransactionTableModel::TxHashRole);
+ if (!hashVar.isValid()) {
+ return;
+ }
+ uint256 hash;
+ hash.SetHex(hashVar.toString().toStdString());
+
+ // Get the output index
+ QVariant idxVar = selection.at(0).data(TransactionTableModel::OutputIndexRole);
+ if (!idxVar.isValid()) {
+ return;
+ }
+ int outputIdx = idxVar.toInt();
+
+ // Create the outpoint and unlock
+ COutPoint outpoint(hash, outputIdx);
+ model->wallet().unlockCoin(outpoint);
+
+ // Refresh the transaction view to update the display
+ model->getTransactionTableModel()->refreshWallet(true);
+}
+
void TransactionView::copyAddress()
{
GUIUtil::copyEntryData(transactionView, 0, TransactionTableModel::AddressRole);
diff --git a/src/qt/transactionview.h b/src/qt/transactionview.h
index 0e43845ef6a9..bc76f2dbe869 100644
--- a/src/qt/transactionview.h
+++ b/src/qt/transactionview.h
@@ -77,6 +77,7 @@ class TransactionView : public QWidget
QAction *resendAction;
QAction *copyAddressAction{nullptr};
QAction *copyLabelAction{nullptr};
+ QAction *unlockDustAction{nullptr};
QWidget *createDateRangeWidget();
void updateCalendarWidgets();
@@ -102,6 +103,7 @@ private Q_SLOTS:
void updateCoinJoinVisibility();
void abandonTx();
void resendTx();
+ void unlockDust();
Q_SIGNALS:
void doubleClicked(const QModelIndex&);
diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp
index 0a9a196cfc04..d1a9bf0e260a 100644
--- a/src/qt/walletmodel.cpp
+++ b/src/qt/walletmodel.cpp
@@ -23,6 +23,7 @@
#include
#include
#include
+#include
#include
#include // for GetBoolArg
#include
@@ -64,6 +65,13 @@ WalletModel::WalletModel(std::unique_ptr wallet, ClientModel
recentRequestsTableModel = new RecentRequestsTableModel(this);
subscribeToCoreSignals();
+
+ // Connect dust protection settings change to lock existing dust
+ if (optionsModel) {
+ connect(optionsModel, &OptionsModel::dustProtectionChanged, this, &WalletModel::lockExistingDustOutputs);
+ // Lock existing dust on startup if dust protection is enabled
+ lockExistingDustOutputs();
+ }
}
WalletModel::~WalletModel()
@@ -149,6 +157,94 @@ void WalletModel::updateTransaction()
fForceCheckBalanceChanged = true;
}
+void WalletModel::checkAndLockDustOutputs(const QString& hashStr)
+{
+ // Check if dust protection is enabled
+ if (!optionsModel || !optionsModel->getDustProtection()) {
+ return;
+ }
+
+ CAmount dustThreshold = optionsModel->getDustProtectionThreshold();
+ if (dustThreshold <= 0) {
+ return;
+ }
+
+ uint256 hash;
+ hash.SetHex(hashStr.toStdString());
+
+ // Get the transaction (lighter than getWalletTx)
+ CTransactionRef tx = m_wallet->getTx(hash);
+ if (!tx) {
+ return;
+ }
+
+ // Skip coinbase and special transactions - not dust attacks
+ if (tx->IsCoinBase() || tx->nType != TRANSACTION_NORMAL) {
+ return;
+ }
+
+ // Check if any input belongs to this wallet (isFromMe check)
+ // Early exit on first match
+ for (const auto& txin : tx->vin) {
+ if (m_wallet->txinIsMine(txin)) {
+ return;
+ }
+ }
+
+ // Check each output - threshold first (cheap), then ownership (more expensive)
+ for (size_t i = 0; i < tx->vout.size(); i++) {
+ const CTxOut& txout = tx->vout[i];
+ if (txout.nValue > 0 && txout.nValue <= dustThreshold) {
+ if (m_wallet->txoutIsMine(txout)) {
+ m_wallet->lockCoin(COutPoint(hash, i), /*write_to_db=*/true);
+ }
+ }
+ }
+}
+
+void WalletModel::lockExistingDustOutputs()
+{
+ if (!optionsModel || !optionsModel->getDustProtection()) {
+ return;
+ }
+
+ CAmount dustThreshold = optionsModel->getDustProtectionThreshold();
+ if (dustThreshold <= 0) {
+ return;
+ }
+
+ // Iterate UTXOs (much smaller set than all transactions)
+ for (const auto& [dest, coins] : m_wallet->listCoins()) {
+ for (const auto& [outpoint, wtxout] : coins) {
+ // Skip if already locked
+ if (m_wallet->isLockedCoin(outpoint)) continue;
+
+ // Skip if above threshold
+ if (wtxout.txout.nValue > dustThreshold) continue;
+
+ // Get the transaction to check for coinbase/special tx and isFromMe
+ CTransactionRef tx = m_wallet->getTx(outpoint.hash);
+ if (!tx) continue;
+
+ // Skip coinbase and special transactions
+ if (tx->IsCoinBase() || tx->nType != TRANSACTION_NORMAL) continue;
+
+ // Check if any input is ours (skip self-sends)
+ bool isFromMe = false;
+ for (const auto& txin : tx->vin) {
+ if (m_wallet->txinIsMine(txin)) {
+ isFromMe = true;
+ break;
+ }
+ }
+ if (isFromMe) continue;
+
+ // External dust - lock it
+ m_wallet->lockCoin(outpoint, /*write_to_db=*/true);
+ }
+ }
+}
+
void WalletModel::updateNumISLocks()
{
cachedNumISLocks++;
@@ -456,10 +552,16 @@ static void NotifyAddressBookChanged(WalletModel *walletmodel,
static void NotifyTransactionChanged(WalletModel *walletmodel, const uint256 &hash, ChangeType status)
{
- Q_UNUSED(hash);
- Q_UNUSED(status);
bool invoked = QMetaObject::invokeMethod(walletmodel, "updateTransaction", Qt::QueuedConnection);
assert(invoked);
+
+ // For new transactions, check if dust protection should lock UTXOs
+ if (status == CT_NEW) {
+ QString hashStr = QString::fromStdString(hash.ToString());
+ invoked = QMetaObject::invokeMethod(walletmodel, "checkAndLockDustOutputs", Qt::QueuedConnection,
+ Q_ARG(QString, hashStr));
+ assert(invoked);
+ }
}
static void NotifyISLockReceived(WalletModel *walletmodel)
diff --git a/src/qt/walletmodel.h b/src/qt/walletmodel.h
index b33e74a43ada..1113f9b5e33d 100644
--- a/src/qt/walletmodel.h
+++ b/src/qt/walletmodel.h
@@ -241,6 +241,10 @@ public Q_SLOTS:
void updateStatus();
/* New transaction, or transaction changed status */
void updateTransaction();
+ /* Check and lock dust outputs for a new transaction */
+ void checkAndLockDustOutputs(const QString& hash);
+ /* Lock existing dust outputs (called on startup and settings change) */
+ void lockExistingDustOutputs();
/* IS-Lock received */
void updateNumISLocks();
/* ChainLock received */