From 5907a5c7dc228697bcc8646b40dfa2e79e2cace9 Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Mon, 27 Jul 2026 13:27:26 -0700 Subject: [PATCH 1/3] gui: Add ExceptionSafeConnect that takes a lambda There is a variant of QObject::connect which takes 3 arguments. Add a variant of ExceptioNSafeConnect that does the same thing. --- src/qt/guiutil.cpp | 6 +++++- src/qt/guiutil.h | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/qt/guiutil.cpp b/src/qt/guiutil.cpp index e0966b9c38e..7fe022b0e3d 100644 --- a/src/qt/guiutil.cpp +++ b/src/qt/guiutil.cpp @@ -970,7 +970,11 @@ void PrintSlotException( { std::string description = sender->metaObject()->className(); description += "->"; - description += receiver->metaObject()->className(); + if (receiver) { + description += receiver->metaObject()->className(); + } else { + description += "anonymous function"; + } PrintExceptionContinue(exception, description); } diff --git a/src/qt/guiutil.h b/src/qt/guiutil.h index 57d042f6c82..2b1522ee8be 100644 --- a/src/qt/guiutil.h +++ b/src/qt/guiutil.h @@ -399,6 +399,38 @@ namespace GUIUtil }, type); } + template + auto ExceptionSafeConnect( + Sender sender, Signal signal, Slot method) + { + return QObject::connect( + sender, signal, + [sender, method](auto&&... args) { + bool ok{true}; + try { + method(std::forward(args)...); + } catch (const NonFatalCheckError& e) { + PrintSlotException(&e, sender, nullptr); + ok = QMetaObject::invokeMethod( + qApp, "handleNonFatalException", + blockingGUIThreadConnection(), + Q_ARG(QString, QString::fromStdString(e.what()))); + } catch (const std::exception& e) { + PrintSlotException(&e, sender, nullptr); + ok = QMetaObject::invokeMethod( + qApp, "handleRunawayException", + blockingGUIThreadConnection(), + Q_ARG(QString, QString::fromStdString(e.what()))); + } catch (...) { + PrintSlotException(nullptr, sender, nullptr); + ok = QMetaObject::invokeMethod( + qApp, "handleRunawayException", + blockingGUIThreadConnection(), + Q_ARG(QString, "Unknown failure occurred.")); + } + assert(ok); + }); + } /** * Shows a QDialog instance asynchronously, and deletes it on close. From cb51f97f6c59a30c8f79eb066195e9f2e51afd86 Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Tue, 13 May 2025 16:26:18 -0700 Subject: [PATCH 2/3] gui: Menu action for exporting a watchonly wallet --- src/interfaces/wallet.h | 3 +++ src/qt/bitcoingui.cpp | 19 +++++++++++++++++++ src/qt/bitcoingui.h | 1 + src/wallet/interfaces.cpp | 7 +++++++ 4 files changed, 30 insertions(+) diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h index 326361aa802..f080f0705bf 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -302,6 +302,9 @@ public: //! Return pointer to internal wallet class, useful for testing. virtual wallet::CWallet* wallet() { return nullptr; } + + //! Export a watchonly wallet file. See CWallet::ExportWatchOnlyWallet + virtual util::Result exportWatchOnlyWallet(const fs::path& destination) = 0; }; //! Wallet chain client that in addition to having chain client methods for diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index 5f169a951c4..98d9d21bfd2 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -377,6 +377,10 @@ void BitcoinGUI::createActions() m_mask_values_action->setStatusTip(tr("Mask the values in the Overview tab")); m_mask_values_action->setCheckable(true); + m_export_watchonly_action = new QAction(tr("Export watch-only wallet"), this); + m_export_watchonly_action->setEnabled(false); + m_export_watchonly_action->setStatusTip(tr("Export a watch-only version of the current wallet that can be restored onto another node.")); + connect(quitAction, &QAction::triggered, this, &BitcoinGUI::quitRequested); connect(aboutAction, &QAction::triggered, this, &BitcoinGUI::aboutClicked); connect(aboutQtAction, &QAction::triggered, qApp, QApplication::aboutQt); @@ -524,6 +528,18 @@ void BitcoinGUI::createActions() }); connect(m_mask_values_action, &QAction::toggled, this, &BitcoinGUI::setPrivacy); connect(m_mask_values_action, &QAction::toggled, this, &BitcoinGUI::enableHistoryAction); + GUIUtil::ExceptionSafeConnect(m_export_watchonly_action, &QAction::triggered, [this](bool) { + QString destination = GUIUtil::getSaveFileName(this, tr("Save Watch-only Wallet Export"), QString(), QString(), nullptr); + if (destination.isEmpty()) return; + WalletModel* model = walletFrame->currentWalletModel(); + if (!Assume(model)) return; + util::Result export_res = model->wallet().exportWatchOnlyWallet(GUIUtil::QStringToPath(destination)); + if (export_res) { + QMessageBox::information(nullptr, tr("Export Successful"), tr("The wallet has been exported to ") + QString::fromStdString(*export_res)); + } else { + QMessageBox::critical(nullptr, tr("Export Error"), QString::fromStdString(util::ErrorString(export_res).translated)); + } + }); } #endif // ENABLE_WALLET @@ -547,6 +563,7 @@ void BitcoinGUI::createMenuBar() file->addSeparator(); file->addAction(backupWalletAction); file->addAction(m_restore_wallet_action); + file->addAction(m_export_watchonly_action); file->addSeparator(); file->addAction(openAction); file->addAction(signMessageAction); @@ -832,6 +849,7 @@ void BitcoinGUI::setCurrentWallet(WalletModel* wallet_model) break; } } + m_export_watchonly_action->setEnabled(!wallet_model->wallet().privateKeysDisabled()); updateWindowTitle(); } @@ -866,6 +884,7 @@ void BitcoinGUI::setWalletActionsEnabled(bool enabled) openAction->setEnabled(enabled); m_close_wallet_action->setEnabled(enabled); m_close_all_wallets_action->setEnabled(enabled); + m_export_watchonly_action->setEnabled(enabled); } void BitcoinGUI::createTrayIcon() diff --git a/src/qt/bitcoingui.h b/src/qt/bitcoingui.h index babb8961622..538e1d19bb9 100644 --- a/src/qt/bitcoingui.h +++ b/src/qt/bitcoingui.h @@ -167,6 +167,7 @@ private: QAction* m_mask_values_action{nullptr}; QAction* m_migrate_wallet_action{nullptr}; QMenu* m_migrate_wallet_menu{nullptr}; + QAction* m_export_watchonly_action{nullptr}; #ifdef ENABLE_WALLET QLabel *m_wallet_selector_label = nullptr; QComboBox* m_wallet_selector = nullptr; diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index 9f9876da303..1c5f689e4e5 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -522,6 +523,12 @@ public: } CWallet* wallet() override { return m_wallet.get(); } + util::Result exportWatchOnlyWallet(const fs::path& destination) override { + LOCK(m_wallet->cs_wallet); + m_wallet->TopUpKeyPool(); + return ExportWatchOnlyWallet(*m_wallet, destination, m_context); + } + WalletContext& m_context; std::shared_ptr m_wallet; }; From 6573196e63bf4113d332c802f409e3336aebab41 Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Mon, 27 Jul 2026 12:42:27 -0700 Subject: [PATCH 3/3] doc: Release note for export watchonly wallet gui action --- doc/release-notes-gui-872.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 doc/release-notes-gui-872.md diff --git a/doc/release-notes-gui-872.md b/doc/release-notes-gui-872.md new file mode 100644 index 00000000000..5ea70345b73 --- /dev/null +++ b/doc/release-notes-gui-872.md @@ -0,0 +1,7 @@ +GUI +--- + +* A menu action has been added to allow creating a watchonly wallet file from + an existing descriptor wallet. This option mirrors the `exportwatchonlywallet` + RPC - the exported file can be imported to another node using the Restore + Wallet menu action.