rbf_dialog.py (7446B)
1 # Copyright (C) 2021 The Electrum developers 2 # Distributed under the MIT software license, see the accompanying 3 # file LICENCE or http://www.opensource.org/licenses/mit-license.php 4 5 from typing import TYPE_CHECKING 6 7 from PyQt5.QtWidgets import (QCheckBox, QLabel, QVBoxLayout, QGridLayout, QWidget, 8 QPushButton, QHBoxLayout, QComboBox) 9 10 from .amountedit import FeerateEdit 11 from .fee_slider import FeeSlider, FeeComboBox 12 from .util import (ColorScheme, WindowModalDialog, Buttons, 13 OkButton, WWLabel, CancelButton) 14 15 from electrum.i18n import _ 16 from electrum.transaction import PartialTransaction 17 from electrum.wallet import BumpFeeStrategy 18 19 if TYPE_CHECKING: 20 from .main_window import ElectrumWindow 21 22 23 class _BaseRBFDialog(WindowModalDialog): 24 25 def __init__( 26 self, 27 *, 28 main_window: 'ElectrumWindow', 29 tx: PartialTransaction, 30 txid: str, 31 title: str, 32 help_text: str, 33 ): 34 WindowModalDialog.__init__(self, main_window, title=title) 35 self.window = main_window 36 self.wallet = main_window.wallet 37 self.tx = tx 38 assert txid 39 self.txid = txid 40 41 fee = tx.get_fee() 42 assert fee is not None 43 tx_size = tx.estimated_size() 44 old_fee_rate = fee / tx_size # sat/vbyte 45 vbox = QVBoxLayout(self) 46 vbox.addWidget(WWLabel(help_text)) 47 48 ok_button = OkButton(self) 49 self.adv_button = QPushButton(_("Show advanced settings")) 50 warning_label = WWLabel('\n') 51 warning_label.setStyleSheet(ColorScheme.RED.as_stylesheet()) 52 self.feerate_e = FeerateEdit(lambda: 0) 53 self.feerate_e.setAmount(max(old_fee_rate * 1.5, old_fee_rate + 1)) 54 55 def on_feerate(): 56 fee_rate = self.feerate_e.get_amount() 57 warning_text = '\n' 58 if fee_rate is not None: 59 try: 60 new_tx = self.rbf_func(fee_rate) 61 except Exception as e: 62 new_tx = None 63 warning_text = str(e).replace('\n', ' ') 64 else: 65 new_tx = None 66 ok_button.setEnabled(new_tx is not None) 67 warning_label.setText(warning_text) 68 69 self.feerate_e.textChanged.connect(on_feerate) 70 71 def on_slider(dyn, pos, fee_rate): 72 fee_slider.activate() 73 if fee_rate is not None: 74 self.feerate_e.setAmount(fee_rate / 1000) 75 76 fee_slider = FeeSlider(self.window, self.window.config, on_slider) 77 fee_combo = FeeComboBox(fee_slider) 78 fee_slider.deactivate() 79 self.feerate_e.textEdited.connect(fee_slider.deactivate) 80 81 grid = QGridLayout() 82 grid.addWidget(QLabel(_('Current Fee') + ':'), 0, 0) 83 grid.addWidget(QLabel(self.window.format_amount(fee) + ' ' + self.window.base_unit()), 0, 1) 84 grid.addWidget(QLabel(_('Current Fee rate') + ':'), 1, 0) 85 grid.addWidget(QLabel(self.window.format_fee_rate(1000 * old_fee_rate)), 1, 1) 86 grid.addWidget(QLabel(_('New Fee rate') + ':'), 2, 0) 87 grid.addWidget(self.feerate_e, 2, 1) 88 grid.addWidget(fee_slider, 3, 1) 89 grid.addWidget(fee_combo, 3, 2) 90 vbox.addLayout(grid) 91 self._add_advanced_options_cont(vbox) 92 vbox.addWidget(warning_label) 93 94 btns_hbox = QHBoxLayout() 95 btns_hbox.addWidget(self.adv_button) 96 btns_hbox.addStretch(1) 97 btns_hbox.addWidget(CancelButton(self)) 98 btns_hbox.addWidget(ok_button) 99 vbox.addLayout(btns_hbox) 100 101 def rbf_func(self, fee_rate) -> PartialTransaction: 102 raise NotImplementedError() # implemented by subclasses 103 104 def _add_advanced_options_cont(self, vbox: QVBoxLayout) -> None: 105 adv_vbox = QVBoxLayout() 106 adv_vbox.setContentsMargins(0, 0, 0, 0) 107 adv_widget = QWidget() 108 adv_widget.setLayout(adv_vbox) 109 adv_widget.setVisible(False) 110 def show_adv_settings(): 111 self.adv_button.setEnabled(False) 112 adv_widget.setVisible(True) 113 self.adv_button.clicked.connect(show_adv_settings) 114 self._add_advanced_options(adv_vbox) 115 vbox.addWidget(adv_widget) 116 117 def _add_advanced_options(self, adv_vbox: QVBoxLayout) -> None: 118 self.cb_rbf = QCheckBox(_('Keep Replace-By-Fee enabled')) 119 self.cb_rbf.setChecked(True) 120 adv_vbox.addWidget(self.cb_rbf) 121 122 def run(self) -> None: 123 if not self.exec_(): 124 return 125 is_rbf = self.cb_rbf.isChecked() 126 new_fee_rate = self.feerate_e.get_amount() 127 try: 128 new_tx = self.rbf_func(new_fee_rate) 129 except Exception as e: 130 self.window.show_error(str(e)) 131 return 132 new_tx.set_rbf(is_rbf) 133 tx_label = self.wallet.get_label_for_txid(self.txid) 134 self.window.show_transaction(new_tx, tx_desc=tx_label) 135 # TODO maybe save tx_label as label for new tx?? 136 137 138 class BumpFeeDialog(_BaseRBFDialog): 139 140 def __init__( 141 self, 142 *, 143 main_window: 'ElectrumWindow', 144 tx: PartialTransaction, 145 txid: str, 146 ): 147 help_text = _("Increase your transaction's fee to improve its position in mempool.") 148 _BaseRBFDialog.__init__( 149 self, 150 main_window=main_window, 151 tx=tx, 152 txid=txid, 153 title=_('Bump Fee'), 154 help_text=help_text, 155 ) 156 157 def rbf_func(self, fee_rate): 158 return self.wallet.bump_fee( 159 tx=self.tx, 160 txid=self.txid, 161 new_fee_rate=fee_rate, 162 coins=self.window.get_coins(), 163 strategies=self.option_index_to_strats[self.strat_combo.currentIndex()], 164 ) 165 166 def _add_advanced_options(self, adv_vbox: QVBoxLayout) -> None: 167 self.cb_rbf = QCheckBox(_('Keep Replace-By-Fee enabled')) 168 self.cb_rbf.setChecked(True) 169 adv_vbox.addWidget(self.cb_rbf) 170 171 self.strat_combo = QComboBox() 172 options = [ 173 _("decrease change, or add new inputs, or decrease any outputs"), 174 _("decrease change, or decrease any outputs"), 175 _("decrease payment"), 176 ] 177 self.option_index_to_strats = { 178 0: [BumpFeeStrategy.COINCHOOSER, BumpFeeStrategy.DECREASE_CHANGE], 179 1: [BumpFeeStrategy.DECREASE_CHANGE], 180 2: [BumpFeeStrategy.DECREASE_PAYMENT], 181 } 182 self.strat_combo.addItems(options) 183 self.strat_combo.setCurrentIndex(0) 184 strat_hbox = QHBoxLayout() 185 strat_hbox.addWidget(QLabel(_("Strategy") + ":")) 186 strat_hbox.addWidget(self.strat_combo) 187 strat_hbox.addStretch(1) 188 adv_vbox.addLayout(strat_hbox) 189 190 191 class DSCancelDialog(_BaseRBFDialog): 192 193 def __init__( 194 self, 195 *, 196 main_window: 'ElectrumWindow', 197 tx: PartialTransaction, 198 txid: str, 199 ): 200 help_text = _( 201 "Cancel an unconfirmed RBF transaction by double-spending " 202 "its inputs back to your wallet with a higher fee.") 203 _BaseRBFDialog.__init__( 204 self, 205 main_window=main_window, 206 tx=tx, 207 txid=txid, 208 title=_('Cancel transaction'), 209 help_text=help_text, 210 ) 211 212 def rbf_func(self, fee_rate): 213 return self.wallet.dscancel(tx=self.tx, new_fee_rate=fee_rate)