Skip to content
Bitcoin Roots

v29.4-roots.4 · Commit 26: [PATCH 26/37] feat(qt): enhance privacy-aware coin control

src/qt/coincontroldialog.cpp

Before: src/qt/coincontroldialog.cpp · After: src/qt/coincontroldialog.cpp

File metadata
index febf1ee82fa01e85937561e2100b44e3e8cf3627..d9ade71911933090d65ee010561f6dc37c6bca08 100644

Download original patch · Release notes

Diff rows 1–223 of 223. Line numbers refer to the original before/after files.

+ Added− RemovedUnmarked lines are context.

Scroll the code area horizontally to read long lines. Before is on the left; after is on the right. Unified view fits narrow screens more easily.

src/qt/coincontroldialog.cpp CHANGED
@@ -25,9 +25,14 @@
25
  #include <QDialogButtonBox>
26
  #include <QFlags>
27
  #include <QIcon>
 
28
  #include <QSettings>
 
29
  #include <QTreeWidget>
30
 
 
 
 
31
  using wallet::CCoinControl;
32
 
33
  QList<CAmount> CoinControlDialog::payAmounts;
@@ -35,7 +40,9 @@ bool CoinControlDialog::fSubtractFeeFromAmount = false;
35
 
36
  bool CCoinControlWidgetItem::operator<(const QTreeWidgetItem &other) const {
37
  int column = treeWidget()->sortColumn();
38
- if (column == CoinControlDialog::COLUMN_AMOUNT || column == CoinControlDialog::COLUMN_DATE || column == CoinControlDialog::COLUMN_CONFIRMATIONS)
 
 
39
  return data(column, Qt::UserRole).toLongLong() < other.data(column, Qt::UserRole).toLongLong();
40
  return QTreeWidgetItem::operator<(other);
41
  }
@@ -98,6 +105,9 @@ CoinControlDialog::CoinControlDialog(CCoinControl& coin_control, WalletModel* _m
98
 
99
  // (un)select all
100
  connect(ui->pushButtonSelectAll, &QPushButton::clicked, this, &CoinControlDialog::buttonSelectAllClicked);
 
 
 
101
 
102
  ui->treeWidget->setColumnWidth(COLUMN_CHECKBOX, 84);
103
  ui->treeWidget->setColumnWidth(COLUMN_AMOUNT, 110);
@@ -105,6 +115,14 @@ CoinControlDialog::CoinControlDialog(CCoinControl& coin_control, WalletModel* _m
105
  ui->treeWidget->setColumnWidth(COLUMN_ADDRESS, 320);
106
  ui->treeWidget->setColumnWidth(COLUMN_DATE, 130);
107
  ui->treeWidget->setColumnWidth(COLUMN_CONFIRMATIONS, 110);
 
 
 
 
 
 
 
 
108
 
109
  // default view is sorted by amount desc
110
  sortView(COLUMN_AMOUNT, Qt::DescendingOrder);
@@ -318,6 +336,30 @@ void CoinControlDialog::headerSectionClicked(int logicalIndex)
318
  }
319
  }
320
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
  // toggle tree mode
322
  void CoinControlDialog::radioTreeMode(bool checked)
323
  {
@@ -366,6 +408,26 @@ void CoinControlDialog::updateLabelLocked()
366
  else ui->labelLocked->setVisible(false);
367
  }
368
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
  void CoinControlDialog::updateLabels(CCoinControl& m_coin_control, WalletModel *model, QDialog* dialog)
370
  {
371
  if (!model)
@@ -502,6 +564,9 @@ void CoinControlDialog::updateLabels(CCoinControl& m_coin_control, WalletModel *
502
  QLabel *l4 = dialog->findChild<QLabel *>("labelCoinControlAfterFee");
503
  QLabel *l5 = dialog->findChild<QLabel *>("labelCoinControlBytes");
504
  QLabel *l8 = dialog->findChild<QLabel *>("labelCoinControlChange");
 
 
 
505
 
506
  // enable/disable "change"
507
  dialog->findChild<QLabel *>("labelCoinControlChangeText") ->setEnabled(nPayAmount > 0);
@@ -514,6 +579,10 @@ void CoinControlDialog::updateLabels(CCoinControl& m_coin_control, WalletModel *
514
  l4->setText(BitcoinUnits::formatWithUnit(nDisplayUnit, nAfterFee)); // After Fee
515
  l5->setText(((nBytes > 0) ? ASYMP_UTF8 : "") + QString::number(nBytes)); // Bytes
516
  l8->setText(BitcoinUnits::formatWithUnit(nDisplayUnit, nChange)); // Change
 
 
 
 
517
  if (nPayFee > 0)
518
  {
519
  l3->setText(ASYMP_UTF8 + l3->text());
@@ -555,7 +624,9 @@ void CoinControlDialog::updateView()
555
  if (!model || !model->getOptionsModel() || !model->getAddressTableModel())
556
  return;
557
 
558
- bool treeMode = ui->radioTreeMode->isChecked();
 
 
559
 
560
  ui->treeWidget->clear();
561
  ui->treeWidget->setEnabled(false); // performance, otherwise updateLabels would be called for every checked checkbox
@@ -565,7 +636,21 @@ void CoinControlDialog::updateView()
565
 
566
  BitcoinUnit nDisplayUnit = model->getOptionsModel()->getDisplayUnit();
567
 
568
- for (const auto& coins : model->wallet().listCoins()) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
569
  CCoinControlWidgetItem* itemWalletAddress{nullptr};
570
  QString sWalletAddress = QString::fromStdString(EncodeDestination(coins.first));
571
  QString sWalletLabel = model->getAddressTableModel()->labelForAddress(sWalletAddress);
@@ -585,13 +670,15 @@ void CoinControlDialog::updateView()
585
 
586
  // address
587
  itemWalletAddress->setText(COLUMN_ADDRESS, sWalletAddress);
 
 
588
  }
589
 
590
  CAmount nSum = 0;
591
  int nChildren = 0;
592
  for (const auto& outpair : coins.second) {
593
  const COutPoint& output = std::get<0>(outpair);
594
- const interfaces::WalletTxOut& out = std::get<1>(outpair);
595
  nSum += out.txout.nValue;
596
  nChildren++;
597
 
@@ -640,14 +727,35 @@ void CoinControlDialog::updateView()
640
  itemOutput->setText(COLUMN_CONFIRMATIONS, QString::number(out.depth_in_main_chain));
641
  itemOutput->setData(COLUMN_CONFIRMATIONS, Qt::UserRole, QVariant((qlonglong)out.depth_in_main_chain));
642
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
643
  // transaction hash
644
  itemOutput->setData(COLUMN_ADDRESS, TxHashRole, QString::fromStdString(output.hash.GetHex()));
645
 
646
  // vout index
647
  itemOutput->setData(COLUMN_ADDRESS, VOutRole, output.n);
648
 
649
- // disable locked coins
650
- if (model->wallet().isLockedCoin(output))
651
  {
652
  m_coin_control.UnSelect(output); // just to be sure
653
  itemOutput->setDisabled(true);
@@ -657,6 +765,10 @@ void CoinControlDialog::updateView()
657
  // set checkbox
658
  if (m_coin_control.IsSelected(output))
659
  itemOutput->setCheckState(COLUMN_CHECKBOX, Qt::Checked);
 
 
 
 
660
  }
661
 
662
  // amount
@@ -679,4 +791,6 @@ void CoinControlDialog::updateView()
679
  // sort view
680
  sortView(sortColumn, sortOrder);
681
  ui->treeWidget->setEnabled(true);
 
 
682
  }
 
25
  #include <QDialogButtonBox>
26
  #include <QFlags>
27
  #include <QIcon>
28
+ #include <QLineEdit>
29
  #include <QSettings>
30
+ #include <QStringList>
31
  #include <QTreeWidget>
32
 
33
+ #include <limits>
34
+ #include <map>
35
+
36
  using wallet::CCoinControl;
37
 
38
  QList<CAmount> CoinControlDialog::payAmounts;
 
40
 
41
  bool CCoinControlWidgetItem::operator<(const QTreeWidgetItem &other) const {
42
  int column = treeWidget()->sortColumn();
43
+ if (column == CoinControlDialog::COLUMN_AMOUNT || column == CoinControlDialog::COLUMN_EFFECTIVE_VALUE ||
44
+ column == CoinControlDialog::COLUMN_INPUT_BYTES || column == CoinControlDialog::COLUMN_DATE ||
45
+ column == CoinControlDialog::COLUMN_CONFIRMATIONS)
46
  return data(column, Qt::UserRole).toLongLong() < other.data(column, Qt::UserRole).toLongLong();
47
  return QTreeWidgetItem::operator<(other);
48
  }
 
105
 
106
  // (un)select all
107
  connect(ui->pushButtonSelectAll, &QPushButton::clicked, this, &CoinControlDialog::buttonSelectAllClicked);
108
+ connect(ui->lineEditFilter, &QLineEdit::textChanged, this, &CoinControlDialog::filterChanged);
109
+ connect(model, &WalletModel::balanceChanged, this, &CoinControlDialog::walletChanged);
110
+ connect(model, &WalletModel::unload, this, &QDialog::reject);
111
 
112
  ui->treeWidget->setColumnWidth(COLUMN_CHECKBOX, 84);
113
  ui->treeWidget->setColumnWidth(COLUMN_AMOUNT, 110);
 
115
  ui->treeWidget->setColumnWidth(COLUMN_ADDRESS, 320);
116
  ui->treeWidget->setColumnWidth(COLUMN_DATE, 130);
117
  ui->treeWidget->setColumnWidth(COLUMN_CONFIRMATIONS, 110);
118
+ ui->treeWidget->setColumnWidth(COLUMN_STATUS, 180);
119
+ ui->treeWidget->setColumnWidth(COLUMN_EFFECTIVE_VALUE, 125);
120
+ ui->treeWidget->setColumnWidth(COLUMN_INPUT_BYTES, 95);
121
+
122
+ ui->lineEditFilter->setAccessibleName(tr("Filter coin selection"));
123
+ ui->lineEditFilter->setAccessibleDescription(tr("Filters outpoints, addresses, labels, amounts, and wallet state without changing selected inputs."));
124
+ ui->treeWidget->setAccessibleName(tr("Coin selection list"));
125
+ ui->treeWidget->setAccessibleDescription(tr("Use Space to select an eligible input. Locked, unsafe, watch-only, and reused states are shown in the Status column."));
126
 
127
  // default view is sorted by amount desc
128
  sortView(COLUMN_AMOUNT, Qt::DescendingOrder);
 
336
  }
337
  }
338
 
339
+ void CoinControlDialog::filterChanged(const QString& filter)
340
+ {
341
+ const QString needle{filter.trimmed()};
342
+ const auto matches = [&needle](QTreeWidgetItem* item) {
343
+ if (needle.isEmpty()) return true;
344
+ for (int column = COLUMN_AMOUNT; column <= COLUMN_STATUS; ++column) {
345
+ if (item->text(column).contains(needle, Qt::CaseInsensitive)) return true;
346
+ }
347
+ return item->data(COLUMN_ADDRESS, TxHashRole).toString().contains(needle, Qt::CaseInsensitive);
348
+ };
349
+ for (int i = 0; i < ui->treeWidget->topLevelItemCount(); ++i) {
350
+ QTreeWidgetItem* parent{ui->treeWidget->topLevelItem(i)};
351
+ const bool group_matches{matches(parent)};
352
+ bool has_visible_child{false};
353
+ for (int child = 0; child < parent->childCount(); ++child) {
354
+ const bool visible{group_matches || matches(parent->child(child))};
355
+ parent->child(child)->setHidden(!visible);
356
+ has_visible_child |= visible;
357
+ }
358
+ parent->setHidden(parent->childCount() ? !has_visible_child && !group_matches : !group_matches);
359
+ if (!needle.isEmpty() && parent->childCount()) parent->setExpanded(has_visible_child);
360
+ }
361
+ }
362
+
363
  // toggle tree mode
364
  void CoinControlDialog::radioTreeMode(bool checked)
365
  {
 
408
  else ui->labelLocked->setVisible(false);
409
  }
410
 
411
+ void CoinControlDialog::updateAvailableLabel()
412
+ {
413
+ ui->labelAvailable->setText(tr("Available: %1 (%2 eligible)")
414
+ .arg(BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), m_available_amount))
415
+ .arg(m_available_count));
416
+ }
417
+
418
+ void CoinControlDialog::walletChanged()
419
+ {
420
+ // A balance update can represent a competing spend, a reorg, or a lock
421
+ // change. Keep intent only for harmless presentation refreshes; wallet
422
+ // changes require the operator to deliberately select inputs again.
423
+ m_coin_control.UnSelectAll();
424
+ updateView();
425
+ updateLabelLocked();
426
+ CoinControlDialog::updateLabels(m_coin_control, model, this);
427
+ ui->labelSelectionNotice->setText(tr("Input selection cleared after a wallet change. Review the current inputs."));
428
+ ui->labelSelectionNotice->setVisible(true);
429
+ }
430
+
431
  void CoinControlDialog::updateLabels(CCoinControl& m_coin_control, WalletModel *model, QDialog* dialog)
432
  {
433
  if (!model)
 
564
  QLabel *l4 = dialog->findChild<QLabel *>("labelCoinControlAfterFee");
565
  QLabel *l5 = dialog->findChild<QLabel *>("labelCoinControlBytes");
566
  QLabel *l8 = dialog->findChild<QLabel *>("labelCoinControlChange");
567
+ QLabel *fee_rate = dialog->findChild<QLabel *>("labelCoinControlFeeRate");
568
+ QLabel *rbf = dialog->findChild<QLabel *>("labelCoinControlRbf");
569
+ QLabel *dust = dialog->findChild<QLabel *>("labelCoinControlDust");
570
 
571
  // enable/disable "change"
572
  dialog->findChild<QLabel *>("labelCoinControlChangeText") ->setEnabled(nPayAmount > 0);
 
579
  l4->setText(BitcoinUnits::formatWithUnit(nDisplayUnit, nAfterFee)); // After Fee
580
  l5->setText(((nBytes > 0) ? ASYMP_UTF8 : "") + QString::number(nBytes)); // Bytes
581
  l8->setText(BitcoinUnits::formatWithUnit(nDisplayUnit, nChange)); // Change
582
+ if (fee_rate) fee_rate->setText(nBytes > 0 ? tr("%1 sat/vB").arg(QString::number(static_cast<double>(nPayFee) / nBytes, 'f', 1)) : tr("—"));
583
+ if (rbf) rbf->setText(m_coin_control.m_signal_bip125_rbf ? tr("Signals Replace-By-Fee") : tr("Does not signal Replace-By-Fee"));
584
+ if (dust) dust->setText(nQuantity > 0 && nChange == 0 && nPayAmount > 0 && !CoinControlDialog::fSubtractFeeFromAmount
585
+ ? tr("No change output (change may have been added to the fee)") : tr("No dust change detected"));
586
  if (nPayFee > 0)
587
  {
588
  l3->setText(ASYMP_UTF8 + l3->text());
 
624
  if (!model || !model->getOptionsModel() || !model->getAddressTableModel())
625
  return;
626
 
627
+ const bool treeMode = ui->radioTreeMode->isChecked();
628
+ m_available_amount = 0;
629
+ m_available_count = 0;
630
 
631
  ui->treeWidget->clear();
632
  ui->treeWidget->setEnabled(false); // performance, otherwise updateLabels would be called for every checked checkbox
 
636
 
637
  BitcoinUnit nDisplayUnit = model->getOptionsModel()->getDisplayUnit();
638
 
639
+ const auto coin_groups = model->wallet().listCoins();
640
+ std::vector<COutPoint> outpoints;
641
+ for (const auto& coins : coin_groups) {
642
+ for (const auto& outpair : coins.second) outpoints.push_back(std::get<0>(outpair));
643
+ }
644
+ // Use the same fee choice that the wallet will use for the current coin
645
+ // control. This makes effective value a wallet calculation, rather than
646
+ // a GUI approximation. The 1000-vbyte query converts the selected wallet
647
+ // fee into CFeeRate's sat/kvB representation.
648
+ const CFeeRate fee_rate{m_coin_control.m_feerate.value_or(
649
+ CFeeRate(model->wallet().getMinimumFee(1000, m_coin_control, /*returned_target=*/nullptr, /*reason=*/nullptr)))};
650
+ std::map<COutPoint, interfaces::WalletTxOut> coin_facts;
651
+ for (const auto& coin : model->wallet().getCoins(outpoints, fee_rate)) coin_facts.emplace(coin.outpoint, coin);
652
+
653
+ for (const auto& coins : coin_groups) {
654
  CCoinControlWidgetItem* itemWalletAddress{nullptr};
655
  QString sWalletAddress = QString::fromStdString(EncodeDestination(coins.first));
656
  QString sWalletLabel = model->getAddressTableModel()->labelForAddress(sWalletAddress);
 
670
 
671
  // address
672
  itemWalletAddress->setText(COLUMN_ADDRESS, sWalletAddress);
673
+ itemWalletAddress->setText(COLUMN_STATUS, tr("Grouped by wallet address"));
674
+ itemWalletAddress->setToolTip(COLUMN_STATUS, tr("Grouping reflects wallet address relationships. Selecting this group combines its inputs; it does not measure privacy."));
675
  }
676
 
677
  CAmount nSum = 0;
678
  int nChildren = 0;
679
  for (const auto& outpair : coins.second) {
680
  const COutPoint& output = std::get<0>(outpair);
681
+ const interfaces::WalletTxOut& out = coin_facts.at(output);
682
  nSum += out.txout.nValue;
683
  nChildren++;
684
 
 
727
  itemOutput->setText(COLUMN_CONFIRMATIONS, QString::number(out.depth_in_main_chain));
728
  itemOutput->setData(COLUMN_CONFIRMATIONS, Qt::UserRole, QVariant((qlonglong)out.depth_in_main_chain));
729
 
730
+ QStringList states;
731
+ if (out.is_locked) states << tr("Locked");
732
+ if (!out.is_safe) states << tr("Unsafe");
733
+ if (!out.is_spendable && out.is_solvable) states << tr("Watch-only");
734
+ if (!out.is_spendable && !out.is_solvable) states << tr("Not spendable");
735
+ if (out.is_reused) states << tr("Reused address");
736
+ if (out.is_immature) states << tr("Immature");
737
+ if (out.is_change) states << tr("Change");
738
+ if (states.isEmpty()) states << tr("Eligible");
739
+ itemOutput->setText(COLUMN_STATUS, states.join(", "));
740
+ itemOutput->setToolTip(COLUMN_STATUS, tr("Wallet-reported state. Reuse and grouping describe observable wallet facts; they are not a privacy score."));
741
+ if (out.effective_value) {
742
+ itemOutput->setText(COLUMN_EFFECTIVE_VALUE, BitcoinUnits::format(nDisplayUnit, *out.effective_value));
743
+ itemOutput->setData(COLUMN_EFFECTIVE_VALUE, Qt::UserRole, QVariant((qlonglong)*out.effective_value));
744
+ } else {
745
+ itemOutput->setText(COLUMN_EFFECTIVE_VALUE, tr("Not estimated"));
746
+ itemOutput->setData(COLUMN_EFFECTIVE_VALUE, Qt::UserRole, QVariant(std::numeric_limits<qlonglong>::min()));
747
+ }
748
+ itemOutput->setText(COLUMN_INPUT_BYTES, out.input_bytes >= 0 ? QString::number(out.input_bytes) : tr("Unknown"));
749
+ itemOutput->setData(COLUMN_INPUT_BYTES, Qt::UserRole, QVariant((qlonglong)out.input_bytes));
750
+
751
  // transaction hash
752
  itemOutput->setData(COLUMN_ADDRESS, TxHashRole, QString::fromStdString(output.hash.GetHex()));
753
 
754
  // vout index
755
  itemOutput->setData(COLUMN_ADDRESS, VOutRole, output.n);
756
 
757
+ // Wallet-owned locked state prevents selection, but remains visible.
758
+ if (out.is_locked)
759
  {
760
  m_coin_control.UnSelect(output); // just to be sure
761
  itemOutput->setDisabled(true);
 
765
  // set checkbox
766
  if (m_coin_control.IsSelected(output))
767
  itemOutput->setCheckState(COLUMN_CHECKBOX, Qt::Checked);
768
+ if (out.is_spendable && out.is_safe && !out.is_locked && !out.is_immature) {
769
+ m_available_amount += out.txout.nValue;
770
+ ++m_available_count;
771
+ }
772
  }
773
 
774
  // amount
 
791
  // sort view
792
  sortView(sortColumn, sortOrder);
793
  ui->treeWidget->setEnabled(true);
794
+ updateAvailableLabel();
795
+ filterChanged(ui->lineEditFilter->text());
796
  }