1#!/usr/local/bin/python3.8 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 26from decimal import Decimal 27from typing import TYPE_CHECKING, Optional, Union 28 29from PyQt5.QtWidgets import QVBoxLayout, QLabel, QGridLayout, QPushButton, QLineEdit 30 31from electrum.i18n import _ 32from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates 33from electrum.plugin import run_hook 34from electrum.transaction import Transaction, PartialTransaction 35from electrum.simple_config import FEERATE_WARNING_HIGH_FEE, FEE_RATIO_HIGH_WARNING 36from electrum.wallet import InternalAddressCorruption 37 38from .util import (WindowModalDialog, ColorScheme, HelpLabel, Buttons, CancelButton, 39 BlockingWaitingDialog, PasswordLineEdit) 40 41from .fee_slider import FeeSlider, FeeComboBox 42 43if TYPE_CHECKING: 44 from .main_window import ElectrumWindow 45 46 47 48class 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 127class 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) 260