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