electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

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)