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)