Browse Source

wallet.bump_fee: the glorious return of BumpFeeStrategy :D

gui/qt/rbf_dialog.py (old) lines 57-64 were implementing logic that should not be part of GUI code.
Case in point, gui/qml/qetxfinalizer.py (old) lines 511-513 duplicated half of that logic but not the other half.
That logic is now moved to wallet.get_bumpfee_strategies_for_tx().

More context: a user on irc got confused when using the qml gui. They were sending "max" and wanted to bump_fee.
The qml gui selected the "preserve_payment" strategy by default, using which there was no solution, and the user
did not notice that the strategy can be altered (via the "method" dropdown). The qt gui had logic to select
"decrease_payment" by default in such a case (which does find a solution to bump) but this logic was not
duplicated in the qml gui.
Instead of duplicating the logic, this commit moves it to shared lib code.
master
SomberNight 2 years ago
parent
commit
7f64ecc4bd
No known key found for this signature in database
GPG Key ID: B33B5F232C6271E9
  1. 4
      electrum/commands.py
  2. 6
      electrum/gui/qml/components/RbfBumpFeeDialog.qml
  3. 28
      electrum/gui/qml/qetxfinalizer.py
  4. 29
      electrum/gui/qt/rbf_dialog.py
  5. 14
      electrum/tests/test_wallet_vertical.py
  6. 47
      electrum/wallet.py

4
electrum/commands.py

@ -52,7 +52,7 @@ from .transaction import (Transaction, multisig_script, TxOutput, PartialTransac
from . import transaction from . import transaction
from .invoices import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED from .invoices import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED
from .synchronizer import Notifier from .synchronizer import Notifier
from .wallet import Abstract_Wallet, create_new_wallet, restore_wallet_from_text, Deterministic_Wallet from .wallet import Abstract_Wallet, create_new_wallet, restore_wallet_from_text, Deterministic_Wallet, BumpFeeStrategy
from .address_synchronizer import TX_HEIGHT_LOCAL from .address_synchronizer import TX_HEIGHT_LOCAL
from .mnemonic import Mnemonic from .mnemonic import Mnemonic
from .lnutil import SENT, RECEIVED from .lnutil import SENT, RECEIVED
@ -819,7 +819,7 @@ class Commands:
tx=tx, tx=tx,
txid=tx.txid(), txid=tx.txid(),
coins=coins, coins=coins,
decrease_payment=decrease_payment, strategy=BumpFeeStrategy.DECREASE_PAYMENT if decrease_payment else BumpFeeStrategy.PRESERVE_PAYMENT,
new_fee_rate=new_fee_rate) new_fee_rate=new_fee_rate)
if not unsigned: if not unsigned:
wallet.sign_transaction(new_tx, password) wallet.sign_transaction(new_tx, password)

6
electrum/gui/qml/components/RbfBumpFeeDialog.qml

@ -62,15 +62,11 @@ ElDialog {
ElComboBox { ElComboBox {
id: bumpMethodComboBox id: bumpMethodComboBox
enabled: rbffeebumper.canChangeBumpMethod
textRole: 'text' textRole: 'text'
valueRole: 'value' valueRole: 'value'
model: [ model: rbffeebumper.bumpMethodsAvailable
{ text: qsTr('Preserve payment'), value: 'preserve_payment' },
{ text: qsTr('Decrease payment'), value: 'decrease_payment' }
]
onCurrentValueChanged: { onCurrentValueChanged: {
if (activeFocus) if (activeFocus)
rbffeebumper.bumpMethod = currentValue rbffeebumper.bumpMethod = currentValue

28
electrum/gui/qml/qetxfinalizer.py

@ -8,7 +8,7 @@ from electrum.logging import get_logger
from electrum.i18n import _ from electrum.i18n import _
from electrum.transaction import PartialTxOutput, PartialTransaction, Transaction from electrum.transaction import PartialTxOutput, PartialTransaction, Transaction
from electrum.util import NotEnoughFunds, profiler from electrum.util import NotEnoughFunds, profiler
from electrum.wallet import CannotBumpFee, CannotDoubleSpendTx, CannotCPFP from electrum.wallet import CannotBumpFee, CannotDoubleSpendTx, CannotCPFP, BumpFeeStrategy
from electrum.plugin import run_hook from electrum.plugin import run_hook
from .qewallet import QEWallet from .qewallet import QEWallet
@ -460,13 +460,8 @@ class QETxRbfFeeBumper(TxFeeSlider, TxMonMixin):
self._oldfee_rate = 0 self._oldfee_rate = 0
self._orig_tx = None self._orig_tx = None
self._rbf = True self._rbf = True
self._bump_method = 'preserve_payment' self._bump_method = BumpFeeStrategy.PRESERVE_PAYMENT.name
self._can_change_bump_method = True self._bump_methods_available = []
canChangeBumpMethodChanged = pyqtSignal()
@pyqtProperty(bool, notify=canChangeBumpMethodChanged)
def canChangeBumpMethod(self):
return self._can_change_bump_method
oldfeeChanged = pyqtSignal() oldfeeChanged = pyqtSignal()
@pyqtProperty(QEAmount, notify=oldfeeChanged) @pyqtProperty(QEAmount, notify=oldfeeChanged)
@ -496,21 +491,26 @@ class QETxRbfFeeBumper(TxFeeSlider, TxMonMixin):
return self._bump_method return self._bump_method
@bumpMethod.setter @bumpMethod.setter
def bumpMethod(self, bumpmethod): def bumpMethod(self, bumpmethod: str) -> None:
assert self._can_change_bump_method
if self._bump_method != bumpmethod: if self._bump_method != bumpmethod:
self._bump_method = bumpmethod self._bump_method = bumpmethod
self.bumpMethodChanged.emit() self.bumpMethodChanged.emit()
self.update() self.update()
bumpMethodsAvailableChanged = pyqtSignal()
@pyqtProperty('QVariantList', notify=bumpMethodsAvailableChanged)
def bumpMethodsAvailable(self):
return self._bump_methods_available
def get_tx(self): def get_tx(self):
assert self._txid assert self._txid
self._orig_tx = self._wallet.wallet.db.get_transaction(self._txid) self._orig_tx = self._wallet.wallet.db.get_transaction(self._txid)
assert self._orig_tx assert self._orig_tx
if self._wallet.wallet.get_swap_by_funding_tx(self._orig_tx): strategies, def_strat_idx = self._wallet.wallet.get_bumpfee_strategies_for_tx(tx=self._orig_tx, txid=self._txid)
self._can_change_bump_method = False self._bump_methods_available = [{'value': strat.name, 'text': strat.text()} for strat in strategies]
self.canChangeBumpMethodChanged.emit() self.bumpMethodsAvailableChanged.emit()
self.bumpMethod = strategies[def_strat_idx].name
if not isinstance(self._orig_tx, PartialTransaction): if not isinstance(self._orig_tx, PartialTransaction):
self._orig_tx = PartialTransaction.from_tx(self._orig_tx) self._orig_tx = PartialTransaction.from_tx(self._orig_tx)
@ -547,7 +547,7 @@ class QETxRbfFeeBumper(TxFeeSlider, TxMonMixin):
tx=self._orig_tx, tx=self._orig_tx,
txid=self._txid, txid=self._txid,
new_fee_rate=new_fee_rate, new_fee_rate=new_fee_rate,
decrease_payment=self._bump_method=='decrease_payment' strategy=BumpFeeStrategy[self._bump_method],
) )
except CannotBumpFee as e: except CannotBumpFee as e:
self._valid = False self._valid = False

29
electrum/gui/qt/rbf_dialog.py

@ -15,7 +15,7 @@ from .util import (ColorScheme, WindowModalDialog, Buttons,
from electrum.i18n import _ from electrum.i18n import _
from electrum.transaction import PartialTransaction from electrum.transaction import PartialTransaction
from electrum.wallet import CannotRBFTx from electrum.wallet import CannotRBFTx, BumpFeeStrategy
if TYPE_CHECKING: if TYPE_CHECKING:
from .main_window import ElectrumWindow from .main_window import ElectrumWindow
@ -54,19 +54,13 @@ class _BaseRBFDialog(TxEditor):
self.feerate_e.setAmount(new_fee_rate) self.feerate_e.setAmount(new_fee_rate)
self.update() self.update()
self.fee_slider.deactivate() self.fee_slider.deactivate()
# are we paying max?
invoices = self.wallet.get_relevant_invoices_for_tx(txid)
if len(invoices) == 1 and len(invoices[0].outputs) == 1:
if invoices[0].outputs[0].value == '!':
self.set_decrease_payment()
# do not decrease payment if it is a swap
if self.wallet.get_swap_by_funding_tx(self.old_tx):
self.method_combo.setEnabled(False)
def create_grid(self): def create_grid(self):
self.method_label = QLabel(_('Method') + ':') self.method_label = QLabel(_('Method') + ':')
self.method_combo = QComboBox() self.method_combo = QComboBox()
self.method_combo.addItems([_('Preserve payment'), _('Decrease payment')]) self._strategies, def_strat_idx = self.wallet.get_bumpfee_strategies_for_tx(tx=self.old_tx, txid=self.old_txid)
self.method_combo.addItems([strat.text() for strat in self._strategies])
self.method_combo.setCurrentIndex(def_strat_idx)
self.method_combo.currentIndexChanged.connect(self.trigger_update) self.method_combo.currentIndexChanged.connect(self.trigger_update)
self.method_combo.setFocusPolicy(Qt.NoFocus) self.method_combo.setFocusPolicy(Qt.NoFocus)
old_size_label = TxSizeLabel() old_size_label = TxSizeLabel()
@ -93,12 +87,6 @@ class _BaseRBFDialog(TxEditor):
grid.addWidget(self.locktime_e, 5, 1, 1, 2) grid.addWidget(self.locktime_e, 5, 1, 1, 2)
return grid return grid
def is_decrease_payment(self):
return self.method_combo.currentIndex() == 1
def set_decrease_payment(self):
self.method_combo.setCurrentIndex(1)
def run(self) -> None: def run(self) -> None:
if not self.exec_(): if not self.exec_():
return return
@ -133,10 +121,12 @@ class _BaseRBFDialog(TxEditor):
if not self.tx: if not self.tx:
return return
delta = self.tx.get_fee() - self.old_tx.get_fee() delta = self.tx.get_fee() - self.old_tx.get_fee()
if not self.is_decrease_payment(): if self._strategies[self.method_combo.currentIndex()] == BumpFeeStrategy.PRESERVE_PAYMENT:
msg = _("You will pay {} more.").format(self.main_window.format_amount_and_units(delta)) msg = _("You will pay {} more.").format(self.main_window.format_amount_and_units(delta))
else: elif self._strategies[self.method_combo.currentIndex()] == BumpFeeStrategy.DECREASE_PAYMENT:
msg = _("The recipient will receive {} less.").format(self.main_window.format_amount_and_units(delta)) msg = _("The recipient will receive {} less.").format(self.main_window.format_amount_and_units(delta))
else:
raise Exception(f"unknown strategy: {self=}")
messages.insert(0, msg) messages.insert(0, msg)
return messages return messages
@ -164,7 +154,8 @@ class BumpFeeDialog(_BaseRBFDialog):
txid=self.old_txid, txid=self.old_txid,
new_fee_rate=fee_rate, new_fee_rate=fee_rate,
coins=self.main_window.get_coins(nonlocal_only=True, confirmed_only=confirmed_only), coins=self.main_window.get_coins(nonlocal_only=True, confirmed_only=confirmed_only),
decrease_payment=self.is_decrease_payment()) strategy=self._strategies[self.method_combo.currentIndex()],
)
class DSCancelDialog(_BaseRBFDialog): class DSCancelDialog(_BaseRBFDialog):

14
electrum/tests/test_wallet_vertical.py

@ -12,7 +12,7 @@ from electrum import SimpleConfig
from electrum import util from electrum import util
from electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT from electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT
from electrum.wallet import (sweep, Multisig_Wallet, Standard_Wallet, Imported_Wallet, from electrum.wallet import (sweep, Multisig_Wallet, Standard_Wallet, Imported_Wallet,
restore_wallet_from_text, Abstract_Wallet, CannotBumpFee) restore_wallet_from_text, Abstract_Wallet, CannotBumpFee, BumpFeeStrategy)
from electrum.util import ( from electrum.util import (
bfh, NotEnoughFunds, UnrelatedTransactionException, bfh, NotEnoughFunds, UnrelatedTransactionException,
UserFacingException) UserFacingException)
@ -1229,7 +1229,7 @@ class TestWalletSending(ElectrumTestCase):
tx = wallet.bump_fee( tx = wallet.bump_fee(
tx=tx_from_any(orig_rbf_tx.serialize()), tx=tx_from_any(orig_rbf_tx.serialize()),
new_fee_rate=60, new_fee_rate=60,
decrease_payment=True, strategy=BumpFeeStrategy.DECREASE_PAYMENT,
) )
tx.locktime = 1936085 tx.locktime = 1936085
tx.version = 2 tx.version = 2
@ -1271,7 +1271,7 @@ class TestWalletSending(ElectrumTestCase):
tx = wallet.bump_fee( tx = wallet.bump_fee(
tx=tx_from_any(orig_rbf_tx.serialize()), tx=tx_from_any(orig_rbf_tx.serialize()),
new_fee_rate=60, new_fee_rate=60,
decrease_payment=True, strategy=BumpFeeStrategy.DECREASE_PAYMENT,
) )
tx.locktime = 1936095 tx.locktime = 1936095
tx.version = 2 tx.version = 2
@ -1313,19 +1313,19 @@ class TestWalletSending(ElectrumTestCase):
tx = wallet.bump_fee( tx = wallet.bump_fee(
tx=tx_from_any(orig_rbf_tx.serialize()), tx=tx_from_any(orig_rbf_tx.serialize()),
new_fee_rate=99999, new_fee_rate=99999,
decrease_payment=True, strategy=BumpFeeStrategy.DECREASE_PAYMENT,
) )
with self.assertRaises(CannotBumpFee): with self.assertRaises(CannotBumpFee):
tx = wallet.bump_fee( tx = wallet.bump_fee(
tx=tx_from_any(orig_rbf_tx.serialize()), tx=tx_from_any(orig_rbf_tx.serialize()),
new_fee_rate=99999, new_fee_rate=99999,
decrease_payment=False, strategy=BumpFeeStrategy.PRESERVE_PAYMENT,
) )
tx = wallet.bump_fee( tx = wallet.bump_fee(
tx=tx_from_any(orig_rbf_tx.serialize()), tx=tx_from_any(orig_rbf_tx.serialize()),
new_fee_rate=60, new_fee_rate=60,
decrease_payment=True, strategy=BumpFeeStrategy.DECREASE_PAYMENT,
) )
tx.locktime = 1936085 tx.locktime = 1936085
tx.version = 2 tx.version = 2
@ -1647,7 +1647,7 @@ class TestWalletSending(ElectrumTestCase):
self.assertEqual((0, 0, 0), wallet.get_balance()) self.assertEqual((0, 0, 0), wallet.get_balance())
# bump tx # bump tx
tx = wallet.bump_fee(tx=tx_from_any(tx.serialize()), new_fee_rate=70.0, decrease_payment=True) tx = wallet.bump_fee(tx=tx_from_any(tx.serialize()), new_fee_rate=70.0, strategy=BumpFeeStrategy.DECREASE_PAYMENT)
tx.locktime = 1325500 tx.locktime = 1325500
tx.version = 1 tx.version = 1
if simulate_moving_txs: if simulate_moving_txs:

47
electrum/wallet.py

@ -247,6 +247,23 @@ class InternalAddressCorruption(Exception):
"Please restore your wallet from seed, and compare the addresses in both files") "Please restore your wallet from seed, and compare the addresses in both files")
class BumpFeeStrategy(enum.Enum):
PRESERVE_PAYMENT = enum.auto()
DECREASE_PAYMENT = enum.auto()
@classmethod
def all(cls) -> Sequence['BumpFeeStrategy']:
return list(BumpFeeStrategy.__members__.values())
def text(self) -> str:
if self == self.PRESERVE_PAYMENT:
return _('Preserve payment')
elif self == self.DECREASE_PAYMENT:
return _('Decrease payment')
else:
raise Exception(f"unknown strategy: {self=}")
class ReceiveRequestHelp(NamedTuple): class ReceiveRequestHelp(NamedTuple):
# help texts (warnings/errors): # help texts (warnings/errors):
address_help: str address_help: str
@ -1955,6 +1972,28 @@ class Abstract_Wallet(ABC, Logger, EventListener):
def can_export(self): def can_export(self):
return not self.is_watching_only() and hasattr(self.keystore, 'get_private_key') return not self.is_watching_only() and hasattr(self.keystore, 'get_private_key')
def get_bumpfee_strategies_for_tx(
self,
*,
tx: Transaction,
txid: str = None,
) -> Tuple[Sequence[BumpFeeStrategy], int]:
"""Returns tuple(list of available strategies, idx of recommended option among those)."""
txid = txid or tx.txid()
assert txid
assert tx.txid() in (None, txid)
all_strats = BumpFeeStrategy.all()
# are we paying max?
invoices = self.get_relevant_invoices_for_tx(txid)
if len(invoices) == 1 and len(invoices[0].outputs) == 1:
if invoices[0].outputs[0].value == '!':
return all_strats, all_strats.index(BumpFeeStrategy.DECREASE_PAYMENT)
# do not decrease payment if it is a swap
if self.get_swap_by_funding_tx(tx):
return [BumpFeeStrategy.PRESERVE_PAYMENT], 0
# default
return all_strats, all_strats.index(BumpFeeStrategy.PRESERVE_PAYMENT)
def bump_fee( def bump_fee(
self, self,
*, *,
@ -1962,7 +2001,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
txid: str = None, txid: str = None,
new_fee_rate: Union[int, float, Decimal], new_fee_rate: Union[int, float, Decimal],
coins: Sequence[PartialTxInput] = None, coins: Sequence[PartialTxInput] = None,
decrease_payment=False, strategy: BumpFeeStrategy = BumpFeeStrategy.PRESERVE_PAYMENT,
) -> PartialTransaction: ) -> PartialTransaction:
"""Increase the miner fee of 'tx'. """Increase the miner fee of 'tx'.
'new_fee_rate' is the target min rate in sat/vbyte 'new_fee_rate' is the target min rate in sat/vbyte
@ -1991,7 +2030,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
if new_fee_rate <= old_fee_rate: if new_fee_rate <= old_fee_rate:
raise CannotBumpFee(_("The new fee rate needs to be higher than the old fee rate.")) raise CannotBumpFee(_("The new fee rate needs to be higher than the old fee rate."))
if not decrease_payment: if strategy == BumpFeeStrategy.PRESERVE_PAYMENT:
# FIXME: we should try decreasing change first, # FIXME: we should try decreasing change first,
# but it requires updating a bunch of unit tests # but it requires updating a bunch of unit tests
try: try:
@ -2004,9 +2043,11 @@ class Abstract_Wallet(ABC, Logger, EventListener):
except CannotBumpFee as e: except CannotBumpFee as e:
tx_new = self._bump_fee_through_decreasing_change( tx_new = self._bump_fee_through_decreasing_change(
tx=tx, new_fee_rate=new_fee_rate) tx=tx, new_fee_rate=new_fee_rate)
else: elif strategy == BumpFeeStrategy.DECREASE_PAYMENT:
tx_new = self._bump_fee_through_decreasing_payment( tx_new = self._bump_fee_through_decreasing_payment(
tx=tx, new_fee_rate=new_fee_rate) tx=tx, new_fee_rate=new_fee_rate)
else:
raise Exception(f"unknown strategy: {strategy=}")
target_min_fee = new_fee_rate * tx_new.estimated_size() target_min_fee = new_fee_rate * tx_new.estimated_size()
actual_fee = tx_new.get_fee() actual_fee = tx_new.get_fee()

Loading…
Cancel
Save