Skip to content
Bitcoin Roots

v29.4-roots.4 · Commit 24: [PATCH 24/37] feat(qt): add guarded private key sweep dialog

src/qt/test/wallettests.cpp

Before: src/qt/test/wallettests.cpp · After: src/qt/test/wallettests.cpp

File metadata
index 48cc4b7e7a2e3e34dea3be08f14b78dd7ac5f01c..cc49160ce8d221323ea384ea7bc63d194d581153 100644

Download original patch · Release notes

Diff rows 1–181 of 181. 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/test/wallettests.cpp CHANGED
@@ -9,6 +9,7 @@
9
  #include <interfaces/chain.h>
10
  #include <interfaces/node.h>
11
  #include <key_io.h>
 
12
  #include <qt/bitcoinamountfield.h>
13
  #include <qt/bitcoinunits.h>
14
  #include <qt/clientmodel.h>
@@ -21,6 +22,7 @@
21
  #include <qt/recentrequeststablemodel.h>
22
  #include <qt/sendcoinsdialog.h>
23
  #include <qt/sendcoinsentry.h>
 
24
  #include <qt/transactiontablemodel.h>
25
  #include <qt/transactionview.h>
26
  #include <qt/walletmodel.h>
@@ -32,6 +34,9 @@
32
  #include <wallet/wallet.h>
33
 
34
  #include <chrono>
 
 
 
35
  #include <memory>
36
 
37
  #include <QAbstractButton>
@@ -39,8 +44,13 @@
39
  #include <QApplication>
40
  #include <QCheckBox>
41
  #include <QClipboard>
 
 
 
42
  #include <QObject>
43
  #include <QPushButton>
 
 
44
  #include <QTimer>
45
  #include <QVBoxLayout>
46
  #include <QTextEdit>
@@ -59,6 +69,22 @@ using wallet::WalletRescanReserver;
59
 
60
  namespace
61
  {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  //! Press "Yes" or "Cancel" buttons in modal send confirmation dialog.
63
  void ConfirmSend(QString* text = nullptr, QMessageBox::StandardButton confirm_type = QMessageBox::Yes)
64
  {
@@ -283,6 +309,123 @@ void TestGUI(interfaces::Node& node, const std::shared_ptr<CWallet>& wallet)
283
  WalletModel& walletModel = *mini_gui.walletModel;
284
  SendCoinsDialog& sendCoinsDialog = mini_gui.sendCoinsDialog;
285
  TransactionView& transactionView = mini_gui.transactionView;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
 
287
  // Update walletModel cached balance which will trigger an update for the 'labelBalance' QLabel.
288
  walletModel.pollBalanceChanged();
 
9
  #include <interfaces/chain.h>
10
  #include <interfaces/node.h>
11
  #include <key_io.h>
12
+ #include <logging.h>
13
  #include <qt/bitcoinamountfield.h>
14
  #include <qt/bitcoinunits.h>
15
  #include <qt/clientmodel.h>
 
22
  #include <qt/recentrequeststablemodel.h>
23
  #include <qt/sendcoinsdialog.h>
24
  #include <qt/sendcoinsentry.h>
25
+ #include <qt/sweepdialog.h>
26
  #include <qt/transactiontablemodel.h>
27
  #include <qt/transactionview.h>
28
  #include <qt/walletmodel.h>
 
34
  #include <wallet/wallet.h>
35
 
36
  #include <chrono>
37
+ #include <functional>
38
+ #include <initializer_list>
39
+ #include <list>
40
  #include <memory>
41
 
42
  #include <QAbstractButton>
 
44
  #include <QApplication>
45
  #include <QCheckBox>
46
  #include <QClipboard>
47
+ #include <QFont>
48
+ #include <QLabel>
49
+ #include <QLineEdit>
50
  #include <QObject>
51
  #include <QPushButton>
52
+ #include <QSettings>
53
+ #include <QTest>
54
  #include <QTimer>
55
  #include <QVBoxLayout>
56
  #include <QTextEdit>
 
69
 
70
  namespace
71
  {
72
+ class LogCapture
73
+ {
74
+ public:
75
+ LogCapture()
76
+ : m_connection{LogInstance().PushBackCallback([this](const std::string& message) { output += message; })}
77
+ {
78
+ }
79
+
80
+ ~LogCapture() { LogInstance().DeleteCallback(m_connection); }
81
+
82
+ std::string output;
83
+
84
+ private:
85
+ std::list<std::function<void(const std::string&)>>::iterator m_connection;
86
+ };
87
+
88
  //! Press "Yes" or "Cancel" buttons in modal send confirmation dialog.
89
  void ConfirmSend(QString* text = nullptr, QMessageBox::StandardButton confirm_type = QMessageBox::Yes)
90
  {
 
309
  WalletModel& walletModel = *mini_gui.walletModel;
310
  SendCoinsDialog& sendCoinsDialog = mini_gui.sendCoinsDialog;
311
  TransactionView& transactionView = mini_gui.transactionView;
312
+ qApp->processEvents(); // Drain the fixture's queued RemoveWallet notification.
313
+
314
+ const QString sweep_sentinel{"sweep-ui-secret-sentinel"};
315
+ LogCapture sweep_logs;
316
+
317
+ // Verify the guarded sweep dialog contract and asynchronous error path.
318
+ SweepDialog sweep_dialog(&walletModel);
319
+ QLineEdit* sweep_key = sweep_dialog.findChild<QLineEdit*>("sweepPrivateKey");
320
+ QLineEdit* sweep_destination = sweep_dialog.findChild<QLineEdit*>("sweepDestination");
321
+ QPushButton* sweep_preview = sweep_dialog.findChild<QPushButton*>("sweepPreview");
322
+ QPushButton* sweep_broadcast = sweep_dialog.findChild<QPushButton*>("sweepBroadcast");
323
+ QCheckBox* sweep_reveal = sweep_dialog.findChild<QCheckBox*>("sweepReveal");
324
+ QLabel* sweep_result = sweep_dialog.findChild<QLabel*>("sweepResult");
325
+ QVERIFY(sweep_key && sweep_destination && sweep_preview && sweep_broadcast && sweep_reveal && sweep_result);
326
+ QCOMPARE(sweep_key->echoMode(), QLineEdit::Password);
327
+ QCOMPARE(sweep_key->contextMenuPolicy(), Qt::NoContextMenu);
328
+ QCOMPARE(sweep_key->accessibleName(), "Private key");
329
+ QVERIFY(sweep_key->accessibleDescription().contains("Copy, cut"));
330
+ QCOMPARE(sweep_reveal->accessibleName(), "Show private key");
331
+ QVERIFY(sweep_reveal->accessibleDescription().contains("Copy and cut"));
332
+ QCOMPARE(sweep_destination->accessibleName(), "Destination address");
333
+ QVERIFY(sweep_destination->accessibleDescription().contains("selected wallet"));
334
+ QCOMPARE(sweep_result->accessibleName(), "Sweep preview");
335
+ QVERIFY(sweep_result->accessibleDescription().contains("Non-secret"));
336
+ QCOMPARE(sweep_preview->accessibleName(), "Preview sweep");
337
+ QCOMPARE(sweep_broadcast->accessibleName(), "Broadcast sweep");
338
+ sweep_dialog.show();
339
+ sweep_key->setFocus();
340
+ QTRY_COMPARE(qApp->focusWidget(), static_cast<QWidget*>(sweep_key));
341
+ QTest::keyClick(sweep_key, Qt::Key_Tab);
342
+ QTRY_COMPARE(qApp->focusWidget(), static_cast<QWidget*>(sweep_reveal));
343
+ QTest::keyClick(sweep_reveal, Qt::Key_Tab);
344
+ QTRY_COMPARE(qApp->focusWidget(), static_cast<QWidget*>(sweep_destination));
345
+ QTest::keyClick(sweep_destination, Qt::Key_Tab);
346
+ QTRY_COMPARE(qApp->focusWidget(), static_cast<QWidget*>(sweep_preview));
347
+ sweep_reveal->setChecked(true);
348
+ QCOMPARE(sweep_key->echoMode(), QLineEdit::Normal);
349
+ sweep_reveal->setChecked(false);
350
+ QCOMPARE(sweep_key->echoMode(), QLineEdit::Password);
351
+ QVERIFY(!sweep_broadcast->isEnabled());
352
+ auto sweep_dest{walletModel.wallet().getNewDestination(OutputType::BECH32, "")};
353
+ QVERIFY(sweep_dest);
354
+ sweep_destination->setText(QString::fromStdString(EncodeDestination(*sweep_dest)));
355
+ sweep_key->setText(sweep_sentinel);
356
+ QVERIFY(QMetaObject::invokeMethod(&sweep_dialog, "preview"));
357
+ QVERIFY(!sweep_key->isEnabled());
358
+ QVERIFY(sweep_key->text().isEmpty());
359
+ QTRY_VERIFY(sweep_key->isEnabled());
360
+ QVERIFY(!sweep_result->text().contains(sweep_sentinel));
361
+ QVERIFY(!QString::fromStdString(sweep_logs.output).contains(sweep_sentinel));
362
+
363
+ sweep_key->setText("not-a-private-key");
364
+ QVERIFY(QMetaObject::invokeMethod(&sweep_dialog, "preview"));
365
+ QVERIFY(!sweep_key->isEnabled());
366
+ QVERIFY(sweep_key->text().isEmpty());
367
+ // A competing wallet update must discard the transient key and preview
368
+ // immediately, rather than relying on broadcast-time stale detection.
369
+ walletModel.updateTransaction();
370
+ QVERIFY(sweep_key->isEnabled());
371
+ QVERIFY(sweep_key->text().isEmpty());
372
+ QVERIFY(!sweep_broadcast->isEnabled());
373
+ QVERIFY(sweep_result->text().contains("fresh preview"));
374
+ QTRY_VERIFY_WITH_TIMEOUT(sweep_key->isEnabled(), 5000);
375
+ QVERIFY(!sweep_result->text().contains("not-a-private-key"));
376
+ QVERIFY(!sweep_broadcast->isEnabled());
377
+ sweep_dialog.reject();
378
+ QVERIFY(sweep_key->text().isEmpty());
379
+
380
+ const auto settings_contain_sentinel = [&] {
381
+ QSettings settings;
382
+ for (const QString& key : settings.allKeys()) {
383
+ if (key.contains(sweep_sentinel) || settings.value(key).toString().contains(sweep_sentinel)) return true;
384
+ }
385
+ return false;
386
+ };
387
+ const auto widgets_contain_sentinel = [&] {
388
+ for (QWidget* widget : qApp->allWidgets()) {
389
+ if (auto* line_edit = qobject_cast<QLineEdit*>(widget); line_edit && line_edit->text().contains(sweep_sentinel)) return true;
390
+ if (auto* label = qobject_cast<QLabel*>(widget); label && label->text().contains(sweep_sentinel)) return true;
391
+ if (auto* button = qobject_cast<QAbstractButton*>(widget); button && button->text().contains(sweep_sentinel)) return true;
392
+ }
393
+ return false;
394
+ };
395
+ QVERIFY(!settings_contain_sentinel());
396
+ QVERIFY(!widgets_contain_sentinel());
397
+ for (int i = 0; i < 3; ++i) {
398
+ auto dialog = std::make_unique<SweepDialog>(&walletModel);
399
+ QLineEdit* key = dialog->findChild<QLineEdit*>("sweepPrivateKey");
400
+ QVERIFY(key);
401
+ key->setText(sweep_sentinel);
402
+ dialog->reject();
403
+ QVERIFY(key->text().isEmpty());
404
+ dialog.reset();
405
+ QVERIFY(!widgets_contain_sentinel());
406
+ }
407
+ QVERIFY(!settings_contain_sentinel());
408
+
409
+ const auto verify_layout = [&](const QFont& font, const QSize& size) {
410
+ SweepDialog dialog(&walletModel);
411
+ dialog.setFont(font);
412
+ dialog.resize(size);
413
+ dialog.show();
414
+ QTRY_VERIFY(dialog.isVisible());
415
+ QVERIFY(dialog.sizeHint().width() <= size.width());
416
+ QVERIFY(dialog.sizeHint().height() <= size.height());
417
+ for (QWidget* widget : std::initializer_list<QWidget*>{dialog.findChild<QLineEdit*>("sweepPrivateKey"), dialog.findChild<QLineEdit*>("sweepDestination"),
418
+ dialog.findChild<QCheckBox*>("sweepReveal"), dialog.findChild<QPushButton*>("sweepPreview"),
419
+ dialog.findChild<QPushButton*>("sweepBroadcast"), dialog.findChild<QLabel*>("sweepResult")}) {
420
+ QVERIFY(widget && widget->isVisible());
421
+ QVERIFY(dialog.contentsRect().contains(widget->mapTo(&dialog, widget->rect().center())));
422
+ }
423
+ dialog.reject();
424
+ };
425
+ verify_layout(sweep_dialog.font(), QSize{1024, 700});
426
+ QFont large_font{sweep_dialog.font()};
427
+ large_font.setPointSize(24);
428
+ verify_layout(large_font, QSize{1600, 1000});
429
 
430
  // Update walletModel cached balance which will trigger an update for the 'labelBalance' QLabel.
431
  walletModel.pollBalanceChanged();