From ac38b4a5949e61a4f0dd26cc3831921c6c80d037 Mon Sep 17 00:00:00 2001 From: Oren Date: Mon, 26 May 2025 18:02:58 +0300 Subject: [PATCH 01/10] don't use config FEE_POLICY Long Term recovery transactions should have a high fee policy, because we don't know when we will broadcast them. On the other hand, they won't need to be urgent when broadcasted either. --- electrum/plugins/timelock_recovery/qt.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/electrum/plugins/timelock_recovery/qt.py b/electrum/plugins/timelock_recovery/qt.py index 7a7dc032c..7e646c2c7 100644 --- a/electrum/plugins/timelock_recovery/qt.py +++ b/electrum/plugins/timelock_recovery/qt.py @@ -176,8 +176,7 @@ class Plugin(TimelockRecoveryPlugin): plan_dialog = WindowModalDialog(context.main_window, "Timelock Recovery") plan_dialog.setContentsMargins(11, 11, 1, 1) plan_dialog.resize(800, plan_dialog.height()) - - fee_policy = FeePolicy(context.main_window.config.FEE_POLICY) + fee_policy = FeePolicy('eta:1') create_cancel_cb = QCheckBox('', checked=False) alert_tx_label = QLabel('') recovery_tx_label = QLabel('') From d1a15ae8f6b6d30f74b091f29c2132cb72dcc5a4 Mon Sep 17 00:00:00 2001 From: Oren Date: Sat, 24 May 2025 01:54:51 +0300 Subject: [PATCH 02/10] throw exception if signing is not complete There could be flows where sign_transaction will return without actually signing the transaction. We also want to add the ability to sign the transactions externally, so here we check if they are already signed. --- electrum/plugins/timelock_recovery/qt.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/electrum/plugins/timelock_recovery/qt.py b/electrum/plugins/timelock_recovery/qt.py index 7e646c2c7..5c1e2976e 100644 --- a/electrum/plugins/timelock_recovery/qt.py +++ b/electrum/plugins/timelock_recovery/qt.py @@ -414,11 +414,20 @@ class Plugin(TimelockRecoveryPlugin): password = main_window.get_password() def task(): - wallet.sign_transaction(context.alert_tx, password, ignore_warnings=True) + if not context.alert_tx.is_complete(): + wallet.sign_transaction(context.alert_tx, password, ignore_warnings=True) + if not context.alert_tx.is_complete(): + raise Exception(_("Alert transaction signing was not completed")) context.add_input_info() - wallet.sign_transaction(context.recovery_tx, password, ignore_warnings=True) + if not context.recovery_tx.is_complete(): + wallet.sign_transaction(context.recovery_tx, password, ignore_warnings=True) + if not context.recovery_tx.is_complete(): + raise Exception(_("Recovery transaction signing was not completed")) if context.cancellation_tx is not None: - wallet.sign_transaction(context.cancellation_tx, password, ignore_warnings=True) + if not context.cancellation_tx.is_complete(): + wallet.sign_transaction(context.cancellation_tx, password, ignore_warnings=True) + if not context.cancellation_tx.is_complete(): + raise Exception(_("Cancellation transaction signing was not completed")) def on_success(result): self.create_download_dialog(context) From 7eb29f9a6b936faf53d41f45556ef688cc1ecb56 Mon Sep 17 00:00:00 2001 From: Oren Date: Sat, 24 May 2025 02:13:39 +0300 Subject: [PATCH 03/10] watch-only wallets should sign externally The Next button should be clicked only after the transactions have been signed --- electrum/plugins/timelock_recovery/qt.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/electrum/plugins/timelock_recovery/qt.py b/electrum/plugins/timelock_recovery/qt.py index 5c1e2976e..30da03788 100644 --- a/electrum/plugins/timelock_recovery/qt.py +++ b/electrum/plugins/timelock_recovery/qt.py @@ -226,6 +226,7 @@ class Plugin(TimelockRecoveryPlugin): view_recovery_tx_button.setEnabled(False) view_cancellation_tx_button.setEnabled(False) next_button.setEnabled(False) + next_button.setToolTip("") return try: context.alert_tx = context.make_unsigned_alert_tx(fee_policy) @@ -247,13 +248,20 @@ class Plugin(TimelockRecoveryPlugin): payto_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True)) payto_e.setToolTip("Not enough funds to create the transactions.") next_button.setEnabled(False) + next_button.setToolTip("") return view_alert_tx_button.setEnabled(True) view_recovery_tx_button.setEnabled(True) view_cancellation_tx_button.setEnabled(True) payto_e.setStyleSheet(ColorScheme.GREEN.as_stylesheet(True)) payto_e.setToolTip("") + if context.main_window.wallet.is_watching_only(): + if not context.alert_tx.is_complete() or not context.recovery_tx.is_complete() or (context.cancellation_tx is not None and not context.cancellation_tx.is_complete()): + next_button.setEnabled(False) + next_button.setToolTip(_("This is a watching-only wallet. You must sign the transactions externally - use the View button of each transaction.")) + return next_button.setEnabled(True) + next_button.setToolTip("") payto_e.paymentIdentifierChanged.connect(update_transactions) From 78c04259315b4b5529909ae1ced8851731b1e62d Mon Sep 17 00:00:00 2001 From: Oren Date: Sat, 24 May 2025 02:30:00 +0300 Subject: [PATCH 04/10] return tx in on_closed callback --- electrum/gui/qt/main_window.py | 2 ++ electrum/gui/qt/transaction_dialog.py | 6 +++--- electrum/plugins/psbt_nostr/qt.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 15a4fa91a..d21d0d035 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1183,6 +1183,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): *, external_keypairs: Mapping[bytes, bytes] = None, invoice: Invoice = None, + on_closed: Callable[[Optional[Transaction]], None] = None, show_sign_button: bool = True, show_broadcast_button: bool = True, ): @@ -1191,6 +1192,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): parent=self, external_keypairs=external_keypairs, invoice=invoice, + on_closed=on_closed, show_sign_button=show_sign_button, show_broadcast_button=show_broadcast_button, ) diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index e3c4e914d..0ebaf8c98 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -427,7 +427,7 @@ def show_transaction( prompt_if_unsaved: bool = False, external_keypairs: Mapping[bytes, bytes] = None, invoice: 'Invoice' = None, - on_closed: Callable[[], None] = None, + on_closed: Callable[[Optional[Transaction]], None] = None, show_sign_button: bool = True, show_broadcast_button: bool = True, ): @@ -463,7 +463,7 @@ class TxDialog(QDialog, MessageBoxMixin): prompt_if_unsaved: bool, external_keypairs: Mapping[bytes, bytes] = None, invoice: 'Invoice' = None, - on_closed: Callable[[], None] = None, + on_closed: Callable[[Optional[Transaction]], None] = None, ): '''Transactions in the wallet will show their description. Pass desc to give a description for txs not yet in the wallet. @@ -640,7 +640,7 @@ class TxDialog(QDialog, MessageBoxMixin): self._fetch_txin_data_fut = None if self.on_closed: - self.on_closed() + self.on_closed(self.tx) def reject(self): # Override escape-key to close normally (and invoke closeEvent) diff --git a/electrum/plugins/psbt_nostr/qt.py b/electrum/plugins/psbt_nostr/qt.py index b86c13e64..dc9f86f71 100644 --- a/electrum/plugins/psbt_nostr/qt.py +++ b/electrum/plugins/psbt_nostr/qt.py @@ -145,7 +145,7 @@ class QtCosignerWallet(EventListener, CosignerWallet): self.add_transaction_to_wallet(tx, label=label, on_failure=self.on_add_fail) self.window.update_tabs() - def on_tx_dialog_closed(self, event_id): + def on_tx_dialog_closed(self, event_id, _tx: Optional['Transaction']): self.mark_pending_event_rcvd(event_id) def on_add_fail(self, msg: str): From fb535516d37569a9ce21f2c9a69d81d5c9285a16 Mon Sep 17 00:00:00 2001 From: Oren Date: Mon, 26 May 2025 19:31:46 +0300 Subject: [PATCH 05/10] Rename labels to fee labels --- electrum/plugins/timelock_recovery/qt.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/electrum/plugins/timelock_recovery/qt.py b/electrum/plugins/timelock_recovery/qt.py index 30da03788..0c28a7f5a 100644 --- a/electrum/plugins/timelock_recovery/qt.py +++ b/electrum/plugins/timelock_recovery/qt.py @@ -178,9 +178,9 @@ class Plugin(TimelockRecoveryPlugin): plan_dialog.resize(800, plan_dialog.height()) fee_policy = FeePolicy('eta:1') create_cancel_cb = QCheckBox('', checked=False) - alert_tx_label = QLabel('') - recovery_tx_label = QLabel('') - cancellation_tx_label = QLabel('') + alert_tx_fee_label = QLabel('') + recovery_tx_fee_label = QLabel('') + cancellation_tx_fee_label = QLabel('') if not context.get_alert_address(): plan_dialog.show_error(''.join([ @@ -231,14 +231,14 @@ class Plugin(TimelockRecoveryPlugin): try: context.alert_tx = context.make_unsigned_alert_tx(fee_policy) assert all(tx_input.is_segwit() for tx_input in context.alert_tx.inputs()) - alert_tx_label.setText(_("Fee: {}").format(self.config.format_amount_and_units(context.alert_tx.get_fee()))) + alert_tx_fee_label.setText(_("Fee: {}").format(self.config.format_amount_and_units(context.alert_tx.get_fee()))) context.recovery_tx = context.make_unsigned_recovery_tx(fee_policy) assert all(tx_input.is_segwit() for tx_input in context.recovery_tx.inputs()) - recovery_tx_label.setText(_("Fee: {}").format(self.config.format_amount_and_units(context.recovery_tx.get_fee()))) + recovery_tx_fee_label.setText(_("Fee: {}").format(self.config.format_amount_and_units(context.recovery_tx.get_fee()))) if create_cancel_cb.isChecked(): context.cancellation_tx = context.make_unsigned_cancellation_tx(fee_policy) assert all(tx_input.is_segwit() for tx_input in context.cancellation_tx.inputs()) - cancellation_tx_label.setText(_("Fee: {}").format(self.config.format_amount_and_units(context.cancellation_tx.get_fee()))) + cancellation_tx_fee_label.setText(_("Fee: {}").format(self.config.format_amount_and_units(context.cancellation_tx.get_fee()))) else: context.cancellation_tx = None except NotEnoughFunds: @@ -322,14 +322,14 @@ class Plugin(TimelockRecoveryPlugin): grid_row += 1 plan_grid.addWidget(QLabel('Alert transaction'), grid_row, 0) - plan_grid.addWidget(alert_tx_label, grid_row, 1, 1, 3) + plan_grid.addWidget(alert_tx_fee_label, grid_row, 1, 1, 3) view_alert_tx_button = QPushButton(_('View')) view_alert_tx_button.clicked.connect(lambda: context.main_window.show_transaction(context.alert_tx, show_sign_button=False, show_broadcast_button=False)) plan_grid.addWidget(view_alert_tx_button, grid_row, 4) grid_row += 1 plan_grid.addWidget(QLabel('Recovery transaction'), grid_row, 0) - plan_grid.addWidget(recovery_tx_label, grid_row, 1, 1, 3) + plan_grid.addWidget(recovery_tx_fee_label, grid_row, 1, 1, 3) view_recovery_tx_button = QPushButton(_('View')) view_recovery_tx_button.clicked.connect(lambda: context.main_window.show_transaction(context.recovery_tx, show_sign_button=False, show_broadcast_button=False)) plan_grid.addWidget(view_recovery_tx_button, grid_row, 4) @@ -337,7 +337,7 @@ class Plugin(TimelockRecoveryPlugin): cancellation_label = QLabel('Cancellation transaction') plan_grid.addWidget(cancellation_label, grid_row, 0) - plan_grid.addWidget(cancellation_tx_label, grid_row, 1, 1, 3) + plan_grid.addWidget(cancellation_tx_fee_label, grid_row, 1, 1, 3) view_cancellation_tx_button = QPushButton(_('View')) view_cancellation_tx_button.clicked.connect(lambda: context.main_window.show_transaction(context.cancellation_tx, show_sign_button=False, show_broadcast_button=False)) plan_grid.addWidget(view_cancellation_tx_button, grid_row, 4) @@ -349,7 +349,7 @@ class Plugin(TimelockRecoveryPlugin): def on_cb_change(x): cancellation_label.setVisible(x) - cancellation_tx_label.setVisible(x) + cancellation_tx_fee_label.setVisible(x) view_cancellation_tx_button.setVisible(x) update_transactions() create_cancel_cb.stateChanged.connect(on_cb_change) From 2e96886960a2e40e216fc10962ebe3593f23dac3 Mon Sep 17 00:00:00 2001 From: Oren Date: Mon, 26 May 2025 19:29:12 +0300 Subject: [PATCH 06/10] labels to show signed txes --- electrum/plugins/timelock_recovery/qt.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/electrum/plugins/timelock_recovery/qt.py b/electrum/plugins/timelock_recovery/qt.py index 0c28a7f5a..1037a6630 100644 --- a/electrum/plugins/timelock_recovery/qt.py +++ b/electrum/plugins/timelock_recovery/qt.py @@ -179,8 +179,11 @@ class Plugin(TimelockRecoveryPlugin): fee_policy = FeePolicy('eta:1') create_cancel_cb = QCheckBox('', checked=False) alert_tx_fee_label = QLabel('') + alert_tx_complete_label = QLabel('') recovery_tx_fee_label = QLabel('') + recovery_tx_complete_label = QLabel('') cancellation_tx_fee_label = QLabel('') + cancellation_tx_complete_label = QLabel('') if not context.get_alert_address(): plan_dialog.show_error(''.join([ @@ -231,20 +234,30 @@ class Plugin(TimelockRecoveryPlugin): try: context.alert_tx = context.make_unsigned_alert_tx(fee_policy) assert all(tx_input.is_segwit() for tx_input in context.alert_tx.inputs()) + alert_tx_complete_label.setText(_("✓ Signed") if context.alert_tx.is_complete() else "") alert_tx_fee_label.setText(_("Fee: {}").format(self.config.format_amount_and_units(context.alert_tx.get_fee()))) context.recovery_tx = context.make_unsigned_recovery_tx(fee_policy) assert all(tx_input.is_segwit() for tx_input in context.recovery_tx.inputs()) + recovery_tx_complete_label.setText(_("✓ Signed") if context.recovery_tx.is_complete() else "") recovery_tx_fee_label.setText(_("Fee: {}").format(self.config.format_amount_and_units(context.recovery_tx.get_fee()))) if create_cancel_cb.isChecked(): context.cancellation_tx = context.make_unsigned_cancellation_tx(fee_policy) assert all(tx_input.is_segwit() for tx_input in context.cancellation_tx.inputs()) + cancellation_tx_complete_label.setText(_("✓ Signed") if context.cancellation_tx.is_complete() else "") cancellation_tx_fee_label.setText(_("Fee: {}").format(self.config.format_amount_and_units(context.cancellation_tx.get_fee()))) else: context.cancellation_tx = None + cancellation_tx_complete_label.setText(_("✓ Signed") if context.cancellation_tx is not None and context.cancellation_tx.is_complete() else "") except NotEnoughFunds: view_alert_tx_button.setEnabled(False) + alert_tx_complete_label.setText("") + alert_tx_fee_label.setText("") view_recovery_tx_button.setEnabled(False) + recovery_tx_complete_label.setText("") + recovery_tx_fee_label.setText("") view_cancellation_tx_button.setEnabled(False) + cancellation_tx_complete_label.setText("") + cancellation_tx_fee_label.setText("") payto_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True)) payto_e.setToolTip("Not enough funds to create the transactions.") next_button.setEnabled(False) @@ -322,14 +335,16 @@ class Plugin(TimelockRecoveryPlugin): grid_row += 1 plan_grid.addWidget(QLabel('Alert transaction'), grid_row, 0) - plan_grid.addWidget(alert_tx_fee_label, grid_row, 1, 1, 3) + plan_grid.addWidget(alert_tx_fee_label, grid_row, 1, 1, 2) + plan_grid.addWidget(alert_tx_complete_label, grid_row, 3) view_alert_tx_button = QPushButton(_('View')) view_alert_tx_button.clicked.connect(lambda: context.main_window.show_transaction(context.alert_tx, show_sign_button=False, show_broadcast_button=False)) plan_grid.addWidget(view_alert_tx_button, grid_row, 4) grid_row += 1 plan_grid.addWidget(QLabel('Recovery transaction'), grid_row, 0) - plan_grid.addWidget(recovery_tx_fee_label, grid_row, 1, 1, 3) + plan_grid.addWidget(recovery_tx_fee_label, grid_row, 1, 1, 2) + plan_grid.addWidget(recovery_tx_complete_label, grid_row, 3) view_recovery_tx_button = QPushButton(_('View')) view_recovery_tx_button.clicked.connect(lambda: context.main_window.show_transaction(context.recovery_tx, show_sign_button=False, show_broadcast_button=False)) plan_grid.addWidget(view_recovery_tx_button, grid_row, 4) @@ -337,7 +352,8 @@ class Plugin(TimelockRecoveryPlugin): cancellation_label = QLabel('Cancellation transaction') plan_grid.addWidget(cancellation_label, grid_row, 0) - plan_grid.addWidget(cancellation_tx_fee_label, grid_row, 1, 1, 3) + plan_grid.addWidget(cancellation_tx_fee_label, grid_row, 1, 1, 2) + plan_grid.addWidget(cancellation_tx_complete_label, grid_row, 3) view_cancellation_tx_button = QPushButton(_('View')) view_cancellation_tx_button.clicked.connect(lambda: context.main_window.show_transaction(context.cancellation_tx, show_sign_button=False, show_broadcast_button=False)) plan_grid.addWidget(view_cancellation_tx_button, grid_row, 4) From fb40bbe96b7ebd529f0b16eb12dc171261b03c21 Mon Sep 17 00:00:00 2001 From: Oren Date: Sat, 24 May 2025 03:41:19 +0300 Subject: [PATCH 07/10] keep the same locktime We don't want the txid to change because the new transaction has a new random locktime. --- electrum/plugins/timelock_recovery/timelock_recovery.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/electrum/plugins/timelock_recovery/timelock_recovery.py b/electrum/plugins/timelock_recovery/timelock_recovery.py index 1368e8c0d..cbf896eaa 100644 --- a/electrum/plugins/timelock_recovery/timelock_recovery.py +++ b/electrum/plugins/timelock_recovery/timelock_recovery.py @@ -89,6 +89,7 @@ class TimelockRecoveryContext: outputs=alert_tx_outputs, fee_policy=fee_policy, is_sweep=False, + locktime=self.alert_tx.locktime if self.alert_tx else None, ) def _alert_tx_output(self) -> Tuple[int, 'TxOutput']: @@ -122,6 +123,7 @@ class TimelockRecoveryContext: outputs=[output for output in self.outputs if output.value != 0], fee_policy=fee_policy, is_sweep=False, + locktime=self.recovery_tx.locktime if self.recovery_tx else None, ) def add_input_info(self): @@ -143,6 +145,7 @@ class TimelockRecoveryContext: ], fee_policy=fee_policy, is_sweep=False, + locktime=self.cancellation_tx.locktime if self.cancellation_tx else None, ) class TimelockRecoveryPlugin(BasePlugin): From 951ca76fc2d56748e2b8082a531e1154e331be2c Mon Sep 17 00:00:00 2001 From: Oren Date: Mon, 26 May 2025 19:30:13 +0300 Subject: [PATCH 08/10] allow signing transaction from View --- electrum/plugins/timelock_recovery/qt.py | 43 +++++++++++++++---- .../timelock_recovery/timelock_recovery.py | 9 ++-- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/electrum/plugins/timelock_recovery/qt.py b/electrum/plugins/timelock_recovery/qt.py index 1037a6630..0a32b18e2 100644 --- a/electrum/plugins/timelock_recovery/qt.py +++ b/electrum/plugins/timelock_recovery/qt.py @@ -35,7 +35,7 @@ from electrum.gui.qt.paytoedit import PayToEdit from electrum.payment_identifier import PaymentIdentifierType from electrum.plugin import hook from electrum.i18n import _ -from electrum.transaction import PartialTxOutput +from electrum.transaction import PartialTxOutput, Transaction from electrum.util import NotEnoughFunds, make_dir from electrum.gui.qt.util import ColorScheme, WindowModalDialog, Buttons, HelpLabel from electrum.gui.qt.util import read_QIcon_from_bytes, read_QPixmap_from_bytes, WaitingDialog @@ -232,16 +232,26 @@ class Plugin(TimelockRecoveryPlugin): next_button.setToolTip("") return try: - context.alert_tx = context.make_unsigned_alert_tx(fee_policy) + new_alert_tx = context.make_unsigned_alert_tx(fee_policy) + alert_changed = False + if not context.alert_tx or context.alert_tx.txid() != new_alert_tx.txid(): + context.alert_tx = new_alert_tx + alert_changed = True assert all(tx_input.is_segwit() for tx_input in context.alert_tx.inputs()) alert_tx_complete_label.setText(_("✓ Signed") if context.alert_tx.is_complete() else "") alert_tx_fee_label.setText(_("Fee: {}").format(self.config.format_amount_and_units(context.alert_tx.get_fee()))) - context.recovery_tx = context.make_unsigned_recovery_tx(fee_policy) + new_recovery_tx = context.make_unsigned_recovery_tx(fee_policy) + if alert_changed or not context.recovery_tx or context.recovery_tx.txid() != new_recovery_tx.txid(): + context.recovery_tx = new_recovery_tx + context.add_input_info_to_recovery_tx() assert all(tx_input.is_segwit() for tx_input in context.recovery_tx.inputs()) recovery_tx_complete_label.setText(_("✓ Signed") if context.recovery_tx.is_complete() else "") recovery_tx_fee_label.setText(_("Fee: {}").format(self.config.format_amount_and_units(context.recovery_tx.get_fee()))) if create_cancel_cb.isChecked(): - context.cancellation_tx = context.make_unsigned_cancellation_tx(fee_policy) + new_cancellation_tx = context.make_unsigned_cancellation_tx(fee_policy) + if alert_changed or not context.cancellation_tx or context.cancellation_tx.txid() != new_cancellation_tx.txid(): + context.cancellation_tx = new_cancellation_tx + context.add_input_info_to_cancellation_tx() assert all(tx_input.is_segwit() for tx_input in context.cancellation_tx.inputs()) cancellation_tx_complete_label.setText(_("✓ Signed") if context.cancellation_tx.is_complete() else "") cancellation_tx_fee_label.setText(_("Fee: {}").format(self.config.format_amount_and_units(context.cancellation_tx.get_fee()))) @@ -338,7 +348,15 @@ class Plugin(TimelockRecoveryPlugin): plan_grid.addWidget(alert_tx_fee_label, grid_row, 1, 1, 2) plan_grid.addWidget(alert_tx_complete_label, grid_row, 3) view_alert_tx_button = QPushButton(_('View')) - view_alert_tx_button.clicked.connect(lambda: context.main_window.show_transaction(context.alert_tx, show_sign_button=False, show_broadcast_button=False)) + def on_alert_tx_closed(tx: Optional[Transaction]): + if tx is not None and context.alert_tx is not None and tx.txid() == context.alert_tx.txid() and tx.is_complete(): + old_alert_tx_complete = context.alert_tx and context.alert_tx.is_complete() + context.alert_tx = tx + if not old_alert_tx_complete and context.alert_tx.is_complete(): + context.add_input_info_to_recovery_tx() + context.add_input_info_to_cancellation_tx() + update_transactions() + view_alert_tx_button.clicked.connect(lambda: context.main_window.show_transaction(context.alert_tx, show_broadcast_button=False, on_closed=on_alert_tx_closed)) plan_grid.addWidget(view_alert_tx_button, grid_row, 4) grid_row += 1 @@ -346,7 +364,11 @@ class Plugin(TimelockRecoveryPlugin): plan_grid.addWidget(recovery_tx_fee_label, grid_row, 1, 1, 2) plan_grid.addWidget(recovery_tx_complete_label, grid_row, 3) view_recovery_tx_button = QPushButton(_('View')) - view_recovery_tx_button.clicked.connect(lambda: context.main_window.show_transaction(context.recovery_tx, show_sign_button=False, show_broadcast_button=False)) + def on_recovery_tx_closed(tx: Optional[Transaction]): + if tx is not None and context.recovery_tx is not None and tx.txid() == context.recovery_tx.txid() and tx.is_complete(): + context.recovery_tx = tx + update_transactions() + view_recovery_tx_button.clicked.connect(lambda: context.main_window.show_transaction(context.recovery_tx, show_broadcast_button=False, on_closed=on_recovery_tx_closed)) plan_grid.addWidget(view_recovery_tx_button, grid_row, 4) grid_row += 1 @@ -355,7 +377,11 @@ class Plugin(TimelockRecoveryPlugin): plan_grid.addWidget(cancellation_tx_fee_label, grid_row, 1, 1, 2) plan_grid.addWidget(cancellation_tx_complete_label, grid_row, 3) view_cancellation_tx_button = QPushButton(_('View')) - view_cancellation_tx_button.clicked.connect(lambda: context.main_window.show_transaction(context.cancellation_tx, show_sign_button=False, show_broadcast_button=False)) + def on_cancellation_tx_closed(tx: Optional[Transaction]): + if tx is not None and context.cancellation_tx is not None and tx.txid() == context.cancellation_tx.txid() and tx.is_complete(): + context.cancellation_tx = tx + update_transactions() + view_cancellation_tx_button.clicked.connect(lambda: context.main_window.show_transaction(context.cancellation_tx, show_broadcast_button=False, on_closed=on_cancellation_tx_closed)) plan_grid.addWidget(view_cancellation_tx_button, grid_row, 4) grid_row += 1 @@ -440,9 +466,10 @@ class Plugin(TimelockRecoveryPlugin): def task(): if not context.alert_tx.is_complete(): wallet.sign_transaction(context.alert_tx, password, ignore_warnings=True) + context.add_input_info_to_recovery_tx() + context.add_input_info_to_cancellation_tx() if not context.alert_tx.is_complete(): raise Exception(_("Alert transaction signing was not completed")) - context.add_input_info() if not context.recovery_tx.is_complete(): wallet.sign_transaction(context.recovery_tx, password, ignore_warnings=True) if not context.recovery_tx.is_complete(): diff --git a/electrum/plugins/timelock_recovery/timelock_recovery.py b/electrum/plugins/timelock_recovery/timelock_recovery.py index cbf896eaa..27cb67947 100644 --- a/electrum/plugins/timelock_recovery/timelock_recovery.py +++ b/electrum/plugins/timelock_recovery/timelock_recovery.py @@ -126,9 +126,12 @@ class TimelockRecoveryContext: locktime=self.recovery_tx.locktime if self.recovery_tx else None, ) - def add_input_info(self): - self.recovery_tx.inputs()[0].utxo = self.alert_tx - if self.cancellation_tx: + def add_input_info_to_recovery_tx(self): + if self.recovery_tx and self.alert_tx.is_complete(): + self.recovery_tx.inputs()[0].utxo = self.alert_tx + + def add_input_info_to_cancellation_tx(self): + if self.cancellation_tx and self.alert_tx.is_complete(): self.cancellation_tx.inputs()[0].utxo = self.alert_tx def make_unsigned_cancellation_tx(self, fee_policy) -> 'PartialTransaction': From 53b3c1de3e52a7687efd770570684924e21c79f7 Mon Sep 17 00:00:00 2001 From: Oren Date: Sat, 24 May 2025 04:01:41 +0300 Subject: [PATCH 09/10] control prompt_if_unsaved In the current logic, even if prompt_if_unsaved was False, the prompt will appear if the signing is complete (and unsaved). --- electrum/gui/qt/transaction_dialog.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index 0ebaf8c98..c0a4f7435 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -461,6 +461,7 @@ class TxDialog(QDialog, MessageBoxMixin): *, parent: 'ElectrumWindow', prompt_if_unsaved: bool, + prompt_if_complete_unsaved: bool = True, external_keypairs: Mapping[bytes, bytes] = None, invoice: 'Invoice' = None, on_closed: Callable[[Optional[Transaction]], None] = None, @@ -477,6 +478,7 @@ class TxDialog(QDialog, MessageBoxMixin): self.wallet = parent.wallet self.invoice = invoice self.prompt_if_unsaved = prompt_if_unsaved + self.prompt_if_complete_unsaved = prompt_if_complete_unsaved self.on_closed = on_closed self.saved = False self.desc = None @@ -711,7 +713,7 @@ class TxDialog(QDialog, MessageBoxMixin): def sign(self): def sign_done(success): - if self.tx.is_complete(): + if self.tx.is_complete() and self.prompt_if_complete_unsaved: self.prompt_if_unsaved = True self.saved = False self.update() From afc62ebb774bf17512eaf0232e3ffc18ef836a4e Mon Sep 17 00:00:00 2001 From: Oren Date: Sat, 24 May 2025 04:09:07 +0300 Subject: [PATCH 10/10] Ok to press Sign and not save as a file It's ok to click the View button, then press Sign, and then close the window the signed transaction will be used by the on_closed callback --- electrum/gui/qt/main_window.py | 2 ++ electrum/gui/qt/transaction_dialog.py | 2 ++ electrum/plugins/timelock_recovery/qt.py | 21 ++++++++++++++++++--- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index d21d0d035..1e97d27cc 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1181,6 +1181,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): self, tx: Transaction, *, + prompt_if_complete_unsaved: bool = True, external_keypairs: Mapping[bytes, bytes] = None, invoice: Invoice = None, on_closed: Callable[[Optional[Transaction]], None] = None, @@ -1190,6 +1191,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): show_transaction( tx, parent=self, + prompt_if_complete_unsaved=prompt_if_complete_unsaved, external_keypairs=external_keypairs, invoice=invoice, on_closed=on_closed, diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index c0a4f7435..1e0bbe2cc 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -425,6 +425,7 @@ def show_transaction( *, parent: 'ElectrumWindow', prompt_if_unsaved: bool = False, + prompt_if_complete_unsaved: bool = True, external_keypairs: Mapping[bytes, bytes] = None, invoice: 'Invoice' = None, on_closed: Callable[[Optional[Transaction]], None] = None, @@ -436,6 +437,7 @@ def show_transaction( tx, parent=parent, prompt_if_unsaved=prompt_if_unsaved, + prompt_if_complete_unsaved=prompt_if_complete_unsaved, external_keypairs=external_keypairs, invoice=invoice, on_closed=on_closed, diff --git a/electrum/plugins/timelock_recovery/qt.py b/electrum/plugins/timelock_recovery/qt.py index 0a32b18e2..79655c1c8 100644 --- a/electrum/plugins/timelock_recovery/qt.py +++ b/electrum/plugins/timelock_recovery/qt.py @@ -356,7 +356,12 @@ class Plugin(TimelockRecoveryPlugin): context.add_input_info_to_recovery_tx() context.add_input_info_to_cancellation_tx() update_transactions() - view_alert_tx_button.clicked.connect(lambda: context.main_window.show_transaction(context.alert_tx, show_broadcast_button=False, on_closed=on_alert_tx_closed)) + view_alert_tx_button.clicked.connect(lambda: context.main_window.show_transaction( + context.alert_tx, + prompt_if_complete_unsaved=False, + show_broadcast_button=False, + on_closed=on_alert_tx_closed + )) plan_grid.addWidget(view_alert_tx_button, grid_row, 4) grid_row += 1 @@ -368,7 +373,12 @@ class Plugin(TimelockRecoveryPlugin): if tx is not None and context.recovery_tx is not None and tx.txid() == context.recovery_tx.txid() and tx.is_complete(): context.recovery_tx = tx update_transactions() - view_recovery_tx_button.clicked.connect(lambda: context.main_window.show_transaction(context.recovery_tx, show_broadcast_button=False, on_closed=on_recovery_tx_closed)) + view_recovery_tx_button.clicked.connect(lambda: context.main_window.show_transaction( + context.recovery_tx, + prompt_if_complete_unsaved=False, + show_broadcast_button=False, + on_closed=on_recovery_tx_closed + )) plan_grid.addWidget(view_recovery_tx_button, grid_row, 4) grid_row += 1 @@ -381,7 +391,12 @@ class Plugin(TimelockRecoveryPlugin): if tx is not None and context.cancellation_tx is not None and tx.txid() == context.cancellation_tx.txid() and tx.is_complete(): context.cancellation_tx = tx update_transactions() - view_cancellation_tx_button.clicked.connect(lambda: context.main_window.show_transaction(context.cancellation_tx, show_broadcast_button=False, on_closed=on_cancellation_tx_closed)) + view_cancellation_tx_button.clicked.connect(lambda: context.main_window.show_transaction( + context.cancellation_tx, + prompt_if_complete_unsaved=False, + show_broadcast_button=False, + on_closed=on_cancellation_tx_closed + )) plan_grid.addWidget(view_cancellation_tx_button, grid_row, 4) grid_row += 1