Skip to content

Commit

Permalink
Simplify RBF user experience:
Browse files Browse the repository at this point in the history
 - replace complex strategies with a simpler choice,
   between preserving or decreasing the payment.
 - Always expose that choice to the user.
 - Show the resulting fees to the user before they click OK
  • Loading branch information
ecdsa committed Dec 13, 2022
1 parent 4d3f50e commit a383f56
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 171 deletions.
21 changes: 4 additions & 17 deletions electrum/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
tx_from_any, PartialTxInput, TxOutpoint)
from .invoices import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED
from .synchronizer import Notifier
from .wallet import Abstract_Wallet, create_new_wallet, restore_wallet_from_text, Deterministic_Wallet, BumpFeeStrategy
from .wallet import Abstract_Wallet, create_new_wallet, restore_wallet_from_text, Deterministic_Wallet
from .address_synchronizer import TX_HEIGHT_LOCAL
from .mnemonic import Mnemonic
from .lnutil import SENT, RECEIVED
Expand Down Expand Up @@ -755,31 +755,18 @@ async def onchain_history(self, year=None, show_addresses=False, show_fiat=False
return json_normalize(wallet.get_detailed_history(**kwargs))

@command('wp')
async def bumpfee(self, tx, new_fee_rate, from_coins=None, strategies=None, password=None, unsigned=False, wallet: Abstract_Wallet = None):
async def bumpfee(self, tx, new_fee_rate, from_coins=None, decrease_payment=False, password=None, unsigned=False, wallet: Abstract_Wallet = None):
""" Bump the Fee for an unconfirmed Transaction """
tx = Transaction(tx)
domain_coins = from_coins.split(',') if from_coins else None
coins = wallet.get_spendable_coins(None)
if domain_coins is not None:
coins = [coin for coin in coins if (coin.prevout.to_str() in domain_coins)]
strategies = strategies.split(',') if strategies else None
bumpfee_strategies = None
if strategies is not None:
bumpfee_strategies = []
for strategy in strategies:
if strategy == 'CoinChooser':
bumpfee_strategies.append(BumpFeeStrategy.COINCHOOSER)
elif strategy == 'DecreaseChange':
bumpfee_strategies.append(BumpFeeStrategy.DECREASE_CHANGE)
elif strategy == 'DecreasePayment':
bumpfee_strategies.append(BumpFeeStrategy.DECREASE_PAYMENT)
else:
raise Exception("Invalid Choice of Strategies")
new_tx = wallet.bump_fee(
tx=tx,
txid=tx.txid(),
coins=coins,
strategies=bumpfee_strategies,
decrease_payment=decrease_payment,
new_fee_rate=new_fee_rate)
if not unsigned:
wallet.sign_transaction(new_tx, password)
Expand Down Expand Up @@ -1413,6 +1400,7 @@ def eval_bool(x: str) -> bool:
'privkey': (None, "Private key. Set to '?' to get a prompt."),
'unsigned': ("-u", "Do not sign transaction"),
'rbf': (None, "Whether to signal opt-in Replace-By-Fee in the transaction (true/false)"),
'decrease_payment': (None, "Whether payment amount will be decreased (true/false)"),
'locktime': (None, "Set locktime block number"),
'addtransaction': (None,'Whether transaction is to be used for broadcasting afterwards. Adds transaction to the wallet'),
'domain': ("-D", "List of addresses"),
Expand All @@ -1436,7 +1424,6 @@ def eval_bool(x: str) -> bool:
'gossip': (None, "Apply command to gossip node instead of wallet"),
'connection_string': (None, "Lightning network node ID or network address"),
'new_fee_rate': (None, "The Updated/Increased Transaction fee rate (in sat/byte)"),
'strategies': (None, "Select RBF any one or multiple RBF strategies in any order, separated by ','; Options : 'CoinChooser','DecreaseChange','DecreasePayment' "),
'from_amount': (None, "Amount to convert (default: 1)"),
'from_ccy': (None, "Currency to convert from"),
'to_ccy': (None, "Currency to convert to"),
Expand Down
207 changes: 103 additions & 104 deletions electrum/gui/qt/rbf_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

from electrum.i18n import _
from electrum.transaction import PartialTransaction
from electrum.wallet import BumpFeeStrategy
from electrum.wallet import CannotBumpFee

if TYPE_CHECKING:
from .main_window import ElectrumWindow
Expand All @@ -28,47 +28,30 @@ def __init__(
main_window: 'ElectrumWindow',
tx: PartialTransaction,
txid: str,
title: str,
help_text: str,
):
title: str):

WindowModalDialog.__init__(self, main_window, title=title)
self.window = main_window
self.wallet = main_window.wallet
self.tx = tx
self.new_tx = None
assert txid
self.txid = txid
self.message = ''

fee = tx.get_fee()
assert fee is not None
tx_size = tx.estimated_size()
old_fee_rate = fee / tx_size # sat/vbyte
self.old_fee_rate = old_fee_rate = fee / tx_size # sat/vbyte
vbox = QVBoxLayout(self)
vbox.addWidget(WWLabel(help_text))

ok_button = OkButton(self)
self.adv_button = QPushButton(_("Show advanced settings"))
self.adv_button.setEnabled(False)
self.adv_button.setVisible(False)
warning_label = WWLabel('\n')
warning_label.setStyleSheet(ColorScheme.RED.as_stylesheet())
vbox.addWidget(WWLabel(self.help_text))
vbox.addStretch(1)

self.ok_button = OkButton(self)
self.message_label = QLabel('')
self.feerate_e = FeerateEdit(lambda: 0)
self.feerate_e.setAmount(max(old_fee_rate * 1.5, old_fee_rate + 1))

def on_feerate():
fee_rate = self.feerate_e.get_amount()
warning_text = '\n'
if fee_rate is not None:
try:
new_tx = self.rbf_func(fee_rate)
except Exception as e:
new_tx = None
warning_text = str(e).replace('\n', ' ')
else:
new_tx = None
ok_button.setEnabled(new_tx is not None)
warning_label.setText(warning_text)

self.feerate_e.textChanged.connect(on_feerate)
self.feerate_e.textChanged.connect(self.update)

def on_slider(dyn, pos, fee_rate):
fee_slider.activate()
Expand All @@ -81,130 +64,146 @@ def on_slider(dyn, pos, fee_rate):
self.feerate_e.textEdited.connect(fee_slider.deactivate)

grid = QGridLayout()
grid.addWidget(QLabel(_('Current Fee') + ':'), 0, 0)
grid.addWidget(QLabel(self.window.format_amount(fee) + ' ' + self.window.base_unit()), 0, 1)
grid.addWidget(QLabel(_('Current Fee rate') + ':'), 1, 0)
grid.addWidget(QLabel(self.window.format_fee_rate(1000 * old_fee_rate)), 1, 1)
grid.addWidget(QLabel(_('New Fee rate') + ':'), 2, 0)
grid.addWidget(self.feerate_e, 2, 1)
grid.addWidget(fee_slider, 3, 1)
grid.addWidget(fee_combo, 3, 2)
vbox.addLayout(grid)
self._add_advanced_options_cont(vbox)
vbox.addWidget(warning_label)

self.method_label = QLabel(_('Method') + ':')
self.method_combo = QComboBox()
self.method_combo.addItems([_('Preserve payment'), _('Decrease payment')])
self.method_combo.currentIndexChanged.connect(self.update)
grid.addWidget(self.method_label, 0, 0)
grid.addWidget(self.method_combo, 0, 1)

grid.addWidget(QLabel(_('Current fee') + ':'), 1, 0)
grid.addWidget(QLabel(self.window.format_amount_and_units(fee)), 1, 1)
grid.addWidget(QLabel(_('Current fee rate') + ':'), 2, 0)
grid.addWidget(QLabel(self.window.format_fee_rate(1000 * old_fee_rate)), 2, 1)

grid.addWidget(QLabel(_('New fee rate') + ':'), 3, 0)
grid.addWidget(self.feerate_e, 3, 1)
grid.addWidget(fee_slider, 3, 2)
grid.addWidget(fee_combo, 3, 3)
grid.addWidget(self.message_label, 5, 0, 1, 3)

vbox.addLayout(grid)
vbox.addStretch(1)
btns_hbox = QHBoxLayout()
btns_hbox.addWidget(self.adv_button)
btns_hbox.addStretch(1)
btns_hbox.addWidget(CancelButton(self))
btns_hbox.addWidget(ok_button)
btns_hbox.addWidget(self.ok_button)
vbox.addLayout(btns_hbox)

new_fee_rate = old_fee_rate + max(1, old_fee_rate // 20)
self.feerate_e.setAmount(new_fee_rate)
self._update_tx(new_fee_rate)
self._update_message()
# give focus to fee slider
fee_slider.activate()
fee_slider.setFocus()
# 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()

def is_decrease_payment(self):
return self.method_combo.currentIndex() == 1

def set_decrease_payment(self):
self.method_combo.setCurrentIndex(1)

def rbf_func(self, fee_rate) -> PartialTransaction:
raise NotImplementedError() # implemented by subclasses

def _add_advanced_options_cont(self, vbox: QVBoxLayout) -> None:
adv_vbox = QVBoxLayout()
adv_vbox.setContentsMargins(0, 0, 0, 0)
adv_widget = QWidget()
adv_widget.setLayout(adv_vbox)
adv_widget.setVisible(False)
def show_adv_settings():
self.adv_button.setEnabled(False)
adv_widget.setVisible(True)
self.adv_button.clicked.connect(show_adv_settings)
self._add_advanced_options(adv_vbox)
vbox.addWidget(adv_widget)

def _add_advanced_options(self, adv_vbox: QVBoxLayout) -> None:
pass

def run(self) -> None:
if not self.exec_():
return
new_fee_rate = self.feerate_e.get_amount()
try:
new_tx = self.rbf_func(new_fee_rate)
except Exception as e:
self.window.show_error(str(e))
return
new_tx.set_rbf(True)
self.new_tx.set_rbf(True)
tx_label = self.wallet.get_label_for_txid(self.txid)
self.window.show_transaction(new_tx, tx_desc=tx_label)
self.window.show_transaction(self.new_tx, tx_desc=tx_label)
# TODO maybe save tx_label as label for new tx??

def update(self):
fee_rate = self.feerate_e.get_amount()
self._update_tx(fee_rate)
self._update_message()

def _update_tx(self, fee_rate):
if fee_rate is None:
self.new_tx = None
self.message = ''
elif fee_rate <= self.old_fee_rate:
self.new_tx = None
self.message = _("The new fee rate needs to be higher than the old fee rate.")
else:
try:
self.new_tx = self.rbf_func(fee_rate)
except CannotBumpFee as e:
self.new_tx = None
self.message = str(e)
if not self.new_tx:
return
delta = self.new_tx.get_fee() - self.tx.get_fee()
if not self.is_decrease_payment():
self.message = _("You will pay {} more.").format(self.window.format_amount_and_units(delta))
else:
self.message = _("The recipient will receive {} less.").format(self.window.format_amount_and_units(delta))

def _update_message(self):
enabled = bool(self.new_tx)
self.ok_button.setEnabled(enabled)
if enabled:
style = ColorScheme.BLUE.as_stylesheet()
else:
style = ColorScheme.RED.as_stylesheet()
self.message_label.setStyleSheet(style)
self.message_label.setText(self.message)


class BumpFeeDialog(_BaseRBFDialog):

help_text = _("Increase your transaction's fee to improve its position in mempool.")

def __init__(
self,
*,
main_window: 'ElectrumWindow',
tx: PartialTransaction,
txid: str,
):
help_text = _("Increase your transaction's fee to improve its position in mempool.")
txid: str):
_BaseRBFDialog.__init__(
self,
main_window=main_window,
tx=tx,
txid=txid,
title=_('Bump Fee'),
help_text=help_text,
)
title=_('Bump Fee'))

def rbf_func(self, fee_rate):
return self.wallet.bump_fee(
tx=self.tx,
txid=self.txid,
new_fee_rate=fee_rate,
coins=self.window.get_coins(),
strategies=self.option_index_to_strats[self.strat_combo.currentIndex()],
)

def _add_advanced_options(self, adv_vbox: QVBoxLayout) -> None:
self.adv_button.setVisible(True)
self.adv_button.setEnabled(True)
self.strat_combo = QComboBox()
options = [
_("decrease change, or add new inputs, or decrease any outputs"),
_("decrease change, or decrease any outputs"),
_("decrease payment"),
]
self.option_index_to_strats = {
0: [BumpFeeStrategy.COINCHOOSER, BumpFeeStrategy.DECREASE_CHANGE],
1: [BumpFeeStrategy.DECREASE_CHANGE],
2: [BumpFeeStrategy.DECREASE_PAYMENT],
}
self.strat_combo.addItems(options)
self.strat_combo.setCurrentIndex(0)
strat_hbox = QHBoxLayout()
strat_hbox.addWidget(QLabel(_("Strategy") + ":"))
strat_hbox.addWidget(self.strat_combo)
strat_hbox.addStretch(1)
adv_vbox.addLayout(strat_hbox)
decrease_payment=self.is_decrease_payment())


class DSCancelDialog(_BaseRBFDialog):

help_text = _(
"Cancel an unconfirmed transaction by replacing it with "
"a higher-fee transaction that spends back to your wallet.")

def __init__(
self,
*,
main_window: 'ElectrumWindow',
tx: PartialTransaction,
txid: str,
):
help_text = _(
"Cancel an unconfirmed RBF transaction by double-spending "
"its inputs back to your wallet with a higher fee.")
txid: str):
_BaseRBFDialog.__init__(
self,
main_window=main_window,
tx=tx,
txid=txid,
title=_('Cancel transaction'),
help_text=help_text,
)
title=_('Cancel transaction'))
self.method_label.setVisible(False)
self.method_combo.setVisible(False)

def rbf_func(self, fee_rate):
return self.wallet.dscancel(tx=self.tx, new_fee_rate=fee_rate)
8 changes: 4 additions & 4 deletions electrum/tests/test_wallet_vertical.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from electrum import util
from electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT
from electrum.wallet import (sweep, Multisig_Wallet, Standard_Wallet, Imported_Wallet,
restore_wallet_from_text, Abstract_Wallet, BumpFeeStrategy)
restore_wallet_from_text, Abstract_Wallet)
from electrum.util import (
bfh, bh2u, NotEnoughFunds, UnrelatedTransactionException,
UserFacingException)
Expand Down Expand Up @@ -1199,7 +1199,7 @@ def _bump_fee_p2wpkh_decrease_payment(self, *, simulate_moving_txs, config):
tx = wallet.bump_fee(
tx=tx_from_any(orig_rbf_tx.serialize()),
new_fee_rate=60,
strategies=[BumpFeeStrategy.DECREASE_PAYMENT],
decrease_payment=True,
)
tx.locktime = 1936085
tx.version = 2
Expand Down Expand Up @@ -1241,7 +1241,7 @@ def _bump_fee_p2wpkh_decrease_payment_batch(self, *, simulate_moving_txs, config
tx = wallet.bump_fee(
tx=tx_from_any(orig_rbf_tx.serialize()),
new_fee_rate=60,
strategies=[BumpFeeStrategy.DECREASE_PAYMENT],
decrease_payment=True,
)
tx.locktime = 1936095
tx.version = 2
Expand Down Expand Up @@ -1574,7 +1574,7 @@ def _bump_fee_when_user_sends_max(self, *, simulate_moving_txs, config):
self.assertEqual((0, 0, 0), wallet.get_balance())

# bump tx
tx = wallet.bump_fee(tx=tx_from_any(tx.serialize()), new_fee_rate=70.0)
tx = wallet.bump_fee(tx=tx_from_any(tx.serialize()), new_fee_rate=70.0, decrease_payment=True)
tx.locktime = 1325500
tx.version = 1
if simulate_moving_txs:
Expand Down
Loading

0 comments on commit a383f56

Please sign in to comment.