Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions electrum/gui/qt/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -1181,16 +1181,20 @@ def show_transaction(
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,
)
Expand Down
12 changes: 8 additions & 4 deletions electrum/gui/qt/transaction_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
):
Expand All @@ -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,
Expand All @@ -461,9 +463,10 @@ def __init__(
*,
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.
Expand All @@ -477,6 +480,7 @@ def __init__(
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
Expand Down Expand Up @@ -640,7 +644,7 @@ def closeEvent(self, event):
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)
Expand Down Expand Up @@ -711,7 +715,7 @@ def show_qr(self, *, tx: Transaction = None):

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()
Expand Down
2 changes: 1 addition & 1 deletion electrum/plugins/psbt_nostr/qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ def on_receive(self, pubkey, event_id, tx, label):
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):
Expand Down
120 changes: 97 additions & 23 deletions electrum/plugins/timelock_recovery/qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
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
Expand Down Expand Up @@ -176,12 +176,14 @@ def create_plan_dialog(self, context: TimelockRecoveryContext) -> bool:
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([
Expand Down Expand Up @@ -227,34 +229,62 @@ def update_transactions():
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)
Expand Down Expand Up @@ -315,24 +345,58 @@ def update_transactions():
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

Expand All @@ -342,7 +406,7 @@ def update_transactions():

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)
Expand Down Expand Up @@ -415,11 +479,21 @@ def start_plan(self, context: TimelockRecoveryContext):
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)
Expand Down
12 changes: 9 additions & 3 deletions electrum/plugins/timelock_recovery/timelock_recovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ def make_unsigned_alert_tx(self, fee_policy) -> 'PartialTransaction':
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']:
Expand Down Expand Up @@ -122,11 +123,15 @@ def make_unsigned_recovery_tx(self, fee_policy) -> 'PartialTransaction':
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':
Expand All @@ -143,6 +148,7 @@ def make_unsigned_cancellation_tx(self, fee_policy) -> 'PartialTransaction':
],
fee_policy=fee_policy,
is_sweep=False,
locktime=self.cancellation_tx.locktime if self.cancellation_tx else None,
)

class TimelockRecoveryPlugin(BasePlugin):
Expand Down