From ab14c3e1382c1af48baff73b790aecfbd069eb8a Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 3 Mar 2025 15:48:13 +0100 Subject: [PATCH] tx batching in GUI: - discard config.WALLET_BATCH_RBF - allow the user to choose base_tx from a list of batching candidates in ConfirmTxDialog --- electrum/gui/qt/confirm_tx_dialog.py | 65 +++++++++++++++++----------- electrum/gui/qt/main_window.py | 16 ++++--- electrum/gui/qt/send_tab.py | 19 +++++--- electrum/simple_config.py | 7 --- electrum/wallet.py | 21 ++++----- tests/test_wallet_vertical.py | 37 ++++++++-------- 6 files changed, 88 insertions(+), 77 deletions(-) diff --git a/electrum/gui/qt/confirm_tx_dialog.py b/electrum/gui/qt/confirm_tx_dialog.py index 7a13cbeb8..8a9297b28 100644 --- a/electrum/gui/qt/confirm_tx_dialog.py +++ b/electrum/gui/qt/confirm_tx_dialog.py @@ -30,7 +30,7 @@ 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 +from PyQt6.QtWidgets import QHBoxLayout, QVBoxLayout, QLabel, QGridLayout, QPushButton, QToolButton, QMenu, QComboBox from electrum.i18n import _ from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates @@ -57,11 +57,14 @@ from .locktimeedit import LockTimeEdit class TxEditor(WindowModalDialog): - def __init__(self, *, title='', - window: 'ElectrumWindow', - make_tx, - output_value: Union[int, str], - allow_preview=True): + def __init__( + self, *, title='', + window: 'ElectrumWindow', + make_tx, + output_value: Union[int, str], + allow_preview=True, + batching_candidates=None, + ): WindowModalDialog.__init__(self, window, title=title) self.main_window = window @@ -82,6 +85,8 @@ class TxEditor(WindowModalDialog): # preview is disabled for lightning channel funding self.allow_preview = allow_preview self.is_preview = False + self._base_tx = None # for batching + self.batching_candidates = batching_candidates self.locktime_e = LockTimeEdit(self) self.locktime_e.valueEdited.connect(self.trigger_update) @@ -106,7 +111,7 @@ class TxEditor(WindowModalDialog): vbox.addStretch(1) vbox.addLayout(buttons) - self.set_io_visible(self.config.GUI_QT_TX_EDITOR_SHOW_IO) + 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.update_fee_target() @@ -114,6 +119,9 @@ class TxEditor(WindowModalDialog): self.main_window.gui_object.timer.timeout.connect(self.timer_actions) + def is_batching(self) -> bool: + return self._base_tx is not None + def timer_actions(self): if self.needs_update: self.update() @@ -365,6 +373,15 @@ class TxEditor(WindowModalDialog): self.ok_button.clicked.connect(self.on_send) self.ok_button.setDefault(True) buttons = Buttons(CancelButton(self), self.preview_button, self.ok_button) + + if self.batching_candidates is not None and len(self.batching_candidates) > 0: + batching_combo = QComboBox() + batching_combo.addItems([_('Do not batch')] + [_('Batch with') + ' ' + tx.txid()[0:10] for tx in self.batching_candidates]) + 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() + batching_combo.currentIndexChanged.connect(on_batching_combo) return buttons def create_top_bar(self, text): @@ -404,7 +421,6 @@ class TxEditor(WindowModalDialog): _('This may result in higher transactions fees.') ])) self.use_multi_change_menu.setEnabled(self.wallet.use_change) - add_cv_action(self.config.cv.WALLET_BATCH_RBF, self.toggle_batch_rbf) 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) @@ -441,9 +457,7 @@ class TxEditor(WindowModalDialog): self.wallet.db.put('multiple_change', self.wallet.multiple_change) self.trigger_update() - def toggle_batch_rbf(self): - b = not self.config.WALLET_BATCH_RBF - self.config.WALLET_BATCH_RBF = b + def update_batching(self): self.trigger_update() def toggle_merge_duplicate_outputs(self): @@ -462,9 +476,8 @@ class TxEditor(WindowModalDialog): self.trigger_update() def toggle_io_visibility(self): - b = not self.config.GUI_QT_TX_EDITOR_SHOW_IO - self.config.GUI_QT_TX_EDITOR_SHOW_IO = b - self.set_io_visible(b) + 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): @@ -479,8 +492,8 @@ class TxEditor(WindowModalDialog): self.set_locktime_visible(b) self.resize_to_fit_content() - def set_io_visible(self, b): - self.io_widget.setVisible(b) + def set_io_visible(self): + self.io_widget.setVisible(self.config.GUI_QT_TX_EDITOR_SHOW_IO) def set_fee_edit_visible(self, b): detailed = [self.feerounding_icon, self.feerate_e, self.fee_e] @@ -560,7 +573,7 @@ class TxEditor(WindowModalDialog): if any((txin.block_height is not None and txin.block_height<=0) for txin in self.tx.inputs()): messages.append(_('This transaction will spend unconfirmed coins.')) # warn if we merge from mempool - if self.tx.rbf_merge_txid: + if self.is_batching(): messages.append(_('This payment will be merged with another existing transaction.')) # warn if we use multiple change outputs num_change = sum(int(o.is_change) for o in self.tx.outputs()) @@ -603,7 +616,7 @@ class TxEditor(WindowModalDialog): class ConfirmTxDialog(TxEditor): help_text = '' #_('Set the mining fee of your transaction') - def __init__(self, *, window: 'ElectrumWindow', make_tx, output_value: Union[int, str], allow_preview=True): + def __init__(self, *, window: 'ElectrumWindow', make_tx, output_value: Union[int, str], allow_preview=True, batching_candidates=None): TxEditor.__init__( self, @@ -611,8 +624,9 @@ class ConfirmTxDialog(TxEditor): make_tx=make_tx, output_value=output_value, title=_("New Transaction"), # todo: adapt title for channel funding tx, swaps - allow_preview=allow_preview) - + allow_preview=allow_preview, # false for channel funding + batching_candidates=batching_candidates, + ) self.trigger_update() def _update_amount_label(self): @@ -631,8 +645,9 @@ class ConfirmTxDialog(TxEditor): def update_tx(self, *, fallback_to_zero_fee: bool = False): fee_policy = self.get_fee_policy() confirmed_only = self.config.WALLET_SPEND_CONFIRMED_ONLY + base_tx = self._base_tx try: - self.tx = self.make_tx(fee_policy, confirmed_only=confirmed_only) + self.tx = self.make_tx(fee_policy, confirmed_only=confirmed_only, base_tx=base_tx) self.not_enough_funds = False self.no_dynfee_estimates = False except NotEnoughFunds: @@ -640,7 +655,7 @@ class ConfirmTxDialog(TxEditor): self.tx = None if fallback_to_zero_fee: try: - self.tx = self.make_tx(FixedFeePolicy(0), confirmed_only=confirmed_only) + self.tx = self.make_tx(FixedFeePolicy(0), confirmed_only=confirmed_only, base_tx=base_tx) except BaseException: return else: @@ -650,7 +665,7 @@ class ConfirmTxDialog(TxEditor): self.no_dynfee_estimates = True self.tx = None try: - self.tx = self.make_tx(FixedFeePolicy(0), confirmed_only=confirmed_only) + self.tx = self.make_tx(FixedFeePolicy(0), confirmed_only=confirmed_only, base_tx=base_tx) except NotEnoughFunds: self.not_enough_funds = True return @@ -665,7 +680,7 @@ class ConfirmTxDialog(TxEditor): def can_pay_assuming_zero_fees(self, confirmed_only) -> bool: # called in send_tab.py try: - tx = self.make_tx(FixedFeePolicy(0), confirmed_only=confirmed_only) + tx = self.make_tx(FixedFeePolicy(0), confirmed_only=confirmed_only, base_tx=None) except NotEnoughFunds: return False else: @@ -688,7 +703,7 @@ class ConfirmTxDialog(TxEditor): grid.addWidget(HelpLabel(_("Mining Fee") + ": ", msg), 1, 0) grid.addLayout(self.fee_hbox, 1, 1, 1, 3) - grid.addWidget(HelpLabel(_("Fee target") + ": ", self.fee_combo.help_msg), 3, 0) + grid.addWidget(HelpLabel(_("Fee policy") + ": ", self.fee_combo.help_msg), 3, 0) grid.addLayout(self.fee_target_hbox, 3, 1, 1, 3) grid.setColumnStretch(4, 1) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 5d0416069..2b1dadbbb 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1385,11 +1385,13 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): WaitingDialog(self, msg, task, on_success, on_failure) def mktx_for_open_channel(self, *, funding_sat, node_id): - make_tx = lambda fee_policy, *, confirmed_only=False: self.wallet.lnworker.mktx_for_open_channel( - coins = self.get_coins(nonlocal_only=True, confirmed_only=confirmed_only), - funding_sat=funding_sat, - node_id=node_id, - fee_policy=fee_policy) + def make_tx(fee_policy, *, confirmed_only=False, base_tx=None): + assert base_tx is None + return self.wallet.lnworker.mktx_for_open_channel( + coins = self.get_coins(nonlocal_only=True, confirmed_only=confirmed_only), + funding_sat=funding_sat, + node_id=node_id, + fee_policy=fee_policy) return make_tx def open_channel(self, connect_str, funding_sat, push_amt): @@ -1409,8 +1411,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): return self._open_channel(connect_str, funding_sat, push_amt, funding_tx) - def confirm_tx_dialog(self, make_tx, output_value, allow_preview=True): - d = ConfirmTxDialog(window=self, make_tx=make_tx, output_value=output_value, allow_preview=allow_preview) + def confirm_tx_dialog(self, make_tx, output_value, allow_preview=True, batching_candidates=None): + d = ConfirmTxDialog(window=self, make_tx=make_tx, output_value=output_value, allow_preview=allow_preview, batching_candidates=batching_candidates) if d.not_enough_funds: # note: use confirmed_only=False here, regardless of config setting, # as the user needs to get to ConfirmTxDialog to change the config setting diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 597d278c8..0df5ebfaf 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -325,16 +325,23 @@ class SendTab(QWidget, MessageBoxMixin, Logger): # we call get_coins inside make_tx, so that inputs can be changed dynamically if get_coins is None: get_coins = self.window.get_coins - make_tx = lambda fee_policy, *, confirmed_only=False: self.wallet.make_unsigned_transaction( - coins=get_coins(nonlocal_only=nonlocal_only, confirmed_only=confirmed_only), - fee_policy=fee_policy, - outputs=outputs, - is_sweep=is_sweep) + + def make_tx(fee_policy, *, confirmed_only=False, base_tx=False): + coins = get_coins(nonlocal_only=nonlocal_only, confirmed_only=confirmed_only) + return self.wallet.make_unsigned_transaction( + fee_policy=fee_policy, + coins=coins, + outputs=outputs, + base_tx=base_tx, + is_sweep=is_sweep, + send_change_to_lightning=self.config.WALLET_SEND_CHANGE_TO_LIGHTNING, + ) output_values = [x.value for x in outputs] is_max = any(parse_max_spend(outval) for outval in output_values) output_value = '!' if is_max else sum(output_values) - tx, is_preview = self.window.confirm_tx_dialog(make_tx, output_value) + candidates = self.wallet.get_candidates_for_batching(outputs, []) # coins not used + tx, is_preview = self.window.confirm_tx_dialog(make_tx, output_value, batching_candidates=candidates) if tx is None: # user cancelled return diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 4a13708f9..6287c46b9 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -575,13 +575,6 @@ class SimpleConfig(Logger): NETWORK_TIMEOUT = ConfigVar('network_timeout', default=None, type_=int) NETWORK_BOOKMARKED_SERVERS = ConfigVar('network_bookmarked_servers', default=None) - WALLET_BATCH_RBF = ConfigVar( - 'batch_rbf', default=False, type_=bool, - short_desc=lambda: _('Batch unconfirmed transactions'), - long_desc=lambda: ( - _('If you check this box, your unconfirmed transactions will be consolidated into a single transaction.') + '\n' + - _('This will save fees, but might have unwanted effects in terms of privacy')), - ) WALLET_MERGE_DUPLICATE_OUTPUTS = ConfigVar( 'wallet_merge_duplicate_outputs', default=False, type_=bool, short_desc=lambda: _('Merge duplicate outputs'), diff --git a/electrum/wallet.py b/electrum/wallet.py index 418251bb8..1ad4fd0e2 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -1718,16 +1718,17 @@ class Abstract_Wallet(ABC, Logger, EventListener): def dust_threshold(self): return dust_threshold(self.network) - def get_unconfirmed_base_tx_for_batching(self, outputs, coins) -> Optional[Transaction]: - candidate = None + def get_candidates_for_batching(self, outputs, coins) -> Sequence[Transaction]: + candidates = [] domain = self.get_addresses() for hist_item in self.adb.get_history(domain): # tx should not be mined yet if hist_item.tx_mined_status.conf > 0: continue # conservative future proofing of code: only allow known unconfirmed types - if hist_item.tx_mined_status.height not in (TX_HEIGHT_UNCONFIRMED, - TX_HEIGHT_UNCONF_PARENT, - TX_HEIGHT_LOCAL): + if hist_item.tx_mined_status.height not in ( + TX_HEIGHT_UNCONFIRMED, + TX_HEIGHT_UNCONF_PARENT, + TX_HEIGHT_LOCAL): continue # tx should be "outgoing" from wallet if hist_item.delta >= 0: @@ -1753,12 +1754,8 @@ class Abstract_Wallet(ABC, Logger, EventListener): output_amount = sum(o.value for o in outputs) if output_amount > remaining_amount + change_amount: continue - # prefer txns already in mempool (vs local) - if hist_item.tx_mined_status.height == TX_HEIGHT_LOCAL: - candidate = tx - continue - return tx - return candidate + candidates.append(tx) + return candidates def get_change_addresses_for_new_transaction( self, preferred_change_addr=None, *, allow_reusing_used_change_addrs: bool = True, @@ -1843,8 +1840,6 @@ class Abstract_Wallet(ABC, Logger, EventListener): if inputs: input_set = set(txin.prevout for txin in inputs) coins = [coin for coin in coins if (coin.prevout not in input_set)] - if base_tx is None and self.config.WALLET_BATCH_RBF: - base_tx = self.get_unconfirmed_base_tx_for_batching(outputs, coins) # prevent side-effect with '!' outputs = copy.deepcopy(outputs) diff --git a/tests/test_wallet_vertical.py b/tests/test_wallet_vertical.py index 77e2d2528..f3b1d9a36 100644 --- a/tests/test_wallet_vertical.py +++ b/tests/test_wallet_vertical.py @@ -1893,8 +1893,6 @@ class TestWalletSending(ElectrumTestCase): async def _rbf_batching(self, *, simulate_moving_txs, config): wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage', config=config) - wallet.config.WALLET_BATCH_RBF = True - # bootstrap wallet (incoming funding_tx1) funding_tx1 = Transaction('01000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb391400') funding_txid1 = funding_tx1.txid() @@ -1944,7 +1942,7 @@ class TestWalletSending(ElectrumTestCase): # no new input will be needed. just a new output, and change decreased. outputs = [PartialTxOutput.from_address_and_value('tb1qy6xmdj96v5dzt3j08hgc05yk3kltqsnmw4r6ry', 2_500_000)] coins = wallet.get_spendable_coins(domain=None) - tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(20000)) + tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(20000), base_tx=tx) tx.set_rbf(True) tx.locktime = 1325499 tx.version = 1 @@ -1975,7 +1973,7 @@ class TestWalletSending(ElectrumTestCase): # new input will be needed! outputs = [PartialTxOutput.from_address_and_value('2NCVwbmEpvaXKHpXUGJfJr9iB5vtRN3vcut', 6_000_000)] coins = wallet.get_spendable_coins(domain=None) - tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(100000)) + tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(100000), base_tx=tx) tx.set_rbf(True) tx.locktime = 1325499 tx.version = 1 @@ -2023,25 +2021,26 @@ class TestWalletSending(ElectrumTestCase): # create outgoing tx2 outputs = [PartialTxOutput.from_address_and_value("tb1qkfn0fude7z789uys2u7sf80kd4805zpvs3na0h", 90_000)] - for batch_rbf in (False, True): - with self.subTest(batch_rbf=batch_rbf): - coins = wallet.get_spendable_coins(domain=None) - self.assertEqual(2, len(coins)) + coins = wallet.get_spendable_coins(domain=None) + self.assertEqual(2, len(coins)) + + candidates = wallet.get_candidates_for_batching(outputs, coins) + self.assertEqual(candidates, []) + with self.assertRaises(NotEnoughFunds): + wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(1000), base_tx=toself_tx) + + tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(1000)) + tx.set_rbf(True) + tx.locktime = 2423302 + tx.version = 2 + wallet.sign_transaction(tx, password=None) + self.assertEqual('02000000000102bbef0182c2c746bd28517b6fd27ba9eef9c7fb5982efd27bd612cc5a28615a3a0000000000fdffffffbbef0182c2c746bd28517b6fd27ba9eef9c7fb5982efd27bd612cc5a28615a3a0100000000fdffffff02602200000000000016001413fabce9be995554a722fc4e1c5ae53ebfd58164905f010000000000160014b266f4f1b9f0bc72f090573d049df66d4efa082c0247304402205c50b9ddb1b3ead6214d7d9707c74ba29ff547880d017aae2459db156bf85b9b022041134562fffa3dccf1ac05d9b07da62a8d57dd158d25d22d1965a011325e64aa012102c72b815ba00ccb0b469cc61a0ceb843d974e630cf34abcfac178838f1974f68f02473044022049774c32b0ad046b7acdb4acc38107b6b1be57c0d167643a48cbc045850c86c202205189ed61342fc52a377c2865a879c4c2606de98eebd6bf4d73874d62329668c70121033484c8ed83c359d1c3e569accb04b77988daab9408fc82869051c10d0749ac2006fa2400', str(tx)) - wallet.config.WALLET_BATCH_RBF = batch_rbf - tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(1000)) - tx.set_rbf(True) - tx.locktime = 2423302 - tx.version = 2 - wallet.sign_transaction(tx, password=None) - self.assertEqual('02000000000102bbef0182c2c746bd28517b6fd27ba9eef9c7fb5982efd27bd612cc5a28615a3a0000000000fdffffffbbef0182c2c746bd28517b6fd27ba9eef9c7fb5982efd27bd612cc5a28615a3a0100000000fdffffff02602200000000000016001413fabce9be995554a722fc4e1c5ae53ebfd58164905f010000000000160014b266f4f1b9f0bc72f090573d049df66d4efa082c0247304402205c50b9ddb1b3ead6214d7d9707c74ba29ff547880d017aae2459db156bf85b9b022041134562fffa3dccf1ac05d9b07da62a8d57dd158d25d22d1965a011325e64aa012102c72b815ba00ccb0b469cc61a0ceb843d974e630cf34abcfac178838f1974f68f02473044022049774c32b0ad046b7acdb4acc38107b6b1be57c0d167643a48cbc045850c86c202205189ed61342fc52a377c2865a879c4c2606de98eebd6bf4d73874d62329668c70121033484c8ed83c359d1c3e569accb04b77988daab9408fc82869051c10d0749ac2006fa2400', - str(tx)) async def test_rbf_batching__merge_duplicate_outputs(self): """txos paying to the same address might be merged into a single output with a larger value""" wallet = self.create_standard_wallet_from_seed('response era cable net spike again observe dumb wage wonder sail tortoise', config=self.config) - wallet.config.WALLET_BATCH_RBF = True # bootstrap wallet (incoming funding_tx0): for 500k sat funding_tx = Transaction('02000000000101013548c9019890e27ce9e58766de05f18ea40ede70751fb6cd7a3a1715ece0a30100000000fdffffff0220a1070000000000160014542266519a44eb9b903761d40c6fe1055d33fa05485a080000000000160014bc69f7d82c403a9f35dfb6d1a4531d6b19cab0e3024730440220346b200f21c3024e1d51fb4ecddbdbd68bd24ae7b9dfd501519f6dcbeb7c052402200617e3ce7b0eb308e30caf23894fb0388b68fb1c15dd0681dd13ae5e735f148101210360d0c9ef15b8b6a16912d341ad218a4e4e4e07e9347f4a2dbc7ca8d974f8bc9ec1ad2600') @@ -2067,7 +2066,7 @@ class TestWalletSending(ElectrumTestCase): # second payment to dest_addr (merged) outputs2 = [PartialTxOutput.from_address_and_value(dest_addr, 100_000)] coins = wallet.get_spendable_coins(domain=None) - tx2 = wallet.make_unsigned_transaction(coins=coins, outputs=outputs2, fee_policy=FixedFeePolicy(3000)) + tx2 = wallet.make_unsigned_transaction(coins=coins, outputs=outputs2, fee_policy=FixedFeePolicy(3000), base_tx=tx1) tx2.set_rbf(True) tx2.locktime = 2534850 tx2.version = 2 @@ -2086,7 +2085,7 @@ class TestWalletSending(ElectrumTestCase): # second payment to dest_addr (not merged, just duplicate outputs) outputs2 = [PartialTxOutput.from_address_and_value(dest_addr, 100_000)] coins = wallet.get_spendable_coins(domain=None) - tx3 = wallet.make_unsigned_transaction(coins=coins, outputs=outputs2, fee_policy=FixedFeePolicy(3000)) + tx3 = wallet.make_unsigned_transaction(coins=coins, outputs=outputs2, fee_policy=FixedFeePolicy(3000), base_tx=tx1) tx3.set_rbf(True) tx3.locktime = 2534850 tx3.version = 2