v29.4-roots.3 · Commit 26: [PATCH 26/30] 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. The two line-number columns refer to before and after.
src/qt/coincontroldialog.cpp
CHANGED
|
@@ -25,9 +25,14 @@
|
|
|
25
25
|
#include <QDialogButtonBox>
|
|
26
26
|
#include <QFlags>
|
|
27
27
|
#include <QIcon>
|
|
28
|
+
#include <QLineEdit>
|
|
28
29
|
#include <QSettings>
|
|
30
|
+
#include <QStringList>
|
|
29
31
|
#include <QTreeWidget>
|
|
30
32
|
|
|
33
|
+
#include <limits>
|
|
34
|
+
#include <map>
|
|
35
|
+
|
|
31
36
|
using wallet::CCoinControl;
|
|
32
37
|
|
|
33
38
|
QList<CAmount> CoinControlDialog::payAmounts;
|
|
@@ -35,7 +40,9 @@ bool CoinControlDialog::fSubtractFeeFromAmount = false;
|
|
|
35
40
|
|
|
36
41
|
bool CCoinControlWidgetItem::operator<(const QTreeWidgetItem &other) const {
|
|
37
42
|
int column = treeWidget()->sortColumn();
|
|
38
|
-
if (column == CoinControlDialog::COLUMN_AMOUNT || column == CoinControlDialog::COLUMN_DATE || column == CoinControlDialog::COLUMN_CONFIRMATIONS)
|
|
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)
|
|
39
46
|
return data(column, Qt::UserRole).toLongLong() < other.data(column, Qt::UserRole).toLongLong();
|
|
40
47
|
return QTreeWidgetItem::operator<(other);
|
|
41
48
|
}
|
|
@@ -98,6 +105,9 @@ CoinControlDialog::CoinControlDialog(CCoinControl& coin_control, WalletModel* _m
|
|
|
98
105
|
|
|
99
106
|
// (un)select all
|
|
100
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);
|
|
101
111
|
|
|
102
112
|
ui->treeWidget->setColumnWidth(COLUMN_CHECKBOX, 84);
|
|
103
113
|
ui->treeWidget->setColumnWidth(COLUMN_AMOUNT, 110);
|
|
@@ -105,6 +115,14 @@ CoinControlDialog::CoinControlDialog(CCoinControl& coin_control, WalletModel* _m
|
|
|
105
115
|
ui->treeWidget->setColumnWidth(COLUMN_ADDRESS, 320);
|
|
106
116
|
ui->treeWidget->setColumnWidth(COLUMN_DATE, 130);
|
|
107
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."));
|
|
108
126
|
|
|
109
127
|
// default view is sorted by amount desc
|
|
110
128
|
sortView(COLUMN_AMOUNT, Qt::DescendingOrder);
|
|
@@ -318,6 +336,30 @@ void CoinControlDialog::headerSectionClicked(int logicalIndex)
|
|
|
318
336
|
}
|
|
319
337
|
}
|
|
320
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
|
+
|
|
321
363
|
// toggle tree mode
|
|
322
364
|
void CoinControlDialog::radioTreeMode(bool checked)
|
|
323
365
|
{
|
|
@@ -366,6 +408,26 @@ void CoinControlDialog::updateLabelLocked()
|
|
|
366
408
|
else ui->labelLocked->setVisible(false);
|
|
367
409
|
}
|
|
368
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
|
+
|
|
369
431
|
void CoinControlDialog::updateLabels(CCoinControl& m_coin_control, WalletModel *model, QDialog* dialog)
|
|
370
432
|
{
|
|
371
433
|
if (!model)
|
|
@@ -502,6 +564,9 @@ void CoinControlDialog::updateLabels(CCoinControl& m_coin_control, WalletModel *
|
|
|
502
564
|
QLabel *l4 = dialog->findChild<QLabel *>("labelCoinControlAfterFee");
|
|
503
565
|
QLabel *l5 = dialog->findChild<QLabel *>("labelCoinControlBytes");
|
|
504
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");
|
|
505
570
|
|
|
506
571
|
// enable/disable "change"
|
|
507
572
|
dialog->findChild<QLabel *>("labelCoinControlChangeText") ->setEnabled(nPayAmount > 0);
|
|
@@ -514,6 +579,10 @@ void CoinControlDialog::updateLabels(CCoinControl& m_coin_control, WalletModel *
|
|
|
514
579
|
l4->setText(BitcoinUnits::formatWithUnit(nDisplayUnit, nAfterFee)); // After Fee
|
|
515
580
|
l5->setText(((nBytes > 0) ? ASYMP_UTF8 : "") + QString::number(nBytes)); // Bytes
|
|
516
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"));
|
|
517
586
|
if (nPayFee > 0)
|
|
518
587
|
{
|
|
519
588
|
l3->setText(ASYMP_UTF8 + l3->text());
|
|
@@ -555,7 +624,9 @@ void CoinControlDialog::updateView()
|
|
|
555
624
|
if (!model || !model->getOptionsModel() || !model->getAddressTableModel())
|
|
556
625
|
return;
|
|
557
626
|
|
|
558
|
-
bool treeMode = ui->radioTreeMode->isChecked();
|
|
627
|
+
const bool treeMode = ui->radioTreeMode->isChecked();
|
|
628
|
+
m_available_amount = 0;
|
|
629
|
+
m_available_count = 0;
|
|
559
630
|
|
|
560
631
|
ui->treeWidget->clear();
|
|
561
632
|
ui->treeWidget->setEnabled(false); // performance, otherwise updateLabels would be called for every checked checkbox
|
|
@@ -565,7 +636,21 @@ void CoinControlDialog::updateView()
|
|
|
565
636
|
|
|
566
637
|
BitcoinUnit nDisplayUnit = model->getOptionsModel()->getDisplayUnit();
|
|
567
638
|
|
|
568
|
-
for (const auto& coins : model->wallet().listCoins()) {
|
|
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) {
|
|
569
654
|
CCoinControlWidgetItem* itemWalletAddress{nullptr};
|
|
570
655
|
QString sWalletAddress = QString::fromStdString(EncodeDestination(coins.first));
|
|
571
656
|
QString sWalletLabel = model->getAddressTableModel()->labelForAddress(sWalletAddress);
|
|
@@ -585,13 +670,15 @@ void CoinControlDialog::updateView()
|
|
|
585
670
|
|
|
586
671
|
// address
|
|
587
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."));
|
|
588
675
|
}
|
|
589
676
|
|
|
590
677
|
CAmount nSum = 0;
|
|
591
678
|
int nChildren = 0;
|
|
592
679
|
for (const auto& outpair : coins.second) {
|
|
593
680
|
const COutPoint& output = std::get<0>(outpair);
|
|
594
|
-
const interfaces::WalletTxOut& out = std::get<1>(outpair);
|
|
681
|
+
const interfaces::WalletTxOut& out = coin_facts.at(output);
|
|
595
682
|
nSum += out.txout.nValue;
|
|
596
683
|
nChildren++;
|
|
597
684
|
|
|
@@ -640,14 +727,35 @@ void CoinControlDialog::updateView()
|
|
|
640
727
|
itemOutput->setText(COLUMN_CONFIRMATIONS, QString::number(out.depth_in_main_chain));
|
|
641
728
|
itemOutput->setData(COLUMN_CONFIRMATIONS, Qt::UserRole, QVariant((qlonglong)out.depth_in_main_chain));
|
|
642
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
|
+
|
|
643
751
|
// transaction hash
|
|
644
752
|
itemOutput->setData(COLUMN_ADDRESS, TxHashRole, QString::fromStdString(output.hash.GetHex()));
|
|
645
753
|
|
|
646
754
|
// vout index
|
|
647
755
|
itemOutput->setData(COLUMN_ADDRESS, VOutRole, output.n);
|
|
648
756
|
|
|
649
|
-
// disable locked coins
|
|
650
|
-
if (model->wallet().isLockedCoin(output))
|
|
757
|
+
// Wallet-owned locked state prevents selection, but remains visible.
|
|
758
|
+
if (out.is_locked)
|
|
651
759
|
{
|
|
652
760
|
m_coin_control.UnSelect(output); // just to be sure
|
|
653
761
|
itemOutput->setDisabled(true);
|
|
@@ -657,6 +765,10 @@ void CoinControlDialog::updateView()
|
|
|
657
765
|
// set checkbox
|
|
658
766
|
if (m_coin_control.IsSelected(output))
|
|
659
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
|
+
}
|
|
660
772
|
}
|
|
661
773
|
|
|
662
774
|
// amount
|
|
@@ -679,4 +791,6 @@ void CoinControlDialog::updateView()
|
|
|
679
791
|
// sort view
|
|
680
792
|
sortView(sortColumn, sortOrder);
|
|
681
793
|
ui->treeWidget->setEnabled(true);
|
|
794
|
+
updateAvailableLabel();
|
|
795
|
+
filterChanged(ui->lineEditFilter->text());
|
|
682
796
|
}
|