tconfirm_tx_dialog.py - electrum - Electrum Bitcoin wallet
HTML git clone https://git.parazyd.org/electrum
DIR Log
DIR Files
DIR Refs
DIR Submodules
---
tconfirm_tx_dialog.py (10277B)
---
1 #!/usr/bin/env python
2 #
3 # Electrum - lightweight Bitcoin client
4 # Copyright (2019) The Electrum Developers
5 #
6 # Permission is hereby granted, free of charge, to any person
7 # obtaining a copy of this software and associated documentation files
8 # (the "Software"), to deal in the Software without restriction,
9 # including without limitation the rights to use, copy, modify, merge,
10 # publish, distribute, sublicense, and/or sell copies of the Software,
11 # and to permit persons to whom the Software is furnished to do so,
12 # subject to the following conditions:
13 #
14 # The above copyright notice and this permission notice shall be
15 # included in all copies or substantial portions of the Software.
16 #
17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
21 # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
22 # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
23 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24 # SOFTWARE.
25
26 from decimal import Decimal
27 from typing import TYPE_CHECKING, Optional, Union
28
29 from PyQt5.QtWidgets import QVBoxLayout, QLabel, QGridLayout, QPushButton, QLineEdit
30
31 from electrum.i18n import _
32 from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates
33 from electrum.plugin import run_hook
34 from electrum.transaction import Transaction, PartialTransaction
35 from electrum.simple_config import FEERATE_WARNING_HIGH_FEE, FEE_RATIO_HIGH_WARNING
36 from electrum.wallet import InternalAddressCorruption
37
38 from .util import (WindowModalDialog, ColorScheme, HelpLabel, Buttons, CancelButton,
39 BlockingWaitingDialog, PasswordLineEdit)
40
41 from .fee_slider import FeeSlider, FeeComboBox
42
43 if TYPE_CHECKING:
44 from .main_window import ElectrumWindow
45
46
47
48 class TxEditor:
49
50 def __init__(self, *, window: 'ElectrumWindow', make_tx,
51 output_value: Union[int, str] = None, is_sweep: bool):
52 self.main_window = window
53 self.make_tx = make_tx
54 self.output_value = output_value
55 self.tx = None # type: Optional[PartialTransaction]
56 self.config = window.config
57 self.wallet = window.wallet
58 self.not_enough_funds = False
59 self.no_dynfee_estimates = False
60 self.needs_update = False
61 self.password_required = self.wallet.has_keystore_encryption() and not is_sweep
62 self.main_window.gui_object.timer.timeout.connect(self.timer_actions)
63
64 def timer_actions(self):
65 if self.needs_update:
66 self.update_tx()
67 self.update()
68 self.needs_update = False
69
70 def fee_slider_callback(self, dyn, pos, fee_rate):
71 if dyn:
72 if self.config.use_mempool_fees():
73 self.config.set_key('depth_level', pos, False)
74 else:
75 self.config.set_key('fee_level', pos, False)
76 else:
77 self.config.set_key('fee_per_kb', fee_rate, False)
78 self.needs_update = True
79
80 def get_fee_estimator(self):
81 return None
82
83 def update_tx(self, *, fallback_to_zero_fee: bool = False):
84 fee_estimator = self.get_fee_estimator()
85 try:
86 self.tx = self.make_tx(fee_estimator)
87 self.not_enough_funds = False
88 self.no_dynfee_estimates = False
89 except NotEnoughFunds:
90 self.not_enough_funds = True
91 self.tx = None
92 if fallback_to_zero_fee:
93 try:
94 self.tx = self.make_tx(0)
95 except BaseException:
96 return
97 else:
98 return
99 except NoDynamicFeeEstimates:
100 self.no_dynfee_estimates = True
101 self.tx = None
102 try:
103 self.tx = self.make_tx(0)
104 except NotEnoughFunds:
105 self.not_enough_funds = True
106 return
107 except BaseException:
108 return
109 except InternalAddressCorruption as e:
110 self.tx = None
111 self.main_window.show_error(str(e))
112 raise
113 use_rbf = bool(self.config.get('use_rbf', True))
114 self.tx.set_rbf(use_rbf)
115
116 def have_enough_funds_assuming_zero_fees(self) -> bool:
117 try:
118 tx = self.make_tx(0)
119 except NotEnoughFunds:
120 return False
121 else:
122 return True
123
124
125
126
127 class ConfirmTxDialog(TxEditor, WindowModalDialog):
128 # set fee and return password (after pw check)
129
130 def __init__(self, *, window: 'ElectrumWindow', make_tx, output_value: Union[int, str], is_sweep: bool):
131
132 TxEditor.__init__(self, window=window, make_tx=make_tx, output_value=output_value, is_sweep=is_sweep)
133 WindowModalDialog.__init__(self, window, _("Confirm Transaction"))
134 vbox = QVBoxLayout()
135 self.setLayout(vbox)
136 grid = QGridLayout()
137 vbox.addLayout(grid)
138 self.amount_label = QLabel('')
139 grid.addWidget(QLabel(_("Amount to be sent") + ": "), 0, 0)
140 grid.addWidget(self.amount_label, 0, 1)
141
142 msg = _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
143 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
144 + _('A suggested fee is automatically added to this field. You may override it. The suggested fee increases with the size of the transaction.')
145 self.fee_label = QLabel('')
146 grid.addWidget(HelpLabel(_("Mining fee") + ": ", msg), 1, 0)
147 grid.addWidget(self.fee_label, 1, 1)
148
149 self.extra_fee_label = QLabel(_("Additional fees") + ": ")
150 self.extra_fee_label.setVisible(False)
151 self.extra_fee_value = QLabel('')
152 self.extra_fee_value.setVisible(False)
153 grid.addWidget(self.extra_fee_label, 2, 0)
154 grid.addWidget(self.extra_fee_value, 2, 1)
155
156 self.fee_slider = FeeSlider(self, self.config, self.fee_slider_callback)
157 self.fee_combo = FeeComboBox(self.fee_slider)
158 grid.addWidget(HelpLabel(_("Fee rate") + ": ", self.fee_combo.help_msg), 5, 0)
159 grid.addWidget(self.fee_slider, 5, 1)
160 grid.addWidget(self.fee_combo, 5, 2)
161
162 self.message_label = QLabel(self.default_message())
163 grid.addWidget(self.message_label, 6, 0, 1, -1)
164 self.pw_label = QLabel(_('Password'))
165 self.pw_label.setVisible(self.password_required)
166 self.pw = PasswordLineEdit()
167 self.pw.setVisible(self.password_required)
168 grid.addWidget(self.pw_label, 8, 0)
169 grid.addWidget(self.pw, 8, 1, 1, -1)
170 self.preview_button = QPushButton(_('Advanced'))
171 self.preview_button.clicked.connect(self.on_preview)
172 grid.addWidget(self.preview_button, 0, 2)
173 self.send_button = QPushButton(_('Send'))
174 self.send_button.clicked.connect(self.on_send)
175 self.send_button.setDefault(True)
176 vbox.addLayout(Buttons(CancelButton(self), self.send_button))
177 BlockingWaitingDialog(window, _("Preparing transaction..."), self.update_tx)
178 self.update()
179 self.is_send = False
180
181 def default_message(self):
182 return _('Enter your password to proceed') if self.password_required else _('Click Send to proceed')
183
184 def on_preview(self):
185 self.accept()
186
187 def run(self):
188 cancelled = not self.exec_()
189 password = self.pw.text() or None
190 return cancelled, self.is_send, password, self.tx
191
192 def on_send(self):
193 password = self.pw.text() or None
194 if self.password_required:
195 if password is None:
196 self.main_window.show_error(_("Password required"), parent=self)
197 return
198 try:
199 self.wallet.check_password(password)
200 except Exception as e:
201 self.main_window.show_error(str(e), parent=self)
202 return
203 self.is_send = True
204 self.accept()
205
206 def toggle_send_button(self, enable: bool, *, message: str = None):
207 if message is None:
208 self.message_label.setStyleSheet(None)
209 self.message_label.setText(self.default_message())
210 else:
211 self.message_label.setStyleSheet(ColorScheme.RED.as_stylesheet())
212 self.message_label.setText(message)
213 self.pw.setEnabled(enable)
214 self.send_button.setEnabled(enable)
215
216 def _update_amount_label(self):
217 tx = self.tx
218 if self.output_value == '!':
219 if tx:
220 amount = tx.output_value()
221 amount_str = self.main_window.format_amount_and_units(amount)
222 else:
223 amount_str = "max"
224 else:
225 amount = self.output_value
226 amount_str = self.main_window.format_amount_and_units(amount)
227 self.amount_label.setText(amount_str)
228
229 def update(self):
230 tx = self.tx
231 self._update_amount_label()
232
233 if self.not_enough_funds:
234 text = self.main_window.get_text_not_enough_funds_mentioning_frozen()
235 self.toggle_send_button(False, message=text)
236 return
237
238 if not tx:
239 return
240
241 fee = tx.get_fee()
242 assert fee is not None
243 self.fee_label.setText(self.main_window.format_amount_and_units(fee))
244 x_fee = run_hook('get_tx_extra_fee', self.wallet, tx)
245 if x_fee:
246 x_fee_address, x_fee_amount = x_fee
247 self.extra_fee_label.setVisible(True)
248 self.extra_fee_value.setVisible(True)
249 self.extra_fee_value.setText(self.main_window.format_amount_and_units(x_fee_amount))
250
251 amount = tx.output_value() if self.output_value == '!' else self.output_value
252 tx_size = tx.estimated_size()
253 fee_warning_tuple = self.wallet.get_tx_fee_warning(
254 invoice_amt=amount, tx_size=tx_size, fee=fee)
255 if fee_warning_tuple:
256 allow_send, long_warning, short_warning = fee_warning_tuple
257 self.toggle_send_button(allow_send, message=long_warning)
258 else:
259 self.toggle_send_button(True)