diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index 1d559a762..bd5c57de4 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -54,6 +54,16 @@ TX_TIMESTAMP_INF = 999_999_999_999 TX_HEIGHT_INF = 10 ** 9 +from enum import IntEnum, auto + +class TxMinedDepth(IntEnum): + """ IntEnum because we call min() in get_deepest_tx_mined_depth_for_txids """ + DEEP = auto() + SHALLOW = auto() + MEMPOOL = auto() + FREE = auto() + + class HistoryItem(NamedTuple): txid: str tx_mined_status: TxMinedInfo @@ -966,8 +976,14 @@ class AddressSynchronizer(Logger, EventListener): return coins def is_used(self, address: str) -> bool: + """Whether any tx ever touched `address`.""" return self.get_address_history_len(address) != 0 + def is_used_as_from_address(self, address: str) -> bool: + """Whether any tx ever spent from `address`.""" + received, sent = self.get_addr_io(address) + return len(sent) > 0 + def is_empty(self, address: str) -> bool: coins = self.get_addr_utxo(address) return not bool(coins) @@ -990,3 +1006,46 @@ class AddressSynchronizer(Logger, EventListener): tx_age = self.get_local_height() - tx_height + 1 max_conf = max(max_conf, tx_age) return max_conf >= req_conf + + def get_spender(self, outpoint: str) -> str: + """ + returns txid spending outpoint. + subscribes to addresses as a side effect. + """ + prev_txid, index = outpoint.split(':') + spender_txid = self.db.get_spent_outpoint(prev_txid, int(index)) + # discard local spenders + tx_mined_status = self.get_tx_height(spender_txid) + if tx_mined_status.height in [TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE]: + spender_txid = None + if not spender_txid: + return + spender_tx = self.get_transaction(spender_txid) + for i, o in enumerate(spender_tx.outputs()): + if o.address is None: + continue + if not self.is_mine(o.address): + self.add_address(o.address) + return spender_txid + + def get_tx_mined_depth(self, txid: str): + if not txid: + return TxMinedDepth.FREE + tx_mined_depth = self.get_tx_height(txid) + height, conf = tx_mined_depth.height, tx_mined_depth.conf + if conf > 20: + return TxMinedDepth.DEEP + elif conf > 0: + return TxMinedDepth.SHALLOW + elif height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT): + return TxMinedDepth.MEMPOOL + elif height in (TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE): + return TxMinedDepth.FREE + elif height > 0 and conf == 0: + # unverified but claimed to be mined + return TxMinedDepth.MEMPOOL + else: + raise NotImplementedError() + + def is_deeply_mined(self, txid): + return self.get_tx_mined_depth(txid) == TxMinedDepth.DEEP diff --git a/electrum/commands.py b/electrum/commands.py index b131f1807..5f24c5f19 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -778,28 +778,21 @@ class Commands(Logger): async def payto(self, destination, amount, fee=None, feerate=None, from_addr=None, from_coins=None, change_addr=None, nocheck=False, unsigned=False, rbf=True, password=None, locktime=None, addtransaction=False, wallet: Abstract_Wallet = None): """Create a transaction. """ - self.nocheck = nocheck - fee_policy = self._get_fee_policy(fee, feerate) - domain_addr = from_addr.split(',') if from_addr else None - domain_coins = from_coins.split(',') if from_coins else None - change_addr = self._resolver(change_addr, wallet) - domain_addr = None if domain_addr is None else map(self._resolver, domain_addr, repeat(wallet)) - amount_sat = satoshis_or_max(amount) - outputs = [PartialTxOutput.from_address_and_value(destination, amount_sat)] - tx = wallet.create_transaction( - outputs, - fee_policy=fee_policy, + return await self.paytomany( + outputs=[(destination, amount),], + fee=fee, + feerate=feerate, + from_addr=from_addr, + from_coins=from_coins, change_addr=change_addr, - domain_addr=domain_addr, - domain_coins=domain_coins, - sign=not unsigned, + nocheck=nocheck, + unsigned=unsigned, rbf=rbf, password=password, - locktime=locktime) - result = tx.serialize() - if addtransaction: - await self.addtransaction(result, wallet=wallet) - return result + locktime=locktime, + addtransaction=addtransaction, + wallet=wallet, + ) @command('wp') async def paytomany(self, outputs, fee=None, feerate=None, from_addr=None, from_coins=None, change_addr=None, @@ -816,16 +809,19 @@ class Commands(Logger): address = self._resolver(address, wallet) amount_sat = satoshis_or_max(amount) final_outputs.append(PartialTxOutput.from_address_and_value(address, amount_sat)) - tx = wallet.create_transaction( - final_outputs, + coins = wallet.get_spendable_coins(domain_addr) + if domain_coins is not None: + coins = [coin for coin in coins if (coin.prevout.to_str() in domain_coins)] + tx = wallet.make_unsigned_transaction( + outputs=final_outputs, fee_policy=fee_policy, change_addr=change_addr, - domain_addr=domain_addr, - domain_coins=domain_coins, - sign=not unsigned, + coins=coins, rbf=rbf, - password=password, - locktime=locktime) + locktime=locktime, + ) + if not unsigned: + wallet.sign_transaction(tx, password) result = tx.serialize() if addtransaction: await self.addtransaction(result, wallet=wallet) diff --git a/electrum/gui/qt/confirm_tx_dialog.py b/electrum/gui/qt/confirm_tx_dialog.py index e14629699..41449adff 100644 --- a/electrum/gui/qt/confirm_tx_dialog.py +++ b/electrum/gui/qt/confirm_tx_dialog.py @@ -29,7 +29,6 @@ from typing import TYPE_CHECKING, Optional, Union, Callable from PyQt6.QtCore import Qt from PyQt6.QtGui import QIcon - from PyQt6.QtWidgets import QHBoxLayout, QVBoxLayout, QLabel, QGridLayout, QPushButton, QToolButton, QMenu, QComboBox from electrum.i18n import _ @@ -38,21 +37,19 @@ from electrum.util import quantize_feerate from electrum.plugin import run_hook from electrum.transaction import Transaction, PartialTransaction from electrum.wallet import InternalAddressCorruption -from electrum.simple_config import SimpleConfig from electrum.bitcoin import DummyAddress from electrum.fee_policy import FeePolicy, FixedFeePolicy from .util import (WindowModalDialog, ColorScheme, HelpLabel, Buttons, CancelButton, WWLabel, read_QIcon) - -if TYPE_CHECKING: - from electrum.simple_config import ConfigVarWithConfig - from .main_window import ElectrumWindow - from .transaction_dialog import TxSizeLabel, TxFiatLabel, TxInOutWidget from .fee_slider import FeeSlider, FeeComboBox from .amountedit import FeerateEdit, BTCAmountEdit from .locktimeedit import LockTimeEdit +from .my_treeview import QMenuWithConfig + +if TYPE_CHECKING: + from .main_window import ElectrumWindow class TxEditor(WindowModalDialog): @@ -112,8 +109,8 @@ class TxEditor(WindowModalDialog): vbox.addLayout(buttons) self.set_io_visible() - self.set_fee_edit_visible(self.config.GUI_QT_TX_EDITOR_SHOW_FEE_DETAILS) - self.set_locktime_visible(self.config.GUI_QT_TX_EDITOR_SHOW_LOCKTIME) + self.set_fee_edit_visible() + self.set_locktime_visible() self.update_fee_target() self.resize(self.layout().sizeHint()) @@ -380,42 +377,37 @@ class TxEditor(WindowModalDialog): buttons.insertWidget(0, batching_combo) def on_batching_combo(x): self._base_tx = self.batching_candidates[x - 1] if x > 0 else None - self.update_batching() + self.trigger_update() batching_combo.currentIndexChanged.connect(on_batching_combo) return buttons def create_top_bar(self, text): - self.pref_menu = QMenu() - self.pref_menu.setToolTipsVisible(True) + self.pref_menu = QMenuWithConfig(self.config) - def add_pref_action(b, action, text, tooltip): - m = self.pref_menu.addAction(text, action) - m.setCheckable(True) - m.setChecked(b) - m.setToolTip(tooltip) - return m - - def add_cv_action(configvar: 'ConfigVarWithConfig', action: Callable[[], None]): - b = configvar.get() - short_desc = configvar.get_short_desc() - assert short_desc is not None, f"short_desc missing for {configvar}" - tooltip = configvar.get_long_desc() or "" - return add_pref_action(b, action, short_desc, tooltip) - - add_cv_action(self.config.cv.GUI_QT_TX_EDITOR_SHOW_IO, self.toggle_io_visibility) - add_cv_action(self.config.cv.GUI_QT_TX_EDITOR_SHOW_FEE_DETAILS, self.toggle_fee_details) - add_cv_action(self.config.cv.GUI_QT_TX_EDITOR_SHOW_LOCKTIME, self.toggle_locktime) + def cb(): + self.set_io_visible() + self.resize_to_fit_content() + self.pref_menu.addConfig(self.config.cv.GUI_QT_TX_EDITOR_SHOW_IO, callback=cb) + def cb(): + self.set_fee_edit_visible() + self.resize_to_fit_content() + self.pref_menu.addConfig(self.config.cv.GUI_QT_TX_EDITOR_SHOW_FEE_DETAILS, callback=cb) + def cb(): + self.set_locktime_visible() + self.resize_to_fit_content() + self.pref_menu.addConfig(self.config.cv.GUI_QT_TX_EDITOR_SHOW_LOCKTIME, callback=cb) self.pref_menu.addSeparator() - add_cv_action(self.config.cv.WALLET_SEND_CHANGE_TO_LIGHTNING, self.toggle_send_change_to_lightning) - add_pref_action( - self.wallet.use_change, - self.toggle_use_change, + self.pref_menu.addConfig(self.config.cv.WALLET_SEND_CHANGE_TO_LIGHTNING, callback=self.trigger_update) + self.pref_menu.addToggle( _('Use change addresses'), - _('Using change addresses makes it more difficult for other people to track your transactions.')) - self.use_multi_change_menu = add_pref_action( - self.wallet.multiple_change, self.toggle_multiple_change, - _('Use multiple change addresses',), - '\n'.join([ + self.toggle_use_change, + default_state=self.wallet.use_change, + tooltip=_('Using change addresses makes it more difficult for other people to track your transactions.')) + self.use_multi_change_menu = self.pref_menu.addToggle( + _('Use multiple change addresses'), + self.toggle_multiple_change, + default_state=self.wallet.multiple_change, + tooltip='\n'.join([ _('In some cases, use up to 3 change addresses in order to break ' 'up large coin amounts and obfuscate the recipient address.'), _('This may result in higher transactions fees.') @@ -423,9 +415,9 @@ class TxEditor(WindowModalDialog): self.use_multi_change_menu.setEnabled(self.wallet.use_change) # fixme: some of these options (WALLET_SEND_CHANGE_TO_LIGHTNING, WALLET_MERGE_DUPLICATE_OUTPUTS) # only make sense when we create a new tx, and should not be visible/enabled in rbf dialog - add_cv_action(self.config.cv.WALLET_MERGE_DUPLICATE_OUTPUTS, self.toggle_merge_duplicate_outputs) - add_cv_action(self.config.cv.WALLET_SPEND_CONFIRMED_ONLY, self.toggle_confirmed_only) - add_cv_action(self.config.cv.WALLET_COIN_CHOOSER_OUTPUT_ROUNDING, self.toggle_output_rounding) + self.pref_menu.addConfig(self.config.cv.WALLET_MERGE_DUPLICATE_OUTPUTS, callback=self.trigger_update) + self.pref_menu.addConfig(self.config.cv.WALLET_SPEND_CONFIRMED_ONLY, callback=self.trigger_update) + self.pref_menu.addConfig(self.config.cv.WALLET_COIN_CHOOSER_OUTPUT_ROUNDING, callback=self.trigger_update) self.pref_button = QToolButton() self.pref_button.setIcon(read_QIcon("preferences.png")) self.pref_button.setMenu(self.pref_menu) @@ -443,11 +435,6 @@ class TxEditor(WindowModalDialog): self.resize(size) self.resize(size) - def toggle_output_rounding(self): - b = not self.config.WALLET_COIN_CHOOSER_OUTPUT_ROUNDING - self.config.WALLET_COIN_CHOOSER_OUTPUT_ROUNDING = b - self.trigger_update() - def toggle_use_change(self): self.wallet.use_change = not self.wallet.use_change self.wallet.db.put('use_change', self.wallet.use_change) @@ -459,45 +446,11 @@ class TxEditor(WindowModalDialog): self.wallet.db.put('multiple_change', self.wallet.multiple_change) self.trigger_update() - def update_batching(self): - self.trigger_update() - - def toggle_merge_duplicate_outputs(self): - b = not self.config.WALLET_MERGE_DUPLICATE_OUTPUTS - self.config.WALLET_MERGE_DUPLICATE_OUTPUTS = b - self.trigger_update() - - def toggle_send_change_to_lightning(self): - b = not self.config.WALLET_SEND_CHANGE_TO_LIGHTNING - self.config.WALLET_SEND_CHANGE_TO_LIGHTNING = b - self.trigger_update() - - def toggle_confirmed_only(self): - b = not self.config.WALLET_SPEND_CONFIRMED_ONLY - self.config.WALLET_SPEND_CONFIRMED_ONLY = b - self.trigger_update() - - def toggle_io_visibility(self): - self.config.GUI_QT_TX_EDITOR_SHOW_IO = not self.config.GUI_QT_TX_EDITOR_SHOW_IO - self.set_io_visible() - self.resize_to_fit_content() - - def toggle_fee_details(self): - b = not self.config.GUI_QT_TX_EDITOR_SHOW_FEE_DETAILS - self.config.GUI_QT_TX_EDITOR_SHOW_FEE_DETAILS = b - self.set_fee_edit_visible(b) - self.resize_to_fit_content() - - def toggle_locktime(self): - b = not self.config.GUI_QT_TX_EDITOR_SHOW_LOCKTIME - self.config.GUI_QT_TX_EDITOR_SHOW_LOCKTIME = b - self.set_locktime_visible(b) - self.resize_to_fit_content() - def set_io_visible(self): self.io_widget.setVisible(self.config.GUI_QT_TX_EDITOR_SHOW_IO) - def set_fee_edit_visible(self, b): + def set_fee_edit_visible(self): + b = self.config.GUI_QT_TX_EDITOR_SHOW_FEE_DETAILS detailed = [self.feerounding_icon, self.feerate_e, self.fee_e] basic = [self.fee_label, self.feerate_label] # first hide, then show @@ -506,7 +459,8 @@ class TxEditor(WindowModalDialog): for w in (detailed if b else basic): w.show() - def set_locktime_visible(self, b): + def set_locktime_visible(self): + b = self.config.GUI_QT_TX_EDITOR_SHOW_LOCKTIME for w in [ self.locktime_e, self.locktime_label]: diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index d432a6fda..7708ab397 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -465,6 +465,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): def on_event_adb_set_future_tx(self, adb, txid): if adb == self.wallet.adb: self.history_model.refresh('set_future_tx') + self.utxo_list.refresh_all() # for coin frozen status + self.update_status() # frozen balance @qt_event_listener def on_event_verified(self, *args): diff --git a/electrum/gui/qt/my_treeview.py b/electrum/gui/qt/my_treeview.py index 791033778..363a15198 100644 --- a/electrum/gui/qt/my_treeview.py +++ b/electrum/gui/qt/my_treeview.py @@ -26,7 +26,7 @@ import enum from decimal import Decimal from typing import (Optional, TYPE_CHECKING, Union, List, Dict, Any, - Sequence, Iterable, Type) + Sequence, Iterable, Type, Callable) from PyQt6.QtGui import (QStandardItem, QStandardItemModel, QShowEvent, QPainter, QHelpEvent, QMouseEvent, QAction) @@ -36,12 +36,7 @@ from PyQt6.QtWidgets import (QLabel, QHBoxLayout, QAbstractItemView, QLineEdit, QWidget, QToolButton, QTreeView, QHeaderView, QStyledItemDelegate, QMenu, QStyleOptionViewItem) -from electrum.i18n import _, languages -from electrum.util import FileImportFailed, FileExportFailed, make_aiohttp_session, resource_path -from electrum.util import EventListener, event_listener -from electrum.invoices import PR_UNPAID, PR_PAID, PR_EXPIRED, PR_INFLIGHT, PR_UNKNOWN, PR_FAILED, PR_ROUTING, PR_UNCONFIRMED -from electrum.logging import Logger -from electrum.qrreader import MissingQrDetectionLib +from electrum.i18n import _ from electrum.simple_config import ConfigVarWithConfig from electrum.gui import messages @@ -60,9 +55,18 @@ class QMenuWithConfig(QMenu): self.setToolTipsVisible(True) self.config = config - def addToggle(self, text: str, callback, *, tooltip='') -> QAction: + def addToggle( + self, + text: str, + callback: Callable[[], None], + *, + tooltip: Optional[str] = None, + default_state: bool = False, + ) -> QAction: m = self.addAction(text, callback) m.setCheckable(True) + m.setChecked(default_state) + tooltip = tooltip or "" m.setToolTip(tooltip) return m @@ -70,7 +74,7 @@ class QMenuWithConfig(QMenu): self, configvar: 'ConfigVarWithConfig', *, - callback=None, + callback: Optional[Callable[[], None]] = None, checked: Optional[bool] = None, # to override initial state of checkbox short_desc: Optional[str] = None, ) -> QAction: @@ -80,16 +84,25 @@ class QMenuWithConfig(QMenu): assert short_desc is not None, f"short_desc missing for {configvar}" if checked is None: checked = bool(configvar.get()) - m = self.addAction(short_desc, lambda: self._do_toggle_config(configvar, callback=callback)) - m.setCheckable(True) - m.setChecked(checked) + tooltip = None if (long_desc := configvar.get_long_desc()) is not None: - m.setToolTip(messages.to_rtf(long_desc)) - return m + tooltip = messages.to_rtf(long_desc) + return self.addToggle( + short_desc, + lambda: self._do_toggle_config(configvar, callback=callback), + tooltip=tooltip, + default_state=checked, + ) - def _do_toggle_config(self, configvar: 'ConfigVarWithConfig', *, callback): + def _do_toggle_config( + self, + configvar: 'ConfigVarWithConfig', + *, + callback: Optional[Callable[[], None]] = None, + ): b = configvar.get() configvar.set(not b) + # call cb after configvar state is updated: if callback: callback() diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py index ae2072168..957f727b0 100644 --- a/electrum/gui/qt/utxo_list.py +++ b/electrum/gui/qt/utxo_list.py @@ -90,6 +90,11 @@ class UTXOList(MyTreeView): toolbar, menu = self.create_toolbar_with_menu('') self.num_coins_label = toolbar.itemAt(0).widget() menu.addAction(_('Coin control'), lambda: self.add_selection_to_coincontrol()) + + def cb(): + self.main_window.utxo_list.refresh_all() # for coin frozen status + self.main_window.update_status() # frozen balance + menu.addConfig(config.cv.WALLET_FREEZE_REUSED_ADDRESS_UTXOS, callback=cb) return toolbar @profiler(min_threshold=0.05) diff --git a/electrum/gui/stdio.py b/electrum/gui/stdio.py index e6abc0866..1871c726e 100644 --- a/electrum/gui/stdio.py +++ b/electrum/gui/stdio.py @@ -212,11 +212,11 @@ class ElectrumGui(BaseElectrumGui, EventListener): if c == "n": return try: - tx = self.wallet.create_transaction( + tx = self.wallet.make_unsigned_transaction( outputs=[PartialTxOutput.from_address_and_value(self.str_recipient, amount)], - password=password, fee_policy=FixedFeePolicy(fee), ) + self.wallet.sign_transaction(tx, password) except Exception as e: print(repr(e)) return diff --git a/electrum/gui/text.py b/electrum/gui/text.py index e009a8012..b51d0d01f 100644 --- a/electrum/gui/text.py +++ b/electrum/gui/text.py @@ -691,11 +691,11 @@ class ElectrumGui(BaseElectrumGui, EventListener): password = None fee_policy = FeePolicy(self.config.FEE_POLICY) try: - tx = self.wallet.create_transaction( + tx = self.wallet.make_unsigned_transaction( outputs=invoice.outputs, - password=password, fee_policy=fee_policy, ) + self.wallet.sign_transaction(tx, password) except Exception as e: self.show_message(repr(e)) return diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py index 086000362..d9e9b1734 100644 --- a/electrum/lnwatcher.py +++ b/electrum/lnwatcher.py @@ -3,11 +3,9 @@ # file LICENCE or http://www.opensource.org/licenses/mit-license.php from typing import TYPE_CHECKING -from enum import IntEnum, auto -from .util import log_exceptions, ignore_exceptions, TxMinedInfo, BelowDustLimit +from .util import TxMinedInfo, BelowDustLimit from .util import EventListener, event_listener -from .address_synchronizer import AddressSynchronizer, TX_HEIGHT_LOCAL, TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE from .transaction import Transaction, TxOutpoint from .logging import Logger @@ -18,45 +16,31 @@ if TYPE_CHECKING: from .lnworker import LNWallet from .lnchannel import AbstractChannel -class TxMinedDepth(IntEnum): - """ IntEnum because we call min() in get_deepest_tx_mined_depth_for_txids """ - DEEP = auto() - SHALLOW = auto() - MEMPOOL = auto() - FREE = auto() - class LNWatcher(Logger, EventListener): LOGGING_SHORTCUT = 'W' - def __init__(self, adb: 'AddressSynchronizer', network: 'Network'): - + def __init__(self, lnworker: 'LNWallet'): + self.lnworker = lnworker Logger.__init__(self) - self.adb = adb - self.config = network.config - self.callbacks = {} # address -> lambda: coroutine - self.network = network + self.adb = lnworker.wallet.adb + self.config = lnworker.config + self.callbacks = {} # address -> lambda function + self.network = None self.register_callbacks() # status gets populated when we run self.channel_status = {} - async def stop(self): + def start_network(self, network: 'Network'): + self.network = network + + def stop(self): self.unregister_callbacks() def get_channel_status(self, outpoint): return self.channel_status.get(outpoint, 'unknown') - def add_channel(self, outpoint: str, address: str) -> None: - assert isinstance(outpoint, str) - assert isinstance(address, str) - cb = lambda: self.check_onchain_situation(address, outpoint) - self.add_callback(address, cb) - - async def unwatch_channel(self, address, funding_outpoint): - self.logger.info(f'unwatching {funding_outpoint}') - self.remove_callback(address) - def remove_callback(self, address): self.callbacks.pop(address, None) @@ -64,58 +48,72 @@ class LNWatcher(Logger, EventListener): self.adb.add_address(address) self.callbacks[address] = callback - @event_listener - async def on_event_blockchain_updated(self, *args): - await self.trigger_callbacks() - - @event_listener - async def on_event_wallet_updated(self, wallet): - # called if we add local tx - if wallet.adb != self.adb: - return - await self.trigger_callbacks() - - @event_listener - async def on_event_adb_added_verified_tx(self, adb, tx_hash): - if adb != self.adb: - return - await self.trigger_callbacks() - - @event_listener - async def on_event_adb_set_up_to_date(self, adb): - if adb != self.adb: - return - await self.trigger_callbacks() - - @log_exceptions - async def trigger_callbacks(self): + def trigger_callbacks(self): if not self.adb.synchronizer: self.logger.info("synchronizer not set yet") return for address, callback in list(self.callbacks.items()): - await callback() + callback() - async def check_onchain_situation(self, address, funding_outpoint): + @event_listener + async def on_event_blockchain_updated(self, *args): + # we invalidate the cache on each new block because + # some processes affect the list of sweep transactions + # (hold invoice preimage revealed, MPP completed, etc) + for chan in self.lnworker.channels.values(): + chan._sweep_info.clear() + self.trigger_callbacks() + + @event_listener + def on_event_wallet_updated(self, wallet): + # called if we add local tx + if wallet.adb != self.adb: + return + self.trigger_callbacks() + + @event_listener + def on_event_adb_added_verified_tx(self, adb, tx_hash): + if adb != self.adb: + return + self.trigger_callbacks() + + @event_listener + def on_event_adb_set_up_to_date(self, adb): + if adb != self.adb: + return + self.trigger_callbacks() + + def add_channel(self, chan: 'AbstractChannel') -> None: + outpoint = chan.funding_outpoint.to_str() + address = chan.get_funding_address() + callback = lambda: self.check_onchain_situation(address, outpoint) + callback() # run once, for side effects + if chan.need_to_subscribe(): + self.add_callback(address, callback) + + def unwatch_channel(self, address, funding_outpoint): + self.logger.info(f'unwatching {funding_outpoint}') + self.remove_callback(address) + + def check_onchain_situation(self, address, funding_outpoint): # early return if address has not been added yet if not self.adb.is_mine(address): return # inspect_tx_candidate might have added new addresses, in which case we return early - if not self.adb.is_up_to_date(): - return funding_txid = funding_outpoint.split(':')[0] funding_height = self.adb.get_tx_height(funding_txid) - closing_txid = self.get_spender(funding_outpoint) + closing_txid = self.adb.get_spender(funding_outpoint) closing_height = self.adb.get_tx_height(closing_txid) if closing_txid: closing_tx = self.adb.get_transaction(closing_txid) if closing_tx: - keep_watching = await self.sweep_commitment_transaction(funding_outpoint, closing_tx) + keep_watching = self.sweep_commitment_transaction(funding_outpoint, closing_tx) else: self.logger.info(f"channel {funding_outpoint} closed by {closing_txid}. still waiting for tx itself...") keep_watching = True else: keep_watching = True - await self.update_channel_state( + self.update_channel_state( funding_outpoint=funding_outpoint, funding_txid=funding_txid, funding_height=funding_height, @@ -123,87 +121,14 @@ class LNWatcher(Logger, EventListener): closing_height=closing_height, keep_watching=keep_watching) if not keep_watching: - await self.unwatch_channel(address, funding_outpoint) - - async def sweep_commitment_transaction(self, funding_outpoint: str, closing_tx: Transaction) -> bool: - raise NotImplementedError() # implemented by subclasses - - async def update_channel_state(self, *, funding_outpoint: str, funding_txid: str, - funding_height: TxMinedInfo, closing_txid: str, - closing_height: TxMinedInfo, keep_watching: bool) -> None: - raise NotImplementedError() # implemented by subclasses - - - def get_spender(self, outpoint) -> str: - """ - returns txid spending outpoint. - subscribes to addresses as a side effect. - """ - prev_txid, index = outpoint.split(':') - spender_txid = self.adb.db.get_spent_outpoint(prev_txid, int(index)) - # discard local spenders - tx_mined_status = self.adb.get_tx_height(spender_txid) - if tx_mined_status.height in [TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE]: - spender_txid = None - if not spender_txid: - return - spender_tx = self.adb.get_transaction(spender_txid) - for i, o in enumerate(spender_tx.outputs()): - if o.address is None: - continue - if not self.adb.is_mine(o.address): - self.adb.add_address(o.address) - return spender_txid - - def get_tx_mined_depth(self, txid: str): - if not txid: - return TxMinedDepth.FREE - tx_mined_depth = self.adb.get_tx_height(txid) - height, conf = tx_mined_depth.height, tx_mined_depth.conf - if conf > 20: - return TxMinedDepth.DEEP - elif conf > 0: - return TxMinedDepth.SHALLOW - elif height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT): - return TxMinedDepth.MEMPOOL - elif height in (TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE): - return TxMinedDepth.FREE - elif height > 0 and conf == 0: - # unverified but claimed to be mined - return TxMinedDepth.MEMPOOL - else: - raise NotImplementedError() - - def is_deeply_mined(self, txid): - return self.get_tx_mined_depth(txid) == TxMinedDepth.DEEP - - - -class LNWalletWatcher(LNWatcher): - - def __init__(self, lnworker: 'LNWallet', network: 'Network'): - self.network = network - self.lnworker = lnworker - LNWatcher.__init__(self, lnworker.wallet.adb, network) - - @event_listener - async def on_event_blockchain_updated(self, *args): - # overload parent method with cache invalidation - # we invalidate the cache on each new block because - # some processes affect the list of sweep transactions - # (hold invoice preimage revealed, MPP completed, etc) - for chan in self.lnworker.channels.values(): - chan._sweep_info.clear() - await self.trigger_callbacks() + self.unwatch_channel(address, funding_outpoint) def diagnostic_name(self): return f"{self.lnworker.wallet.diagnostic_name()}-LNW" - @ignore_exceptions - @log_exceptions - async def update_channel_state(self, *, funding_outpoint: str, funding_txid: str, - funding_height: TxMinedInfo, closing_txid: str, - closing_height: TxMinedInfo, keep_watching: bool) -> None: + def update_channel_state(self, *, funding_outpoint: str, funding_txid: str, + funding_height: TxMinedInfo, closing_txid: str, + closing_height: TxMinedInfo, keep_watching: bool) -> None: chan = self.lnworker.channel_by_txo(funding_outpoint) if not chan: return @@ -213,22 +138,24 @@ class LNWalletWatcher(LNWatcher): closing_txid=closing_txid, closing_height=closing_height, keep_watching=keep_watching) - await self.lnworker.handle_onchain_state(chan) + self.lnworker.handle_onchain_state(chan) - @log_exceptions - async def sweep_commitment_transaction(self, funding_outpoint, closing_tx) -> bool: + def sweep_commitment_transaction(self, funding_outpoint, closing_tx) -> bool: """This function is called when a channel was closed. In this case we need to check for redeemable outputs of the commitment transaction or spenders down the line (HTLC-timeout/success transactions). - Returns whether we should continue to monitor.""" + Returns whether we should continue to monitor. + + Side-effécts: + - sets defaults labels + """ chan = self.lnworker.channel_by_txo(funding_outpoint) if not chan: return False # detect who closed and get information about how to claim outputs sweep_info_dict = chan.sweep_ctx(closing_tx) - #self.logger.info(f"do_breach_remedy: {[x.name for x in sweep_info_dict.values()]}") - keep_watching = False if sweep_info_dict else not self.is_deeply_mined(closing_tx.txid()) + keep_watching = False if sweep_info_dict else not self.adb.is_deeply_mined(closing_tx.txid()) # create and broadcast transactions for prevout, sweep_info in sweep_info_dict.items(): prev_txid, prev_index = prevout.split(':') @@ -238,19 +165,19 @@ class LNWalletWatcher(LNWatcher): # do not keep watching if prevout does not exist self.logger.info(f'prevout does not exist for {name}: {prevout}') continue - spender_txid = self.get_spender(prevout) + spender_txid = self.adb.get_spender(prevout) spender_tx = self.adb.get_transaction(spender_txid) if spender_txid else None if spender_tx: # the spender might be the remote, revoked or not htlc_sweepinfo = chan.maybe_sweep_htlcs(closing_tx, spender_tx) for prevout2, htlc_sweep_info in htlc_sweepinfo.items(): - htlc_tx_spender = self.get_spender(prevout2) + htlc_tx_spender = self.adb.get_spender(prevout2) self.lnworker.wallet.set_default_label(prevout2, htlc_sweep_info.name) if htlc_tx_spender: - keep_watching |= not self.is_deeply_mined(htlc_tx_spender) + keep_watching |= not self.adb.is_deeply_mined(htlc_tx_spender) else: keep_watching |= self.maybe_redeem(htlc_sweep_info) - keep_watching |= not self.is_deeply_mined(spender_txid) + keep_watching |= not self.adb.is_deeply_mined(spender_txid) self.maybe_extract_preimage(chan, spender_tx, prevout) else: keep_watching |= self.maybe_redeem(sweep_info) @@ -270,5 +197,5 @@ class LNWalletWatcher(LNWatcher): spender_txin = spender_tx.inputs()[txin_idx] chan.extract_preimage_from_htlc_txin( spender_txin, - is_deeply_mined=self.is_deeply_mined(spender_tx.txid()), + is_deeply_mined=self.adb.is_deeply_mined(spender_tx.txid()), ) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 525d531fa..24152a9b4 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -69,7 +69,7 @@ from .lnmsg import decode_msg from .lnrouter import ( RouteEdge, LNPaymentRoute, LNPaymentPath, is_route_within_budget, NoChannelPolicy, LNPathInconsistent ) -from .lnwatcher import LNWalletWatcher +from .lnwatcher import LNWatcher from .submarine_swaps import SwapManager from .mpp_split import suggest_splits, SplitConfigRating from .trampoline import ( @@ -817,7 +817,7 @@ class PaySession(Logger): class LNWallet(LNWorker): - lnwatcher: Optional['LNWalletWatcher'] + lnwatcher: Optional['LNWatcher'] MPP_EXPIRY = 120 TIMEOUT_SHUTDOWN_FAIL_PENDING_HTLCS = 3 # seconds PAYMENT_TIMEOUT = 120 @@ -842,7 +842,7 @@ class LNWallet(LNWorker): if self.config.EXPERIMENTAL_LN_FORWARD_PAYMENTS and self.config.LIGHTNING_USE_GOSSIP: features |= LnFeatures.GOSSIP_QUERIES_OPT # signal we have gossip to fetch LNWorker.__init__(self, self.node_keypair, features, config=self.config) - self.lnwatcher = None + self.lnwatcher = LNWatcher(self) self.lnrater: LNRater = None self.payment_info = self.db.get_dict('lightning_payments') # RHASH -> amount, direction, is_paid self.preimages = self.db.get_dict('lightning_preimages') # RHASH -> preimage @@ -890,6 +890,13 @@ class LNWallet(LNWorker): self.nostr_keypair = generate_keypair(BIP32Node.from_xkey(xprv), LnKeyFamily.NOSTR_KEY) self.swap_manager = SwapManager(wallet=self.wallet, lnworker=self) self.onion_message_manager = OnionMessageManager(self) + self.subscribe_to_channels() + + def subscribe_to_channels(self): + for chan in self.channels.values(): + self.lnwatcher.add_channel(chan) + for cb in self.channel_backups.values(): + self.lnwatcher.add_channel(cb) def has_deterministic_node_id(self) -> bool: return bool(self.db.get('lightning_xprv')) @@ -970,18 +977,11 @@ class LNWallet(LNWorker): def start_network(self, network: 'Network'): super().start_network(network) - self.lnwatcher = LNWalletWatcher(self, network) + self.lnwatcher.start_network(network) self.swap_manager.start_network(network) self.lnrater = LNRater(self, network) self.onion_message_manager.start_network(network=network) - for chan in self.channels.values(): - if chan.need_to_subscribe(): - self.lnwatcher.add_channel(chan.funding_outpoint.to_str(), chan.get_funding_address()) - for cb in self.channel_backups.values(): - if cb.need_to_subscribe(): - self.lnwatcher.add_channel(cb.funding_outpoint.to_str(), cb.get_funding_address()) - for coro in [ self.maybe_listen(), self.lnwatcher.trigger_callbacks(), # shortcut (don't block) if funding tx locked and verified @@ -999,7 +999,7 @@ class LNWallet(LNWorker): await self.wait_for_received_pending_htlcs_to_get_removed() await LNWorker.stop(self) if self.lnwatcher: - await self.lnwatcher.stop() + self.lnwatcher.stop() self.lnwatcher = None if self.swap_manager and self.swap_manager.network: # may not be present in tests await self.swap_manager.stop() @@ -1193,15 +1193,15 @@ class LNWallet(LNWorker): if chan.funding_outpoint.to_str() == txo: return chan - async def handle_onchain_state(self, chan: Channel): + def handle_onchain_state(self, chan: Channel): if type(chan) is ChannelBackup: util.trigger_callback('channel', self.wallet, chan) return if (chan.get_state() in (ChannelState.OPEN, ChannelState.SHUTDOWN) - and chan.should_be_closed_due_to_expiring_htlcs(self.network.get_local_height())): + and chan.should_be_closed_due_to_expiring_htlcs(self.wallet.adb.get_local_height())): self.logger.info(f"force-closing due to expiring htlcs") - await self.schedule_force_closing(chan.channel_id) + asyncio.ensure_future(self.schedule_force_closing(chan.channel_id)) elif chan.get_state() == ChannelState.FUNDED: peer = self._peers.get(chan.node_id) @@ -1220,7 +1220,7 @@ class LNWallet(LNWorker): height = self.lnwatcher.adb.get_tx_height(txid).height if height == TX_HEIGHT_LOCAL: self.logger.info('REBROADCASTING CLOSING TX') - await self.network.try_broadcasting(force_close_tx, 'force-close') + asyncio.ensure_future(self.network.try_broadcasting(force_close_tx, 'force-close')) def get_peer_by_static_jit_scid_alias(self, scid_alias: bytes) -> Optional[Peer]: for nodeid, peer in self.peers.items(): @@ -1363,7 +1363,7 @@ class LNWallet(LNWorker): def add_channel(self, chan: Channel): with self.lock: self._channels[chan.channel_id] = chan - self.lnwatcher.add_channel(chan.funding_outpoint.to_str(), chan.get_funding_address()) + self.lnwatcher.add_channel(chan) def add_new_channel(self, chan: Channel): self.add_channel(chan) @@ -1953,7 +1953,7 @@ class LNWallet(LNWorker): We first try to conduct the payment over a single channel. If that fails and mpp is supported by the receiver, we will split the payment.""" trampoline_features = LnFeatures.VAR_ONION_OPT - local_height = self.network.get_local_height() + local_height = self.wallet.adb.get_local_height() fee_related_error = None # type: Optional[FeeBudgetExceeded] if channels: my_active_channels = channels @@ -3069,7 +3069,7 @@ class LNWallet(LNWorker): self.wallet.set_reserved_addresses_for_chan(cb, reserved=True) self.wallet.save_db() util.trigger_callback('channels_updated', self.wallet) - self.lnwatcher.add_channel(cb.funding_outpoint.to_str(), cb.get_funding_address()) + self.lnwatcher.add_channel(cb) def has_conflicting_backup_with(self, remote_node_id: bytes): """ Returns whether we have an active channel with this node on another device, using same local node id. """ @@ -3186,7 +3186,7 @@ class LNWallet(LNWorker): with self.lock: self._channel_backups[bfh(channel_id)] = cb util.trigger_callback('channels_updated', self.wallet) - self.lnwatcher.add_channel(cb.funding_outpoint.to_str(), cb.get_funding_address()) + self.lnwatcher.add_channel(cb) def save_forwarding_failure( self, payment_key:str, *, diff --git a/electrum/plugins/watchtower/watchtower.py b/electrum/plugins/watchtower/watchtower.py index 1b59d328c..b992a66a4 100644 --- a/electrum/plugins/watchtower/watchtower.py +++ b/electrum/plugins/watchtower/watchtower.py @@ -24,19 +24,21 @@ # SOFTWARE. -import asyncio, os +import asyncio +import os from typing import TYPE_CHECKING -from typing import NamedTuple, Dict +from typing import Dict from electrum.util import log_exceptions, random_shuffled_copy -from electrum.plugin import BasePlugin, hook +from electrum.plugin import BasePlugin from electrum.sql_db import SqlDB, sql -from electrum.lnwatcher import LNWatcher from electrum.transaction import Transaction, match_script_against_template from electrum.network import Network from electrum.address_synchronizer import AddressSynchronizer, TX_HEIGHT_LOCAL from electrum.wallet_db import WalletDB from electrum.lnutil import WITNESS_TEMPLATE_RECEIVED_HTLC, WITNESS_TEMPLATE_OFFERED_HTLC +from electrum.logging import Logger +from electrum.util import EventListener, event_listener from .server import WatchTowerServer @@ -60,23 +62,70 @@ class WatchtowerPlugin(BasePlugin): asyncio.run_coroutine_threadsafe(self.network.taskgroup.spawn(self.server.run), self.network.asyncio_loop) -class WatchTower(LNWatcher): +class WatchTower(Logger, EventListener): LOGGING_SHORTCUT = 'W' def __init__(self, network: 'Network'): - adb = AddressSynchronizer(WalletDB('', storage=None, upgrade=True), network.config, name=self.diagnostic_name()) - adb.start_network(network) - LNWatcher.__init__(self, adb, network) + Logger.__init__(self) + self.adb = AddressSynchronizer(WalletDB('', storage=None, upgrade=True), network.config, name=self.diagnostic_name()) + self.adb.start_network(network) + self.config = network.config + self.callbacks = {} # address -> lambda function + self.register_callbacks() + # status gets populated when we run + self.channel_status = {} self.network = network self.sweepstore = SweepStore(os.path.join(self.network.config.path, "watchtower_db"), network) + def remove_callback(self, address): + self.callbacks.pop(address, None) + + def add_callback(self, address, callback): + self.adb.add_address(address) + self.callbacks[address] = callback + + @event_listener + async def on_event_blockchain_updated(self, *args): + await self.trigger_callbacks() + + @event_listener + async def on_event_wallet_updated(self, wallet): + # called if we add local tx + if wallet.adb != self.adb: + return + await self.trigger_callbacks() + + @event_listener + async def on_event_adb_added_verified_tx(self, adb, tx_hash): + if adb != self.adb: + return + await self.trigger_callbacks() + + @event_listener + async def on_event_adb_set_up_to_date(self, adb): + if adb != self.adb: + return + await self.trigger_callbacks() + + @log_exceptions + async def trigger_callbacks(self): + if not self.adb.synchronizer: + self.logger.info("synchronizer not set yet") + return + for address, callback in list(self.callbacks.items()): + await callback() + async def stop(self): - await super().stop() + self.unregister_callbacks() await self.adb.stop() + def add_channel(self, outpoint: str, address: str) -> None: + callback = lambda: self.check_onchain_situation(address, outpoint) + self.add_callback(address, callback) + def diagnostic_name(self): - return "local_tower" + return "watchtower" @log_exceptions async def start_watching(self): @@ -85,6 +134,24 @@ class WatchTower(LNWatcher): for outpoint, address in random_shuffled_copy(lst): self.add_channel(outpoint, address) + async def check_onchain_situation(self, address, funding_outpoint): + # early return if address has not been added yet + if not self.adb.is_mine(address): + return + # inspect_tx_candidate might have added new addresses, in which case we return early + closing_txid = self.adb.get_spender(funding_outpoint) + if closing_txid: + closing_tx = self.adb.get_transaction(closing_txid) + if closing_tx: + keep_watching = await self.sweep_commitment_transaction(funding_outpoint, closing_tx) + else: + self.logger.info(f"channel {funding_outpoint} closed by {closing_txid}. still waiting for tx itself...") + keep_watching = True + else: + keep_watching = True + if not keep_watching: + await self.unwatch_channel(address, funding_outpoint) + def inspect_tx_candidate(self, outpoint, n: int) -> Dict[str, str]: """ returns a dict of spenders for a transaction of interest. @@ -99,7 +166,7 @@ class WatchTower(LNWatcher): if n == 0: if spender_txid is None: self.channel_status[outpoint] = 'open' - elif not self.is_deeply_mined(spender_txid): + elif not self.adb.is_deeply_mined(spender_txid): self.channel_status[outpoint] = 'closed (%d)' % self.adb.get_tx_height(spender_txid).conf else: self.channel_status[outpoint] = 'closed (deep)' @@ -133,7 +200,7 @@ class WatchTower(LNWatcher): if not self.adb.is_mine(o.address): self.adb.add_address(o.address) elif n < 2: - r = self.inspect_tx_candidate(spender_txid+':%d'%i, n+1) + r = self.inspect_tx_candidate(spender_txid + ':%d' % i, n + 1) result.update(r) return result @@ -142,7 +209,7 @@ class WatchTower(LNWatcher): keep_watching = False for prevout, spender in spenders.items(): if spender is not None: - keep_watching |= not self.is_deeply_mined(spender) + keep_watching |= not self.adb.is_deeply_mined(spender) continue sweep_txns = await self.sweepstore.get_sweep_tx(funding_outpoint, prevout) for tx in sweep_txns: @@ -184,14 +251,9 @@ class WatchTower(LNWatcher): return self.network.run_from_another_thread(f()) async def unwatch_channel(self, address, funding_outpoint): - await super().unwatch_channel(address, funding_outpoint) await self.sweepstore.remove_sweep_tx(funding_outpoint) await self.sweepstore.remove_channel(funding_outpoint) - async def update_channel_state(self, *args, **kwargs): - pass - - create_sweep_txs=""" CREATE TABLE IF NOT EXISTS sweep_txs ( @@ -288,5 +350,3 @@ class SweepStore(SqlDB): c = self.conn.cursor() c.execute("SELECT outpoint, address FROM channel_info") return [(r[0], r[1]) for r in c.fetchall()] - - diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 66d7d55c8..c9eaea2a0 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -605,6 +605,13 @@ class SimpleConfig(Logger): short_desc=lambda: _('Send change to Lightning'), long_desc=lambda: _('If possible, send the change of this transaction to your channels, with a submarine swap'), ) + WALLET_FREEZE_REUSED_ADDRESS_UTXOS = ConfigVar( + 'wallet_freeze_reused_address_utxos', default=False, type_=bool, + short_desc=lambda: _('Avoid spending from used addresses'), + long_desc=lambda: _("""Automatically freeze coins received to already used addresses. +This can eliminate a serious privacy issue where a malicious user can track your spends by sending small payments +to a previously-paid address of yours that would then be included with unrelated inputs in your future payments."""), + ) FX_USE_EXCHANGE_RATE = ConfigVar('use_exchange_rate', default=False, type_=bool) FX_CURRENCY = ConfigVar('currency', default='EUR', type_=str) diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index 3704a88ca..d4a1bbaf0 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -179,6 +179,7 @@ class SwapManager(Logger): self.wallet = wallet self.config = wallet.config self.lnworker = lnworker + self.lnwatcher = self.lnworker.lnwatcher self.config = wallet.config self.taskgroup = OldTaskGroup() self.dummy_address = DummyAddress.SWAP @@ -207,7 +208,6 @@ class SwapManager(Logger): return self.logger.info('start_network: starting main loop') self.network = network - self.lnwatcher = self.lnworker.lnwatcher for k, swap in self.swaps.items(): if swap.is_redeemed: continue @@ -321,8 +321,7 @@ class SwapManager(Logger): if sha256(preimage) == swap.payment_hash: return preimage - @log_exceptions - async def _claim_swap(self, swap: SwapData) -> None: + def _claim_swap(self, swap: SwapData) -> None: assert self.network assert self.lnwatcher if not self.lnwatcher.adb.is_up_to_date(): @@ -772,16 +771,15 @@ class SwapManager(Logger): # this is taken care of in wallet._is_rbf_allowed_to_touch_tx_output if tx is None: funding_output = self.create_funding_output(swap) - tx = self.wallet.create_transaction( + tx = self.wallet.make_unsigned_transaction( outputs=[funding_output], rbf=True, - password=password, fee_policy=fee_policy, ) else: tx.replace_output_address(DummyAddress.SWAP, swap.lockup_address) tx.set_rbf(True) - self.wallet.sign_transaction(tx, password) + self.wallet.sign_transaction(tx, password) return tx @log_exceptions diff --git a/electrum/txbatcher.py b/electrum/txbatcher.py index 3800583ac..23300300b 100644 --- a/electrum/txbatcher.py +++ b/electrum/txbatcher.py @@ -391,16 +391,16 @@ class TxBatch(Logger): txin.witness_script = sweep_info.txin.witness_script txin.script_sig = sweep_info.txin.script_sig # create tx - tx = self.wallet.create_transaction( + tx = self.wallet.make_unsigned_transaction( fee_policy=self.fee_policy, base_tx=base_tx, inputs=inputs, outputs=outputs, - password=password, locktime=locktime, BIP69_sort=False, merge_duplicate_outputs=False, ) + self.wallet.sign_transaction(tx, password) # this assert will fail if we merge duplicate outputs for o in outputs: assert o in tx.outputs() assert tx.is_complete() diff --git a/electrum/wallet.py b/electrum/wallet.py index 132c46d62..b92a8df0a 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -1830,7 +1830,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): @profiler(min_threshold=0.1) def make_unsigned_transaction( self, *, - coins: Sequence[PartialTxInput], + coins: Optional[Sequence[PartialTxInput]] = None, outputs: List[PartialTxOutput], inputs: Optional[List[PartialTxInput]] = None, fee_policy: FeePolicy, @@ -1841,9 +1841,13 @@ class Abstract_Wallet(ABC, Logger, EventListener): base_tx: Optional[Transaction] = None, send_change_to_lightning: bool = False, merge_duplicate_outputs: bool = False, + locktime: Optional[int] = None, + tx_version: Optional[int] = None, ) -> PartialTransaction: """Can raise NotEnoughFunds or NoDynamicFeeEstimates.""" + if coins is None: + coins = self.get_spendable_coins() if not inputs and not coins: # any bitcoin tx must have at least 1 input by consensus raise NotEnoughFunds() if any([c.already_has_some_signatures() for c in coins]): @@ -1957,8 +1961,12 @@ class Abstract_Wallet(ABC, Logger, EventListener): tx = PartialTransaction.from_io(list(coins), list(outputs)) assert len(tx.outputs()) > 0, "any bitcoin tx must have at least 1 output by consensus" - # Timelock tx to current height. - tx.locktime = get_locktime_for_new_transaction(self.network) + if locktime is None: + # Timelock tx to current height. + locktime = get_locktime_for_new_transaction(self.network) + tx.locktime = locktime + if tx_version is not None: + tx.version = tx_version tx.rbf_merge_txid = rbf_merge_txid tx.add_info_from_wallet(self) run_hook('make_unsigned_transaction', self, tx) @@ -1976,8 +1984,15 @@ class Abstract_Wallet(ABC, Logger, EventListener): if frozen is not None: # user has explicitly set the state return bool(frozen) # State not set. We implicitly mark certain coins as frozen: + tx_mined_status = self.adb.get_tx_height(utxo.prevout.txid.hex()) + if tx_mined_status.height == TX_HEIGHT_FUTURE: + return True if self._is_coin_small_and_unconfirmed(utxo): return True + addr = utxo.address + assert addr is not None + if self.config.WALLET_FREEZE_REUSED_ADDRESS_UTXOS and self.adb.is_used_as_from_address(addr): + return True return False def _is_coin_small_and_unconfirmed(self, utxo: PartialTxInput) -> bool: @@ -3096,50 +3111,6 @@ class Abstract_Wallet(ABC, Logger, EventListener): def get_all_known_addresses_beyond_gap_limit(self) -> Set[str]: pass - def create_transaction( - self, - outputs, - *, - fee_policy: FeePolicy, - change_addr=None, - domain_addr=None, - domain_coins=None, - sign=True, - rbf=True, - password=None, - locktime=None, - tx_version: Optional[int] = None, - base_tx: Optional[PartialTransaction] = None, - inputs: Optional[List[PartialTxInput]] = None, - send_change_to_lightning: Optional[bool] = None, - merge_duplicate_outputs: Optional[bool] = None, - nonlocal_only: bool = False, - BIP69_sort: bool = True, - ) -> PartialTransaction: - """Helper function for make_unsigned_transaction.""" - coins = self.get_spendable_coins(domain_addr, nonlocal_only=nonlocal_only) - if domain_coins is not None: - coins = [coin for coin in coins if (coin.prevout.to_str() in domain_coins)] - tx = self.make_unsigned_transaction( - coins=coins, - inputs=inputs, - outputs=outputs, - fee_policy=fee_policy, - change_addr=change_addr, - base_tx=base_tx, - send_change_to_lightning=send_change_to_lightning, - merge_duplicate_outputs=merge_duplicate_outputs, - rbf=rbf, - BIP69_sort=BIP69_sort, - ) - if locktime is not None: - tx.locktime = locktime - if tx_version is not None: - tx.version = tx_version - if sign: - self.sign_transaction(tx, password) - return tx - def _check_risk_of_burning_coins_as_fees(self, tx: 'PartialTransaction') -> TxSighashDanger: """Helper method to check if there is risk of burning coins as fees if we sign. Note that if not all inputs are ismine, e.g. coinjoin, the risk is not just about fees. @@ -3390,12 +3361,10 @@ class Abstract_Wallet(ABC, Logger, EventListener): return name = sweep_info.name # outputs = [] will send coins to a change address - tx = self.create_transaction( + tx = self.make_unsigned_transaction( inputs=[txin], outputs=[], - password=None, fee_policy=FixedFeePolicy(0), - sign=False, ) try: self.adb.add_transaction(tx) diff --git a/tests/test_invoices.py b/tests/test_invoices.py index 20accb917..a22a8e189 100644 --- a/tests/test_invoices.py +++ b/tests/test_invoices.py @@ -72,7 +72,8 @@ class TestWalletPaymentRequests(ElectrumTestCase): # get paid onchain wallet2 = self.create_wallet2() # type: Standard_Wallet outputs = [PartialTxOutput.from_address_and_value(pr.get_address(), pr.get_amount_sat())] - tx = wallet2.create_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000)) + tx = wallet2.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000)) + wallet2.sign_transaction(tx, password=None) wallet1.adb.receive_tx_callback(tx, TX_HEIGHT_UNCONFIRMED) self.assertEqual(PR_UNCONFIRMED, wallet1.get_invoice_status(pr)) # tx gets mined @@ -102,7 +103,8 @@ class TestWalletPaymentRequests(ElectrumTestCase): # get paid onchain wallet2 = self.create_wallet2() # type: Standard_Wallet outputs = [PartialTxOutput.from_address_and_value(pr.get_address(), pr.get_amount_sat())] - tx = wallet2.create_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000)) + tx = wallet2.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000)) + wallet2.sign_transaction(tx, password=None) wallet1.adb.receive_tx_callback(tx, TX_HEIGHT_UNCONFIRMED) self.assertEqual(PR_UNCONFIRMED, wallet1.get_invoice_status(pr)) # tx gets mined @@ -132,7 +134,8 @@ class TestWalletPaymentRequests(ElectrumTestCase): # get paid onchain wallet2 = self.create_wallet2() # type: Standard_Wallet outputs = [PartialTxOutput.from_address_and_value(pr.get_address(), pr.get_amount_sat())] - tx = wallet2.create_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000)) + tx = wallet2.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000)) + wallet2.sign_transaction(tx, password=None) wallet1.adb.receive_tx_callback(tx, TX_HEIGHT_UNCONFIRMED) self.assertEqual(PR_UNCONFIRMED, wallet1.get_invoice_status(pr)) # tx mined in the past (before invoice creation) @@ -201,7 +204,8 @@ class TestWalletPaymentRequests(ElectrumTestCase): # pr2 gets paid onchain wallet2 = self.create_wallet2() # type: Standard_Wallet outputs = [PartialTxOutput.from_address_and_value(pr2.get_address(), pr2.get_amount_sat())] - tx = wallet2.create_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000)) + tx = wallet2.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000)) + wallet2.sign_transaction(tx, password=None) wallet1.adb.receive_tx_callback(tx, TX_HEIGHT_UNCONFIRMED) self.assertEqual(PR_UNCONFIRMED, wallet1.get_invoice_status(pr2)) self.assertEqual(pr2, wallet1.get_request_by_addr(addr1)) diff --git a/tests/test_lnpeer.py b/tests/test_lnpeer.py index 940e72247..3b1630cbf 100644 --- a/tests/test_lnpeer.py +++ b/tests/test_lnpeer.py @@ -108,8 +108,12 @@ class MockBlockchain: class MockADB: + def __init__(self): + self._blockchain = MockBlockchain() def add_transaction(self, tx): pass + def get_local_height(self): + return self._blockchain.height() class MockWallet: receive_requests = {} diff --git a/tests/test_wallet_vertical.py b/tests/test_wallet_vertical.py index f2411abfb..d92f8cae0 100644 --- a/tests/test_wallet_vertical.py +++ b/tests/test_wallet_vertical.py @@ -873,7 +873,8 @@ class TestWalletSending(ElectrumTestCase): # wallet1 -> wallet2 outputs = [PartialTxOutput.from_address_and_value(wallet2.get_receiving_address(), 250000)] - tx = wallet1.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False) + tx = wallet1.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False) + wallet1.sign_transaction(tx, password=None) self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_segwit()) @@ -892,7 +893,8 @@ class TestWalletSending(ElectrumTestCase): # wallet2 -> wallet1 outputs = [PartialTxOutput.from_address_and_value(wallet1.get_receiving_address(), 100000)] - tx = wallet2.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False) + tx = wallet2.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False) + wallet2.sign_transaction(tx, password=None) self.assertTrue(tx.is_complete()) self.assertFalse(tx.is_segwit()) @@ -945,7 +947,8 @@ class TestWalletSending(ElectrumTestCase): # wallet1 -> wallet2 outputs = [PartialTxOutput.from_address_and_value(wallet2.get_receiving_address(), 370000)] - tx = wallet1a.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False) + tx = wallet1a.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False) + wallet1a.sign_transaction(tx, password=None) partial_tx = tx.serialize_as_bytes().hex() self.assertEqual("70736274ff01007501000000017120d4e1f2cdfe7df000d632cff74167fb354f0546d5cfc228e5c98756d55cb20100000000feffffff0250a50500000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac2862b1000000000017a9142e517854aa54668128c0e9a3fdd4dec13ad571368700000000000100e0010000000001014121f99dc02f0364d2dab3d08905ff4c36fc76c55437fd90b769c35cc18618280100000000fdffffff02d4c22d00000000001600143fd1bc5d32245850c8cb5be5b09c73ccbb9a0f75001bb7000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e4887024830450221008781c78df0c9d4b5ea057333195d5d76bc29494d773f14fa80e27d2f288b2c360220762531614799b6f0fb8d539b18cb5232ab4253dd4385435157b28a44ff63810d0121033de77d21926e09efd04047ae2d39dbd3fb9db446e8b7ed53e0f70f9c9478f735dac11300220202afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f284730440220751ee3599e59debb8b2aeef61bb5f574f26379cd961caf382d711a507bc632390220598d53e62557c4a5ab8cfb2f8948f37cca06a861714b55c781baf2c3d7a580b501010469522102afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f2821030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf2103e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce53ae220602afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f280c0036e9ac00000000000000002206030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf0c48adc7a00000000000000000220603e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce0cdb692427000000000000000000000100695221022ec6f62b0f3b7c2446f44346bff0a6f06b5fdbc27368be8a36478e0287fe47be21024238f21f90527dc87e945f389f3d1711943b06f0a738d5baab573fc0ab6c98582102b7139e93747d7c77f62af5a38b8a2b009f3456aa94dea9bf21f73a6298c867a253ae2202022ec6f62b0f3b7c2446f44346bff0a6f06b5fdbc27368be8a36478e0287fe47be0cdb69242701000000000000002202024238f21f90527dc87e945f389f3d1711943b06f0a738d5baab573fc0ab6c98580c0036e9ac0100000000000000220202b7139e93747d7c77f62af5a38b8a2b009f3456aa94dea9bf21f73a6298c867a20c48adc7a0010000000000000000", partial_tx) @@ -970,7 +973,7 @@ class TestWalletSending(ElectrumTestCase): # wallet2 -> wallet1 outputs = [PartialTxOutput.from_address_and_value(wallet1a.get_receiving_address(), 100000)] - tx = wallet2.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False, sign=False) + tx = wallet2.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False) self.assertEqual( "pkh(045f7ba332df2a7b4f5d13f246e307c9174cfa9b8b05f3b83410a3c23ef8958d610be285963d67c7bc1feb082f168fa9877c25999963ff8b56b242a852b23e25ed)", tx.inputs()[0].script_descriptor.to_string_no_checksum()) @@ -1044,7 +1047,7 @@ class TestWalletSending(ElectrumTestCase): # wallet1 -> wallet2 outputs = [PartialTxOutput.from_address_and_value(wallet2a.get_receiving_address(), 165000)] - tx = wallet1a.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False, sign=False) + tx = wallet1a.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False) self.assertEqual((0, 2), tx.signature_count()) self.assertEqual( "wsh(sortedmulti(2,[b2e35a7d/1h]tpubD9aPYLPPYw8MxU3cD57LwpV5v7GomHxdv62MSbPcRkp47zwXx69ACUFsKrj8xzuzRrij9FWVhfvkvNqtqsr8ZtefkDsGZ9GLuHzoS6bXyk1/0/0,[53b77ddb/1h]tpubD8spLJysN7v7V1KHvkZ7AwjnXShKafopi7Vu3Ahs2S46FxBPTode8DgGxDo55k4pJvETGScZFwnM5f2Y31EUjteJdhxR73sjr9ieydgah2U/0/0,[43067d63/1h]tpubD8khd1g1tzFeKeaU59QV811hyvhwn9KDfy5sqFJ5m2wJLw6rUt4AZviqutRPXTUAK4SpU2we3y2WBP916Ma8Em4qFGcbYkFvXVfpGYV3oZR/0/0))", @@ -1079,7 +1082,8 @@ class TestWalletSending(ElectrumTestCase): # wallet2 -> wallet1 outputs = [PartialTxOutput.from_address_and_value(wallet1a.get_receiving_address(), 100000)] - tx = wallet2a.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False) + tx = wallet2a.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False) + wallet2a.sign_transaction(tx, password=None) self.assertEqual((1, 2), tx.signature_count()) self.assertEqual( "sh(wsh(sortedmulti(2,[d1dbcc21]tpubDDsv4RpsGViZeEVwivuj3aaKhFQSv1kYsz64mwRoHkqBfw8qBSYEmc8TtyVGotJb44V3pviGzefP9m9hidRg9dPPaDWL2yoRpMW3hdje3Rk/0/0,[17cea914]tpubDCZU2kACPGACYDvAXvZUXQ7cE7msFfCtpah5QCuaz8iarKMLTgR4c2u8RGKdFhbb3YJxzmktDd1rCtF58ksyVgFw28pchY55uwkDiXjY9hU/0/0)))", @@ -1141,7 +1145,8 @@ class TestWalletSending(ElectrumTestCase): # wallet1 -> wallet2 outputs = [PartialTxOutput.from_address_and_value(wallet2.get_receiving_address(), 1000000)] - tx = wallet1a.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False) + tx = wallet1a.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False) + wallet1a.sign_transaction(tx, password=None) self.assertTrue(tx.is_complete()) self.assertFalse(tx.is_segwit()) @@ -1160,7 +1165,8 @@ class TestWalletSending(ElectrumTestCase): # wallet2 -> wallet1 outputs = [PartialTxOutput.from_address_and_value(wallet1a.get_receiving_address(), 300000)] - tx = wallet2.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False) + tx = wallet2.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False) + wallet2.sign_transaction(tx, password=None) self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_segwit()) @@ -2323,7 +2329,7 @@ class TestWalletSending(ElectrumTestCase): # wallet1 creates tx1, with output back to himself outputs = [PartialTxOutput.from_address_and_value("tb1qhye4wfp26kn0l7ynpn5a4hvt539xc3zf0n76t3", 10_000_000)] - tx1 = wallet1.create_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), tx_version=2, rbf=True, sign=False) + tx1 = wallet1.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), tx_version=2, rbf=True) tx1.locktime = 1607022 partial_tx1 = tx1.serialize_as_bytes().hex() self.assertEqual("70736274ff0100710200000001d5bd4f8ebe63f0521f94e2d174b95d4327757a7e74fda3c9ff5c08796318f8d80000000000fdffffff02b82e0f0000000000160014250dbabd5761d7e0773d6147699938dd08ec2eb88096980000000000160014b93357242ad5a6fff8930ce9dadd8ba44a6c44496e8518000001011fc0d8a70000000000160014aba1c9faecc3f8882e641583e8734a3f9d01b15a0100df0200000000010162ecbac2f0c8662f53505d9410fdc56c84c5642ddbd3358d9a27d564e26731130200000000fdffffff02c0d8a70000000000160014aba1c9faecc3f8882e641583e8734a3f9d01b15ab89ed5000000000016001470afbd97b2dc351bd167f714e294b2fd3b60aedf02483045022100c93449989510e279eb14a0193d5c262ae93034b81376a1f6be259c6080d3ba5d0220536ab394f7c20f301d7ec2ef11be6e7b6d492053dce56458931c1b54218ec0fd012103b8f5a11df8e68cf335848e83a41fdad3c7413dc42148248a3799b58c93919ca01085180022060205e8db1b1906219782fadb18e763c0874a3118a17ce931e01707cbde194e041510775087560000008000000000000000000022020240ef5d2efee3b04b313a254df1b13a0b155451581e73943b21f3346bf6e1ba351077508756000000800100000000000000002202024a410b1212e88573561887b2bc38c90c074e4be425b9f3d971a9207825d9d3c8107750875600000080000000000100000000", @@ -2335,7 +2341,7 @@ class TestWalletSending(ElectrumTestCase): # wallet2 creates tx2, with output back to himself outputs = [PartialTxOutput.from_address_and_value("tb1qufnj5k2rrsnpjq7fg6d2pq3q9um6skdyyehw5m", 10_000_000)] - tx2 = wallet2.create_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), tx_version=2, rbf=True, sign=False) + tx2 = wallet2.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), tx_version=2, rbf=True) tx2.locktime = 1607023 partial_tx2 = tx2.serialize_as_bytes().hex() self.assertEqual("70736274ff0100710200000001e546bc0a7c9736e82a07df5c24fe6d05df58a310dc376cf09302842ca7264f930100000000fdffffff02988d07000000000016001453675a59be834aa6d139c3ebea56646a9b160c4c8096980000000000160014e2672a59431c261903c9469aa082202f37a859a46f8518000001011fa037a000000000001600140719d12228c61cab793ecd659c09cfe565a845c30100df02000000000101d5bd4f8ebe63f0521f94e2d174b95d4327757a7e74fda3c9ff5c08796318f8d80100000000fdffffff025066350000000000160014e3aa82aa2e754507d5585c0b6db06cc0cb4927b7a037a000000000001600140719d12228c61cab793ecd659c09cfe565a845c302483045022100f42e27519bd2379c22951c16b038fa6d49164fe6802854f2fdc7ee87fe31a8bc02204ea71e9324781b44bf7fea2f318caf3bedc5b497cbd1b4313fa71f833500bcb7012103a7853e1ee02a1629c8e870ec694a1420aeb98e6f5d071815257028f62d6f784169851800220602275b4fba18bb34e5198a9cfb3e940306658839079b3bda50d504a9cf2bae36f41067f36697000000800000000001000000002202036e4d0a5fb845b2f1c3c868c2ce7212b155b73e91c05be1b7a77c48830831ba4f1067f366970000008001000000000000000022020200062fdea2b0a056b17fa6b91dd87f5b5d838fe1ee84d636a5022f9a340eebcc1067f3669700000080000000000000000000", @@ -3166,8 +3172,9 @@ class TestWalletSending(ElectrumTestCase): # wallet1 -> dummy address outputs = [PartialTxOutput.from_address_and_value(bitcoin.DummyAddress.CHANNEL, 250000)] + tx = wallet1.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False) with self.assertRaises(bitcoin.DummyAddressUsedInTxException): - tx = wallet1.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False) + wallet1.sign_transaction(tx, password=None) coins = wallet1.get_spendable_coins(domain=None) tx = wallet1.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(5000)) @@ -3247,7 +3254,7 @@ class TestWalletOfflineSigning(ElectrumTestCase): # create unsigned tx outputs = [PartialTxOutput.from_address_and_value('tb1qyw3c0rvn6kk2c688y3dygvckn57525y8qnxt3a', 2500000)] - tx = wallet_online.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), rbf=True) + tx = wallet_online.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), rbf=True) tx.locktime = 1446655 tx.version = 1 @@ -3295,7 +3302,7 @@ class TestWalletOfflineSigning(ElectrumTestCase): # create unsigned tx outputs = [PartialTxOutput.from_address_and_value('tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)] - tx = wallet_online.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), rbf=True) + tx = wallet_online.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), rbf=True) tx.locktime = 1325340 tx.version = 1 @@ -3350,7 +3357,7 @@ class TestWalletOfflineSigning(ElectrumTestCase): # create unsigned tx outputs = [PartialTxOutput.from_address_and_value('tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)] - tx = wallet_online.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), rbf=True) + tx = wallet_online.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), rbf=True) tx.locktime = 1325341 tx.version = 1 @@ -3395,7 +3402,7 @@ class TestWalletOfflineSigning(ElectrumTestCase): # create unsigned tx outputs = [PartialTxOutput.from_address_and_value('tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)] - tx = wallet_online.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), rbf=True) + tx = wallet_online.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), rbf=True) tx.locktime = 1325341 tx.version = 1 @@ -3453,7 +3460,7 @@ class TestWalletOfflineSigning(ElectrumTestCase): # create unsigned tx outputs = [PartialTxOutput.from_address_and_value('tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)] - tx = wallet_online.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), rbf=True) + tx = wallet_online.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), rbf=True) tx.locktime = 1325341 tx.version = 1 @@ -3512,7 +3519,7 @@ class TestWalletOfflineSigning(ElectrumTestCase): # create unsigned tx outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] - tx = wallet_online.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), rbf=True) + tx = wallet_online.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), rbf=True) tx.locktime = 1325340 tx.version = 1 @@ -3549,7 +3556,7 @@ class TestWalletOfflineSigning(ElectrumTestCase): # create unsigned tx outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] - tx = wallet_online.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), rbf=True) + tx = wallet_online.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), rbf=True) tx.locktime = 1325340 tx.version = 1 @@ -3589,7 +3596,7 @@ class TestWalletOfflineSigning(ElectrumTestCase): # create unsigned tx outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] - tx = wallet_online.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), rbf=True) + tx = wallet_online.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), rbf=True) tx.locktime = 1325340 tx.version = 1 @@ -3630,7 +3637,7 @@ class TestWalletOfflineSigning(ElectrumTestCase): # create unsigned tx outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] - tx = wallet_online.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), rbf=True) + tx = wallet_online.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), rbf=True) tx.locktime = 1325340 tx.version = 1 @@ -3674,7 +3681,7 @@ class TestWalletOfflineSigning(ElectrumTestCase): # create unsigned tx outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] - tx = wallet_online.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), rbf=True) + tx = wallet_online.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), rbf=True) tx.locktime = 1325340 tx.version = 1 @@ -3715,7 +3722,7 @@ class TestWalletOfflineSigning(ElectrumTestCase): # create unsigned tx outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] - tx = wallet_online.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), rbf=True) + tx = wallet_online.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), rbf=True) tx.locktime = 1325340 tx.version = 1 @@ -3769,7 +3776,7 @@ class TestWalletOfflineSigning(ElectrumTestCase): # create unsigned tx outputs = [PartialTxOutput.from_address_and_value('2MuCQQHJNnrXzQzuqfUCfAwAjPqpyEHbgue', 2500000)] - tx = wallet_online.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), rbf=True) + tx = wallet_online.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), rbf=True) tx.locktime = 1325503 tx.version = 1 @@ -3836,7 +3843,7 @@ class TestWalletOfflineSigning(ElectrumTestCase): # create unsigned tx outputs = [PartialTxOutput.from_address_and_value('2N8CtJRwxb2GCaiWWdSHLZHHLoZy53CCyxf', 2500000)] - tx = wallet_online.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), rbf=True) + tx = wallet_online.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), rbf=True) tx.locktime = 1325504 tx.version = 1 @@ -3906,7 +3913,7 @@ class TestWalletOfflineSigning(ElectrumTestCase): # create unsigned tx outputs = [PartialTxOutput.from_address_and_value('2MyoZVy8T1t94yLmyKu8DP1SmbWvnxbkwRA', 2500000)] - tx = wallet_online.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), rbf=True) + tx = wallet_online.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), rbf=True) tx.locktime = 1325505 tx.version = 1 @@ -4229,7 +4236,7 @@ class TestWalletHistory_HelperFns(ElectrumTestCase): # wallet1 -> wallet2 outputs = [PartialTxOutput.from_address_and_value("2MuUcGmQ2mLN3vjTuqDSgZpk4LPKDsuPmhN", 165000)] - tx = wallet1.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False, sign=False) + tx = wallet1.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False) self.assertEqual( "wsh(sortedmulti(2,[b2e35a7d/1h]tpubD9aPYLPPYw8MxU3cD57LwpV5v7GomHxdv62MSbPcRkp47zwXx69ACUFsKrj8xzuzRrij9FWVhfvkvNqtqsr8ZtefkDsGZ9GLuHzoS6bXyk1/0/0,[53b77ddb/1h]tpubD8spLJysN7v7V1KHvkZ7AwjnXShKafopi7Vu3Ahs2S46FxBPTode8DgGxDo55k4pJvETGScZFwnM5f2Y31EUjteJdhxR73sjr9ieydgah2U/0/0,[43067d63/1h]tpubD8khd1g1tzFeKeaU59QV811hyvhwn9KDfy5sqFJ5m2wJLw6rUt4AZviqutRPXTUAK4SpU2we3y2WBP916Ma8Em4qFGcbYkFvXVfpGYV3oZR/0/0))", tx.inputs()[0].script_descriptor.to_string_no_checksum())