v31.1-roots.1 · Commit 6: [PATCH 06/17] feat(wallet): preserve Roots signing and spend controls on v31
src/qt/test/wallettests.cpp
Before: src/qt/test/wallettests.cpp · After: src/qt/test/wallettests.cpp
File metadata
index 19b25650b7886ceb4e12dad37fe246107ab42c04..41b00b2313b920956b7434f1174ac9dadb1d87c9 100644
Download original patch · Release notes
Diff rows 1–400 of 588. 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/test/wallettests.cpp
CHANGED
|
@@ -5,13 +5,17 @@
|
|
|
5
5
|
#include <qt/test/wallettests.h>
|
|
6
6
|
#include <qt/test/util.h>
|
|
7
7
|
|
|
8
|
+
#include <util/rbf.h>
|
|
8
9
|
#include <wallet/coincontrol.h>
|
|
9
10
|
#include <interfaces/chain.h>
|
|
10
11
|
#include <interfaces/node.h>
|
|
11
12
|
#include <key_io.h>
|
|
13
|
+
#include <logging.h>
|
|
14
|
+
#include <policy/feerate.h>
|
|
12
15
|
#include <qt/bitcoinamountfield.h>
|
|
13
16
|
#include <qt/bitcoinunits.h>
|
|
14
17
|
#include <qt/clientmodel.h>
|
|
18
|
+
#include <qt/coincontroldialog.h>
|
|
15
19
|
#include <qt/optionsmodel.h>
|
|
16
20
|
#include <qt/overviewpage.h>
|
|
17
21
|
#include <qt/platformstyle.h>
|
|
@@ -21,16 +25,23 @@
|
|
|
21
25
|
#include <qt/recentrequeststablemodel.h>
|
|
22
26
|
#include <qt/sendcoinsdialog.h>
|
|
23
27
|
#include <qt/sendcoinsentry.h>
|
|
28
|
+
#include <qt/sweepdialog.h>
|
|
24
29
|
#include <qt/transactiontablemodel.h>
|
|
25
30
|
#include <qt/transactionview.h>
|
|
26
31
|
#include <qt/walletmodel.h>
|
|
27
32
|
#include <script/solver.h>
|
|
33
|
+
#include <support/lockedpool.h>
|
|
28
34
|
#include <test/util/setup_common.h>
|
|
29
35
|
#include <validation.h>
|
|
30
36
|
#include <wallet/test/util.h>
|
|
31
37
|
#include <wallet/wallet.h>
|
|
32
38
|
|
|
33
39
|
#include <chrono>
|
|
40
|
+
#include <functional>
|
|
41
|
+
#include <future>
|
|
42
|
+
#include <thread>
|
|
43
|
+
#include <initializer_list>
|
|
44
|
+
#include <list>
|
|
34
45
|
#include <memory>
|
|
35
46
|
|
|
36
47
|
#include <QAbstractButton>
|
|
@@ -38,9 +49,19 @@
|
|
|
38
49
|
#include <QApplication>
|
|
39
50
|
#include <QCheckBox>
|
|
40
51
|
#include <QClipboard>
|
|
52
|
+
#include <QFont>
|
|
53
|
+
#include <QDir>
|
|
54
|
+
#include <QLabel>
|
|
55
|
+
#include <QLineEdit>
|
|
41
56
|
#include <QObject>
|
|
42
57
|
#include <QPushButton>
|
|
58
|
+
#include <QRadioButton>
|
|
59
|
+
#include <QSettings>
|
|
60
|
+
#include <QScrollBar>
|
|
61
|
+
#include <QScreen>
|
|
62
|
+
#include <QTest>
|
|
43
63
|
#include <QTimer>
|
|
64
|
+
#include <QTreeWidget>
|
|
44
65
|
#include <QVBoxLayout>
|
|
45
66
|
#include <QTextEdit>
|
|
46
67
|
#include <QListView>
|
|
@@ -56,8 +77,55 @@ using wallet::WalletContext;
|
|
|
56
77
|
using wallet::WalletDescriptor;
|
|
57
78
|
using wallet::WalletRescanReserver;
|
|
58
79
|
|
|
80
|
+
struct SweepDialogTestAccess
|
|
81
|
+
{
|
|
82
|
+
static SecureString& Cache(SweepDialog& dialog) { return dialog.m_private_key_cache; }
|
|
83
|
+
static void ClearCache(SweepDialog& dialog) { dialog.clearPrivateKeyCache(); }
|
|
84
|
+
};
|
|
85
|
+
|
|
59
86
|
namespace
|
|
60
87
|
{
|
|
88
|
+
class LogCapture
|
|
89
|
+
{
|
|
90
|
+
public:
|
|
91
|
+
LogCapture()
|
|
92
|
+
: m_connection{LogInstance().PushBackCallback([this](const std::string& message) { output += message; })}
|
|
93
|
+
{
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
~LogCapture() { LogInstance().DeleteCallback(m_connection); }
|
|
97
|
+
|
|
98
|
+
std::string output;
|
|
99
|
+
|
|
100
|
+
private:
|
|
101
|
+
std::list<std::function<void(const std::string&)>>::iterator m_connection;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
//! Optional evidence uses the actual wallet widgets and disposable regtest fixture.
|
|
105
|
+
bool CaptureWalletWidget(QWidget& widget, const QString& name)
|
|
106
|
+
{
|
|
107
|
+
const QString directory{qEnvironmentVariable("ROOTS_WALLET_CAPTURE_DIR")};
|
|
108
|
+
if (directory.isEmpty()) return true;
|
|
109
|
+
const auto dimensions{qEnvironmentVariable("ROOTS_WALLET_CAPTURE_SIZE", "1280x800").split('x')};
|
|
110
|
+
if (dimensions.size() != 2) return false;
|
|
111
|
+
const QSize size{dimensions[0].toInt(), dimensions[1].toInt()};
|
|
112
|
+
// Native confirmation boxes choose their own size. Capture the actual
|
|
113
|
+
// screen at the requested resolution, rather than forcing their layout.
|
|
114
|
+
if (qobject_cast<QMessageBox*>(&widget)) {
|
|
115
|
+
widget.show();
|
|
116
|
+
qApp->processEvents();
|
|
117
|
+
widget.move(std::max(0, (size.width() - widget.width()) / 2), std::max(0, (size.height() - widget.height()) / 2));
|
|
118
|
+
QTest::qWait(50);
|
|
119
|
+
const auto screenshot{widget.screen()->grabWindow(0)};
|
|
120
|
+
return screenshot.size() == size && QDir().mkpath(directory) && screenshot.save(directory + '/' + name + ".png");
|
|
121
|
+
}
|
|
122
|
+
widget.resize(size);
|
|
123
|
+
widget.show();
|
|
124
|
+
qApp->processEvents();
|
|
125
|
+
QTest::qWait(50);
|
|
126
|
+
return widget.size() == size && QDir().mkpath(directory) && widget.grab().save(directory + '/' + name + ".png");
|
|
127
|
+
}
|
|
128
|
+
|
|
61
129
|
//! Press "Yes" or "Cancel" buttons in modal send confirmation dialog.
|
|
62
130
|
void ConfirmSend(QString* text = nullptr, QMessageBox::StandardButton confirm_type = QMessageBox::Yes)
|
|
63
131
|
{
|
|
@@ -66,9 +134,11 @@ void ConfirmSend(QString* text = nullptr, QMessageBox::StandardButton confirm_ty
|
|
|
66
134
|
if (widget->inherits("SendConfirmationDialog")) {
|
|
67
135
|
SendConfirmationDialog* dialog = qobject_cast<SendConfirmationDialog*>(widget);
|
|
68
136
|
if (text) *text = dialog->text();
|
|
137
|
+
const bool captured{CaptureWalletWidget(*dialog, dialog->text().contains("This transaction signals") ? "send-confirmation-rbf" : "send-confirmation-no-rbf")};
|
|
69
138
|
QAbstractButton* button = dialog->button(confirm_type);
|
|
70
139
|
button->setEnabled(true);
|
|
71
140
|
button->click();
|
|
141
|
+
QVERIFY(captured);
|
|
72
142
|
}
|
|
73
143
|
}
|
|
74
144
|
});
|
|
@@ -76,8 +146,9 @@ void ConfirmSend(QString* text = nullptr, QMessageBox::StandardButton confirm_ty
|
|
|
76
146
|
|
|
77
147
|
//! Send coins to address and return txid.
|
|
78
148
|
Txid SendCoins(CWallet& wallet, SendCoinsDialog& sendCoinsDialog, const CTxDestination& address, CAmount amount, bool rbf,
|
|
79
|
-
QMessageBox::StandardButton confirm_type = QMessageBox::Yes)
|
|
149
|
+
QMessageBox::StandardButton confirm_type = QMessageBox::Yes, QString* confirmation = nullptr)
|
|
80
150
|
{
|
|
151
|
+
qApp->processEvents(); // Deliver preceding transaction notifications before preparing another send.
|
|
81
152
|
QVBoxLayout* entries = sendCoinsDialog.findChild<QVBoxLayout*>("entries");
|
|
82
153
|
SendCoinsEntry* entry = qobject_cast<SendCoinsEntry*>(entries->itemAt(0)->widget());
|
|
83
154
|
entry->findChild<QValidatedLineEdit*>("payTo")->setText(QString::fromStdString(EncodeDestination(address)));
|
|
@@ -90,7 +161,7 @@ Txid SendCoins(CWallet& wallet, SendCoinsDialog& sendCoinsDialog, const CTxDesti
|
|
|
90
161
|
boost::signals2::scoped_connection c(wallet.NotifyTransactionChanged.connect([&txid](const Txid& hash, ChangeType status) {
|
|
91
162
|
if (status == CT_NEW) txid = hash;
|
|
92
163
|
}));
|
|
93
|
-
ConfirmSend(/*text=*/nullptr, confirm_type);
|
|
164
|
+
ConfirmSend(confirmation, confirm_type);
|
|
94
165
|
bool invoked = QMetaObject::invokeMethod(&sendCoinsDialog, "sendButtonClicked", Q_ARG(bool, false));
|
|
95
166
|
assert(invoked);
|
|
96
167
|
return txid;
|
|
@@ -266,11 +337,152 @@ void TestGUI(interfaces::Node& node, const std::shared_ptr<CWallet>& wallet)
|
|
|
266
337
|
{
|
|
267
338
|
// Create widgets for sending coins and listing transactions.
|
|
268
339
|
std::unique_ptr<const PlatformStyle> platformStyle(PlatformStyle::instantiate("other"));
|
|
340
|
+
wallet->m_signal_rbf = false;
|
|
269
341
|
MiniGUI mini_gui(node, platformStyle.get());
|
|
270
342
|
mini_gui.initModelForWallet(node, wallet, platformStyle.get());
|
|
271
343
|
WalletModel& walletModel = *mini_gui.walletModel;
|
|
272
344
|
SendCoinsDialog& sendCoinsDialog = mini_gui.sendCoinsDialog;
|
|
273
345
|
TransactionView& transactionView = mini_gui.transactionView;
|
|
346
|
+
qApp->processEvents(); // Drain the fixture's queued RemoveWallet notification.
|
|
347
|
+
|
|
348
|
+
const QString sweep_sentinel{"sweep-ui-secret-sentinel"};
|
|
349
|
+
LogCapture sweep_logs;
|
|
350
|
+
|
|
351
|
+
// Verify the guarded sweep dialog contract and asynchronous error path.
|
|
352
|
+
SweepDialog sweep_dialog(&walletModel);
|
|
353
|
+
auto& sweep_cache{SweepDialogTestAccess::Cache(sweep_dialog)};
|
|
354
|
+
// Disposal must release locked storage while the dialog stays alive;
|
|
355
|
+
// observe allocator accounting rather than dereferencing freed memory.
|
|
356
|
+
const auto locked_bytes_before{LockedPoolManager::Instance().stats().used};
|
|
357
|
+
sweep_cache.assign(52, 's');
|
|
358
|
+
QVERIFY(LockedPoolManager::Instance().stats().used > locked_bytes_before);
|
|
359
|
+
SweepDialogTestAccess::ClearCache(sweep_dialog);
|
|
360
|
+
QVERIFY(sweep_cache.empty());
|
|
361
|
+
QCOMPARE(LockedPoolManager::Instance().stats().used, locked_bytes_before);
|
|
362
|
+
sweep_cache.assign(4, 's');
|
|
363
|
+
SweepDialogTestAccess::ClearCache(sweep_dialog);
|
|
364
|
+
QVERIFY(sweep_cache.empty());
|
|
365
|
+
QCOMPARE(LockedPoolManager::Instance().stats().used, locked_bytes_before);
|
|
366
|
+
QLineEdit* sweep_key = sweep_dialog.findChild<QLineEdit*>("sweepPrivateKey");
|
|
367
|
+
QLineEdit* sweep_destination = sweep_dialog.findChild<QLineEdit*>("sweepDestination");
|
|
368
|
+
QPushButton* sweep_preview = sweep_dialog.findChild<QPushButton*>("sweepPreview");
|
|
369
|
+
QPushButton* sweep_broadcast = sweep_dialog.findChild<QPushButton*>("sweepBroadcast");
|
|
370
|
+
QCheckBox* sweep_reveal = sweep_dialog.findChild<QCheckBox*>("sweepReveal");
|
|
371
|
+
QLabel* sweep_result = sweep_dialog.findChild<QLabel*>("sweepResult");
|
|
372
|
+
QVERIFY(sweep_key && sweep_destination && sweep_preview && sweep_broadcast && sweep_reveal && sweep_result);
|
|
373
|
+
QCOMPARE(sweep_key->echoMode(), QLineEdit::Password);
|
|
374
|
+
QCOMPARE(sweep_key->contextMenuPolicy(), Qt::NoContextMenu);
|
|
375
|
+
QCOMPARE(sweep_key->accessibleName(), "Private key");
|
|
376
|
+
QVERIFY(sweep_key->accessibleDescription().contains("Copy, cut"));
|
|
377
|
+
QCOMPARE(sweep_reveal->accessibleName(), "Show private key");
|
|
378
|
+
QVERIFY(sweep_reveal->accessibleDescription().contains("Copy and cut"));
|
|
379
|
+
QCOMPARE(sweep_destination->accessibleName(), "Destination address");
|
|
380
|
+
QVERIFY(sweep_destination->accessibleDescription().contains("selected wallet"));
|
|
381
|
+
QVERIFY(sweep_destination->accessibleDescription().contains("Watch-only"));
|
|
382
|
+
QCOMPARE(sweep_result->accessibleName(), "Sweep preview");
|
|
383
|
+
QVERIFY(sweep_result->accessibleDescription().contains("Non-secret"));
|
|
384
|
+
QCOMPARE(sweep_preview->accessibleName(), "Preview sweep");
|
|
385
|
+
QVERIFY(sweep_preview->accessibleDescription().contains("Replace-By-Fee"));
|
|
386
|
+
QCOMPARE(sweep_broadcast->accessibleName(), "Broadcast sweep");
|
|
387
|
+
QVERIFY(sweep_broadcast->accessibleDescription().contains("fees changed"));
|
|
388
|
+
sweep_dialog.show();
|
|
389
|
+
sweep_key->setFocus();
|
|
390
|
+
QTRY_COMPARE(qApp->focusWidget(), static_cast<QWidget*>(sweep_key));
|
|
391
|
+
QTest::keyClick(sweep_key, Qt::Key_Tab);
|
|
392
|
+
QTRY_COMPARE(qApp->focusWidget(), static_cast<QWidget*>(sweep_reveal));
|
|
393
|
+
QTest::keyClick(sweep_reveal, Qt::Key_Tab);
|
|
394
|
+
QTRY_COMPARE(qApp->focusWidget(), static_cast<QWidget*>(sweep_destination));
|
|
395
|
+
QTest::keyClick(sweep_destination, Qt::Key_Tab);
|
|
396
|
+
QTRY_COMPARE(qApp->focusWidget(), static_cast<QWidget*>(sweep_preview));
|
|
397
|
+
sweep_reveal->setChecked(true);
|
|
398
|
+
QCOMPARE(sweep_key->echoMode(), QLineEdit::Normal);
|
|
399
|
+
sweep_reveal->setChecked(false);
|
|
400
|
+
QCOMPARE(sweep_key->echoMode(), QLineEdit::Password);
|
|
401
|
+
QVERIFY(!sweep_broadcast->isEnabled());
|
|
402
|
+
auto sweep_dest{walletModel.wallet().getNewDestination(OutputType::BECH32, "")};
|
|
403
|
+
QVERIFY(sweep_dest);
|
|
404
|
+
sweep_destination->setText(QString::fromStdString(EncodeDestination(*sweep_dest)));
|
|
405
|
+
sweep_key->setText(sweep_sentinel);
|
|
406
|
+
QVERIFY(QMetaObject::invokeMethod(&sweep_dialog, "preview"));
|
|
407
|
+
QVERIFY(!sweep_key->isEnabled());
|
|
408
|
+
QVERIFY(sweep_key->text().isEmpty());
|
|
409
|
+
QTRY_VERIFY(sweep_key->isEnabled());
|
|
410
|
+
QVERIFY(sweep_cache.empty());
|
|
411
|
+
QCOMPARE(sweep_cache.capacity(), SecureString{}.capacity());
|
|
412
|
+
QVERIFY(!sweep_result->text().contains(sweep_sentinel));
|
|
413
|
+
QVERIFY(!QString::fromStdString(sweep_logs.output).contains(sweep_sentinel));
|
|
414
|
+
|
|
415
|
+
sweep_key->setText("not-a-private-key");
|
|
416
|
+
QVERIFY(QMetaObject::invokeMethod(&sweep_dialog, "preview"));
|
|
417
|
+
QVERIFY(!sweep_key->isEnabled());
|
|
418
|
+
QVERIFY(sweep_key->text().isEmpty());
|
|
419
|
+
// A competing wallet update must discard the transient key and preview
|
|
420
|
+
// immediately, rather than relying on broadcast-time stale detection.
|
|
421
|
+
walletModel.updateTransaction();
|
|
422
|
+
QVERIFY(sweep_cache.empty());
|
|
423
|
+
QCOMPARE(sweep_cache.capacity(), SecureString{}.capacity());
|
|
424
|
+
QVERIFY(sweep_key->isEnabled());
|
|
425
|
+
QVERIFY(sweep_key->text().isEmpty());
|
|
426
|
+
QVERIFY(!sweep_broadcast->isEnabled());
|
|
427
|
+
QVERIFY(sweep_result->text().contains("fresh preview"));
|
|
428
|
+
QVERIFY(CaptureWalletWidget(sweep_dialog, "sweep-cancelled"));
|
|
429
|
+
QTRY_VERIFY_WITH_TIMEOUT(sweep_key->isEnabled(), 5000);
|
|
430
|
+
QVERIFY(!sweep_result->text().contains("not-a-private-key"));
|
|
431
|
+
QVERIFY(!sweep_broadcast->isEnabled());
|
|
432
|
+
sweep_dialog.reject();
|
|
433
|
+
QVERIFY(sweep_key->text().isEmpty());
|
|
434
|
+
QVERIFY(sweep_cache.empty());
|
|
435
|
+
QCOMPARE(sweep_cache.capacity(), SecureString{}.capacity());
|
|
436
|
+
|
|
437
|
+
const auto settings_contain_sentinel = [&] {
|
|
438
|
+
QSettings settings;
|
|
439
|
+
for (const QString& key : settings.allKeys()) {
|
|
440
|
+
if (key.contains(sweep_sentinel) || settings.value(key).toString().contains(sweep_sentinel)) return true;
|
|
441
|
+
}
|
|
442
|
+
return false;
|
|
443
|
+
};
|
|
444
|
+
const auto widgets_contain_sentinel = [&] {
|
|
445
|
+
for (QWidget* widget : qApp->allWidgets()) {
|
|
446
|
+
if (auto* line_edit = qobject_cast<QLineEdit*>(widget); line_edit && line_edit->text().contains(sweep_sentinel)) return true;
|
|
447
|
+
if (auto* label = qobject_cast<QLabel*>(widget); label && label->text().contains(sweep_sentinel)) return true;
|
|
448
|
+
if (auto* button = qobject_cast<QAbstractButton*>(widget); button && button->text().contains(sweep_sentinel)) return true;
|
|
449
|
+
}
|
|
450
|
+
return false;
|
|
451
|
+
};
|
|
452
|
+
QVERIFY(!settings_contain_sentinel());
|
|
453
|
+
QVERIFY(!widgets_contain_sentinel());
|
|
454
|
+
for (int i = 0; i < 3; ++i) {
|
|
455
|
+
auto dialog = std::make_unique<SweepDialog>(&walletModel);
|
|
456
|
+
QLineEdit* key = dialog->findChild<QLineEdit*>("sweepPrivateKey");
|
|
457
|
+
QVERIFY(key);
|
|
458
|
+
key->setText(sweep_sentinel);
|
|
459
|
+
dialog->reject();
|
|
460
|
+
QVERIFY(key->text().isEmpty());
|
|
461
|
+
dialog.reset();
|
|
462
|
+
QVERIFY(!widgets_contain_sentinel());
|
|
463
|
+
}
|
|
464
|
+
QVERIFY(!settings_contain_sentinel());
|
|
465
|
+
|
|
466
|
+
const auto verify_layout = [&](const QFont& font, const QSize& size) {
|
|
467
|
+
SweepDialog dialog(&walletModel);
|
|
468
|
+
dialog.setFont(font);
|
|
469
|
+
dialog.resize(size);
|
|
470
|
+
dialog.show();
|
|
471
|
+
QTRY_VERIFY(dialog.isVisible());
|
|
472
|
+
QVERIFY(dialog.sizeHint().width() <= size.width());
|
|
473
|
+
QVERIFY(dialog.sizeHint().height() <= size.height());
|
|
474
|
+
for (QWidget* widget : std::initializer_list<QWidget*>{dialog.findChild<QLineEdit*>("sweepPrivateKey"), dialog.findChild<QLineEdit*>("sweepDestination"),
|
|
475
|
+
dialog.findChild<QCheckBox*>("sweepReveal"), dialog.findChild<QPushButton*>("sweepPreview"),
|
|
476
|
+
dialog.findChild<QPushButton*>("sweepBroadcast"), dialog.findChild<QLabel*>("sweepResult")}) {
|
|
477
|
+
QVERIFY(widget && widget->isVisible());
|
|
478
|
+
QVERIFY(dialog.contentsRect().contains(widget->mapTo(&dialog, widget->rect().center())));
|
|
479
|
+
}
|
|
480
|
+
dialog.reject();
|
|
481
|
+
};
|
|
482
|
+
verify_layout(sweep_dialog.font(), QSize{1024, 700});
|
|
483
|
+
QFont large_font{sweep_dialog.font()};
|
|
484
|
+
large_font.setPointSize(24);
|
|
485
|
+
verify_layout(large_font, QSize{1600, 1000});
|
|
274
486
|
|
|
275
487
|
// Update walletModel cached balance which will trigger an update for the 'labelBalance' QLabel.
|
|
276
488
|
walletModel.pollBalanceChanged();
|
|
@@ -280,16 +492,157 @@ void TestGUI(interfaces::Node& node, const std::shared_ptr<CWallet>& wallet)
|
|
|
280
492
|
// Check 'UseAvailableBalance' functionality
|
|
281
493
|
VerifyUseAvailableBalance(sendCoinsDialog, walletModel);
|
|
282
494
|
|
|
495
|
+
auto* entries_for_capture{sendCoinsDialog.findChild<QVBoxLayout*>("entries")};
|
|
496
|
+
auto* recipient_for_capture{qobject_cast<SendCoinsEntry*>(entries_for_capture->itemAt(0)->widget())};
|
|
497
|
+
recipient_for_capture->findChild<QCheckBox*>("checkboxSubtractFeeFromAmount")->setChecked(false);
|
|
498
|
+
recipient_for_capture->findChild<BitcoinAmountField*>("payAmount")->setValue(COIN);
|
|
499
|
+
sendCoinsDialog.getCoinControl()->m_feerate = CFeeRate{1000};
|
|
500
|
+
|
|
501
|
+
// The coin control view presents wallet-owned facts and keeps filtering a
|
|
502
|
+
// display-only operation: it must not alter the selected outpoints.
|
|
503
|
+
const auto capture_coins{walletModel.wallet().listCoins()};
|
|
504
|
+
QVERIFY(!capture_coins.empty());
|
|
505
|
+
const COutPoint locked_output{std::get<0>(capture_coins.begin()->second.front())};
|
|
506
|
+
walletModel.wallet().lockCoin(locked_output, false);
|
|
507
|
+
CoinControlDialog coin_control_dialog(*sendCoinsDialog.getCoinControl(), &walletModel, platformStyle.get());
|
|
508
|
+
QTreeWidget* coin_tree = coin_control_dialog.findChild<QTreeWidget*>("treeWidget");
|
|
509
|
+
QLineEdit* coin_filter = coin_control_dialog.findChild<QLineEdit*>("lineEditFilter");
|
|
510
|
+
QLabel* available = coin_control_dialog.findChild<QLabel*>("labelAvailable");
|
|
511
|
+
QLabel* fee_rate = coin_control_dialog.findChild<QLabel*>("labelCoinControlFeeRate");
|
|
512
|
+
QLabel* rbf_summary = coin_control_dialog.findChild<QLabel*>("labelCoinControlRbf");
|
|
513
|
+
QLabel* selection_notice = coin_control_dialog.findChild<QLabel*>("labelSelectionNotice");
|
|
514
|
+
QVERIFY(coin_tree && coin_filter && available && fee_rate && rbf_summary && selection_notice);
|
|
515
|
+
QCOMPARE(coin_tree->headerItem()->text(6), "Status");
|
|
516
|
+
QCOMPARE(coin_tree->headerItem()->text(7), "Effective value");
|
|
517
|
+
QCOMPARE(coin_tree->headerItem()->text(8), "Input bytes");
|
|
518
|
+
QVERIFY(available->text().contains("eligible"));
|
|
519
|
+
// Populate an eligible selection while retaining the separate locked row.
|
|
520
|
+
for (int i = 0; i < coin_tree->topLevelItemCount(); ++i) {
|
|
521
|
+
auto* parent{coin_tree->topLevelItem(i)};
|
|
522
|
+
if (parent->childCount() == 0 && !parent->isDisabled()) { parent->setCheckState(0, Qt::Checked); break; }
|
|
523
|
+
for (int j = 0; j < parent->childCount(); ++j) {
|
|
524
|
+
auto* item{parent->child(j)};
|
|
525
|
+
if (!item->isDisabled()) { item->setCheckState(0, Qt::Checked); break; }
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
coin_tree->expandAll();
|
|
529
|
+
QVERIFY(CaptureWalletWidget(coin_control_dialog, "coin-control"));
|
|
530
|
+
coin_tree->horizontalScrollBar()->setValue(coin_tree->horizontalScrollBar()->maximum());
|
|
531
|
+
QVERIFY(CaptureWalletWidget(coin_control_dialog, "coin-control-details"));
|
|
532
|
+
coin_tree->horizontalScrollBar()->setValue(0);
|
|
533
|
+
QVERIFY(rbf_summary->text().contains("Replace-By-Fee"));
|
|
534
|
+
sendCoinsDialog.getCoinControl()->m_signal_bip125_rbf = false;
|
|
535
|
+
CoinControlDialog::updateLabels(*sendCoinsDialog.getCoinControl(), &walletModel, &coin_control_dialog);
|
|
536
|
+
QVERIFY(rbf_summary->text().startsWith("Does not signal"));
|
|
537
|
+
QCOMPARE(coin_filter->accessibleName(), "Filter coin selection");
|
|
538
|
+
QVERIFY(coin_tree->accessibleDescription().contains("Status column"));
|
|
539
|
+
const auto selected_before_filter = sendCoinsDialog.getCoinControl()->ListSelected();
|
|
540
|
+
coin_filter->setText("not-a-wallet-coin");
|
|
541
|
+
qApp->processEvents();
|
|
542
|
+
for (int row = 0; row < coin_tree->topLevelItemCount(); ++row) QVERIFY(coin_tree->topLevelItem(row)->isHidden());
|
|
543
|
+
QVERIFY(sendCoinsDialog.getCoinControl()->ListSelected() == selected_before_filter);
|
|
544
|
+
coin_filter->clear();
|
|
545
|
+
QVERIFY(coin_tree->focusPolicy() != Qt::NoFocus);
|
|
546
|
+
walletModel.updateTransaction(); // Invalidate even when the balance is unchanged.
|
|
547
|
+
QVERIFY(!sendCoinsDialog.getCoinControl()->HasSelected());
|
|
548
|
+
QVERIFY(!selection_notice->isHidden());
|
|
549
|
+
QVERIFY(selection_notice->text().contains("cleared"));
|
|
550
|
+
coin_control_dialog.reject();
|
|
551
|
+
walletModel.wallet().unlockCoin(locked_output);
|
|
552
|
+
|
|
553
|
+
// Effective value must use the active coin-control fee rate, not fall back
|
|
554
|
+
// to face value. Check the formatted value, its numeric sorting key, and
|
|
555
|
+
// the item sorting implementation independently of localized labels.
|
|
556
|
+
const CFeeRate effective_fee_rate{1000};
|
|
557
|
+
const auto coin_groups = walletModel.wallet().listCoins();
|
|
558
|
+
QVERIFY(!coin_groups.empty());
|
|
559
|
+
const COutPoint effective_outpoint{std::get<0>(coin_groups.begin()->second.front())};
|
|
560
|
+
const auto effective_coins = walletModel.wallet().getCoins({effective_outpoint}, effective_fee_rate);
|
|
561
|
+
QCOMPARE(effective_coins.size(), 1U);
|
|
562
|
+
const auto& effective_coin = effective_coins.front();
|
|
563
|
+
QVERIFY(effective_coin.effective_value);
|
|
564
|
+
QVERIFY(*effective_coin.effective_value < effective_coin.txout.nValue);
|
|
565
|
+
|
|
566
|
+
wallet::CCoinControl effective_control;
|
|
567
|
+
effective_control.m_feerate = effective_fee_rate;
|
|
568
|
+
CoinControlDialog effective_dialog(effective_control, &walletModel, platformStyle.get());
|
|
569
|
+
QRadioButton* list_mode = effective_dialog.findChild<QRadioButton*>("radioListMode");
|
|
570
|
+
QTreeWidget* effective_tree = effective_dialog.findChild<QTreeWidget*>("treeWidget");
|
|
571
|
+
QVERIFY(list_mode && effective_tree);
|
|
572
|
+
list_mode->setChecked(true);
|
|
573
|
+
qApp->processEvents();
|
|
574
|
+
QTreeWidgetItem* effective_item{nullptr};
|
|
575
|
+
for (int row = 0; row < effective_tree->topLevelItemCount(); ++row) {
|
|
576
|
+
QTreeWidgetItem* item = effective_tree->topLevelItem(row);
|
|
577
|
+
if (item->data(3, Qt::UserRole).toString() == QString::fromStdString(effective_outpoint.hash.GetHex()) &&
|
|
578
|
+
item->data(3, Qt::UserRole + 1).toUInt() == effective_outpoint.n) {
|
|
579
|
+
effective_item = item;
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
QVERIFY(effective_item);
|
|
584
|
+
QCOMPARE(effective_item->text(7), BitcoinUnits::format(walletModel.getOptionsModel()->getDisplayUnit(), *effective_coin.effective_value));
|
|
585
|
+
QCOMPARE(effective_item->data(7, Qt::UserRole).toLongLong(), qlonglong{*effective_coin.effective_value});
|
|
586
|
+
|
|
587
|
+
QTreeWidget effective_sort_tree;
|
|
588
|
+
effective_sort_tree.setColumnCount(9);
|
|
589
|
+
auto* larger_effective = new CCoinControlWidgetItem(&effective_sort_tree);
|
|
590
|
+
larger_effective->setText(7, "0.00000001");
|
|
591
|
+
larger_effective->setData(7, Qt::UserRole, qlonglong{*effective_coin.effective_value + 1});
|
|
592
|
+
auto* smaller_effective = new CCoinControlWidgetItem(&effective_sort_tree);
|
|
593
|
+
smaller_effective->setText(7, "9.99999999");
|
|
594
|
+
smaller_effective->setData(7, Qt::UserRole, qlonglong{*effective_coin.effective_value});
|
|
595
|
+
effective_sort_tree.sortItems(7, Qt::AscendingOrder);
|
|
596
|
+
QCOMPARE(effective_sort_tree.topLevelItem(0)->data(7, Qt::UserRole).toLongLong(), qlonglong{*effective_coin.effective_value});
|