electrum

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

confirm_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)