/ src / qt / test / wallettests.cpp
wallettests.cpp
  1  // Copyright (c) 2015-present The Bitcoin Core developers
  2  // Distributed under the MIT software license, see the accompanying
  3  // file COPYING or http://www.opensource.org/licenses/mit-license.php.
  4  
  5  #include <qt/test/wallettests.h>
  6  #include <qt/test/util.h>
  7  
  8  #include <wallet/coincontrol.h>
  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>
 15  #include <qt/optionsmodel.h>
 16  #include <qt/overviewpage.h>
 17  #include <qt/platformstyle.h>
 18  #include <qt/qvalidatedlineedit.h>
 19  #include <qt/receivecoinsdialog.h>
 20  #include <qt/receiverequestdialog.h>
 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>
 27  #include <script/solver.h>
 28  #include <test/util/setup_common.h>
 29  #include <validation.h>
 30  #include <wallet/test/util.h>
 31  #include <wallet/wallet.h>
 32  
 33  #include <chrono>
 34  #include <memory>
 35  
 36  #include <QAbstractButton>
 37  #include <QAction>
 38  #include <QApplication>
 39  #include <QCheckBox>
 40  #include <QClipboard>
 41  #include <QObject>
 42  #include <QPushButton>
 43  #include <QTimer>
 44  #include <QVBoxLayout>
 45  #include <QTextEdit>
 46  #include <QListView>
 47  #include <QDialogButtonBox>
 48  
 49  using wallet::AddWallet;
 50  using wallet::CWallet;
 51  using wallet::CreateMockableWalletDatabase;
 52  using wallet::RemoveWallet;
 53  using wallet::WALLET_FLAG_DESCRIPTORS;
 54  using wallet::WALLET_FLAG_DISABLE_PRIVATE_KEYS;
 55  using wallet::WalletContext;
 56  using wallet::WalletDescriptor;
 57  using wallet::WalletRescanReserver;
 58  
 59  namespace
 60  {
 61  //! Press "Yes" or "Cancel" buttons in modal send confirmation dialog.
 62  void ConfirmSend(QString* text = nullptr, QMessageBox::StandardButton confirm_type = QMessageBox::Yes)
 63  {
 64      QTimer::singleShot(0, [text, confirm_type]() {
 65          for (QWidget* widget : QApplication::topLevelWidgets()) {
 66              if (widget->inherits("SendConfirmationDialog")) {
 67                  SendConfirmationDialog* dialog = qobject_cast<SendConfirmationDialog*>(widget);
 68                  if (text) *text = dialog->text();
 69                  QAbstractButton* button = dialog->button(confirm_type);
 70                  button->setEnabled(true);
 71                  button->click();
 72              }
 73          }
 74      });
 75  }
 76  
 77  //! Send coins to address and return txid.
 78  Txid SendCoins(CWallet& wallet, SendCoinsDialog& sendCoinsDialog, const CTxDestination& address, CAmount amount, bool rbf,
 79                    QMessageBox::StandardButton confirm_type = QMessageBox::Yes)
 80  {
 81      QVBoxLayout* entries = sendCoinsDialog.findChild<QVBoxLayout*>("entries");
 82      SendCoinsEntry* entry = qobject_cast<SendCoinsEntry*>(entries->itemAt(0)->widget());
 83      entry->findChild<QValidatedLineEdit*>("payTo")->setText(QString::fromStdString(EncodeDestination(address)));
 84      entry->findChild<BitcoinAmountField*>("payAmount")->setValue(amount);
 85      sendCoinsDialog.findChild<QFrame*>("frameFee")
 86          ->findChild<QFrame*>("frameFeeSelection")
 87          ->findChild<QCheckBox*>("optInRBF")
 88          ->setCheckState(rbf ? Qt::Checked : Qt::Unchecked);
 89      Txid txid;
 90      btcsignals::scoped_connection c(wallet.NotifyTransactionChanged.connect([&txid](const Txid& hash, ChangeType status) {
 91          if (status == CT_NEW) txid = hash;
 92      }));
 93      ConfirmSend(/*text=*/nullptr, confirm_type);
 94      bool invoked = QMetaObject::invokeMethod(&sendCoinsDialog, "sendButtonClicked", Q_ARG(bool, false));
 95      assert(invoked);
 96      return txid;
 97  }
 98  
 99  //! Find index of txid in transaction list.
100  QModelIndex FindTx(const QAbstractItemModel& model, const Txid& txid)
101  {
102      QString hash = QString::fromStdString(txid.ToString());
103      int rows = model.rowCount({});
104      for (int row = 0; row < rows; ++row) {
105          QModelIndex index = model.index(row, 0, {});
106          if (model.data(index, TransactionTableModel::TxHashRole) == hash) {
107              return index;
108          }
109      }
110      return {};
111  }
112  
113  //! Invoke bumpfee on txid and check results.
114  void BumpFee(TransactionView& view, const Txid& txid, bool expectDisabled, std::string expectError, bool cancel)
115  {
116      QTableView* table = view.findChild<QTableView*>("transactionView");
117      QModelIndex index = FindTx(*table->selectionModel()->model(), txid);
118      QVERIFY2(index.isValid(), "Could not find BumpFee txid");
119  
120      // Select row in table, invoke context menu, and make sure bumpfee action is
121      // enabled or disabled as expected.
122      QAction* action = view.findChild<QAction*>("bumpFeeAction");
123      table->selectionModel()->select(index, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
124      action->setEnabled(expectDisabled);
125      table->customContextMenuRequested({});
126      QCOMPARE(action->isEnabled(), !expectDisabled);
127  
128      action->setEnabled(true);
129      QString text;
130      if (expectError.empty()) {
131          ConfirmSend(&text, cancel ? QMessageBox::Cancel : QMessageBox::Yes);
132      } else {
133          ConfirmMessage(&text, 0ms);
134      }
135      action->trigger();
136      QVERIFY(text.indexOf(QString::fromStdString(expectError)) != -1);
137  }
138  
139  void CompareBalance(WalletModel& walletModel, CAmount expected_balance, QLabel* balance_label_to_check)
140  {
141      BitcoinUnit unit = walletModel.getOptionsModel()->getDisplayUnit();
142      QString balanceComparison = BitcoinUnits::formatWithUnit(unit, expected_balance, false, BitcoinUnits::SeparatorStyle::ALWAYS);
143      QCOMPARE(balance_label_to_check->text().trimmed(), balanceComparison);
144  }
145  
146  // Verify the 'useAvailableBalance' functionality. With and without manually selected coins.
147  // Case 1: No coin control selected coins.
148  // 'useAvailableBalance' should fill the amount edit box with the total available balance
149  // Case 2: With coin control selected coins.
150  // 'useAvailableBalance' should fill the amount edit box with the sum of the selected coins values.
151  void VerifyUseAvailableBalance(SendCoinsDialog& sendCoinsDialog, const WalletModel& walletModel)
152  {
153      // Verify first entry amount and "useAvailableBalance" button
154      QVBoxLayout* entries = sendCoinsDialog.findChild<QVBoxLayout*>("entries");
155      QVERIFY(entries->count() == 1); // only one entry
156      SendCoinsEntry* send_entry = qobject_cast<SendCoinsEntry*>(entries->itemAt(0)->widget());
157      QVERIFY(send_entry->getValue().amount == 0);
158      // Now click "useAvailableBalance", check updated balance (the entire wallet balance should be set)
159      Q_EMIT send_entry->useAvailableBalance(send_entry);
160      QVERIFY(send_entry->getValue().amount == walletModel.getCachedBalance().balance);
161  
162      // Now manually select two coins and click on "useAvailableBalance". Then check updated balance
163      // (only the sum of the selected coins should be set).
164      int COINS_TO_SELECT = 2;
165      auto coins = walletModel.wallet().listCoins();
166      CAmount sum_selected_coins = 0;
167      int selected = 0;
168      QVERIFY(coins.size() == 1); // context check, coins received only on one destination
169      for (const auto& [outpoint, tx_out] : coins.begin()->second) {
170          sendCoinsDialog.getCoinControl()->Select(outpoint);
171          sum_selected_coins += tx_out.txout.nValue;
172          if (++selected == COINS_TO_SELECT) break;
173      }
174      QVERIFY(selected == COINS_TO_SELECT);
175  
176      // Now that we have 2 coins selected, "useAvailableBalance" should update the balance label only with
177      // the sum of them.
178      Q_EMIT send_entry->useAvailableBalance(send_entry);
179      QVERIFY(send_entry->getValue().amount == sum_selected_coins);
180  }
181  
182  void SyncUpWallet(const std::shared_ptr<CWallet>& wallet, interfaces::Node& node)
183  {
184      WalletRescanReserver reserver(*wallet);
185      reserver.reserve();
186      CWallet::ScanResult result = wallet->ScanForWalletTransactions(Params().GetConsensus().hashGenesisBlock, /*start_height=*/0, /*max_height=*/{}, reserver, /*fUpdate=*/true, /*save_progress=*/false);
187      QCOMPARE(result.status, CWallet::ScanResult::SUCCESS);
188      QCOMPARE(result.last_scanned_block, WITH_LOCK(node.context()->chainman->GetMutex(), return node.context()->chainman->ActiveChain().Tip()->GetBlockHash()));
189      QVERIFY(result.last_failed_block.IsNull());
190  }
191  
192  std::shared_ptr<CWallet> SetupDescriptorsWallet(interfaces::Node& node, TestChain100Setup& test, bool watch_only = false)
193  {
194      std::shared_ptr<CWallet> wallet = std::make_shared<CWallet>(node.context()->chain.get(), "", CreateMockableWalletDatabase());
195      LOCK(wallet->cs_wallet);
196      wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
197      if (watch_only) {
198          wallet->SetWalletFlag(WALLET_FLAG_DISABLE_PRIVATE_KEYS);
199      } else {
200          wallet->SetupDescriptorScriptPubKeyMans();
201      }
202  
203      // Add the coinbase key
204      FlatSigningProvider provider;
205      std::string error;
206      std::string key_str;
207      if (watch_only) {
208          key_str = HexStr(test.coinbaseKey.GetPubKey());
209      } else {
210          key_str = EncodeSecret(test.coinbaseKey);
211      }
212      auto descs = Parse("combo(" + key_str + ")", provider, error, /* require_checksum=*/ false);
213      assert(!descs.empty());
214      assert(descs.size() == 1);
215      auto& desc = descs.at(0);
216      WalletDescriptor w_desc(std::move(desc), 0, 0, 1, 1);
217      Assert(wallet->AddWalletDescriptor(w_desc, provider, "", false));
218      const PKHash dest{test.coinbaseKey.GetPubKey()};
219      wallet->SetAddressBook(dest, "", wallet::AddressPurpose::RECEIVE);
220      wallet->SetLastBlockProcessed(105, WITH_LOCK(node.context()->chainman->GetMutex(), return node.context()->chainman->ActiveChain().Tip()->GetBlockHash()));
221      SyncUpWallet(wallet, node);
222      wallet->SetBroadcastTransactions(true);
223      return wallet;
224  }
225  
226  struct MiniGUI {
227  public:
228      SendCoinsDialog sendCoinsDialog;
229      TransactionView transactionView;
230      OptionsModel optionsModel;
231      std::unique_ptr<ClientModel> clientModel;
232      std::unique_ptr<WalletModel> walletModel;
233  
234      MiniGUI(interfaces::Node& node, const PlatformStyle* platformStyle) : sendCoinsDialog(platformStyle), transactionView(platformStyle), optionsModel(node) {
235          bilingual_str error;
236          QVERIFY(optionsModel.Init(error));
237          clientModel = std::make_unique<ClientModel>(node, &optionsModel);
238      }
239  
240      void initModelForWallet(interfaces::Node& node, const std::shared_ptr<CWallet>& wallet, const PlatformStyle* platformStyle)
241      {
242          WalletContext& context = *node.walletLoader().context();
243          AddWallet(context, wallet);
244          walletModel = std::make_unique<WalletModel>(interfaces::MakeWallet(context, wallet), *clientModel, platformStyle);
245          RemoveWallet(context, wallet, /* load_on_start= */ std::nullopt);
246          sendCoinsDialog.setModel(walletModel.get());
247          transactionView.setModel(walletModel.get());
248      }
249  
250  };
251  
252  //! Simple qt wallet tests.
253  //
254  // Test widgets can be debugged interactively calling show() on them and
255  // manually running the event loop, e.g.:
256  //
257  //     sendCoinsDialog.show();
258  //     QEventLoop().exec();
259  //
260  // This also requires overriding the default minimal Qt platform:
261  //
262  //     QT_QPA_PLATFORM=xcb     build/bin/test_bitcoin-qt  # Linux
263  //     QT_QPA_PLATFORM=windows build/bin/test_bitcoin-qt  # Windows
264  //     QT_QPA_PLATFORM=cocoa   build/bin/test_bitcoin-qt  # macOS
265  void TestGUI(interfaces::Node& node, const std::shared_ptr<CWallet>& wallet)
266  {
267      // Create widgets for sending coins and listing transactions.
268      std::unique_ptr<const PlatformStyle> platformStyle(PlatformStyle::instantiate("other"));
269      MiniGUI mini_gui(node, platformStyle.get());
270      mini_gui.initModelForWallet(node, wallet, platformStyle.get());
271      WalletModel& walletModel = *mini_gui.walletModel;
272      SendCoinsDialog& sendCoinsDialog = mini_gui.sendCoinsDialog;
273      TransactionView& transactionView = mini_gui.transactionView;
274  
275      // Update walletModel cached balance which will trigger an update for the 'labelBalance' QLabel.
276      walletModel.pollBalanceChanged();
277      // Check balance in send dialog
278      CompareBalance(walletModel, walletModel.wallet().getBalance(), sendCoinsDialog.findChild<QLabel*>("labelBalance"));
279  
280      // Check 'UseAvailableBalance' functionality
281      VerifyUseAvailableBalance(sendCoinsDialog, walletModel);
282  
283      // Send two transactions, and verify they are added to transaction list.
284      TransactionTableModel* transactionTableModel = walletModel.getTransactionTableModel();
285      QCOMPARE(transactionTableModel->rowCount({}), 105);
286      Txid txid1 = SendCoins(*wallet.get(), sendCoinsDialog, PKHash(), 5 * COIN, /*rbf=*/false);
287      Txid txid2 = SendCoins(*wallet.get(), sendCoinsDialog, PKHash(), 10 * COIN, /*rbf=*/true);
288      // Transaction table model updates on a QueuedConnection, so process events to ensure it's updated.
289      qApp->processEvents();
290      QCOMPARE(transactionTableModel->rowCount({}), 107);
291      QVERIFY(FindTx(*transactionTableModel, txid1).isValid());
292      QVERIFY(FindTx(*transactionTableModel, txid2).isValid());
293  
294      // Call bumpfee. Test canceled fullrbf bump, canceled bip-125-rbf bump, passing bump, and then failing bump.
295      BumpFee(transactionView, txid1, /*expectDisabled=*/false, /*expectError=*/{}, /*cancel=*/true);
296      BumpFee(transactionView, txid2, /*expectDisabled=*/false, /*expectError=*/{}, /*cancel=*/true);
297      BumpFee(transactionView, txid2, /*expectDisabled=*/false, /*expectError=*/{}, /*cancel=*/false);
298      BumpFee(transactionView, txid2, /*expectDisabled=*/true, /*expectError=*/"already bumped", /*cancel=*/false);
299  
300      // Check current balance on OverviewPage
301      OverviewPage overviewPage(platformStyle.get());
302      overviewPage.setWalletModel(&walletModel);
303      walletModel.pollBalanceChanged(); // Manual balance polling update
304      CompareBalance(walletModel, walletModel.wallet().getBalance(), overviewPage.findChild<QLabel*>("labelBalance"));
305  
306      // Check Request Payment button
307      ReceiveCoinsDialog receiveCoinsDialog(platformStyle.get());
308      receiveCoinsDialog.setModel(&walletModel);
309      RecentRequestsTableModel* requestTableModel = walletModel.getRecentRequestsTableModel();
310  
311      // Label input
312      QLineEdit* labelInput = receiveCoinsDialog.findChild<QLineEdit*>("reqLabel");
313      labelInput->setText("TEST_LABEL_1");
314  
315      // Amount input
316      BitcoinAmountField* amountInput = receiveCoinsDialog.findChild<BitcoinAmountField*>("reqAmount");
317      amountInput->setValue(1);
318  
319      // Message input
320      QLineEdit* messageInput = receiveCoinsDialog.findChild<QLineEdit*>("reqMessage");
321      messageInput->setText("TEST_MESSAGE_1");
322      int initialRowCount = requestTableModel->rowCount({});
323      QPushButton* requestPaymentButton = receiveCoinsDialog.findChild<QPushButton*>("receiveButton");
324      requestPaymentButton->click();
325      QString address;
326      for (QWidget* widget : QApplication::topLevelWidgets()) {
327          if (widget->inherits("ReceiveRequestDialog")) {
328              ReceiveRequestDialog* receiveRequestDialog = qobject_cast<ReceiveRequestDialog*>(widget);
329              QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("payment_header")->text(), QString("Payment information"));
330              QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("uri_tag")->text(), QString("URI:"));
331              QString uri = receiveRequestDialog->QObject::findChild<QLabel*>("uri_content")->text();
332              QCOMPARE(uri.count("bitcoin:"), 2);
333              QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("address_tag")->text(), QString("Address:"));
334              QVERIFY(address.isEmpty());
335              address = receiveRequestDialog->QObject::findChild<QLabel*>("address_content")->text();
336              QVERIFY(!address.isEmpty());
337  
338              QCOMPARE(uri.count("amount=0.00000001"), 2);
339              QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("amount_tag")->text(), QString("Amount:"));
340              QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("amount_content")->text(), QString::fromStdString("0.00000001 " + CURRENCY_UNIT));
341  
342              QCOMPARE(uri.count("label=TEST_LABEL_1"), 2);
343              QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("label_tag")->text(), QString("Label:"));
344              QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("label_content")->text(), QString("TEST_LABEL_1"));
345  
346              QCOMPARE(uri.count("message=TEST_MESSAGE_1"), 2);
347              QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("message_tag")->text(), QString("Message:"));
348              QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("message_content")->text(), QString("TEST_MESSAGE_1"));
349          }
350      }
351  
352      // Clear button
353      QPushButton* clearButton = receiveCoinsDialog.findChild<QPushButton*>("clearButton");
354      clearButton->click();
355      QCOMPARE(labelInput->text(), QString(""));
356      QCOMPARE(amountInput->value(), CAmount(0));
357      QCOMPARE(messageInput->text(), QString(""));
358  
359      // Check addition to history
360      int currentRowCount = requestTableModel->rowCount({});
361      QCOMPARE(currentRowCount, initialRowCount+1);
362  
363      // Check addition to wallet
364      std::vector<std::string> requests = walletModel.wallet().getAddressReceiveRequests();
365      QCOMPARE(requests.size(), size_t{1});
366      RecentRequestEntry entry;
367      SpanReader{MakeByteSpan(requests[0])} >> entry;
368      QCOMPARE(entry.nVersion, int{1});
369      QCOMPARE(entry.id, int64_t{1});
370      QVERIFY(entry.date.isValid());
371      QCOMPARE(entry.recipient.address, address);
372      QCOMPARE(entry.recipient.label, QString{"TEST_LABEL_1"});
373      QCOMPARE(entry.recipient.amount, CAmount{1});
374      QCOMPARE(entry.recipient.message, QString{"TEST_MESSAGE_1"});
375      QCOMPARE(entry.recipient.sPaymentRequest, std::string{});
376      QCOMPARE(entry.recipient.authenticatedMerchant, QString{});
377  
378      // Check Remove button
379      QTableView* table = receiveCoinsDialog.findChild<QTableView*>("recentRequestsView");
380      table->selectRow(currentRowCount-1);
381      QPushButton* removeRequestButton = receiveCoinsDialog.findChild<QPushButton*>("removeRequestButton");
382      removeRequestButton->click();
383      QCOMPARE(requestTableModel->rowCount({}), currentRowCount-1);
384  
385      // Check removal from wallet
386      QCOMPARE(walletModel.wallet().getAddressReceiveRequests().size(), size_t{0});
387  }
388  
389  void TestGUIWatchOnly(interfaces::Node& node, TestChain100Setup& test)
390  {
391      const std::shared_ptr<CWallet>& wallet = SetupDescriptorsWallet(node, test, /*watch_only=*/true);
392  
393      // Create widgets and init models
394      std::unique_ptr<const PlatformStyle> platformStyle(PlatformStyle::instantiate("other"));
395      MiniGUI mini_gui(node, platformStyle.get());
396      mini_gui.initModelForWallet(node, wallet, platformStyle.get());
397      WalletModel& walletModel = *mini_gui.walletModel;
398      SendCoinsDialog& sendCoinsDialog = mini_gui.sendCoinsDialog;
399  
400      // Update walletModel cached balance which will trigger an update for the 'labelBalance' QLabel.
401      walletModel.pollBalanceChanged();
402      // Check balance in send dialog
403      CompareBalance(walletModel, walletModel.wallet().getBalances().balance,
404                     sendCoinsDialog.findChild<QLabel*>("labelBalance"));
405  
406      // Set change address
407      sendCoinsDialog.getCoinControl()->destChange = PKHash{test.coinbaseKey.GetPubKey()};
408  
409      // Time to reject "save" PSBT dialog ('SendCoins' locks the main thread until the dialog receives the event).
410      QTimer timer;
411      timer.setInterval(500);
412      QObject::connect(&timer, &QTimer::timeout, [&](){
413          for (QWidget* widget : QApplication::topLevelWidgets()) {
414              if (widget->inherits("QMessageBox") && widget->objectName().compare("psbt_copied_message") == 0) {
415                  QMessageBox* dialog = qobject_cast<QMessageBox*>(widget);
416                  QAbstractButton* button = dialog->button(QMessageBox::Discard);
417                  button->setEnabled(true);
418                  button->click();
419                  timer.stop();
420                  break;
421              }
422          }
423      });
424      timer.start(500);
425  
426      // Send tx and verify PSBT copied to the clipboard.
427      SendCoins(*wallet.get(), sendCoinsDialog, PKHash(), 5 * COIN, /*rbf=*/false, QMessageBox::Save);
428      const std::string& psbt_string = QApplication::clipboard()->text().toStdString();
429      QVERIFY(!psbt_string.empty());
430  
431      // Decode psbt
432      std::optional<std::vector<unsigned char>> decoded_psbt = DecodeBase64(psbt_string);
433      QVERIFY(decoded_psbt);
434      PartiallySignedTransaction psbt;
435      std::string err;
436      QVERIFY(DecodeRawPSBT(psbt, MakeByteSpan(*decoded_psbt), err));
437  }
438  
439  void TestGUI(interfaces::Node& node)
440  {
441      // Set up wallet and chain with 105 blocks (5 mature blocks for spending).
442      TestChain100Setup test;
443      for (int i = 0; i < 5; ++i) {
444          test.CreateAndProcessBlock({}, GetScriptForRawPubKey(test.coinbaseKey.GetPubKey()));
445      }
446      auto wallet_loader = interfaces::MakeWalletLoader(*test.m_node.chain, *Assert(test.m_node.args));
447      test.m_node.wallet_loader = wallet_loader.get();
448      node.setContext(&test.m_node);
449  
450      // "Full" GUI tests, use descriptor wallet
451      const std::shared_ptr<CWallet>& desc_wallet = SetupDescriptorsWallet(node, test);
452      TestGUI(node, desc_wallet);
453  
454      // Legacy watch-only wallet test
455      // Verify PSBT creation.
456      TestGUIWatchOnly(node, test);
457  }
458  
459  } // namespace
460  
461  void WalletTests::walletTests()
462  {
463  #ifdef Q_OS_MACOS
464      if (QApplication::platformName() == "minimal") {
465          // Disable for mac on "minimal" platform to avoid crashes inside the Qt
466          // framework when it tries to look up unimplemented cocoa functions,
467          // and fails to handle returned nulls
468          // (https://bugreports.qt.io/browse/QTBUG-49686).
469          qWarning() << "Skipping WalletTests on mac build with 'minimal' platform set due to Qt bugs. To run AppTests, invoke "
470                        "with 'QT_QPA_PLATFORM=cocoa test_bitcoin-qt' on mac, or else use a linux or windows build.";
471          return;
472      }
473  #endif
474      TestGUI(m_node);
475  }