From 478fb483e90854344756fcca5e85f9b7b954cbf1 Mon Sep 17 00:00:00 2001 From: f321x Date: Wed, 13 Aug 2025 10:45:52 +0200 Subject: [PATCH] fix: psbt_nostr: don't allow to save tx without txid Stops the psbt nostr plugin from trying to save transactions without txid to the wallet history and doesn't give the user the option to do so. --- electrum/plugins/psbt_nostr/psbt_nostr.py | 1 + electrum/plugins/psbt_nostr/qml.py | 9 ++++--- .../psbt_nostr/qml/PsbtReceiveDialog.qml | 2 ++ electrum/plugins/psbt_nostr/qml/main.qml | 5 ++-- electrum/plugins/psbt_nostr/qt.py | 25 ++++++++++++------- 5 files changed, 27 insertions(+), 15 deletions(-) diff --git a/electrum/plugins/psbt_nostr/psbt_nostr.py b/electrum/plugins/psbt_nostr/psbt_nostr.py index b084c0e41..de085bf8f 100644 --- a/electrum/plugins/psbt_nostr/psbt_nostr.py +++ b/electrum/plugins/psbt_nostr/psbt_nostr.py @@ -278,6 +278,7 @@ class CosignerWallet(Logger): on_failure: Callable = None, on_success: Callable = None ) -> None: + assert tx.txid(), "Shouldn't allow to save tx without txid" try: # TODO: adding tx should be handled more gracefully here: # 1) don't replace tx with same tx with less signatures diff --git a/electrum/plugins/psbt_nostr/qml.py b/electrum/plugins/psbt_nostr/qml.py index 7275fc382..3ad7ec158 100644 --- a/electrum/plugins/psbt_nostr/qml.py +++ b/electrum/plugins/psbt_nostr/qml.py @@ -48,7 +48,7 @@ class QReceiveSignalObject(QObject): QObject.__init__(self) self._plugin = plugin - cosignerReceivedPsbt = pyqtSignal(str, str, str, str) + cosignerReceivedPsbt = pyqtSignal(str, str, str, str, bool) sendPsbtFailed = pyqtSignal(str, arguments=['reason']) sendPsbtSuccess = pyqtSignal() @@ -138,7 +138,8 @@ class QmlCosignerWallet(EventListener, CosignerWallet): def on_event_psbt_nostr_received(self, wallet, pubkey, event_id, tx: 'PartialTransaction', label: str): if self.wallet == wallet: self.tx = tx - self.plugin.so.cosignerReceivedPsbt.emit(pubkey, event_id, tx.serialize(), label) + can_be_saved = tx.txid() is not None + self.plugin.so.cosignerReceivedPsbt.emit(pubkey, event_id, tx.serialize(), label, can_be_saved) def close(self): super().close() @@ -173,5 +174,5 @@ class QmlCosignerWallet(EventListener, CosignerWallet): def reject_psbt(self, event_id): self.mark_pending_event_rcvd(event_id) - def on_add_fail(self): - self.logger.error('failed to add tx to wallet') + def on_add_fail(self, error_msg: str): + self.logger.error(f'failed to add tx to wallet: {error_msg}') diff --git a/electrum/plugins/psbt_nostr/qml/PsbtReceiveDialog.qml b/electrum/plugins/psbt_nostr/qml/PsbtReceiveDialog.qml index 19d1352e4..286324ff8 100644 --- a/electrum/plugins/psbt_nostr/qml/PsbtReceiveDialog.qml +++ b/electrum/plugins/psbt_nostr/qml/PsbtReceiveDialog.qml @@ -17,6 +17,7 @@ ElDialog { } property string tx_label + property bool can_be_saved property int choice: PsbtReceiveDialog.Choice.None // TODO: it might be better to defer popup until no dialogs are shown @@ -81,6 +82,7 @@ ElDialog { Layout.preferredWidth: 1 text: qsTr('Save to Wallet') icon.source: Qt.resolvedUrl('../../../gui/icons/wallet.png') + visible: dialog.can_be_saved onClicked: { choice = PsbtReceiveDialog.Choice.Save doAccept() diff --git a/electrum/plugins/psbt_nostr/qml/main.qml b/electrum/plugins/psbt_nostr/qml/main.qml index 2c2d5f9d6..3990738f3 100644 --- a/electrum/plugins/psbt_nostr/qml/main.qml +++ b/electrum/plugins/psbt_nostr/qml/main.qml @@ -7,9 +7,10 @@ import "../../../gui/qml/components/controls" Item { Connections { target: AppController ? AppController.plugin('psbt_nostr') : null - function onCosignerReceivedPsbt(pubkey, event, tx, label) { + function onCosignerReceivedPsbt(pubkey, event, tx, label, can_be_saved) { var dialog = psbtReceiveDialog.createObject(app, { - tx_label: label + tx_label: label, + can_be_saved: can_be_saved }) dialog.accepted.connect(function () { if (dialog.choice == PsbtReceiveDialog.Choice.Open) { diff --git a/electrum/plugins/psbt_nostr/qt.py b/electrum/plugins/psbt_nostr/qt.py index dc9f86f71..eee328570 100644 --- a/electrum/plugins/psbt_nostr/qt.py +++ b/electrum/plugins/psbt_nostr/qt.py @@ -102,7 +102,8 @@ class QtCosignerWallet(EventListener, CosignerWallet): self.obj.cosignerReceivedPsbt.emit(*args) # put on UI thread via signal def send_to_cosigners(self, tx: Union['Transaction', 'PartialTransaction'], label: str): - self.add_transaction_to_wallet(tx, label=label, on_failure=self.on_add_fail) + if tx.txid(): + self.add_transaction_to_wallet(tx, label=label, on_failure=self.on_add_fail) self.send_psbt(tx, label) def do_send(self, messages: List[Tuple[str, dict]], txid: Optional[str] = None): @@ -120,8 +121,10 @@ class QtCosignerWallet(EventListener, CosignerWallet): except Exception as e: self.window.show_error(str(e)) return - self.window.show_message( - _("Your transaction was sent to your cosigners via Nostr.") + '\n\n' + txid) + message = _("Your transaction was sent to your cosigners via Nostr.") + if txid: + message += '\n\n' + txid + self.window.show_message(message) def on_receive(self, pubkey, event_id, tx, label): msg = '
'.join([ @@ -129,13 +132,17 @@ class QtCosignerWallet(EventListener, CosignerWallet): _("A transaction was received from your cosigner with label:
{}
").format(label), _("Do you want to open it now?") ]) - result = self.window.show_message(msg, rich_text=True, icon=QMessageBox.Icon.Question, buttons=[ - QMessageBox.StandardButton.Open, - (QPushButton('Discard'), QMessageBox.ButtonRole.DestructiveRole, 100), - (QPushButton('Save to wallet'), QMessageBox.ButtonRole.AcceptRole, 101)] - ) + buttons = [ + QMessageBox.StandardButton.Open, + (QPushButton('Discard'), QMessageBox.ButtonRole.DestructiveRole, 100), + ] + if tx.txid(): # cannot add tx without txid to wallet history (e.g. unsigned legacy tx) + buttons.append( + (QPushButton('Save to wallet'), QMessageBox.ButtonRole.AcceptRole, 101) # type: ignore + ) + result = self.window.show_message(msg, rich_text=True, icon=QMessageBox.Icon.Question, buttons=buttons) if result == QMessageBox.StandardButton.Open: - if label: + if label and tx.txid(): self.wallet.set_label(tx.txid(), label) show_transaction(tx, parent=self.window, prompt_if_unsaved=True, on_closed=partial(self.on_tx_dialog_closed, event_id)) else: