Skip to content

Commit dfbcea0

Browse files
committed
move unsigned tx making
1 parent ffdfdfb commit dfbcea0

File tree

2 files changed

+88
-80
lines changed

2 files changed

+88
-80
lines changed

electrum/plugins/timelock_recovery/qt.py

+10-77
Original file line numberDiff line numberDiff line change
@@ -31,28 +31,27 @@
3131
from electrum.gui.common_qt.util import draw_qr
3232
from electrum.gui.fonts import get_font_id
3333
from electrum.gui.qt.paytoedit import PayToEdit
34-
from electrum.bitcoin import COIN, address_to_script, DummyAddress
34+
from electrum.bitcoin import DummyAddress
3535
from electrum.payment_identifier import PaymentIdentifierType
3636
from electrum.plugin import hook, run_hook
3737
from electrum.i18n import _
38-
from electrum.transaction import PartialTxInput, PartialTxOutput, TxOutpoint
39-
from electrum.util import make_dir, bfh
38+
from electrum.transaction import PartialTxOutput
39+
from electrum.util import make_dir
4040
from electrum.gui.qt.util import ColorScheme, WindowModalDialog, Buttons, HelpLabel
4141
from electrum.gui.qt.main_window import StatusBarButton
4242
from electrum.gui.qt.util import read_QIcon_from_bytes, read_QPixmap_from_bytes
4343

4444
from . import version as plugin_version
45-
from .timelock_recovery import TimelockRecoveryPlugin, TimelockRecoveryContext, PartialTxInputWithFixedNsequence
45+
from .timelock_recovery import TimelockRecoveryPlugin, TimelockRecoveryContext
4646

4747

4848
if TYPE_CHECKING:
4949
from electrum.gui.qt import ElectrumGui
50-
from electrum.transaction import PartialTransaction, TxOutput
50+
from electrum.transaction import PartialTransaction
5151
from PyQt6.QtWidgets import QStatusBar
5252

5353

5454
AGREEMENT_TEXT = "I understand that using this wallet after generating a Timelock Recovery plan might break the plan"
55-
ANCHOR_OUTPUT_AMOUNT_SATS = 600
5655
MIN_LOCKTIME_DAYS = 2
5756
# 0xFFFF * 512 seconds = 388.36 days.
5857
MAX_LOCKTIME_DAYS = 388
@@ -350,21 +349,9 @@ def _verify_step1_details(self, context: TimelockRecoveryContext, next_button: Q
350349
next_button.setEnabled(True)
351350

352351
def create_alert_fee_dialog(self, context: TimelockRecoveryContext):
353-
alert_transaction_outputs = [
354-
PartialTxOutput(scriptpubkey=address_to_script(context.get_alert_address()), value='!'),
355-
] + [
356-
PartialTxOutput(scriptpubkey=output.scriptpubkey, value=ANCHOR_OUTPUT_AMOUNT_SATS)
357-
for output in context.outputs
358-
]
359-
make_tx = lambda fee_est, *, confirmed_only=False: context.wallet.make_unsigned_transaction(
360-
coins=context.main_window.get_coins(confirmed_only=confirmed_only),
361-
outputs=alert_transaction_outputs,
362-
fee=fee_est,
363-
is_sweep=False,
364-
)
365352
tx: Optional['PartialTransaction']
366353
is_preview: bool
367-
tx, is_preview = context.main_window.confirm_tx_dialog(make_tx, '!', allow_preview=False)
354+
tx, is_preview = context.main_window.confirm_tx_dialog(context.make_unsigned_alert_tx, '!', allow_preview=False)
368355
if tx is None or is_preview or tx.has_dummy_output(DummyAddress.SWAP):
369356
return
370357
if not tx.is_segwit():
@@ -392,39 +379,9 @@ def sign_done(success: bool):
392379
)
393380

394381
def create_recovery_fee_dialog(self, context: TimelockRecoveryContext):
395-
prevouts: List[Tuple[int, 'TxOutput']] = [
396-
(index, tx_output) for index, tx_output in enumerate(context.alert_tx.outputs())
397-
if tx_output.address == context.get_alert_address() and tx_output.value != ANCHOR_OUTPUT_AMOUNT_SATS
398-
]
399-
if len(prevouts) != 1:
400-
context.main_window.show_error(_("Expected 1 output from the Alert transaction to the Alert Address, but got %d." % len(prevouts)))
401-
return
402-
prevout_index: int
403-
prevout: 'TxOutput'
404-
(prevout_index, prevout) = prevouts[0]
405-
406-
nsequence: int = round(context.timelock_days * 24 * 60 * 60 / 512)
407-
if nsequence > 0xFFFF:
408-
# Safety check - not expected to happen
409-
raise ValueError("Sequence number is too large")
410-
nsequence += 0x00400000 # time based lock instead of block-height based lock
411-
tx_input = PartialTxInputWithFixedNsequence(
412-
prevout=TxOutpoint(txid=bfh(context.alert_tx.txid()), out_idx=prevout_index),
413-
nsequence=nsequence,
414-
)
415-
tx_input.utxo = context.alert_tx
416-
tx_input.witness_utxo = prevout
417-
418-
make_tx = lambda fee_est, *, confirmed_only=False: context.wallet.make_unsigned_transaction(
419-
coins=[tx_input],
420-
outputs=[output for output in context.outputs if output.value != 0],
421-
fee=fee_est,
422-
is_sweep=False,
423-
)
424-
425382
tx: Optional['PartialTransaction']
426383
is_preview: bool
427-
tx, is_preview = context.main_window.confirm_tx_dialog(make_tx, '!', allow_preview=False)
384+
tx, is_preview = context.main_window.confirm_tx_dialog(context.make_unsigned_recovery_tx, '!', allow_preview=False)
428385
if tx is None or is_preview or tx.has_dummy_output(DummyAddress.SWAP):
429386
return
430387
if not tx.is_segwit():
@@ -545,33 +502,9 @@ def create_cancellation_dialog(self, context: TimelockRecoveryContext):
545502
return bool(cancel_dialog.exec())
546503

547504
def create_cancellation_fee_dialog(self, context: TimelockRecoveryContext):
548-
prevouts = [
549-
(index, tx_output) for index, tx_output in enumerate(context.alert_tx.outputs())
550-
if tx_output.address == context.get_alert_address() and tx_output.value != ANCHOR_OUTPUT_AMOUNT_SATS
551-
]
552-
if len(prevouts) != 1:
553-
context.main_window.show_error(_("Expected 1 output from the Alert transaction to the Alert Address, but got %d." % len(prevouts)))
554-
return
555-
(prevout_index, prevout) = prevouts[0]
556-
557-
tx_input = PartialTxInput(
558-
prevout=TxOutpoint(txid=bfh(context.alert_tx.txid()), out_idx=prevout_index),
559-
)
560-
tx_input.utxo = context.alert_tx
561-
tx_input.witness_utxo = prevout
562-
563-
make_tx = lambda fee_est, *, confirmed_only=False: context.wallet.make_unsigned_transaction(
564-
coins=[tx_input],
565-
outputs=[
566-
PartialTxOutput(scriptpubkey=address_to_script(context.get_cancellation_address()), value='!'),
567-
],
568-
fee=fee_est,
569-
is_sweep=False,
570-
)
571-
572505
tx: Optional['PartialTransaction']
573506
is_preview: bool
574-
tx, is_preview = context.main_window.confirm_tx_dialog(make_tx, '!', allow_preview=False)
507+
tx, is_preview = context.main_window.confirm_tx_dialog(context.make_unsigned_cancellation_tx, '!', allow_preview=False)
575508
if tx is None or is_preview or tx.has_dummy_output(DummyAddress.SWAP):
576509
return
577510
if not tx.is_segwit():
@@ -733,7 +666,7 @@ def _save_recovery_plan_json(self, context: TimelockRecoveryContext, download_di
733666
"wallet_version": version.ELECTRUM_VERSION,
734667
"wallet_name": context.wallet_name,
735668
"timelock_days": context.timelock_days,
736-
"anchor_amount_sats": ANCHOR_OUTPUT_AMOUNT_SATS,
669+
"anchor_amount_sats": context.ANCHOR_OUTPUT_AMOUNT_SATS,
737670
"anchor_addresses": [output.address for output in context.outputs],
738671
"alert_address": context.get_alert_address(),
739672
"alert_inputs": [tx_input.prevout.to_str() for tx_input in context.alert_tx.inputs()],
@@ -935,7 +868,7 @@ def _save_recovery_plan_pdf(self, context: TimelockRecoveryContext, download_dia
935868
f"as we'll explain later):\n"
936869
)
937870
for output in context.alert_tx.outputs():
938-
if output.address != context.get_alert_address() and output.value == ANCHOR_OUTPUT_AMOUNT_SATS:
871+
if output.address != context.get_alert_address() and output.value == context.ANCHOR_OUTPUT_AMOUNT_SATS:
939872
step1_text += f"• {output.address}\n"
940873
else:
941874
step1_text += "except for a small fee.\n"

electrum/plugins/timelock_recovery/timelock_recovery.py

+78-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
from datetime import datetime
2-
from typing import TYPE_CHECKING, List, Optional
2+
from typing import TYPE_CHECKING, List, Optional, Tuple
3+
from electrum.bitcoin import address_to_script
34
from electrum.plugin import BasePlugin
4-
from electrum.transaction import PartialTxInput
5+
from electrum.transaction import PartialTxOutput, PartialTxInput, TxOutpoint
6+
from electrum.util import bfh
57

68
if TYPE_CHECKING:
79
from electrum.gui.qt import ElectrumWindow
8-
from electrum.transaction import PartialTxOutput, PartialTransaction
10+
from electrum.transaction import PartialTransaction, TxOutput
911
from electrum.wallet import Abstract_Wallet
1012

1113
ALERT_ADDRESS_LABEL = "Timelock Recovery Alert Address"
@@ -40,6 +42,11 @@ class TimelockRecoveryContext:
4042
recovery_plan_created_at: Optional[datetime] = None
4143
_alert_address: Optional[str] = None
4244
_cancellation_address: Optional[str] = None
45+
_alert_tx_outputs: Optional[List[PartialTxOutput]] = None
46+
_recovery_tx_input: Optional[PartialTxInputWithFixedNsequence] = None
47+
_cancellation_tx_input: Optional[PartialTxInput] = None
48+
49+
ANCHOR_OUTPUT_AMOUNT_SATS = 600
4350

4451
def __init__(self, main_window: 'ElectrumWindow'):
4552
self.main_window = main_window
@@ -72,6 +79,74 @@ def get_cancellation_address(self) -> str:
7279
self._cancellation_address = self._get_address_by_label(CANCELLATION_ADDRESS_LABEL)
7380
return self._cancellation_address
7481

82+
def make_unsigned_alert_tx(self, fee_est, *, confirmed_only=False) -> 'PartialTransaction':
83+
if self._alert_tx_outputs is None:
84+
self._alert_tx_outputs = [
85+
PartialTxOutput(scriptpubkey=address_to_script(self.get_alert_address()), value='!'),
86+
] + [
87+
PartialTxOutput(scriptpubkey=output.scriptpubkey, value=self.ANCHOR_OUTPUT_AMOUNT_SATS)
88+
for output in self.outputs
89+
]
90+
return self.wallet.make_unsigned_transaction(
91+
coins=self.main_window.get_coins(confirmed_only=confirmed_only),
92+
outputs=self._alert_tx_outputs,
93+
fee=fee_est,
94+
is_sweep=False,
95+
)
96+
97+
def _alert_tx_output(self) -> Tuple[int, 'TxOutput']:
98+
tx_outputs: List[Tuple[int, 'TxOutput']] = [
99+
(index, tx_output) for index, tx_output in enumerate(self.alert_tx.outputs())
100+
if tx_output.address == self.get_alert_address() and tx_output.value != self.ANCHOR_OUTPUT_AMOUNT_SATS
101+
]
102+
if len(tx_outputs) != 1:
103+
# Safety check - not expected to happen
104+
raise ValueError(f"Expected 1 output from the Alert transaction to the Alert Address, but got {len(tx_outputs)}.")
105+
return tx_outputs[0]
106+
107+
def _alert_tx_outpoint(self, out_idx: int) -> TxOutpoint:
108+
return TxOutpoint(txid=bfh(self.alert_tx.txid()), out_idx=out_idx)
109+
110+
def make_unsigned_recovery_tx(self, fee_est, *, confirmed_only=False) -> 'PartialTransaction':
111+
if self._recovery_tx_input is None:
112+
prevout_index, prevout = self._alert_tx_output()
113+
nsequence: int = round(self.timelock_days * 24 * 60 * 60 / 512)
114+
if nsequence > 0xFFFF:
115+
# Safety check - not expected to happen
116+
raise ValueError("Sequence number is too large")
117+
nsequence += 0x00400000 # time based lock instead of block-height based lock
118+
self._recovery_tx_input = PartialTxInputWithFixedNsequence(
119+
prevout=self._alert_tx_outpoint(prevout_index),
120+
nsequence=nsequence,
121+
)
122+
self._recovery_tx_input.utxo = self.alert_tx
123+
self._recovery_tx_input.witness_utxo = prevout
124+
125+
return self.wallet.make_unsigned_transaction(
126+
coins=[self._recovery_tx_input],
127+
outputs=[output for output in self.outputs if output.value != 0],
128+
fee=fee_est,
129+
is_sweep=False,
130+
)
131+
132+
def make_unsigned_cancellation_tx(self, fee_est, *, confirmed_only=False) -> 'PartialTransaction':
133+
if self._cancellation_tx_input is None:
134+
prevout_index, prevout = self._alert_tx_output()
135+
self._cancellation_tx_input = PartialTxInput(
136+
prevout=self._alert_tx_outpoint(prevout_index),
137+
)
138+
self._cancellation_tx_input.utxo = self.alert_tx
139+
self._cancellation_tx_input.witness_utxo = prevout
140+
141+
return self.wallet.make_unsigned_transaction(
142+
coins=[self._cancellation_tx_input],
143+
outputs=[
144+
PartialTxOutput(scriptpubkey=address_to_script(self.get_cancellation_address()), value='!'),
145+
],
146+
fee=fee_est,
147+
is_sweep=False,
148+
)
149+
75150
class TimelockRecoveryPlugin(BasePlugin):
76151
def __init__(self, parent, config, name):
77152
BasePlugin.__init__(self, parent, config, name)

0 commit comments

Comments
 (0)