Merge pull request #9862 from oren-z0/fix-timelock-recovery-watch-wallets
Fix Timelock Recovery plugin for watch wallets
This commit is contained in:
@@ -1181,16 +1181,20 @@ 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,
|
||||
show_sign_button: bool = True,
|
||||
show_broadcast_button: bool = True,
|
||||
):
|
||||
show_transaction(
|
||||
tx,
|
||||
parent=self,
|
||||
prompt_if_complete_unsaved=prompt_if_complete_unsaved,
|
||||
external_keypairs=external_keypairs,
|
||||
invoice=invoice,
|
||||
on_closed=on_closed,
|
||||
show_sign_button=show_sign_button,
|
||||
show_broadcast_button=show_broadcast_button,
|
||||
)
|
||||
|
||||
@@ -425,9 +425,10 @@ 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[[], None] = None,
|
||||
on_closed: Callable[[Optional[Transaction]], None] = None,
|
||||
show_sign_button: bool = True,
|
||||
show_broadcast_button: bool = True,
|
||||
):
|
||||
@@ -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,
|
||||
@@ -461,9 +463,10 @@ 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[[], 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.
|
||||
@@ -477,6 +480,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
|
||||
@@ -640,7 +644,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)
|
||||
@@ -711,7 +715,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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
@@ -176,12 +176,14 @@ 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('')
|
||||
cancellation_tx_label = QLabel('')
|
||||
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([
|
||||
@@ -227,34 +229,62 @@ 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)
|
||||
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_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)
|
||||
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())))
|
||||
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_label.setText(_("Fee: {}").format(self.config.format_amount_and_units(context.recovery_tx.get_fee())))
|
||||
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_label.setText(_("Fee: {}").format(self.config.format_amount_and_units(context.cancellation_tx.get_fee())))
|
||||
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)
|
||||
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)
|
||||
@@ -315,24 +345,58 @@ 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, 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,
|
||||
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
|
||||
|
||||
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, 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,
|
||||
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
|
||||
|
||||
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, 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,
|
||||
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
|
||||
|
||||
@@ -342,7 +406,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)
|
||||
@@ -415,11 +479,21 @@ class Plugin(TimelockRecoveryPlugin):
|
||||
password = main_window.get_password()
|
||||
|
||||
def task():
|
||||
wallet.sign_transaction(context.alert_tx, password, ignore_warnings=True)
|
||||
context.add_input_info()
|
||||
wallet.sign_transaction(context.recovery_tx, password, ignore_warnings=True)
|
||||
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"))
|
||||
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)
|
||||
|
||||
@@ -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,11 +123,15 @@ 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):
|
||||
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':
|
||||
@@ -143,6 +148,7 @@ class TimelockRecoveryContext:
|
||||
],
|
||||
fee_policy=fee_policy,
|
||||
is_sweep=False,
|
||||
locktime=self.cancellation_tx.locktime if self.cancellation_tx else None,
|
||||
)
|
||||
|
||||
class TimelockRecoveryPlugin(BasePlugin):
|
||||
|
||||
Reference in New Issue
Block a user