3535#include < QAction>
3636#include < QApplication>
3737#include < QCheckBox>
38+ #include < QClipboard>
39+ #include < QObject>
3840#include < QPushButton>
3941#include < QTimer>
4042#include < QVBoxLayout>
@@ -47,21 +49,22 @@ using wallet::CWallet;
4749using wallet::CreateMockWalletDatabase;
4850using wallet::RemoveWallet;
4951using wallet::WALLET_FLAG_DESCRIPTORS;
52+ using wallet::WALLET_FLAG_DISABLE_PRIVATE_KEYS;
5053using wallet::WalletContext;
5154using wallet::WalletDescriptor;
5255using wallet::WalletRescanReserver;
5356
5457namespace
5558{
5659// ! Press "Yes" or "Cancel" buttons in modal send confirmation dialog.
57- void ConfirmSend (QString* text = nullptr , bool cancel = false )
60+ void ConfirmSend (QString* text = nullptr , QMessageBox::StandardButton confirm_type = QMessageBox::Yes )
5861{
59- QTimer::singleShot (0 , [text, cancel ]() {
62+ QTimer::singleShot (0 , [text, confirm_type ]() {
6063 for (QWidget* widget : QApplication::topLevelWidgets ()) {
6164 if (widget->inherits (" SendConfirmationDialog" )) {
6265 SendConfirmationDialog* dialog = qobject_cast<SendConfirmationDialog*>(widget);
6366 if (text) *text = dialog->text ();
64- QAbstractButton* button = dialog->button (cancel ? QMessageBox::Cancel : QMessageBox::Yes );
67+ QAbstractButton* button = dialog->button (confirm_type );
6568 button->setEnabled (true );
6669 button->click ();
6770 }
@@ -70,7 +73,8 @@ void ConfirmSend(QString* text = nullptr, bool cancel = false)
7073}
7174
7275// ! Send coins to address and return txid.
73- uint256 SendCoins (CWallet& wallet, SendCoinsDialog& sendCoinsDialog, const CTxDestination& address, CAmount amount, bool rbf)
76+ uint256 SendCoins (CWallet& wallet, SendCoinsDialog& sendCoinsDialog, const CTxDestination& address, CAmount amount, bool rbf,
77+ QMessageBox::StandardButton confirm_type = QMessageBox::Yes)
7478{
7579 QVBoxLayout* entries = sendCoinsDialog.findChild <QVBoxLayout*>(" entries" );
7680 SendCoinsEntry* entry = qobject_cast<SendCoinsEntry*>(entries->itemAt (0 )->widget ());
@@ -84,7 +88,7 @@ uint256 SendCoins(CWallet& wallet, SendCoinsDialog& sendCoinsDialog, const CTxDe
8488 boost::signals2::scoped_connection c (wallet.NotifyTransactionChanged .connect ([&txid](const uint256& hash, ChangeType status) {
8589 if (status == CT_NEW) txid = hash;
8690 }));
87- ConfirmSend ();
91+ ConfirmSend (/* text= */ nullptr , confirm_type );
8892 bool invoked = QMetaObject::invokeMethod (&sendCoinsDialog, " sendButtonClicked" , Q_ARG (bool , false ));
8993 assert (invoked);
9094 return txid;
@@ -122,7 +126,7 @@ void BumpFee(TransactionView& view, const uint256& txid, bool expectDisabled, st
122126 action->setEnabled (true );
123127 QString text;
124128 if (expectError.empty ()) {
125- ConfirmSend (&text, cancel);
129+ ConfirmSend (&text, cancel ? QMessageBox::Cancel : QMessageBox::Yes );
126130 } else {
127131 ConfirmMessage (&text, 0ms);
128132 }
@@ -183,6 +187,24 @@ void SyncUpWallet(const std::shared_ptr<CWallet>& wallet, interfaces::Node& node
183187 QVERIFY (result.last_failed_block .IsNull ());
184188}
185189
190+ std::shared_ptr<CWallet> SetupLegacyWatchOnlyWallet (interfaces::Node& node, TestChain100Setup& test)
191+ {
192+ std::shared_ptr<CWallet> wallet = std::make_shared<CWallet>(node.context ()->chain .get (), " " , CreateMockWalletDatabase ());
193+ wallet->LoadWallet ();
194+ {
195+ LOCK (wallet->cs_wallet );
196+ wallet->SetWalletFlag (WALLET_FLAG_DISABLE_PRIVATE_KEYS);
197+ wallet->SetupLegacyScriptPubKeyMan ();
198+ // Add watched key
199+ CPubKey pubKey = test.coinbaseKey .GetPubKey ();
200+ bool import_keys = wallet->ImportPubKeys ({pubKey.GetID ()}, {{pubKey.GetID (), pubKey}} , /* key_origins=*/ {}, /* add_keypool=*/ false , /* internal=*/ false , /* timestamp=*/ 1 );
201+ assert (import_keys);
202+ wallet->SetLastBlockProcessed (105 , WITH_LOCK (node.context ()->chainman ->GetMutex (), return node.context ()->chainman ->ActiveChain ().Tip ()->GetBlockHash ()));
203+ }
204+ SyncUpWallet (wallet, node);
205+ return wallet;
206+ }
207+
186208std::shared_ptr<CWallet> SetupDescriptorsWallet (interfaces::Node& node, TestChain100Setup& test)
187209{
188210 std::shared_ptr<CWallet> wallet = std::make_shared<CWallet>(node.context ()->chain .get (), " " , CreateMockWalletDatabase ());
@@ -369,6 +391,56 @@ void TestGUI(interfaces::Node& node, const std::shared_ptr<CWallet>& wallet)
369391 QCOMPARE (walletModel.wallet ().getAddressReceiveRequests ().size (), size_t {0 });
370392}
371393
394+ void TestGUIWatchOnly (interfaces::Node& node, TestChain100Setup& test)
395+ {
396+ const std::shared_ptr<CWallet>& wallet = SetupLegacyWatchOnlyWallet (node, test);
397+
398+ // Create widgets and init models
399+ std::unique_ptr<const PlatformStyle> platformStyle (PlatformStyle::instantiate (" other" ));
400+ MiniGUI mini_gui (node, platformStyle.get ());
401+ mini_gui.initModelForWallet (node, wallet, platformStyle.get ());
402+ WalletModel& walletModel = *mini_gui.walletModel ;
403+ SendCoinsDialog& sendCoinsDialog = mini_gui.sendCoinsDialog ;
404+
405+ // Update walletModel cached balance which will trigger an update for the 'labelBalance' QLabel.
406+ walletModel.pollBalanceChanged ();
407+ // Check balance in send dialog
408+ CompareBalance (walletModel, walletModel.wallet ().getBalances ().watch_only_balance ,
409+ sendCoinsDialog.findChild <QLabel*>(" labelBalance" ));
410+
411+ // Set change address
412+ sendCoinsDialog.getCoinControl ()->destChange = GetDestinationForKey (test.coinbaseKey .GetPubKey (), OutputType::LEGACY);
413+
414+ // Time to reject "save" PSBT dialog ('SendCoins' locks the main thread until the dialog receives the event).
415+ QTimer timer;
416+ timer.setInterval (500 );
417+ QObject::connect (&timer, &QTimer::timeout, [&](){
418+ for (QWidget* widget : QApplication::topLevelWidgets ()) {
419+ if (widget->inherits (" QMessageBox" )) {
420+ QMessageBox* dialog = qobject_cast<QMessageBox*>(widget);
421+ QAbstractButton* button = dialog->button (QMessageBox::Discard);
422+ button->setEnabled (true );
423+ button->click ();
424+ timer.stop ();
425+ break ;
426+ }
427+ }
428+ });
429+ timer.start (500 );
430+
431+ // Send tx and verify PSBT copied to the clipboard.
432+ SendCoins (*wallet.get (), sendCoinsDialog, PKHash (), 5 * COIN, /* rbf=*/ false , QMessageBox::Save);
433+ const std::string& psbt_string = QApplication::clipboard ()->text ().toStdString ();
434+ QVERIFY (!psbt_string.empty ());
435+
436+ // Decode psbt
437+ std::optional<std::vector<unsigned char >> decoded_psbt = DecodeBase64 (psbt_string);
438+ QVERIFY (decoded_psbt);
439+ PartiallySignedTransaction psbt;
440+ std::string err;
441+ QVERIFY (DecodeRawPSBT (psbt, MakeByteSpan (*decoded_psbt), err));
442+ }
443+
372444void TestGUI (interfaces::Node& node)
373445{
374446 // Set up wallet and chain with 105 blocks (5 mature blocks for spending).
@@ -383,6 +455,10 @@ void TestGUI(interfaces::Node& node)
383455 // "Full" GUI tests, use descriptor wallet
384456 const std::shared_ptr<CWallet>& desc_wallet = SetupDescriptorsWallet (node, test);
385457 TestGUI (node, desc_wallet);
458+
459+ // Legacy watch-only wallet test
460+ // Verify PSBT creation.
461+ TestGUIWatchOnly (node, test);
386462}
387463
388464} // namespace
0 commit comments