Browse Source
wallet: refactor bump_fee; add new strategy; change Qt dialog to have "advanced" buttonpatch-4
ghost43
4 years ago
committed by
GitHub
7 changed files with 446 additions and 109 deletions
@ -0,0 +1,213 @@ |
|||||
|
# Copyright (C) 2021 The Electrum developers |
||||
|
# Distributed under the MIT software license, see the accompanying |
||||
|
# file LICENCE or http://www.opensource.org/licenses/mit-license.php |
||||
|
|
||||
|
from typing import TYPE_CHECKING |
||||
|
|
||||
|
from PyQt5.QtWidgets import (QCheckBox, QLabel, QVBoxLayout, QGridLayout, QWidget, |
||||
|
QPushButton, QHBoxLayout, QComboBox) |
||||
|
|
||||
|
from .amountedit import FeerateEdit |
||||
|
from .fee_slider import FeeSlider, FeeComboBox |
||||
|
from .util import (ColorScheme, WindowModalDialog, Buttons, |
||||
|
OkButton, WWLabel, CancelButton) |
||||
|
|
||||
|
from electrum.i18n import _ |
||||
|
from electrum.transaction import PartialTransaction |
||||
|
from electrum.wallet import BumpFeeStrategy |
||||
|
|
||||
|
if TYPE_CHECKING: |
||||
|
from .main_window import ElectrumWindow |
||||
|
|
||||
|
|
||||
|
class _BaseRBFDialog(WindowModalDialog): |
||||
|
|
||||
|
def __init__( |
||||
|
self, |
||||
|
*, |
||||
|
main_window: 'ElectrumWindow', |
||||
|
tx: PartialTransaction, |
||||
|
txid: str, |
||||
|
title: str, |
||||
|
help_text: str, |
||||
|
): |
||||
|
WindowModalDialog.__init__(self, main_window, title=title) |
||||
|
self.window = main_window |
||||
|
self.wallet = main_window.wallet |
||||
|
self.tx = tx |
||||
|
assert txid |
||||
|
self.txid = txid |
||||
|
|
||||
|
fee = tx.get_fee() |
||||
|
assert fee is not None |
||||
|
tx_size = tx.estimated_size() |
||||
|
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")) |
||||
|
warning_label = WWLabel('\n') |
||||
|
warning_label.setStyleSheet(ColorScheme.RED.as_stylesheet()) |
||||
|
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) |
||||
|
|
||||
|
def on_slider(dyn, pos, fee_rate): |
||||
|
fee_slider.activate() |
||||
|
if fee_rate is not None: |
||||
|
self.feerate_e.setAmount(fee_rate / 1000) |
||||
|
|
||||
|
fee_slider = FeeSlider(self.window, self.window.config, on_slider) |
||||
|
fee_combo = FeeComboBox(fee_slider) |
||||
|
fee_slider.deactivate() |
||||
|
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) |
||||
|
|
||||
|
btns_hbox = QHBoxLayout() |
||||
|
btns_hbox.addWidget(self.adv_button) |
||||
|
btns_hbox.addStretch(1) |
||||
|
btns_hbox.addWidget(CancelButton(self)) |
||||
|
btns_hbox.addWidget(ok_button) |
||||
|
vbox.addLayout(btns_hbox) |
||||
|
|
||||
|
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: |
||||
|
self.cb_rbf = QCheckBox(_('Keep Replace-By-Fee enabled')) |
||||
|
self.cb_rbf.setChecked(True) |
||||
|
adv_vbox.addWidget(self.cb_rbf) |
||||
|
|
||||
|
def run(self) -> None: |
||||
|
if not self.exec_(): |
||||
|
return |
||||
|
is_rbf = self.cb_rbf.isChecked() |
||||
|
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(is_rbf) |
||||
|
tx_label = self.wallet.get_label_for_txid(self.txid) |
||||
|
self.window.show_transaction(new_tx, tx_desc=tx_label) |
||||
|
# TODO maybe save tx_label as label for new tx?? |
||||
|
|
||||
|
|
||||
|
class BumpFeeDialog(_BaseRBFDialog): |
||||
|
|
||||
|
def __init__( |
||||
|
self, |
||||
|
*, |
||||
|
main_window: 'ElectrumWindow', |
||||
|
tx: PartialTransaction, |
||||
|
txid: str, |
||||
|
): |
||||
|
help_text = _("Increase your transaction's fee to improve its position in mempool.") |
||||
|
_BaseRBFDialog.__init__( |
||||
|
self, |
||||
|
main_window=main_window, |
||||
|
tx=tx, |
||||
|
txid=txid, |
||||
|
title=_('Bump Fee'), |
||||
|
help_text=help_text, |
||||
|
) |
||||
|
|
||||
|
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.cb_rbf = QCheckBox(_('Keep Replace-By-Fee enabled')) |
||||
|
self.cb_rbf.setChecked(True) |
||||
|
adv_vbox.addWidget(self.cb_rbf) |
||||
|
|
||||
|
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) |
||||
|
|
||||
|
|
||||
|
class DSCancelDialog(_BaseRBFDialog): |
||||
|
|
||||
|
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.") |
||||
|
_BaseRBFDialog.__init__( |
||||
|
self, |
||||
|
main_window=main_window, |
||||
|
tx=tx, |
||||
|
txid=txid, |
||||
|
title=_('Cancel transaction'), |
||||
|
help_text=help_text, |
||||
|
) |
||||
|
|
||||
|
def rbf_func(self, fee_rate): |
||||
|
return self.wallet.dscancel(tx=self.tx, new_fee_rate=fee_rate) |
Loading…
Reference in new issue