URI: 
       trbf_dialog.py - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
       trbf_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)