wallettests.cpp raw

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