Skip to content
Bitcoin Roots

v30.3-roots.1 · Commit 24: [PATCH 24/68] 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 c9a65d995bc015a858927d6b205b15d0caf518d2..f879ef70db688a472bd9bf6020e2b7281f9216f8 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
  {
@@ -275,6 +301,123 @@ void TestGUI(interfaces::Node& node, const std::shared_ptr<CWallet>& wallet)
275
  WalletModel& walletModel = *mini_gui.walletModel;
276
  SendCoinsDialog& sendCoinsDialog = mini_gui.sendCoinsDialog;
277
  TransactionView& transactionView = mini_gui.transactionView;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
 
279
  // Update walletModel cached balance which will trigger an update for the 'labelBalance' QLabel.
280
  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
  {
 
301
  WalletModel& walletModel = *mini_gui.walletModel;
302
  SendCoinsDialog& sendCoinsDialog = mini_gui.sendCoinsDialog;
303
  TransactionView& transactionView = mini_gui.transactionView;
304
+ qApp->processEvents(); // Drain the fixture's queued RemoveWallet notification.
305
+
306
+ const QString sweep_sentinel{"sweep-ui-secret-sentinel"};
307
+ LogCapture sweep_logs;
308
+
309
+ // Verify the guarded sweep dialog contract and asynchronous error path.
310
+ SweepDialog sweep_dialog(&walletModel);
311
+ QLineEdit* sweep_key = sweep_dialog.findChild<QLineEdit*>("sweepPrivateKey");
312
+ QLineEdit* sweep_destination = sweep_dialog.findChild<QLineEdit*>("sweepDestination");
313
+ QPushButton* sweep_preview = sweep_dialog.findChild<QPushButton*>("sweepPreview");
314
+ QPushButton* sweep_broadcast = sweep_dialog.findChild<QPushButton*>("sweepBroadcast");
315
+ QCheckBox* sweep_reveal = sweep_dialog.findChild<QCheckBox*>("sweepReveal");
316
+ QLabel* sweep_result = sweep_dialog.findChild<QLabel*>("sweepResult");
317
+ QVERIFY(sweep_key && sweep_destination && sweep_preview && sweep_broadcast && sweep_reveal && sweep_result);
318
+ QCOMPARE(sweep_key->echoMode(), QLineEdit::Password);
319
+ QCOMPARE(sweep_key->contextMenuPolicy(), Qt::NoContextMenu);
320
+ QCOMPARE(sweep_key->accessibleName(), "Private key");
321
+ QVERIFY(sweep_key->accessibleDescription().contains("Copy, cut"));
322
+ QCOMPARE(sweep_reveal->accessibleName(), "Show private key");
323
+ QVERIFY(sweep_reveal->accessibleDescription().contains("Copy and cut"));
324
+ QCOMPARE(sweep_destination->accessibleName(), "Destination address");
325
+ QVERIFY(sweep_destination->accessibleDescription().contains("selected wallet"));
326
+ QCOMPARE(sweep_result->accessibleName(), "Sweep preview");
327
+ QVERIFY(sweep_result->accessibleDescription().contains("Non-secret"));
328
+ QCOMPARE(sweep_preview->accessibleName(), "Preview sweep");
329
+ QCOMPARE(sweep_broadcast->accessibleName(), "Broadcast sweep");
330
+ sweep_dialog.show();
331
+ sweep_key->setFocus();
332
+ QTRY_COMPARE(qApp->focusWidget(), static_cast<QWidget*>(sweep_key));
333
+ QTest::keyClick(sweep_key, Qt::Key_Tab);
334
+ QTRY_COMPARE(qApp->focusWidget(), static_cast<QWidget*>(sweep_reveal));
335
+ QTest::keyClick(sweep_reveal, Qt::Key_Tab);
336
+ QTRY_COMPARE(qApp->focusWidget(), static_cast<QWidget*>(sweep_destination));
337
+ QTest::keyClick(sweep_destination, Qt::Key_Tab);
338
+ QTRY_COMPARE(qApp->focusWidget(), static_cast<QWidget*>(sweep_preview));
339
+ sweep_reveal->setChecked(true);
340
+ QCOMPARE(sweep_key->echoMode(), QLineEdit::Normal);
341
+ sweep_reveal->setChecked(false);
342
+ QCOMPARE(sweep_key->echoMode(), QLineEdit::Password);
343
+ QVERIFY(!sweep_broadcast->isEnabled());
344
+ auto sweep_dest{walletModel.wallet().getNewDestination(OutputType::BECH32, "")};
345
+ QVERIFY(sweep_dest);
346
+ sweep_destination->setText(QString::fromStdString(EncodeDestination(*sweep_dest)));
347
+ sweep_key->setText(sweep_sentinel);
348
+ QVERIFY(QMetaObject::invokeMethod(&sweep_dialog, "preview"));
349
+ QVERIFY(!sweep_key->isEnabled());
350
+ QVERIFY(sweep_key->text().isEmpty());
351
+ QTRY_VERIFY(sweep_key->isEnabled());
352
+ QVERIFY(!sweep_result->text().contains(sweep_sentinel));
353
+ QVERIFY(!QString::fromStdString(sweep_logs.output).contains(sweep_sentinel));
354
+
355
+ sweep_key->setText("not-a-private-key");
356
+ QVERIFY(QMetaObject::invokeMethod(&sweep_dialog, "preview"));
357
+ QVERIFY(!sweep_key->isEnabled());
358
+ QVERIFY(sweep_key->text().isEmpty());
359
+ // A competing wallet update must discard the transient key and preview
360
+ // immediately, rather than relying on broadcast-time stale detection.
361
+ walletModel.updateTransaction();
362
+ QVERIFY(sweep_key->isEnabled());
363
+ QVERIFY(sweep_key->text().isEmpty());
364
+ QVERIFY(!sweep_broadcast->isEnabled());
365
+ QVERIFY(sweep_result->text().contains("fresh preview"));
366
+ QTRY_VERIFY_WITH_TIMEOUT(sweep_key->isEnabled(), 5000);
367
+ QVERIFY(!sweep_result->text().contains("not-a-private-key"));
368
+ QVERIFY(!sweep_broadcast->isEnabled());
369
+ sweep_dialog.reject();
370
+ QVERIFY(sweep_key->text().isEmpty());
371
+
372
+ const auto settings_contain_sentinel = [&] {
373
+ QSettings settings;
374
+ for (const QString& key : settings.allKeys()) {
375
+ if (key.contains(sweep_sentinel) || settings.value(key).toString().contains(sweep_sentinel)) return true;
376
+ }
377
+ return false;
378
+ };
379
+ const auto widgets_contain_sentinel = [&] {
380
+ for (QWidget* widget : qApp->allWidgets()) {
381
+ if (auto* line_edit = qobject_cast<QLineEdit*>(widget); line_edit && line_edit->text().contains(sweep_sentinel)) return true;
382
+ if (auto* label = qobject_cast<QLabel*>(widget); label && label->text().contains(sweep_sentinel)) return true;
383
+ if (auto* button = qobject_cast<QAbstractButton*>(widget); button && button->text().contains(sweep_sentinel)) return true;
384
+ }
385
+ return false;
386
+ };
387
+ QVERIFY(!settings_contain_sentinel());
388
+ QVERIFY(!widgets_contain_sentinel());
389
+ for (int i = 0; i < 3; ++i) {
390
+ auto dialog = std::make_unique<SweepDialog>(&walletModel);
391
+ QLineEdit* key = dialog->findChild<QLineEdit*>("sweepPrivateKey");
392
+ QVERIFY(key);
393
+ key->setText(sweep_sentinel);
394
+ dialog->reject();
395
+ QVERIFY(key->text().isEmpty());
396
+ dialog.reset();
397
+ QVERIFY(!widgets_contain_sentinel());
398
+ }
399
+ QVERIFY(!settings_contain_sentinel());
400
+
401
+ const auto verify_layout = [&](const QFont& font, const QSize& size) {
402
+ SweepDialog dialog(&walletModel);
403
+ dialog.setFont(font);
404
+ dialog.resize(size);
405
+ dialog.show();
406
+ QTRY_VERIFY(dialog.isVisible());
407
+ QVERIFY(dialog.sizeHint().width() <= size.width());
408
+ QVERIFY(dialog.sizeHint().height() <= size.height());
409
+ for (QWidget* widget : std::initializer_list<QWidget*>{dialog.findChild<QLineEdit*>("sweepPrivateKey"), dialog.findChild<QLineEdit*>("sweepDestination"),
410
+ dialog.findChild<QCheckBox*>("sweepReveal"), dialog.findChild<QPushButton*>("sweepPreview"),
411
+ dialog.findChild<QPushButton*>("sweepBroadcast"), dialog.findChild<QLabel*>("sweepResult")}) {
412
+ QVERIFY(widget && widget->isVisible());
413
+ QVERIFY(dialog.contentsRect().contains(widget->mapTo(&dialog, widget->rect().center())));
414
+ }
415
+ dialog.reject();
416
+ };
417
+ verify_layout(sweep_dialog.font(), QSize{1024, 700});
418
+ QFont large_font{sweep_dialog.font()};
419
+ large_font.setPointSize(24);
420
+ verify_layout(large_font, QSize{1600, 1000});
421
 
422
  // Update walletModel cached balance which will trigger an update for the 'labelBalance' QLabel.
423
  walletModel.pollBalanceChanged();