From eea1eb5eb00eebd0cf3b1639049ac2ccfbfa5160 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 13 Mar 2025 17:02:08 +0000 Subject: [PATCH 01/11] wallet: consider "future" coins as frozen by default --- electrum/gui/qt/main_window.py | 2 ++ electrum/wallet.py | 3 +++ 2 files changed, 5 insertions(+) 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/wallet.py b/electrum/wallet.py index cdaec1f2d..e964dd4fb 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -1966,6 +1966,9 @@ 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 return False From d52762a2e84c5d07d75030ebad162f41593a1982 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 14 Mar 2025 00:24:40 +0000 Subject: [PATCH 02/11] wallet: add new config option "FREEZE_REUSED_ADDRESS_UTXOS" Adds a new config option: `WALLET_FREEZE_REUSED_ADDRESS_UTXOS`. This is based on Bitcoin Core's "avoid_reuse" wallet flag. [0] This opt-in feature, if enabled: > 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. Note that currently we only have a single coinchooser policy, `CoinChooserPrivacy`, which interacts well with this option, as it spends all coins from any selected address. However, if we later add a different coinchooser policy, which allowed "partial spends", care should be taken re e.g. disallowing using that when this option is set. Also note that this PR adds this as a config option, but arguably it could be wallet-specific instead, such as `use_change`. [0]: https://github.com/bitcoin/bitcoin/blob/master/doc/release-notes/release-notes-0.19.0.1.md#wallet closes https://github.com/spesmilo/electrum/issues/7497 --- electrum/address_synchronizer.py | 6 ++++++ electrum/gui/qt/confirm_tx_dialog.py | 8 ++++++++ electrum/simple_config.py | 7 +++++++ electrum/wallet.py | 4 ++++ 4 files changed, 25 insertions(+) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index 1d559a762..37b2533bd 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -966,8 +966,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) diff --git a/electrum/gui/qt/confirm_tx_dialog.py b/electrum/gui/qt/confirm_tx_dialog.py index e14629699..6e85e5ff9 100644 --- a/electrum/gui/qt/confirm_tx_dialog.py +++ b/electrum/gui/qt/confirm_tx_dialog.py @@ -426,6 +426,7 @@ class TxEditor(WindowModalDialog): 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) + add_cv_action(self.config.cv.WALLET_FREEZE_REUSED_ADDRESS_UTXOS, self.toggle_freeze_reused_address_utxos) self.pref_button = QToolButton() self.pref_button.setIcon(read_QIcon("preferences.png")) self.pref_button.setMenu(self.pref_menu) @@ -448,6 +449,13 @@ class TxEditor(WindowModalDialog): self.config.WALLET_COIN_CHOOSER_OUTPUT_ROUNDING = b self.trigger_update() + def toggle_freeze_reused_address_utxos(self): + b = not self.config.WALLET_FREEZE_REUSED_ADDRESS_UTXOS + self.config.WALLET_FREEZE_REUSED_ADDRESS_UTXOS = b + self.trigger_update() + self.main_window.utxo_list.refresh_all() # for coin frozen status + self.main_window.update_status() # frozen balance + def toggle_use_change(self): self.wallet.use_change = not self.wallet.use_change self.wallet.db.put('use_change', self.wallet.use_change) 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/wallet.py b/electrum/wallet.py index cdaec1f2d..fa73b048c 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -1968,6 +1968,10 @@ class Abstract_Wallet(ABC, Logger, EventListener): # State not set. We implicitly mark certain coins as frozen: 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: From fbebe7de1aef55bf941ff7db8a1da93bd2e81d7d Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 7 Feb 2025 09:52:03 +0100 Subject: [PATCH 03/11] Make lnwatcher not async This fixes offline history not having the proper labels --- electrum/lnwatcher.py | 124 +++++++++++----------- electrum/lnworker.py | 34 +++--- electrum/plugins/watchtower/watchtower.py | 37 ++++++- electrum/submarine_swaps.py | 5 +- tests/test_lnpeer.py | 4 + 5 files changed, 117 insertions(+), 87 deletions(-) diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py index 086000362..61bd3c08f 100644 --- a/electrum/lnwatcher.py +++ b/electrum/lnwatcher.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING from enum import IntEnum, auto -from .util import log_exceptions, ignore_exceptions, TxMinedInfo, BelowDustLimit +from .util import log_exceptions, 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 @@ -17,6 +17,8 @@ if TYPE_CHECKING: from .lnsweep import SweepInfo from .lnworker import LNWallet from .lnchannel import AbstractChannel + from .simple_config import SimpleConfig + class TxMinedDepth(IntEnum): """ IntEnum because we call min() in get_deepest_tx_mined_depth_for_txids """ @@ -30,30 +32,27 @@ class LNWatcher(Logger, EventListener): LOGGING_SHORTCUT = 'W' - def __init__(self, adb: 'AddressSynchronizer', network: 'Network'): + def __init__(self, adb: 'AddressSynchronizer', config: 'SimpleConfig'): Logger.__init__(self) self.adb = adb - self.config = network.config - self.callbacks = {} # address -> lambda: coroutine - self.network = network + self.config = config + self.callbacks = {} # address -> lambda: coroutine + self.network = None self.register_callbacks() # status gets populated when we run self.channel_status = {} + def start_network(self, network: 'Network'): + self.network = network + async 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): + def unwatch_channel(self, address, funding_outpoint): self.logger.info(f'unwatching {funding_outpoint}') self.remove_callback(address) @@ -93,46 +92,7 @@ class LNWatcher(Logger, EventListener): self.logger.info("synchronizer not set yet") return for address, callback in list(self.callbacks.items()): - await callback() - - 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 - 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_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) - 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( - funding_outpoint=funding_outpoint, - funding_txid=funding_txid, - funding_height=funding_height, - closing_txid=closing_txid, - 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 - + callback() def get_spender(self, outpoint) -> str: """ @@ -181,10 +141,45 @@ class LNWatcher(Logger, EventListener): class LNWalletWatcher(LNWatcher): - def __init__(self, lnworker: 'LNWallet', network: 'Network'): - self.network = network + def __init__(self, lnworker: 'LNWallet'): self.lnworker = lnworker - LNWatcher.__init__(self, lnworker.wallet.adb, network) + LNWatcher.__init__(self, lnworker.wallet.adb, lnworker.config) + + 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 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 + funding_txid = funding_outpoint.split(':')[0] + funding_height = self.adb.get_tx_height(funding_txid) + closing_txid = self.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 = 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 + self.update_channel_state( + funding_outpoint=funding_outpoint, + funding_txid=funding_txid, + funding_height=funding_height, + closing_txid=closing_txid, + closing_height=closing_height, + keep_watching=keep_watching) + if not keep_watching: + self.unwatch_channel(address, funding_outpoint) @event_listener async def on_event_blockchain_updated(self, *args): @@ -199,11 +194,9 @@ class LNWalletWatcher(LNWatcher): 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,15 +206,18 @@ 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 diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 525d531fa..8d72b1d9c 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -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 = LNWalletWatcher(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 @@ -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..ce8fdabc5 100644 --- a/electrum/plugins/watchtower/watchtower.py +++ b/electrum/plugins/watchtower/watchtower.py @@ -75,8 +75,20 @@ class WatchTower(LNWatcher): await super().stop() 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) + + @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() + def diagnostic_name(self): - return "local_tower" + return "watchtower" @log_exceptions async def start_watching(self): @@ -85,6 +97,27 @@ 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 + funding_txid = funding_outpoint.split(':')[0] + funding_height = self.adb.get_tx_height(funding_txid) + closing_txid = self.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) + 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. @@ -188,8 +221,6 @@ class WatchTower(LNWatcher): await self.sweepstore.remove_sweep_tx(funding_outpoint) await self.sweepstore.remove_channel(funding_outpoint) - async def update_channel_state(self, *args, **kwargs): - pass diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index 3704a88ca..25f8653dd 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(): 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 = {} From 8e6be0a36a38f7da9d2f541f43e3a49c1ce991cf Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 14 Mar 2025 11:43:03 +0100 Subject: [PATCH 04/11] Remove inheritance between LNWatcher and Watchtower As LNWatcher is no longer async, there is not enough overlap between these classes to deserve inheritance --- electrum/address_synchronizer.py | 53 ++++++++ electrum/lnwatcher.py | 155 ++++++---------------- electrum/lnworker.py | 8 +- electrum/plugins/watchtower/watchtower.py | 81 +++++++---- 4 files changed, 155 insertions(+), 142 deletions(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index 1d559a762..e1212a527 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 @@ -990,3 +1000,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/lnwatcher.py b/electrum/lnwatcher.py index 61bd3c08f..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, 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 @@ -17,27 +15,18 @@ if TYPE_CHECKING: from .lnsweep import SweepInfo from .lnworker import LNWallet from .lnchannel import AbstractChannel - from .simple_config import SimpleConfig - - -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', config: 'SimpleConfig'): - + def __init__(self, lnworker: 'LNWallet'): + self.lnworker = lnworker Logger.__init__(self) - self.adb = adb - self.config = config - self.callbacks = {} # address -> lambda: coroutine + 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 @@ -46,16 +35,12 @@ class LNWatcher(Logger, EventListener): def start_network(self, network: 'Network'): self.network = network - async def stop(self): + def stop(self): self.unregister_callbacks() def get_channel_status(self, outpoint): return self.channel_status.get(outpoint, 'unknown') - 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) @@ -63,87 +48,40 @@ 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()): callback() - 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: + @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 - 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 + self.trigger_callbacks() - 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() + @event_listener + def on_event_adb_added_verified_tx(self, adb, tx_hash): + if adb != self.adb: + return + self.trigger_callbacks() - def is_deeply_mined(self, txid): - return self.get_tx_mined_depth(txid) == TxMinedDepth.DEEP - - - -class LNWalletWatcher(LNWatcher): - - def __init__(self, lnworker: 'LNWallet'): - self.lnworker = lnworker - LNWatcher.__init__(self, lnworker.wallet.adb, lnworker.config) + @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() @@ -153,6 +91,10 @@ class LNWalletWatcher(LNWatcher): 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): @@ -160,7 +102,7 @@ class LNWalletWatcher(LNWatcher): # inspect_tx_candidate might have added new addresses, in which case we return early 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) @@ -181,16 +123,6 @@ class LNWalletWatcher(LNWatcher): if not keep_watching: self.unwatch_channel(address, funding_outpoint) - @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() - def diagnostic_name(self): return f"{self.lnworker.wallet.diagnostic_name()}-LNW" @@ -223,8 +155,7 @@ class LNWalletWatcher(LNWatcher): 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(':') @@ -234,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) @@ -266,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 8d72b1d9c..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 = LNWalletWatcher(self) + 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 @@ -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() diff --git a/electrum/plugins/watchtower/watchtower.py b/electrum/plugins/watchtower/watchtower.py index ce8fdabc5..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,24 +62,51 @@ 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) - async def stop(self): - await super().stop() - await self.adb.stop() + def remove_callback(self, address): + self.callbacks.pop(address, None) - def add_channel(self, outpoint: str, address: str) -> None: - callback = lambda: self.check_onchain_situation(address, outpoint) - self.add_callback(address, callback) + 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): @@ -87,6 +116,14 @@ class WatchTower(LNWatcher): for address, callback in list(self.callbacks.items()): await callback() + async def stop(self): + 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 "watchtower" @@ -102,10 +139,7 @@ class WatchTower(LNWatcher): if not self.adb.is_mine(address): return # inspect_tx_candidate might have added new addresses, in which case we return early - funding_txid = funding_outpoint.split(':')[0] - funding_height = self.adb.get_tx_height(funding_txid) - closing_txid = self.get_spender(funding_outpoint) - closing_height = self.adb.get_tx_height(closing_txid) + closing_txid = self.adb.get_spender(funding_outpoint) if closing_txid: closing_tx = self.adb.get_transaction(closing_txid) if closing_tx: @@ -132,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)' @@ -166,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 @@ -175,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: @@ -217,13 +251,10 @@ 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) - - create_sweep_txs=""" CREATE TABLE IF NOT EXISTS sweep_txs ( funding_outpoint VARCHAR(34) NOT NULL, @@ -319,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()] - - From 656c109336d9aaf69740a761c5a02d701b736dde Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 14 Mar 2025 15:33:58 +0000 Subject: [PATCH 05/11] qt: refactor TxEditor preferences: use QMenuWithConfig --- electrum/gui/qt/confirm_tx_dialog.py | 133 +++++++++------------------ electrum/gui/qt/my_treeview.py | 43 ++++++--- 2 files changed, 70 insertions(+), 106 deletions(-) diff --git a/electrum/gui/qt/confirm_tx_dialog.py b/electrum/gui/qt/confirm_tx_dialog.py index 6e85e5ff9..61da43266 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,10 +415,14 @@ 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) - add_cv_action(self.config.cv.WALLET_FREEZE_REUSED_ADDRESS_UTXOS, self.toggle_freeze_reused_address_utxos) + 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) + def cb(): + self.trigger_update() + self.main_window.utxo_list.refresh_all() # for coin frozen status + self.main_window.update_status() # frozen balance + self.pref_menu.addConfig(self.config.cv.WALLET_FREEZE_REUSED_ADDRESS_UTXOS, callback=cb) self.pref_button = QToolButton() self.pref_button.setIcon(read_QIcon("preferences.png")) self.pref_button.setMenu(self.pref_menu) @@ -444,18 +440,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_freeze_reused_address_utxos(self): - b = not self.config.WALLET_FREEZE_REUSED_ADDRESS_UTXOS - self.config.WALLET_FREEZE_REUSED_ADDRESS_UTXOS = b - self.trigger_update() - self.main_window.utxo_list.refresh_all() # for coin frozen status - self.main_window.update_status() # frozen balance - def toggle_use_change(self): self.wallet.use_change = not self.wallet.use_change self.wallet.db.put('use_change', self.wallet.use_change) @@ -467,45 +451,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 @@ -514,7 +464,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/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() From fddd4275aa4b80eaa58d0a290db561215e9d13f2 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 14 Mar 2025 15:08:49 +0000 Subject: [PATCH 06/11] qt: move "FREEZE_REUSED_ADDRESS_UTXOS" option to utxo_list toolbar ref https://github.com/spesmilo/electrum/pull/9636 --- electrum/gui/qt/confirm_tx_dialog.py | 5 ----- electrum/gui/qt/utxo_list.py | 5 +++++ 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/electrum/gui/qt/confirm_tx_dialog.py b/electrum/gui/qt/confirm_tx_dialog.py index 61da43266..41449adff 100644 --- a/electrum/gui/qt/confirm_tx_dialog.py +++ b/electrum/gui/qt/confirm_tx_dialog.py @@ -418,11 +418,6 @@ class TxEditor(WindowModalDialog): 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) - def cb(): - self.trigger_update() - self.main_window.utxo_list.refresh_all() # for coin frozen status - self.main_window.update_status() # frozen balance - self.pref_menu.addConfig(self.config.cv.WALLET_FREEZE_REUSED_ADDRESS_UTXOS, callback=cb) self.pref_button = QToolButton() self.pref_button.setIcon(read_QIcon("preferences.png")) self.pref_button.setMenu(self.pref_menu) 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) From 3c3778db9cbfeaa659ee8fb602248040f43bc6fe Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 14 Mar 2025 16:44:46 +0000 Subject: [PATCH 07/11] wallet: towards killing create_transaction: rm "sign" arg --- electrum/commands.py | 8 ++--- electrum/gui/stdio.py | 2 +- electrum/gui/text.py | 2 +- electrum/submarine_swaps.py | 3 +- electrum/txbatcher.py | 2 +- electrum/wallet.py | 6 ---- tests/test_invoices.py | 4 +++ tests/test_wallet_vertical.py | 59 ++++++++++++++++++++--------------- 8 files changed, 45 insertions(+), 41 deletions(-) diff --git a/electrum/commands.py b/electrum/commands.py index b131f1807..dab371749 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -792,10 +792,10 @@ class Commands(Logger): change_addr=change_addr, domain_addr=domain_addr, domain_coins=domain_coins, - sign=not unsigned, rbf=rbf, - password=password, locktime=locktime) + if not unsigned: + wallet.sign_transaction(tx, password) result = tx.serialize() if addtransaction: await self.addtransaction(result, wallet=wallet) @@ -822,10 +822,10 @@ class Commands(Logger): change_addr=change_addr, domain_addr=domain_addr, domain_coins=domain_coins, - sign=not unsigned, rbf=rbf, - password=password, 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/stdio.py b/electrum/gui/stdio.py index e6abc0866..f96f948a6 100644 --- a/electrum/gui/stdio.py +++ b/electrum/gui/stdio.py @@ -214,9 +214,9 @@ class ElectrumGui(BaseElectrumGui, EventListener): try: tx = self.wallet.create_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..dbea07b21 100644 --- a/electrum/gui/text.py +++ b/electrum/gui/text.py @@ -693,9 +693,9 @@ class ElectrumGui(BaseElectrumGui, EventListener): try: tx = self.wallet.create_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/submarine_swaps.py b/electrum/submarine_swaps.py index 25f8653dd..d69524f2a 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -774,13 +774,12 @@ class SwapManager(Logger): tx = self.wallet.create_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..589adc3cd 100644 --- a/electrum/txbatcher.py +++ b/electrum/txbatcher.py @@ -396,11 +396,11 @@ class TxBatch(Logger): 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 2cd193582..5f8f4a0c1 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -3107,9 +3107,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): 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, @@ -3139,8 +3137,6 @@ class Abstract_Wallet(ABC, Logger, EventListener): 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: @@ -3396,9 +3392,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): tx = self.create_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..f967c3092 100644 --- a/tests/test_invoices.py +++ b/tests/test_invoices.py @@ -73,6 +73,7 @@ class TestWalletPaymentRequests(ElectrumTestCase): 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)) + 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 @@ -103,6 +104,7 @@ class TestWalletPaymentRequests(ElectrumTestCase): 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)) + 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 @@ -133,6 +135,7 @@ class TestWalletPaymentRequests(ElectrumTestCase): 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)) + 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) @@ -202,6 +205,7 @@ class TestWalletPaymentRequests(ElectrumTestCase): 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)) + 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_wallet_vertical.py b/tests/test_wallet_vertical.py index f2411abfb..cf1f37d27 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.create_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.create_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.create_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.create_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.create_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.create_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.create_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.create_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.create_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.create_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.create_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.create_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.create_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.create_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.create_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.create_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.create_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.create_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.create_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.create_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.create_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.create_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.create_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.create_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.create_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.create_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()) From cab1dc5c290d657c99dad2e094834c8440083d28 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 14 Mar 2025 17:03:49 +0000 Subject: [PATCH 08/11] wallet: towards killing create_transaction: pass through "locktime", "version" --- electrum/commands.py | 6 ++++-- electrum/wallet.py | 16 ++++++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/electrum/commands.py b/electrum/commands.py index dab371749..50c57b8dc 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -793,7 +793,8 @@ class Commands(Logger): domain_addr=domain_addr, domain_coins=domain_coins, rbf=rbf, - locktime=locktime) + locktime=locktime, + ) if not unsigned: wallet.sign_transaction(tx, password) result = tx.serialize() @@ -823,7 +824,8 @@ class Commands(Logger): domain_addr=domain_addr, domain_coins=domain_coins, rbf=rbf, - locktime=locktime) + locktime=locktime, + ) if not unsigned: wallet.sign_transaction(tx, password) result = tx.serialize() diff --git a/electrum/wallet.py b/electrum/wallet.py index 5f8f4a0c1..d88dd3b6b 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -1841,6 +1841,8 @@ 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.""" @@ -1953,8 +1955,12 @@ class Abstract_Wallet(ABC, Logger, EventListener): outputs[i].value += (amount - distr_amount) tx = PartialTransaction.from_io(list(coins), list(outputs)) - # 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) @@ -3132,11 +3138,9 @@ class Abstract_Wallet(ABC, Logger, EventListener): merge_duplicate_outputs=merge_duplicate_outputs, rbf=rbf, BIP69_sort=BIP69_sort, + locktime=locktime, + tx_version=tx_version, ) - if locktime is not None: - tx.locktime = locktime - if tx_version is not None: - tx.version = tx_version return tx def _check_risk_of_burning_coins_as_fees(self, tx: 'PartialTransaction') -> TxSighashDanger: From 4689a0e78c850299890be59a5ef40ce6b605c1f4 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 14 Mar 2025 17:14:59 +0000 Subject: [PATCH 09/11] wallet: towards killing create_transaction: rm "coins" logic --- electrum/commands.py | 12 ++++++++---- electrum/wallet.py | 11 ++++------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/electrum/commands.py b/electrum/commands.py index 50c57b8dc..4ceb72714 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -786,12 +786,14 @@ class Commands(Logger): 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)] + 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.create_transaction( outputs, fee_policy=fee_policy, change_addr=change_addr, - domain_addr=domain_addr, - domain_coins=domain_coins, + coins=coins, rbf=rbf, locktime=locktime, ) @@ -817,12 +819,14 @@ 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)) + 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.create_transaction( final_outputs, fee_policy=fee_policy, change_addr=change_addr, - domain_addr=domain_addr, - domain_coins=domain_coins, + coins=coins, rbf=rbf, locktime=locktime, ) diff --git a/electrum/wallet.py b/electrum/wallet.py index d88dd3b6b..19559a7bf 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, @@ -1846,6 +1846,8 @@ class Abstract_Wallet(ABC, Logger, EventListener): ) -> 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]): @@ -3111,8 +3113,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): *, fee_policy: FeePolicy, change_addr=None, - domain_addr=None, - domain_coins=None, + coins=None, rbf=True, locktime=None, tx_version: Optional[int] = None, @@ -3120,13 +3121,9 @@ class Abstract_Wallet(ABC, Logger, EventListener): 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, From 977d8b1dd64adcaaaccad0a161b935cc397a46d8 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 14 Mar 2025 17:19:41 +0000 Subject: [PATCH 10/11] wallet: kill create_transaction --- electrum/commands.py | 8 +++--- electrum/gui/stdio.py | 2 +- electrum/gui/text.py | 2 +- electrum/submarine_swaps.py | 2 +- electrum/txbatcher.py | 2 +- electrum/wallet.py | 35 +---------------------- tests/test_invoices.py | 8 +++--- tests/test_wallet_vertical.py | 52 +++++++++++++++++------------------ 8 files changed, 39 insertions(+), 72 deletions(-) diff --git a/electrum/commands.py b/electrum/commands.py index 4ceb72714..d6206c3df 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -789,8 +789,8 @@ class Commands(Logger): 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.create_transaction( - outputs, + tx = wallet.make_unsigned_transaction( + outputs=outputs, fee_policy=fee_policy, change_addr=change_addr, coins=coins, @@ -822,8 +822,8 @@ class Commands(Logger): 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.create_transaction( - final_outputs, + tx = wallet.make_unsigned_transaction( + outputs=final_outputs, fee_policy=fee_policy, change_addr=change_addr, coins=coins, diff --git a/electrum/gui/stdio.py b/electrum/gui/stdio.py index f96f948a6..1871c726e 100644 --- a/electrum/gui/stdio.py +++ b/electrum/gui/stdio.py @@ -212,7 +212,7 @@ 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)], fee_policy=FixedFeePolicy(fee), ) diff --git a/electrum/gui/text.py b/electrum/gui/text.py index dbea07b21..b51d0d01f 100644 --- a/electrum/gui/text.py +++ b/electrum/gui/text.py @@ -691,7 +691,7 @@ 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, fee_policy=fee_policy, ) diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index d69524f2a..d4a1bbaf0 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -771,7 +771,7 @@ 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, fee_policy=fee_policy, diff --git a/electrum/txbatcher.py b/electrum/txbatcher.py index 589adc3cd..23300300b 100644 --- a/electrum/txbatcher.py +++ b/electrum/txbatcher.py @@ -391,7 +391,7 @@ 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, diff --git a/electrum/wallet.py b/electrum/wallet.py index 19559a7bf..206660169 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -3107,39 +3107,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, - coins=None, - rbf=True, - 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, - BIP69_sort: bool = True, - ) -> PartialTransaction: - """Helper function for make_unsigned_transaction.""" - 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, - locktime=locktime, - tx_version=tx_version, - ) - 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,7 +3357,7 @@ 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=[], fee_policy=FixedFeePolicy(0), diff --git a/tests/test_invoices.py b/tests/test_invoices.py index f967c3092..a22a8e189 100644 --- a/tests/test_invoices.py +++ b/tests/test_invoices.py @@ -72,7 +72,7 @@ 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)) @@ -103,7 +103,7 @@ 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)) @@ -134,7 +134,7 @@ 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)) @@ -204,7 +204,7 @@ 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)) diff --git a/tests/test_wallet_vertical.py b/tests/test_wallet_vertical.py index cf1f37d27..d92f8cae0 100644 --- a/tests/test_wallet_vertical.py +++ b/tests/test_wallet_vertical.py @@ -873,7 +873,7 @@ class TestWalletSending(ElectrumTestCase): # wallet1 -> wallet2 outputs = [PartialTxOutput.from_address_and_value(wallet2.get_receiving_address(), 250000)] - tx = wallet1.create_transaction(outputs=outputs, 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()) @@ -893,7 +893,7 @@ class TestWalletSending(ElectrumTestCase): # wallet2 -> wallet1 outputs = [PartialTxOutput.from_address_and_value(wallet1.get_receiving_address(), 100000)] - tx = wallet2.create_transaction(outputs=outputs, 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()) @@ -947,7 +947,7 @@ class TestWalletSending(ElectrumTestCase): # wallet1 -> wallet2 outputs = [PartialTxOutput.from_address_and_value(wallet2.get_receiving_address(), 370000)] - tx = wallet1a.create_transaction(outputs=outputs, 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", @@ -973,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, 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) self.assertEqual( "pkh(045f7ba332df2a7b4f5d13f246e307c9174cfa9b8b05f3b83410a3c23ef8958d610be285963d67c7bc1feb082f168fa9877c25999963ff8b56b242a852b23e25ed)", tx.inputs()[0].script_descriptor.to_string_no_checksum()) @@ -1047,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, 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) 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))", @@ -1082,7 +1082,7 @@ class TestWalletSending(ElectrumTestCase): # wallet2 -> wallet1 outputs = [PartialTxOutput.from_address_and_value(wallet1a.get_receiving_address(), 100000)] - tx = wallet2a.create_transaction(outputs=outputs, 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( @@ -1145,7 +1145,7 @@ class TestWalletSending(ElectrumTestCase): # wallet1 -> wallet2 outputs = [PartialTxOutput.from_address_and_value(wallet2.get_receiving_address(), 1000000)] - tx = wallet1a.create_transaction(outputs=outputs, 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()) @@ -1165,7 +1165,7 @@ class TestWalletSending(ElectrumTestCase): # wallet2 -> wallet1 outputs = [PartialTxOutput.from_address_and_value(wallet1a.get_receiving_address(), 300000)] - tx = wallet2.create_transaction(outputs=outputs, 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()) @@ -2329,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) + 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", @@ -2341,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) + 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", @@ -3172,7 +3172,7 @@ class TestWalletSending(ElectrumTestCase): # wallet1 -> dummy address outputs = [PartialTxOutput.from_address_and_value(bitcoin.DummyAddress.CHANNEL, 250000)] - tx = wallet1.create_transaction(outputs=outputs, 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) with self.assertRaises(bitcoin.DummyAddressUsedInTxException): wallet1.sign_transaction(tx, password=None) @@ -3254,7 +3254,7 @@ class TestWalletOfflineSigning(ElectrumTestCase): # create unsigned tx outputs = [PartialTxOutput.from_address_and_value('tb1qyw3c0rvn6kk2c688y3dygvckn57525y8qnxt3a', 2500000)] - tx = wallet_online.create_transaction(outputs=outputs, 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 @@ -3302,7 +3302,7 @@ class TestWalletOfflineSigning(ElectrumTestCase): # create unsigned tx outputs = [PartialTxOutput.from_address_and_value('tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)] - tx = wallet_online.create_transaction(outputs=outputs, 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 @@ -3357,7 +3357,7 @@ class TestWalletOfflineSigning(ElectrumTestCase): # create unsigned tx outputs = [PartialTxOutput.from_address_and_value('tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)] - tx = wallet_online.create_transaction(outputs=outputs, 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 @@ -3402,7 +3402,7 @@ class TestWalletOfflineSigning(ElectrumTestCase): # create unsigned tx outputs = [PartialTxOutput.from_address_and_value('tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)] - tx = wallet_online.create_transaction(outputs=outputs, 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 @@ -3460,7 +3460,7 @@ class TestWalletOfflineSigning(ElectrumTestCase): # create unsigned tx outputs = [PartialTxOutput.from_address_and_value('tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)] - tx = wallet_online.create_transaction(outputs=outputs, 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 @@ -3519,7 +3519,7 @@ class TestWalletOfflineSigning(ElectrumTestCase): # create unsigned tx outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] - tx = wallet_online.create_transaction(outputs=outputs, 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 @@ -3556,7 +3556,7 @@ class TestWalletOfflineSigning(ElectrumTestCase): # create unsigned tx outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] - tx = wallet_online.create_transaction(outputs=outputs, 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 @@ -3596,7 +3596,7 @@ class TestWalletOfflineSigning(ElectrumTestCase): # create unsigned tx outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] - tx = wallet_online.create_transaction(outputs=outputs, 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 @@ -3637,7 +3637,7 @@ class TestWalletOfflineSigning(ElectrumTestCase): # create unsigned tx outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] - tx = wallet_online.create_transaction(outputs=outputs, 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 @@ -3681,7 +3681,7 @@ class TestWalletOfflineSigning(ElectrumTestCase): # create unsigned tx outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] - tx = wallet_online.create_transaction(outputs=outputs, 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 @@ -3722,7 +3722,7 @@ class TestWalletOfflineSigning(ElectrumTestCase): # create unsigned tx outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] - tx = wallet_online.create_transaction(outputs=outputs, 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 @@ -3776,7 +3776,7 @@ class TestWalletOfflineSigning(ElectrumTestCase): # create unsigned tx outputs = [PartialTxOutput.from_address_and_value('2MuCQQHJNnrXzQzuqfUCfAwAjPqpyEHbgue', 2500000)] - tx = wallet_online.create_transaction(outputs=outputs, 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 @@ -3843,7 +3843,7 @@ class TestWalletOfflineSigning(ElectrumTestCase): # create unsigned tx outputs = [PartialTxOutput.from_address_and_value('2N8CtJRwxb2GCaiWWdSHLZHHLoZy53CCyxf', 2500000)] - tx = wallet_online.create_transaction(outputs=outputs, 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 @@ -3913,7 +3913,7 @@ class TestWalletOfflineSigning(ElectrumTestCase): # create unsigned tx outputs = [PartialTxOutput.from_address_and_value('2MyoZVy8T1t94yLmyKu8DP1SmbWvnxbkwRA', 2500000)] - tx = wallet_online.create_transaction(outputs=outputs, 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 @@ -4236,7 +4236,7 @@ class TestWalletHistory_HelperFns(ElectrumTestCase): # wallet1 -> wallet2 outputs = [PartialTxOutput.from_address_and_value("2MuUcGmQ2mLN3vjTuqDSgZpk4LPKDsuPmhN", 165000)] - tx = wallet1.create_transaction(outputs=outputs, 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) 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()) From 2763e14bb3d04c8f5c979d6cb3098c0a3c461060 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 14 Mar 2025 17:30:15 +0000 Subject: [PATCH 11/11] commands: payto can just call paytomany --- electrum/commands.py | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/electrum/commands.py b/electrum/commands.py index d6206c3df..5f24c5f19 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -778,31 +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)] - 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=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, - coins=coins, + nocheck=nocheck, + unsigned=unsigned, rbf=rbf, + password=password, locktime=locktime, + addtransaction=addtransaction, + wallet=wallet, ) - if not unsigned: - wallet.sign_transaction(tx, password) - result = tx.serialize() - if addtransaction: - await self.addtransaction(result, wallet=wallet) - return result @command('wp') async def paytomany(self, outputs, fee=None, feerate=None, from_addr=None, from_coins=None, change_addr=None,