1
0

tx batching in GUI:

- discard config.WALLET_BATCH_RBF
 - allow the user to choose base_tx from a list of batching
   candidates in ConfirmTxDialog
This commit is contained in:
ThomasV
2025-03-03 15:48:13 +01:00
parent 3b369abf16
commit ab14c3e138
6 changed files with 88 additions and 77 deletions

View File

@@ -30,7 +30,7 @@ from typing import TYPE_CHECKING, Optional, Union, Callable
from PyQt6.QtCore import Qt from PyQt6.QtCore import Qt
from PyQt6.QtGui import QIcon 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.i18n import _
from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates
@@ -57,11 +57,14 @@ from .locktimeedit import LockTimeEdit
class TxEditor(WindowModalDialog): class TxEditor(WindowModalDialog):
def __init__(self, *, title='', def __init__(
window: 'ElectrumWindow', self, *, title='',
make_tx, window: 'ElectrumWindow',
output_value: Union[int, str], make_tx,
allow_preview=True): output_value: Union[int, str],
allow_preview=True,
batching_candidates=None,
):
WindowModalDialog.__init__(self, window, title=title) WindowModalDialog.__init__(self, window, title=title)
self.main_window = window self.main_window = window
@@ -82,6 +85,8 @@ class TxEditor(WindowModalDialog):
# preview is disabled for lightning channel funding # preview is disabled for lightning channel funding
self.allow_preview = allow_preview self.allow_preview = allow_preview
self.is_preview = False self.is_preview = False
self._base_tx = None # for batching
self.batching_candidates = batching_candidates
self.locktime_e = LockTimeEdit(self) self.locktime_e = LockTimeEdit(self)
self.locktime_e.valueEdited.connect(self.trigger_update) self.locktime_e.valueEdited.connect(self.trigger_update)
@@ -106,7 +111,7 @@ class TxEditor(WindowModalDialog):
vbox.addStretch(1) vbox.addStretch(1)
vbox.addLayout(buttons) 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_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_locktime_visible(self.config.GUI_QT_TX_EDITOR_SHOW_LOCKTIME)
self.update_fee_target() self.update_fee_target()
@@ -114,6 +119,9 @@ class TxEditor(WindowModalDialog):
self.main_window.gui_object.timer.timeout.connect(self.timer_actions) 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): def timer_actions(self):
if self.needs_update: if self.needs_update:
self.update() self.update()
@@ -365,6 +373,15 @@ class TxEditor(WindowModalDialog):
self.ok_button.clicked.connect(self.on_send) self.ok_button.clicked.connect(self.on_send)
self.ok_button.setDefault(True) self.ok_button.setDefault(True)
buttons = Buttons(CancelButton(self), self.preview_button, self.ok_button) 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 return buttons
def create_top_bar(self, text): def create_top_bar(self, text):
@@ -404,7 +421,6 @@ class TxEditor(WindowModalDialog):
_('This may result in higher transactions fees.') _('This may result in higher transactions fees.')
])) ]))
self.use_multi_change_menu.setEnabled(self.wallet.use_change) 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_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_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_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.wallet.db.put('multiple_change', self.wallet.multiple_change)
self.trigger_update() self.trigger_update()
def toggle_batch_rbf(self): def update_batching(self):
b = not self.config.WALLET_BATCH_RBF
self.config.WALLET_BATCH_RBF = b
self.trigger_update() self.trigger_update()
def toggle_merge_duplicate_outputs(self): def toggle_merge_duplicate_outputs(self):
@@ -462,9 +476,8 @@ class TxEditor(WindowModalDialog):
self.trigger_update() self.trigger_update()
def toggle_io_visibility(self): def toggle_io_visibility(self):
b = not self.config.GUI_QT_TX_EDITOR_SHOW_IO self.config.GUI_QT_TX_EDITOR_SHOW_IO = not self.config.GUI_QT_TX_EDITOR_SHOW_IO
self.config.GUI_QT_TX_EDITOR_SHOW_IO = b self.set_io_visible()
self.set_io_visible(b)
self.resize_to_fit_content() self.resize_to_fit_content()
def toggle_fee_details(self): def toggle_fee_details(self):
@@ -479,8 +492,8 @@ class TxEditor(WindowModalDialog):
self.set_locktime_visible(b) self.set_locktime_visible(b)
self.resize_to_fit_content() self.resize_to_fit_content()
def set_io_visible(self, b): def set_io_visible(self):
self.io_widget.setVisible(b) 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):
detailed = [self.feerounding_icon, self.feerate_e, self.fee_e] 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()): 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.')) messages.append(_('This transaction will spend unconfirmed coins.'))
# warn if we merge from mempool # 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.')) messages.append(_('This payment will be merged with another existing transaction.'))
# warn if we use multiple change outputs # warn if we use multiple change outputs
num_change = sum(int(o.is_change) for o in self.tx.outputs()) num_change = sum(int(o.is_change) for o in self.tx.outputs())
@@ -603,7 +616,7 @@ class TxEditor(WindowModalDialog):
class ConfirmTxDialog(TxEditor): class ConfirmTxDialog(TxEditor):
help_text = '' #_('Set the mining fee of your transaction') 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__( TxEditor.__init__(
self, self,
@@ -611,8 +624,9 @@ class ConfirmTxDialog(TxEditor):
make_tx=make_tx, make_tx=make_tx,
output_value=output_value, output_value=output_value,
title=_("New Transaction"), # todo: adapt title for channel funding tx, swaps 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() self.trigger_update()
def _update_amount_label(self): def _update_amount_label(self):
@@ -631,8 +645,9 @@ class ConfirmTxDialog(TxEditor):
def update_tx(self, *, fallback_to_zero_fee: bool = False): def update_tx(self, *, fallback_to_zero_fee: bool = False):
fee_policy = self.get_fee_policy() fee_policy = self.get_fee_policy()
confirmed_only = self.config.WALLET_SPEND_CONFIRMED_ONLY confirmed_only = self.config.WALLET_SPEND_CONFIRMED_ONLY
base_tx = self._base_tx
try: 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.not_enough_funds = False
self.no_dynfee_estimates = False self.no_dynfee_estimates = False
except NotEnoughFunds: except NotEnoughFunds:
@@ -640,7 +655,7 @@ class ConfirmTxDialog(TxEditor):
self.tx = None self.tx = None
if fallback_to_zero_fee: if fallback_to_zero_fee:
try: 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: except BaseException:
return return
else: else:
@@ -650,7 +665,7 @@ class ConfirmTxDialog(TxEditor):
self.no_dynfee_estimates = True self.no_dynfee_estimates = True
self.tx = None self.tx = None
try: 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: except NotEnoughFunds:
self.not_enough_funds = True self.not_enough_funds = True
return return
@@ -665,7 +680,7 @@ class ConfirmTxDialog(TxEditor):
def can_pay_assuming_zero_fees(self, confirmed_only) -> bool: def can_pay_assuming_zero_fees(self, confirmed_only) -> bool:
# called in send_tab.py # called in send_tab.py
try: 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: except NotEnoughFunds:
return False return False
else: else:
@@ -688,7 +703,7 @@ class ConfirmTxDialog(TxEditor):
grid.addWidget(HelpLabel(_("Mining Fee") + ": ", msg), 1, 0) grid.addWidget(HelpLabel(_("Mining Fee") + ": ", msg), 1, 0)
grid.addLayout(self.fee_hbox, 1, 1, 1, 3) 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.addLayout(self.fee_target_hbox, 3, 1, 1, 3)
grid.setColumnStretch(4, 1) grid.setColumnStretch(4, 1)

View File

@@ -1385,11 +1385,13 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
WaitingDialog(self, msg, task, on_success, on_failure) WaitingDialog(self, msg, task, on_success, on_failure)
def mktx_for_open_channel(self, *, funding_sat, node_id): 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( def make_tx(fee_policy, *, confirmed_only=False, base_tx=None):
coins = self.get_coins(nonlocal_only=True, confirmed_only=confirmed_only), assert base_tx is None
funding_sat=funding_sat, return self.wallet.lnworker.mktx_for_open_channel(
node_id=node_id, coins = self.get_coins(nonlocal_only=True, confirmed_only=confirmed_only),
fee_policy=fee_policy) funding_sat=funding_sat,
node_id=node_id,
fee_policy=fee_policy)
return make_tx return make_tx
def open_channel(self, connect_str, funding_sat, push_amt): def open_channel(self, connect_str, funding_sat, push_amt):
@@ -1409,8 +1411,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
return return
self._open_channel(connect_str, funding_sat, push_amt, funding_tx) self._open_channel(connect_str, funding_sat, push_amt, funding_tx)
def confirm_tx_dialog(self, make_tx, output_value, allow_preview=True): 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) 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: if d.not_enough_funds:
# note: use confirmed_only=False here, regardless of config setting, # note: use confirmed_only=False here, regardless of config setting,
# as the user needs to get to ConfirmTxDialog to change the config setting # as the user needs to get to ConfirmTxDialog to change the config setting

View File

@@ -325,16 +325,23 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
# we call get_coins inside make_tx, so that inputs can be changed dynamically # we call get_coins inside make_tx, so that inputs can be changed dynamically
if get_coins is None: if get_coins is None:
get_coins = self.window.get_coins 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), def make_tx(fee_policy, *, confirmed_only=False, base_tx=False):
fee_policy=fee_policy, coins = get_coins(nonlocal_only=nonlocal_only, confirmed_only=confirmed_only)
outputs=outputs, return self.wallet.make_unsigned_transaction(
is_sweep=is_sweep) 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] output_values = [x.value for x in outputs]
is_max = any(parse_max_spend(outval) for outval in output_values) is_max = any(parse_max_spend(outval) for outval in output_values)
output_value = '!' if is_max else sum(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: if tx is None:
# user cancelled # user cancelled
return return

View File

@@ -575,13 +575,6 @@ class SimpleConfig(Logger):
NETWORK_TIMEOUT = ConfigVar('network_timeout', default=None, type_=int) NETWORK_TIMEOUT = ConfigVar('network_timeout', default=None, type_=int)
NETWORK_BOOKMARKED_SERVERS = ConfigVar('network_bookmarked_servers', default=None) 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 = ConfigVar(
'wallet_merge_duplicate_outputs', default=False, type_=bool, 'wallet_merge_duplicate_outputs', default=False, type_=bool,
short_desc=lambda: _('Merge duplicate outputs'), short_desc=lambda: _('Merge duplicate outputs'),

View File

@@ -1718,16 +1718,17 @@ class Abstract_Wallet(ABC, Logger, EventListener):
def dust_threshold(self): def dust_threshold(self):
return dust_threshold(self.network) return dust_threshold(self.network)
def get_unconfirmed_base_tx_for_batching(self, outputs, coins) -> Optional[Transaction]: def get_candidates_for_batching(self, outputs, coins) -> Sequence[Transaction]:
candidate = None candidates = []
domain = self.get_addresses() domain = self.get_addresses()
for hist_item in self.adb.get_history(domain): for hist_item in self.adb.get_history(domain):
# tx should not be mined yet # tx should not be mined yet
if hist_item.tx_mined_status.conf > 0: continue if hist_item.tx_mined_status.conf > 0: continue
# conservative future proofing of code: only allow known unconfirmed types # conservative future proofing of code: only allow known unconfirmed types
if hist_item.tx_mined_status.height not in (TX_HEIGHT_UNCONFIRMED, if hist_item.tx_mined_status.height not in (
TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED,
TX_HEIGHT_LOCAL): TX_HEIGHT_UNCONF_PARENT,
TX_HEIGHT_LOCAL):
continue continue
# tx should be "outgoing" from wallet # tx should be "outgoing" from wallet
if hist_item.delta >= 0: if hist_item.delta >= 0:
@@ -1753,12 +1754,8 @@ class Abstract_Wallet(ABC, Logger, EventListener):
output_amount = sum(o.value for o in outputs) output_amount = sum(o.value for o in outputs)
if output_amount > remaining_amount + change_amount: if output_amount > remaining_amount + change_amount:
continue continue
# prefer txns already in mempool (vs local) candidates.append(tx)
if hist_item.tx_mined_status.height == TX_HEIGHT_LOCAL: return candidates
candidate = tx
continue
return tx
return candidate
def get_change_addresses_for_new_transaction( def get_change_addresses_for_new_transaction(
self, preferred_change_addr=None, *, allow_reusing_used_change_addrs: bool = True, self, preferred_change_addr=None, *, allow_reusing_used_change_addrs: bool = True,
@@ -1843,8 +1840,6 @@ class Abstract_Wallet(ABC, Logger, EventListener):
if inputs: if inputs:
input_set = set(txin.prevout for txin in inputs) input_set = set(txin.prevout for txin in inputs)
coins = [coin for coin in coins if (coin.prevout not in input_set)] 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 '!' # prevent side-effect with '!'
outputs = copy.deepcopy(outputs) outputs = copy.deepcopy(outputs)

View File

@@ -1893,8 +1893,6 @@ class TestWalletSending(ElectrumTestCase):
async def _rbf_batching(self, *, simulate_moving_txs, config): 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', wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage',
config=config) config=config)
wallet.config.WALLET_BATCH_RBF = True
# bootstrap wallet (incoming funding_tx1) # bootstrap wallet (incoming funding_tx1)
funding_tx1 = Transaction('01000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb391400') funding_tx1 = Transaction('01000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb391400')
funding_txid1 = funding_tx1.txid() 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. # no new input will be needed. just a new output, and change decreased.
outputs = [PartialTxOutput.from_address_and_value('tb1qy6xmdj96v5dzt3j08hgc05yk3kltqsnmw4r6ry', 2_500_000)] outputs = [PartialTxOutput.from_address_and_value('tb1qy6xmdj96v5dzt3j08hgc05yk3kltqsnmw4r6ry', 2_500_000)]
coins = wallet.get_spendable_coins(domain=None) 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.set_rbf(True)
tx.locktime = 1325499 tx.locktime = 1325499
tx.version = 1 tx.version = 1
@@ -1975,7 +1973,7 @@ class TestWalletSending(ElectrumTestCase):
# new input will be needed! # new input will be needed!
outputs = [PartialTxOutput.from_address_and_value('2NCVwbmEpvaXKHpXUGJfJr9iB5vtRN3vcut', 6_000_000)] outputs = [PartialTxOutput.from_address_and_value('2NCVwbmEpvaXKHpXUGJfJr9iB5vtRN3vcut', 6_000_000)]
coins = wallet.get_spendable_coins(domain=None) 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.set_rbf(True)
tx.locktime = 1325499 tx.locktime = 1325499
tx.version = 1 tx.version = 1
@@ -2023,25 +2021,26 @@ class TestWalletSending(ElectrumTestCase):
# create outgoing tx2 # create outgoing tx2
outputs = [PartialTxOutput.from_address_and_value("tb1qkfn0fude7z789uys2u7sf80kd4805zpvs3na0h", 90_000)] outputs = [PartialTxOutput.from_address_and_value("tb1qkfn0fude7z789uys2u7sf80kd4805zpvs3na0h", 90_000)]
for batch_rbf in (False, True): coins = wallet.get_spendable_coins(domain=None)
with self.subTest(batch_rbf=batch_rbf): 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): 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""" """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', wallet = self.create_standard_wallet_from_seed('response era cable net spike again observe dumb wage wonder sail tortoise',
config=self.config) config=self.config)
wallet.config.WALLET_BATCH_RBF = True
# bootstrap wallet (incoming funding_tx0): for 500k sat # bootstrap wallet (incoming funding_tx0): for 500k sat
funding_tx = Transaction('02000000000101013548c9019890e27ce9e58766de05f18ea40ede70751fb6cd7a3a1715ece0a30100000000fdffffff0220a1070000000000160014542266519a44eb9b903761d40c6fe1055d33fa05485a080000000000160014bc69f7d82c403a9f35dfb6d1a4531d6b19cab0e3024730440220346b200f21c3024e1d51fb4ecddbdbd68bd24ae7b9dfd501519f6dcbeb7c052402200617e3ce7b0eb308e30caf23894fb0388b68fb1c15dd0681dd13ae5e735f148101210360d0c9ef15b8b6a16912d341ad218a4e4e4e07e9347f4a2dbc7ca8d974f8bc9ec1ad2600') funding_tx = Transaction('02000000000101013548c9019890e27ce9e58766de05f18ea40ede70751fb6cd7a3a1715ece0a30100000000fdffffff0220a1070000000000160014542266519a44eb9b903761d40c6fe1055d33fa05485a080000000000160014bc69f7d82c403a9f35dfb6d1a4531d6b19cab0e3024730440220346b200f21c3024e1d51fb4ecddbdbd68bd24ae7b9dfd501519f6dcbeb7c052402200617e3ce7b0eb308e30caf23894fb0388b68fb1c15dd0681dd13ae5e735f148101210360d0c9ef15b8b6a16912d341ad218a4e4e4e07e9347f4a2dbc7ca8d974f8bc9ec1ad2600')
@@ -2067,7 +2066,7 @@ class TestWalletSending(ElectrumTestCase):
# second payment to dest_addr (merged) # second payment to dest_addr (merged)
outputs2 = [PartialTxOutput.from_address_and_value(dest_addr, 100_000)] outputs2 = [PartialTxOutput.from_address_and_value(dest_addr, 100_000)]
coins = wallet.get_spendable_coins(domain=None) 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.set_rbf(True)
tx2.locktime = 2534850 tx2.locktime = 2534850
tx2.version = 2 tx2.version = 2
@@ -2086,7 +2085,7 @@ class TestWalletSending(ElectrumTestCase):
# second payment to dest_addr (not merged, just duplicate outputs) # second payment to dest_addr (not merged, just duplicate outputs)
outputs2 = [PartialTxOutput.from_address_and_value(dest_addr, 100_000)] outputs2 = [PartialTxOutput.from_address_and_value(dest_addr, 100_000)]
coins = wallet.get_spendable_coins(domain=None) 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.set_rbf(True)
tx3.locktime = 2534850 tx3.locktime = 2534850
tx3.version = 2 tx3.version = 2