From 6a9656e453c2419f6f94c93f4832ad55039062c7 Mon Sep 17 00:00:00 2001 From: UdjinM6 Date: Fri, 20 Feb 2026 00:39:56 +0300 Subject: [PATCH 1/3] feat(qt): add "(un)lock all" button to Coin Control dialog Add a new "(un)lock all" button that batch-locks or batch-unlocks all visible UTXOs. If any visible coin is locked, clicking unlocks only the locked ones; if none are locked, clicking locks them all. New `lockCoins()` / `unlockCoins()` interface methods use a single `WalletBatch` for the entire operation, avoiding per-coin DB overhead. Works in both tree and list modes. Co-Authored-By: Claude Opus 4.6 --- src/interfaces/wallet.h | 6 +++++ src/qt/coincontroldialog.cpp | 42 +++++++++++++++++++++++++++++++ src/qt/coincontroldialog.h | 1 + src/qt/forms/coincontroldialog.ui | 18 ++++++++++++- src/wallet/interfaces.cpp | 18 +++++++++++++ 5 files changed, 84 insertions(+), 1 deletion(-) diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h index dd4e53dac764..f5426199139b 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -172,6 +172,12 @@ class Wallet //! List locked coins. virtual std::vector listLockedCoins() = 0; + //! Lock the provided coins in a single batch. + virtual bool lockCoins(const std::vector& outputs) = 0; + + //! Unlock the provided coins in a single batch. + virtual bool unlockCoins(const std::vector& outputs) = 0; + //! List protx coins. virtual std::vector listProTxCoins() = 0; diff --git a/src/qt/coincontroldialog.cpp b/src/qt/coincontroldialog.cpp index 7f2d077bc11b..b6485822e4d3 100644 --- a/src/qt/coincontroldialog.cpp +++ b/src/qt/coincontroldialog.cpp @@ -117,6 +117,9 @@ CoinControlDialog::CoinControlDialog(CCoinControl& coin_control, WalletModel* _m // Toggle lock state connect(ui->pushButtonToggleLock, &QPushButton::clicked, this, &CoinControlDialog::buttonToggleLockClicked); + // (un)lock all + connect(ui->pushButtonLockAll, &QPushButton::clicked, this, &CoinControlDialog::buttonLockAllClicked); + ui->treeWidget->setColumnWidth(COLUMN_CHECKBOX, 94); ui->treeWidget->setColumnWidth(COLUMN_AMOUNT, 100); ui->treeWidget->setColumnWidth(COLUMN_LABEL, 170); @@ -223,6 +226,45 @@ void CoinControlDialog::buttonToggleLockClicked() } } +// (un)lock all +void CoinControlDialog::buttonLockAllClicked() +{ + // Collect all visible UTXOs; track locked ones separately for the unlock path + // (works in both tree and list modes) + std::vector outputs; + std::vector lockedOutputs; + for (int i = 0; i < ui->treeWidget->topLevelItemCount(); i++) { + QTreeWidgetItem* item = ui->treeWidget->topLevelItem(i); + if (item->data(COLUMN_ADDRESS, TxHashRole).toString().length() == 64) { + // List mode: top-level item is a UTXO + const COutPoint outpt(uint256S(item->data(COLUMN_ADDRESS, TxHashRole).toString().toStdString()), + item->data(COLUMN_ADDRESS, VOutRole).toUInt()); + outputs.emplace_back(outpt); + if (model->wallet().isLockedCoin(outpt)) lockedOutputs.emplace_back(outpt); + } else { + // Tree mode: top-level item is an address group; collect children + for (int j = 0; j < item->childCount(); j++) { + QTreeWidgetItem* child = item->child(j); + const COutPoint outpt(uint256S(child->data(COLUMN_ADDRESS, TxHashRole).toString().toStdString()), + child->data(COLUMN_ADDRESS, VOutRole).toUInt()); + outputs.emplace_back(outpt); + if (model->wallet().isLockedCoin(outpt)) lockedOutputs.emplace_back(outpt); + } + } + } + bool should_lock = lockedOutputs.empty(); + bool success = should_lock ? model->wallet().lockCoins(outputs) + : model->wallet().unlockCoins(lockedOutputs); + if (!success) { + QMessageBox::warning(this, tr("Wallet error"), + should_lock ? tr("Failed to lock some coins.") + : tr("Failed to unlock some coins.")); + } + updateView(); + updateLabelLocked(); + CoinControlDialog::updateLabels(m_coin_control, model, this); +} + // context menu void CoinControlDialog::showMenu(const QPoint &point) { diff --git a/src/qt/coincontroldialog.h b/src/qt/coincontroldialog.h index 2449463267a5..5e36b21f6240 100644 --- a/src/qt/coincontroldialog.h +++ b/src/qt/coincontroldialog.h @@ -109,6 +109,7 @@ private Q_SLOTS: void buttonBoxClicked(QAbstractButton*); void buttonSelectAllClicked(); void buttonToggleLockClicked(); + void buttonLockAllClicked(); void updateLabelLocked(); void on_hideButton_clicked(); }; diff --git a/src/qt/forms/coincontroldialog.ui b/src/qt/forms/coincontroldialog.ui index f24eb76221b2..cd24c7da6541 100644 --- a/src/qt/forms/coincontroldialog.ui +++ b/src/qt/forms/coincontroldialog.ui @@ -254,7 +254,7 @@ - + 14 @@ -274,6 +274,22 @@ + + + + + 0 + 0 + + + + (un)lock all + + + false + + + diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index 340d29a35c9e..fee87c192ceb 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -326,6 +326,24 @@ class WalletImpl : public Wallet LOCK(m_wallet->cs_wallet); return m_wallet->ListLockedCoins(); } + bool lockCoins(const std::vector& outputs) override + { + LOCK(m_wallet->cs_wallet); + WalletBatch batch(m_wallet->GetDatabase()); + for (const auto& output : outputs) { + if (!m_wallet->LockCoin(output, &batch)) return false; + } + return true; + } + bool unlockCoins(const std::vector& outputs) override + { + LOCK(m_wallet->cs_wallet); + WalletBatch batch(m_wallet->GetDatabase()); + for (const auto& output : outputs) { + if (!m_wallet->UnlockCoin(output, &batch)) return false; + } + return true; + } std::vector listProTxCoins() override { LOCK(m_wallet->cs_wallet); From 747f88a996c7d08311c2a29fd59c89921af2fad6 Mon Sep 17 00:00:00 2001 From: UdjinM6 Date: Fri, 20 Feb 2026 00:44:48 +0300 Subject: [PATCH 2/3] refactor(qt): remove redundant "toggle lock state" button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new "(un)lock all" button supersedes "toggle lock state" — it handles both locking and unlocking in a single batch operation and works in both tree and list modes. Co-Authored-By: Claude Opus 4.6 --- src/qt/coincontroldialog.cpp | 40 ------------------------------- src/qt/coincontroldialog.h | 1 - src/qt/forms/coincontroldialog.ui | 18 +------------- 3 files changed, 1 insertion(+), 58 deletions(-) diff --git a/src/qt/coincontroldialog.cpp b/src/qt/coincontroldialog.cpp index b6485822e4d3..a498a9994dd9 100644 --- a/src/qt/coincontroldialog.cpp +++ b/src/qt/coincontroldialog.cpp @@ -114,9 +114,6 @@ CoinControlDialog::CoinControlDialog(CCoinControl& coin_control, WalletModel* _m // (un)select all connect(ui->pushButtonSelectAll, &QPushButton::clicked, this, &CoinControlDialog::buttonSelectAllClicked); - // Toggle lock state - connect(ui->pushButtonToggleLock, &QPushButton::clicked, this, &CoinControlDialog::buttonToggleLockClicked); - // (un)lock all connect(ui->pushButtonLockAll, &QPushButton::clicked, this, &CoinControlDialog::buttonLockAllClicked); @@ -189,43 +186,6 @@ void CoinControlDialog::buttonSelectAllClicked() CoinControlDialog::updateLabels(m_coin_control, model, this); } -// Toggle lock state -void CoinControlDialog::buttonToggleLockClicked() -{ - QTreeWidgetItem *item; - // Works in list-mode only - if(ui->radioListMode->isChecked()){ - ui->treeWidget->setEnabled(false); - for (int i = 0; i < ui->treeWidget->topLevelItemCount(); i++){ - item = ui->treeWidget->topLevelItem(i); - COutPoint outpt(uint256S(item->data(COLUMN_ADDRESS, TxHashRole).toString().toStdString()), item->data(COLUMN_ADDRESS, VOutRole).toUInt()); - // Don't toggle the lock state of partially mixed coins if they are not hidden in CoinJoin mode - if (m_coin_control.IsUsingCoinJoin() && !fHideAdditional && !model->isFullyMixed(outpt)) { - continue; - } - if (model->wallet().isLockedCoin(outpt)) { - model->wallet().unlockCoin(outpt); - item->setDisabled(false); - item->setIcon(COLUMN_CHECKBOX, QIcon()); - } - else{ - model->wallet().lockCoin(outpt, /*write_to_db=*/true); - item->setDisabled(true); - item->setIcon(COLUMN_CHECKBOX, GUIUtil::getIcon("lock_closed", GUIUtil::ThemedColor::RED)); - } - updateLabelLocked(); - } - ui->treeWidget->setEnabled(true); - CoinControlDialog::updateLabels(m_coin_control, model, this); - } - else{ - QMessageBox msgBox(this); - msgBox.setObjectName("lockMessageBox"); - msgBox.setText(tr("Please switch to \"List mode\" to use this function.")); - msgBox.exec(); - } -} - // (un)lock all void CoinControlDialog::buttonLockAllClicked() { diff --git a/src/qt/coincontroldialog.h b/src/qt/coincontroldialog.h index 5e36b21f6240..e59b833d8f4c 100644 --- a/src/qt/coincontroldialog.h +++ b/src/qt/coincontroldialog.h @@ -108,7 +108,6 @@ private Q_SLOTS: void headerSectionClicked(int); void buttonBoxClicked(QAbstractButton*); void buttonSelectAllClicked(); - void buttonToggleLockClicked(); void buttonLockAllClicked(); void updateLabelLocked(); void on_hideButton_clicked(); diff --git a/src/qt/forms/coincontroldialog.ui b/src/qt/forms/coincontroldialog.ui index cd24c7da6541..410ef6412b24 100644 --- a/src/qt/forms/coincontroldialog.ui +++ b/src/qt/forms/coincontroldialog.ui @@ -254,7 +254,7 @@ - + 14 @@ -290,22 +290,6 @@ - - - - - 0 - 0 - - - - toggle lock state - - - false - - - From 77bf57cd31a6317a7291c21e73c6baaf9aedbc51 Mon Sep 17 00:00:00 2001 From: UdjinM6 Date: Fri, 20 Feb 2026 01:11:06 +0300 Subject: [PATCH 3/3] refactor: return const set reference from CWallet::ListLockedCoins() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Avoid a pointless set→vector copy: return `const std::set&` directly from the wallet layer, and `std::set` through the interface (copied under lock). In buttonLockAllClicked, fetch the locked set once instead of calling isLockedCoin() per visible UTXO (1 lock acquisition instead of N). Co-Authored-By: Claude Opus 4.6 --- src/interfaces/wallet.h | 3 ++- src/qt/coincontroldialog.cpp | 7 +++++-- src/wallet/interfaces.cpp | 2 +- src/wallet/wallet.cpp | 4 ++-- src/wallet/wallet.h | 2 +- 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h index f5426199139b..423235c2d9e9 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -170,7 +171,7 @@ class Wallet virtual bool isLockedCoin(const COutPoint& output) = 0; //! List locked coins. - virtual std::vector listLockedCoins() = 0; + virtual std::set listLockedCoins() = 0; //! Lock the provided coins in a single batch. virtual bool lockCoins(const std::vector& outputs) = 0; diff --git a/src/qt/coincontroldialog.cpp b/src/qt/coincontroldialog.cpp index a498a9994dd9..192a580ee3b2 100644 --- a/src/qt/coincontroldialog.cpp +++ b/src/qt/coincontroldialog.cpp @@ -189,6 +189,9 @@ void CoinControlDialog::buttonSelectAllClicked() // (un)lock all void CoinControlDialog::buttonLockAllClicked() { + // Fetch the wallet-wide locked set once (single cs_wallet acquisition) + const std::set lockedSet{model->wallet().listLockedCoins()}; + // Collect all visible UTXOs; track locked ones separately for the unlock path // (works in both tree and list modes) std::vector outputs; @@ -200,7 +203,7 @@ void CoinControlDialog::buttonLockAllClicked() const COutPoint outpt(uint256S(item->data(COLUMN_ADDRESS, TxHashRole).toString().toStdString()), item->data(COLUMN_ADDRESS, VOutRole).toUInt()); outputs.emplace_back(outpt); - if (model->wallet().isLockedCoin(outpt)) lockedOutputs.emplace_back(outpt); + if (lockedSet.contains(outpt)) lockedOutputs.emplace_back(outpt); } else { // Tree mode: top-level item is an address group; collect children for (int j = 0; j < item->childCount(); j++) { @@ -208,7 +211,7 @@ void CoinControlDialog::buttonLockAllClicked() const COutPoint outpt(uint256S(child->data(COLUMN_ADDRESS, TxHashRole).toString().toStdString()), child->data(COLUMN_ADDRESS, VOutRole).toUInt()); outputs.emplace_back(outpt); - if (model->wallet().isLockedCoin(outpt)) lockedOutputs.emplace_back(outpt); + if (lockedSet.contains(outpt)) lockedOutputs.emplace_back(outpt); } } } diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index fee87c192ceb..53d7691a26c9 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -321,7 +321,7 @@ class WalletImpl : public Wallet LOCK(m_wallet->cs_wallet); return m_wallet->IsLockedCoin(output); } - std::vector listLockedCoins() override + std::set listLockedCoins() override { LOCK(m_wallet->cs_wallet); return m_wallet->ListLockedCoins(); diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index f1b18d0e4ead..6ed8a693f074 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -2742,10 +2742,10 @@ bool CWallet::IsLockedCoin(const COutPoint& output) const return setLockedCoins.count(output) > 0; } -std::vector CWallet::ListLockedCoins() const +const std::set& CWallet::ListLockedCoins() const { AssertLockHeld(cs_wallet); - return std::vector(setLockedCoins.begin(), setLockedCoins.end()); + return setLockedCoins; } std::vector CWallet::ListProTxCoins() const { return ListProTxCoins(setWalletUTXO); } diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 603601af23b2..308403bab52e 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -582,7 +582,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati bool LockCoin(const COutPoint& output, WalletBatch* batch = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); bool UnlockCoin(const COutPoint& output, WalletBatch* batch = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); bool UnlockAllCoins() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); - std::vector ListLockedCoins() const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + const std::set& ListLockedCoins() const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); std::vector ListProTxCoins() const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); std::vector ListProTxCoins(const std::set& utxos) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); void LockProTxCoins(const std::set& utxos, WalletBatch* batch = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);