Browse Source

Qt: new onchain tx creation flow:

- transaction_dialog is read-only
 - ConfirmTxDialog and RBF dialogs inherit from TxEditor
 - TxEditors are configurable
master
ThomasV 3 years ago
parent
commit
bc3946d2f4
  1. 593
      electrum/gui/qt/confirm_tx_dialog.py
  2. 22
      electrum/gui/qt/fee_slider.py
  3. 15
      electrum/gui/qt/main_window.py
  4. 152
      electrum/gui/qt/rbf_dialog.py
  5. 50
      electrum/gui/qt/send_tab.py
  6. 8
      electrum/gui/qt/settings_dialog.py
  7. 269
      electrum/gui/qt/transaction_dialog.py
  8. 2
      electrum/wallet.py

593
electrum/gui/qt/confirm_tx_dialog.py

@ -24,31 +24,45 @@
# SOFTWARE.
from decimal import Decimal
from functools import partial
from typing import TYPE_CHECKING, Optional, Union
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QVBoxLayout, QLabel, QGridLayout, QPushButton, QLineEdit
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout, QLabel, QGridLayout, QPushButton, QLineEdit, QToolButton, QMenu
from electrum.i18n import _
from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates
from electrum.util import quantize_feerate
from electrum.plugin import run_hook
from electrum.transaction import Transaction, PartialTransaction
from electrum.wallet import InternalAddressCorruption
from electrum.simple_config import SimpleConfig
from .util import (WindowModalDialog, ColorScheme, HelpLabel, Buttons, CancelButton,
BlockingWaitingDialog, PasswordLineEdit, WWLabel)
BlockingWaitingDialog, PasswordLineEdit, WWLabel, read_QIcon)
from .fee_slider import FeeSlider, FeeComboBox
if TYPE_CHECKING:
from .main_window import ElectrumWindow
from .transaction_dialog import TxSizeLabel, TxFiatLabel, TxInOutWidget
from .fee_slider import FeeSlider, FeeComboBox
from .amountedit import FeerateEdit, BTCAmountEdit
from .locktimeedit import LockTimeEdit
class TxEditor(WindowModalDialog):
class TxEditor:
def __init__(self, *, title='',
window: 'ElectrumWindow',
make_tx,
output_value: Union[int, str] = None,
allow_preview=True):
def __init__(self, *, window: 'ElectrumWindow', make_tx,
output_value: Union[int, str] = None, is_sweep: bool):
WindowModalDialog.__init__(self, window, title=title)
self.main_window = window
self.make_tx = make_tx
self.output_value = output_value
@ -58,9 +72,40 @@ class TxEditor:
self.not_enough_funds = False
self.no_dynfee_estimates = False
self.needs_update = False
self.password_required = self.wallet.has_keystore_encryption() and not is_sweep
# preview is disabled for lightning channel funding
self.allow_preview = allow_preview
self.is_preview = False
self.locktime_e = LockTimeEdit(self)
self.locktime_label = QLabel(_("LockTime") + ": ")
self.io_widget = TxInOutWidget(self.main_window, self.wallet)
self.create_fee_controls()
vbox = QVBoxLayout()
self.setLayout(vbox)
top = self.create_top_bar(self.help_text)
grid = self.create_grid()
vbox.addLayout(top)
vbox.addLayout(grid)
self.message_label = WWLabel('\n')
vbox.addWidget(self.message_label)
vbox.addWidget(self.io_widget)
buttons = self.create_buttons_bar()
vbox.addStretch(1)
vbox.addLayout(buttons)
self.set_io_visible(self.config.get('show_tx_io', False))
self.set_fee_edit_visible(self.config.get('show_fee_details', False))
self.set_locktime_visible(self.config.get('show_locktime', False))
self.set_preview_visible(self.config.get('show_preview_button', False))
self.update_fee_target()
self.resize(self.layout().sizeHint())
self.main_window.gui_object.timer.timeout.connect(self.timer_actions)
def timer_actions(self):
if self.needs_update:
self.update_tx()
@ -70,7 +115,7 @@ class TxEditor:
def stop_editor_updates(self):
self.main_window.gui_object.timer.timeout.disconnect(self.timer_actions)
def fee_slider_callback(self, dyn, pos, fee_rate):
def set_fee_config(self, dyn, pos, fee_rate):
if dyn:
if self.config.use_mempool_fees():
self.config.set_key('depth_level', pos, False)
@ -78,10 +123,399 @@ class TxEditor:
self.config.set_key('fee_level', pos, False)
else:
self.config.set_key('fee_per_kb', fee_rate, False)
def update_tx(self, *, fallback_to_zero_fee: bool = False):
raise NotImplementedError()
def update_fee_target(self):
text = self.fee_slider.get_dynfee_target()
self.fee_target.setText(text)
self.fee_target.setVisible(bool(text)) # hide in static mode
def update_feerate_label(self):
self.feerate_label.setText(self.feerate_e.text() + ' ' + self.feerate_e.base_unit())
def create_fee_controls(self):
self.fee_label = QLabel('')
self.fee_label.setTextInteractionFlags(Qt.TextSelectableByMouse)
self.size_label = TxSizeLabel()
self.size_label.setAlignment(Qt.AlignCenter)
self.size_label.setAmount(0)
self.size_label.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet())
self.feerate_label = QLabel('')
self.feerate_label.setTextInteractionFlags(Qt.TextSelectableByMouse)
self.fiat_fee_label = TxFiatLabel()
self.fiat_fee_label.setAlignment(Qt.AlignCenter)
self.fiat_fee_label.setAmount(0)
self.fiat_fee_label.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet())
self.feerate_e = FeerateEdit(lambda: 0)
self.feerate_e.setAmount(self.config.fee_per_byte())
self.feerate_e.textEdited.connect(partial(self.on_fee_or_feerate, self.feerate_e, False))
self.feerate_e.editingFinished.connect(partial(self.on_fee_or_feerate, self.feerate_e, True))
self.update_feerate_label()
self.fee_e = BTCAmountEdit(self.main_window.get_decimal_point)
self.fee_e.textEdited.connect(partial(self.on_fee_or_feerate, self.fee_e, False))
self.fee_e.editingFinished.connect(partial(self.on_fee_or_feerate, self.fee_e, True))
self.feerate_e.setFixedWidth(150)
self.fee_e.setFixedWidth(150)
self.fee_e.textChanged.connect(self.entry_changed)
self.feerate_e.textChanged.connect(self.entry_changed)
self.fee_target = QLabel('')
self.fee_slider = FeeSlider(self, self.config, self.fee_slider_callback)
self.fee_combo = FeeComboBox(self.fee_slider)
def feerounding_onclick():
text = (self.feerounding_text + '\n\n' +
_('To somewhat protect your privacy, Electrum tries to create change with similar precision to other outputs.') + ' ' +
_('At most 100 satoshis might be lost due to this rounding.') + ' ' +
_("You can disable this setting in '{}'.").format(_('Preferences')) + '\n' +
_('Also, dust is not kept as change, but added to the fee.') + '\n' +
_('Also, when batching RBF transactions, BIP 125 imposes a lower bound on the fee.'))
self.show_message(title=_('Fee rounding'), msg=text)
self.feerounding_icon = QToolButton()
self.feerounding_icon.setIcon(QIcon())
self.feerounding_icon.setAutoRaise(True)
self.feerounding_icon.clicked.connect(feerounding_onclick)
self.fee_hbox = fee_hbox = QHBoxLayout()
fee_hbox.addWidget(self.feerate_e)
fee_hbox.addWidget(self.feerate_label)
fee_hbox.addWidget(self.size_label)
fee_hbox.addWidget(self.fee_e)
fee_hbox.addWidget(self.fee_label)
fee_hbox.addWidget(self.fiat_fee_label)
fee_hbox.addWidget(self.feerounding_icon)
fee_hbox.addStretch()
self.fee_target_hbox = fee_target_hbox = QHBoxLayout()
fee_target_hbox.addWidget(self.fee_target)
fee_target_hbox.addWidget(self.fee_slider)
fee_target_hbox.addWidget(self.fee_combo)
fee_target_hbox.addStretch()
# set feerate_label to same size as feerate_e
self.feerate_label.setFixedSize(self.feerate_e.sizeHint())
self.fee_label.setFixedSize(self.fee_e.sizeHint())
self.fee_slider.setFixedWidth(200)
self.fee_target.setFixedSize(self.feerate_e.sizeHint())
def _trigger_update(self):
# set tx to None so that the ok button is disabled while we compute the new tx
self.tx = None
self.update()
self.needs_update = True
def fee_slider_callback(self, dyn, pos, fee_rate):
self.set_fee_config(dyn, pos, fee_rate)
self.fee_slider.activate()
if fee_rate:
fee_rate = Decimal(fee_rate)
self.feerate_e.setAmount(quantize_feerate(fee_rate / 1000))
else:
self.feerate_e.setAmount(None)
self.fee_e.setModified(False)
self.update_fee_target()
self.update_feerate_label()
self._trigger_update()
def on_fee_or_feerate(self, edit_changed, editing_finished):
edit_other = self.feerate_e if edit_changed == self.fee_e else self.fee_e
if editing_finished:
if edit_changed.get_amount() is None:
# This is so that when the user blanks the fee and moves on,
# we go back to auto-calculate mode and put a fee back.
edit_changed.setModified(False)
else:
# edit_changed was edited just now, so make sure we will
# freeze the correct fee setting (this)
edit_other.setModified(False)
self.fee_slider.deactivate()
self._trigger_update()
def is_send_fee_frozen(self):
return self.fee_e.isVisible() and self.fee_e.isModified() \
and (self.fee_e.text() or self.fee_e.hasFocus())
def is_send_feerate_frozen(self):
return self.feerate_e.isVisible() and self.feerate_e.isModified() \
and (self.feerate_e.text() or self.feerate_e.hasFocus())
def set_feerounding_text(self, num_satoshis_added):
self.feerounding_text = (_('Additional {} satoshis are going to be added.')
.format(num_satoshis_added))
def get_fee_estimator(self):
return None
if self.is_send_fee_frozen() and self.fee_e.get_amount() is not None:
fee_estimator = self.fee_e.get_amount()
elif self.is_send_feerate_frozen() and self.feerate_e.get_amount() is not None:
amount = self.feerate_e.get_amount() # sat/byte feerate
amount = 0 if amount is None else amount * 1000 # sat/kilobyte feerate
fee_estimator = partial(
SimpleConfig.estimate_fee_for_feerate, amount)
else:
fee_estimator = None
return fee_estimator
def entry_changed(self):
# blue color denotes auto-filled values
text = ""
fee_color = ColorScheme.DEFAULT
feerate_color = ColorScheme.DEFAULT
if self.not_enough_funds:
fee_color = ColorScheme.RED
feerate_color = ColorScheme.RED
elif self.fee_e.isModified():
feerate_color = ColorScheme.BLUE
elif self.feerate_e.isModified():
fee_color = ColorScheme.BLUE
else:
fee_color = ColorScheme.BLUE
feerate_color = ColorScheme.BLUE
self.fee_e.setStyleSheet(fee_color.as_stylesheet())
self.feerate_e.setStyleSheet(feerate_color.as_stylesheet())
#
self.needs_update = True
def update_fee_fields(self):
freeze_fee = self.is_send_fee_frozen()
freeze_feerate = self.is_send_feerate_frozen()
tx = self.tx
if self.no_dynfee_estimates and tx:
size = tx.estimated_size()
self.size_label.setAmount(size)
#self.size_e.setAmount(size)
if self.not_enough_funds or self.no_dynfee_estimates:
if not freeze_fee:
self.fee_e.setAmount(None)
if not freeze_feerate:
self.feerate_e.setAmount(None)
self.feerounding_icon.setIcon(QIcon())
return
assert tx is not None
size = tx.estimated_size()
fee = tx.get_fee()
#self.size_e.setAmount(size)
self.size_label.setAmount(size)
fiat_fee = self.main_window.format_fiat_and_units(fee)
self.fiat_fee_label.setAmount(fiat_fee)
# Displayed fee/fee_rate values are set according to user input.
# Due to rounding or dropping dust in CoinChooser,
# actual fees often differ somewhat.
if freeze_feerate or self.fee_slider.is_active():
displayed_feerate = self.feerate_e.get_amount()
if displayed_feerate is not None:
displayed_feerate = quantize_feerate(displayed_feerate)
elif self.fee_slider.is_active():
# fallback to actual fee
displayed_feerate = quantize_feerate(fee / size) if fee is not None else None
self.feerate_e.setAmount(displayed_feerate)
displayed_fee = round(displayed_feerate * size) if displayed_feerate is not None else None
self.fee_e.setAmount(displayed_fee)
else:
if freeze_fee:
displayed_fee = self.fee_e.get_amount()
else:
# fallback to actual fee if nothing is frozen
displayed_fee = fee
self.fee_e.setAmount(displayed_fee)
displayed_fee = displayed_fee if displayed_fee else 0
displayed_feerate = quantize_feerate(displayed_fee / size) if displayed_fee is not None else None
self.feerate_e.setAmount(displayed_feerate)
# set fee rounding icon to empty if there is no rounding
feerounding = (fee - displayed_fee) if (fee and displayed_fee is not None) else 0
self.set_feerounding_text(int(feerounding))
self.feerounding_icon.setToolTip(self.feerounding_text)
self.feerounding_icon.setIcon(read_QIcon('info.png') if abs(feerounding) >= 1 else QIcon())
def create_buttons_bar(self):
self.preview_button = QPushButton(_('Preview'))
self.preview_button.clicked.connect(self.on_preview)
self.ok_button = QPushButton(_('OK'))
self.ok_button.clicked.connect(self.on_send)
self.ok_button.setDefault(True)
buttons = Buttons(CancelButton(self), self.preview_button, self.ok_button)
return buttons
def create_top_bar(self, text):
self.pref_menu = QMenu()
self.m1 = self.pref_menu.addAction('Show inputs/outputs', self.toggle_io_visibility)
self.m1.setCheckable(True)
self.m2 = self.pref_menu.addAction('Edit fees', self.toggle_fee_details)
self.m2.setCheckable(True)
self.m3 = self.pref_menu.addAction('Edit Locktime', self.toggle_locktime)
self.m3.setCheckable(True)
self.m4 = self.pref_menu.addAction('Show Preview Button', self.toggle_preview_button)
self.m4.setCheckable(True)
self.m4.setEnabled(self.allow_preview)
self.pref_button = QToolButton()
self.pref_button.setIcon(read_QIcon("preferences.png"))
self.pref_button.setMenu(self.pref_menu)
self.pref_button.setPopupMode(QToolButton.InstantPopup)
hbox = QHBoxLayout()
hbox.addWidget(QLabel(text))
hbox.addStretch()
hbox.addWidget(self.pref_button)
return hbox
def toggle_io_visibility(self):
b = not self.config.get('show_tx_io', False)
self.config.set_key('show_tx_io', b)
self.set_io_visible(b)
#self.resize(self.layout().sizeHint())
self.setFixedSize(self.layout().sizeHint())
def toggle_fee_details(self):
b = not self.config.get('show_fee_details', False)
self.config.set_key('show_fee_details', b)
self.set_fee_edit_visible(b)
self.setFixedSize(self.layout().sizeHint())
def toggle_locktime(self):
b = not self.config.get('show_locktime', False)
self.config.set_key('show_locktime', b)
self.set_locktime_visible(b)
self.setFixedSize(self.layout().sizeHint())
def toggle_preview_button(self):
b = not self.config.get('show_preview_button', False)
self.config.set_key('show_preview_button', b)
self.set_preview_visible(b)
def set_preview_visible(self, b):
b = b and self.allow_preview
self.preview_button.setVisible(b)
self.m4.setChecked(b)
def set_io_visible(self, b):
self.io_widget.setVisible(b)
self.m1.setChecked(b)
def set_fee_edit_visible(self, b):
detailed = [self.feerounding_icon, self.feerate_e, self.fee_e]
basic = [self.fee_label, self.feerate_label]
# first hide, then show
for w in (basic if b else detailed):
w.hide()
for w in (detailed if b else basic):
w.show()
self.m2.setChecked(b)
def set_locktime_visible(self, b):
for w in [
self.locktime_e,
self.locktime_label]:
w.setVisible(b)
self.m3.setChecked(b)
def run(self):
cancelled = not self.exec_()
self.stop_editor_updates()
self.deleteLater() # see #3956
return self.tx if not cancelled else None
def on_send(self):
self.accept()
def on_preview(self):
self.is_preview = True
self.accept()
def toggle_send_button(self, enable: bool, *, message: str = None):
if message is None:
self.message_label.setStyleSheet(None)
self.message_label.setText(' ')
else:
self.message_label.setStyleSheet(ColorScheme.RED.as_stylesheet())
self.message_label.setText(message)
self.setFixedSize(self.layout().sizeHint())
self.preview_button.setEnabled(enable)
self.ok_button.setEnabled(enable)
def update(self):
tx = self.tx
self._update_amount_label()
if self.not_enough_funds:
text = self.main_window.send_tab.get_text_not_enough_funds_mentioning_frozen()
self.toggle_send_button(False, message=text)
return
if not tx:
self.toggle_send_button(False)
return
self.update_fee_fields()
if self.locktime_e.get_locktime() is None:
self.locktime_e.set_locktime(self.tx.locktime)
self.io_widget.update(tx)
fee = tx.get_fee()
assert fee is not None
self.fee_label.setText(self.main_window.config.format_amount_and_units(fee))
fee_rate = fee // tx.estimated_size()
#self.feerate_label.setText(self.main_window.format_amount(fee_rate))
# extra fee
x_fee = run_hook('get_tx_extra_fee', self.wallet, tx)
if x_fee:
x_fee_address, x_fee_amount = x_fee
self.extra_fee_label.setVisible(True)
self.extra_fee_value.setVisible(True)
self.extra_fee_value.setText(self.main_window.format_amount_and_units(x_fee_amount))
amount = tx.output_value() if self.output_value == '!' else self.output_value
tx_size = tx.estimated_size()
fee_warning_tuple = self.wallet.get_tx_fee_warning(
invoice_amt=amount, tx_size=tx_size, fee=fee)
if fee_warning_tuple:
allow_send, long_warning, short_warning = fee_warning_tuple
self.toggle_send_button(allow_send, message=long_warning)
else:
self.toggle_send_button(True)
def _update_amount_label(self):
pass
class ConfirmTxDialog(TxEditor):
help_text = ''#_('Set the mining fee of your transaction')
def __init__(self, *, window: 'ElectrumWindow', make_tx, output_value: Union[int, str], allow_preview=True):
TxEditor.__init__(
self,
window=window,
make_tx=make_tx,
output_value=output_value,
title=_("New Transaction"), # todo: adapt title for channel funding tx, swaps
allow_preview=allow_preview)
BlockingWaitingDialog(window, _("Preparing transaction..."), self.update_tx)
self.update()
def _update_amount_label(self):
tx = self.tx
if self.output_value == '!':
if tx:
amount = tx.output_value()
amount_str = self.main_window.format_amount_and_units(amount)
else:
amount_str = "max"
else:
amount = self.output_value
amount_str = self.main_window.format_amount_and_units(amount)
self.amount_label.setText(amount_str)
def update_tx(self, *, fallback_to_zero_fee: bool = False):
fee_estimator = self.get_fee_estimator()
@ -116,6 +550,7 @@ class TxEditor:
self.tx.set_rbf(True)
def have_enough_funds_assuming_zero_fees(self) -> bool:
# called in send_tab.py
try:
tx = self.make_tx(0)
except NotEnoughFunds:
@ -123,147 +558,39 @@ class TxEditor:
else:
return True
class ConfirmTxDialog(TxEditor, WindowModalDialog):
# set fee and return password (after pw check)
def __init__(self, *, window: 'ElectrumWindow', make_tx, output_value: Union[int, str], is_sweep: bool):
TxEditor.__init__(self, window=window, make_tx=make_tx, output_value=output_value, is_sweep=is_sweep)
WindowModalDialog.__init__(self, window, _("Confirm Transaction"))
vbox = QVBoxLayout()
self.setLayout(vbox)
def create_grid(self):
grid = QGridLayout()
vbox.addLayout(grid)
msg = (_('The amount to be received by the recipient.') + ' '
+ _('Fees are paid by the sender.'))
self.amount_label = QLabel('')
self.amount_label.setTextInteractionFlags(Qt.TextSelectableByMouse)
grid.addWidget(HelpLabel(_("Amount to be sent") + ": ", msg), 0, 0)
grid.addWidget(self.amount_label, 0, 1)
msg = _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
+ _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
+ _('A suggested fee is automatically added to this field. You may override it. The suggested fee increases with the size of the transaction.')
self.fee_label = QLabel('')
self.fee_label.setTextInteractionFlags(Qt.TextSelectableByMouse)
grid.addWidget(HelpLabel(_("Mining fee") + ": ", msg), 1, 0)
grid.addWidget(self.fee_label, 1, 1)
grid.addWidget(HelpLabel(_("Mining Fee") + ": ", msg), 1, 0)
grid.addLayout(self.fee_hbox, 1, 1, 1, 3)
grid.addWidget(HelpLabel(_("Fee target") + ": ", self.fee_combo.help_msg), 3, 0)
grid.addLayout(self.fee_target_hbox, 3, 1, 1, 3)
grid.setColumnStretch(4, 1)
# extra fee
self.extra_fee_label = QLabel(_("Additional fees") + ": ")
self.extra_fee_label.setVisible(False)
self.extra_fee_value = QLabel('')
self.extra_fee_value.setTextInteractionFlags(Qt.TextSelectableByMouse)
self.extra_fee_value.setVisible(False)
grid.addWidget(self.extra_fee_label, 2, 0)
grid.addWidget(self.extra_fee_value, 2, 1)
self.fee_slider = FeeSlider(self, self.config, self.fee_slider_callback)
self.fee_combo = FeeComboBox(self.fee_slider)
grid.addWidget(HelpLabel(_("Fee rate") + ": ", self.fee_combo.help_msg), 5, 0)
grid.addWidget(self.fee_slider, 5, 1)
grid.addWidget(self.fee_combo, 5, 2)
self.message_label = WWLabel(self.default_message())
grid.addWidget(self.message_label, 6, 0, 1, -1)
self.pw_label = QLabel(_('Password'))
self.pw_label.setVisible(self.password_required)
self.pw = PasswordLineEdit()
self.pw.setVisible(self.password_required)
grid.addWidget(self.pw_label, 8, 0)
grid.addWidget(self.pw, 8, 1, 1, -1)
self.preview_button = QPushButton(_('Advanced'))
self.preview_button.clicked.connect(self.on_preview)
grid.addWidget(self.preview_button, 0, 2)
self.send_button = QPushButton(_('Send'))
self.send_button.clicked.connect(self.on_send)
self.send_button.setDefault(True)
vbox.addLayout(Buttons(CancelButton(self), self.send_button))
BlockingWaitingDialog(window, _("Preparing transaction..."), self.update_tx)
self.update()
self.is_send = False
grid.addWidget(self.extra_fee_label, 5, 0)
grid.addWidget(self.extra_fee_value, 5, 1)
def default_message(self):
return _('Enter your password to proceed') if self.password_required else _('Click Send to proceed')
# locktime editor
grid.addWidget(self.locktime_label, 6, 0)
grid.addWidget(self.locktime_e, 6, 1, 1, 2)
def on_preview(self):
self.accept()
def run(self):
cancelled = not self.exec_()
password = self.pw.text() or None
self.stop_editor_updates()
self.deleteLater() # see #3956
return cancelled, self.is_send, password, self.tx
def on_send(self):
password = self.pw.text() or None
if self.password_required:
if password is None:
self.main_window.show_error(_("Password required"), parent=self)
return
try:
self.wallet.check_password(password)
except Exception as e:
self.main_window.show_error(str(e), parent=self)
return
self.is_send = True
self.accept()
def toggle_send_button(self, enable: bool, *, message: str = None):
if message is None:
self.message_label.setStyleSheet(None)
self.message_label.setText(self.default_message())
else:
self.message_label.setStyleSheet(ColorScheme.RED.as_stylesheet())
self.message_label.setText(message)
self.pw.setEnabled(enable)
self.send_button.setEnabled(enable)
def _update_amount_label(self):
tx = self.tx
if self.output_value == '!':
if tx:
amount = tx.output_value()
amount_str = self.main_window.format_amount_and_units(amount)
else:
amount_str = "max"
else:
amount = self.output_value
amount_str = self.main_window.format_amount_and_units(amount)
self.amount_label.setText(amount_str)
def update(self):
tx = self.tx
self._update_amount_label()
if self.not_enough_funds:
text = self.main_window.send_tab.get_text_not_enough_funds_mentioning_frozen()
self.toggle_send_button(False, message=text)
return
if not tx:
return
fee = tx.get_fee()
assert fee is not None
self.fee_label.setText(self.main_window.format_amount_and_units(fee))
x_fee = run_hook('get_tx_extra_fee', self.wallet, tx)
if x_fee:
x_fee_address, x_fee_amount = x_fee
self.extra_fee_label.setVisible(True)
self.extra_fee_value.setVisible(True)
self.extra_fee_value.setText(self.main_window.format_amount_and_units(x_fee_amount))
amount = tx.output_value() if self.output_value == '!' else self.output_value
tx_size = tx.estimated_size()
fee_warning_tuple = self.wallet.get_tx_fee_warning(
invoice_amt=amount, tx_size=tx_size, fee=fee)
if fee_warning_tuple:
allow_send, long_warning, short_warning = fee_warning_tuple
self.toggle_send_button(allow_send, message=long_warning)
else:
self.toggle_send_button(True)
return grid

22
electrum/gui/qt/fee_slider.py

@ -40,13 +40,18 @@ class FeeSlider(QSlider):
self.update()
self.valueChanged.connect(self.moved)
self._active = True
self.setFocusPolicy(Qt.NoFocus)
def get_fee_rate(self, pos):
if self.dyn:
fee_rate = self.config.depth_to_fee(pos) if self.config.use_mempool_fees() else self.config.eta_to_fee(pos)
else:
fee_rate = self.config.static_fee(pos)
return fee_rate
def moved(self, pos):
with self.lock:
if self.dyn:
fee_rate = self.config.depth_to_fee(pos) if self.config.use_mempool_fees() else self.config.eta_to_fee(pos)
else:
fee_rate = self.config.static_fee(pos)
fee_rate = self.get_fee_rate(pos)
tooltip = self.get_tooltip(pos, fee_rate)
QToolTip.showText(QCursor.pos(), tooltip, self)
self.setToolTip(tooltip)
@ -60,6 +65,15 @@ class FeeSlider(QSlider):
else:
return _('Fixed rate') + ': ' + target + '\n' + _('Estimate') + ': ' + estimate
def get_dynfee_target(self):
if not self.dyn:
return ''
pos = self.value()
fee_rate = self.get_fee_rate(pos)
mempool = self.config.use_mempool_fees()
target, estimate = self.config.get_fee_text(pos, True, mempool, fee_rate)
return target
def update(self):
with self.lock:
self.dyn = self.config.is_dynfee()

15
electrum/gui/qt/main_window.py

@ -1258,17 +1258,16 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
msg = messages.MGS_CONFLICTING_BACKUP_INSTANCE
if not self.question(msg):
return
# use ConfirmTxDialog
# we need to know the fee before we broadcast, because the txid is required
make_tx = self.mktx_for_open_channel(funding_sat=funding_sat, node_id=node_id)
d = ConfirmTxDialog(window=self, make_tx=make_tx, output_value=funding_sat, is_sweep=False)
# disable preview button because the user must not broadcast tx before establishment_flow
d.preview_button.setEnabled(False)
cancelled, is_send, password, funding_tx = d.run()
if not is_send:
return
if cancelled:
d = ConfirmTxDialog(window=self, make_tx=make_tx, output_value=funding_sat, allow_preview=False)
funding_tx = d.run()
if not funding_tx:
return
self._open_channel(connect_str, funding_sat, push_amt, funding_tx)
@protected
def _open_channel(self, connect_str, funding_sat, push_amt, funding_tx, password):
# read funding_sat from tx; converts '!' to int value
funding_sat = funding_tx.output_value_for_address(ln_dummy_address())
def task():

152
electrum/gui/qt/rbf_dialog.py

@ -4,6 +4,7 @@
from typing import TYPE_CHECKING
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (QCheckBox, QLabel, QVBoxLayout, QGridLayout, QWidget,
QPushButton, QHBoxLayout, QComboBox)
@ -20,7 +21,9 @@ if TYPE_CHECKING:
from .main_window import ElectrumWindow
class _BaseRBFDialog(WindowModalDialog):
from .confirm_tx_dialog import ConfirmTxDialog, TxEditor, TxSizeLabel, HelpLabel
class _BaseRBFDialog(TxEditor):
def __init__(
self,
@ -30,125 +33,110 @@ class _BaseRBFDialog(WindowModalDialog):
txid: str,
title: str):
WindowModalDialog.__init__(self, main_window, title=title)
self.window = main_window
self.wallet = main_window.wallet
self.tx = tx
self.new_tx = None
self.old_tx = tx
assert txid
self.txid = txid
self.old_txid = txid
self.message = ''
fee = tx.get_fee()
assert fee is not None
tx_size = tx.estimated_size()
self.old_fee_rate = old_fee_rate = fee / tx_size # sat/vbyte
vbox = QVBoxLayout(self)
vbox.addWidget(WWLabel(self.help_text))
vbox.addStretch(1)
self.ok_button = OkButton(self)
self.message_label = QLabel('')
self.feerate_e = FeerateEdit(lambda: 0)
self.feerate_e.setAmount(max(old_fee_rate * 1.5, old_fee_rate + 1))
self.feerate_e.textChanged.connect(self.update)
def on_slider(dyn, pos, fee_rate):
fee_slider.activate()
if fee_rate is not None:
self.feerate_e.setAmount(fee_rate / 1000)
fee_slider = FeeSlider(self.window, self.window.config, on_slider)
fee_combo = FeeComboBox(fee_slider)
fee_slider.deactivate()
self.feerate_e.textEdited.connect(fee_slider.deactivate)
grid = QGridLayout()
self.old_fee = self.old_tx.get_fee()
self.old_tx_size = tx.estimated_size()
self.old_fee_rate = old_fee_rate = self.old_fee / self.old_tx_size # sat/vbyte
self.method_label = QLabel(_('Method') + ':')
self.method_combo = QComboBox()
self.method_combo.addItems([_('Preserve payment'), _('Decrease payment')])
self.method_combo.currentIndexChanged.connect(self.update)
grid.addWidget(self.method_label, 0, 0)
grid.addWidget(self.method_combo, 0, 1)
TxEditor.__init__(
self,
window=main_window,
title=title,
make_tx=self.rbf_func)
grid.addWidget(QLabel(_('Current fee') + ':'), 1, 0)
grid.addWidget(QLabel(self.window.format_amount_and_units(fee)), 1, 1)
grid.addWidget(QLabel(_('Current fee rate') + ':'), 2, 0)
grid.addWidget(QLabel(self.window.format_fee_rate(1000 * old_fee_rate)), 2, 1)
grid.addWidget(QLabel(_('New fee rate') + ':'), 3, 0)
grid.addWidget(self.feerate_e, 3, 1)
grid.addWidget(fee_slider, 3, 2)
grid.addWidget(fee_combo, 3, 3)
grid.addWidget(self.message_label, 5, 0, 1, 3)
vbox.addLayout(grid)
vbox.addStretch(1)
btns_hbox = QHBoxLayout()
btns_hbox.addStretch(1)
btns_hbox.addWidget(CancelButton(self))
btns_hbox.addWidget(self.ok_button)
vbox.addLayout(btns_hbox)
new_fee_rate = old_fee_rate + max(1, old_fee_rate // 20)
new_fee_rate = self.old_fee_rate + max(1, self.old_fee_rate // 20)
self.feerate_e.setAmount(new_fee_rate)
self._update_tx(new_fee_rate)
self._update_message()
# give focus to fee slider
fee_slider.activate()
fee_slider.setFocus()
self.fee_slider.activate()
# are we paying max?
invoices = self.wallet.get_relevant_invoices_for_tx(txid)
if len(invoices) == 1 and len(invoices[0].outputs) == 1:
if invoices[0].outputs[0].value == '!':
self.set_decrease_payment()
def create_grid(self):
self.method_label = QLabel(_('Method') + ':')
self.method_combo = QComboBox()
self.method_combo.addItems([_('Preserve payment'), _('Decrease payment')])
self.method_combo.currentIndexChanged.connect(self.update)
old_size_label = TxSizeLabel()
old_size_label.setAlignment(Qt.AlignCenter)
old_size_label.setAmount(self.old_tx_size)
old_size_label.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet())
current_fee_hbox = QHBoxLayout()
current_fee_hbox.addWidget(QLabel(self.main_window.format_fee_rate(1000 * self.old_fee_rate)))
current_fee_hbox.addWidget(old_size_label)
current_fee_hbox.addWidget(QLabel(self.main_window.format_amount_and_units(self.old_fee)))
current_fee_hbox.addStretch()
grid = QGridLayout()
grid.addWidget(self.method_label, 0, 0)
grid.addWidget(self.method_combo, 0, 1)
grid.addWidget(QLabel(_('Current fee') + ':'), 1, 0)
grid.addLayout(current_fee_hbox, 1, 1, 1, 3)
grid.addWidget(QLabel(_('New fee') + ':'), 2, 0)
grid.addLayout(self.fee_hbox, 2, 1, 1, 3)
grid.addWidget(HelpLabel(_("Fee target") + ": ", self.fee_combo.help_msg), 4, 0)
grid.addLayout(self.fee_target_hbox, 4, 1, 1, 3)
grid.setColumnStretch(4, 1)
# locktime
grid.addWidget(self.locktime_label, 5, 0)
grid.addWidget(self.locktime_e, 5, 1, 1, 2)
return grid
def is_decrease_payment(self):
return self.method_combo.currentIndex() == 1
def set_decrease_payment(self):
self.method_combo.setCurrentIndex(1)
def rbf_func(self, fee_rate) -> PartialTransaction:
raise NotImplementedError() # implemented by subclasses
def run(self) -> None:
if not self.exec_():
return
self.new_tx.set_rbf(True)
tx_label = self.wallet.get_label_for_txid(self.txid)
self.window.show_transaction(self.new_tx, tx_desc=tx_label)
# TODO maybe save tx_label as label for new tx??
def update(self):
if self.is_preview:
self.main_window.show_transaction(self.tx)
return
def sign_done(success):
if success:
self.main_window.broadcast_or_show(self.tx)
self.main_window.sign_tx(
self.tx,
callback=sign_done,
external_keypairs={})
def update_tx(self):
fee_rate = self.feerate_e.get_amount()
self._update_tx(fee_rate)
self._update_message()
def _update_tx(self, fee_rate):
if fee_rate is None:
self.new_tx = None
self.tx = None
self.message = ''
elif fee_rate <= self.old_fee_rate:
self.new_tx = None
self.tx = None
self.message = _("The new fee rate needs to be higher than the old fee rate.")
else:
try:
self.new_tx = self.rbf_func(fee_rate)
self.tx = self.make_tx(fee_rate)
except CannotBumpFee as e:
self.new_tx = None
self.tx = None
self.message = str(e)
if not self.new_tx:
if not self.tx:
return
delta = self.new_tx.get_fee() - self.tx.get_fee()
delta = self.tx.get_fee() - self.old_tx.get_fee()
if not self.is_decrease_payment():
self.message = _("You will pay {} more.").format(self.window.format_amount_and_units(delta))
self.message = _("You will pay {} more.").format(self.main_window.format_amount_and_units(delta))
else:
self.message = _("The recipient will receive {} less.").format(self.window.format_amount_and_units(delta))
self.message = _("The recipient will receive {} less.").format(self.main_window.format_amount_and_units(delta))
def _update_message(self):
enabled = bool(self.new_tx)
enabled = bool(self.tx)
self.ok_button.setEnabled(enabled)
if enabled:
style = ColorScheme.BLUE.as_stylesheet()
@ -177,10 +165,10 @@ class BumpFeeDialog(_BaseRBFDialog):
def rbf_func(self, fee_rate):
return self.wallet.bump_fee(
tx=self.tx,
txid=self.txid,
tx=self.old_tx,
txid=self.old_txid,
new_fee_rate=fee_rate,
coins=self.window.get_coins(),
coins=self.main_window.get_coins(),
decrease_payment=self.is_decrease_payment())
@ -206,4 +194,4 @@ class DSCancelDialog(_BaseRBFDialog):
self.method_combo.setVisible(False)
def rbf_func(self, fee_rate):
return self.wallet.dscancel(tx=self.tx, new_fee_rate=fee_rate)
return self.wallet.dscancel(tx=self.old_tx, new_fee_rate=fee_rate)

50
electrum/gui/qt/send_tab.py

@ -28,7 +28,6 @@ from electrum.lnurl import decode_lnurl, request_lnurl, callback_lnurl, LNURLErr
from .amountedit import AmountEdit, BTCAmountEdit, SizedFreezableLineEdit
from .util import WaitingDialog, HelpLabel, MessageBoxMixin, EnterButton, char_width_in_lineedit
from .confirm_tx_dialog import ConfirmTxDialog
from .transaction_dialog import PreviewTxDialog
if TYPE_CHECKING:
from .main_window import ElectrumWindow
@ -234,7 +233,7 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
output_value = '!'
else:
output_value = sum(output_values)
conf_dlg = ConfirmTxDialog(window=self.window, make_tx=make_tx, output_value=output_value, is_sweep=is_sweep)
conf_dlg = ConfirmTxDialog(window=self.window, make_tx=make_tx, output_value=output_value)
if conf_dlg.not_enough_funds:
# Check if we had enough funds excluding fees,
# if so, still provide opportunity to set lower fees.
@ -243,37 +242,26 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
self.show_message(text)
return
# shortcut to advanced preview (after "enough funds" check!)
if self.config.get('advanced_preview'):
preview_dlg = PreviewTxDialog(
window=self.window,
make_tx=make_tx,
external_keypairs=external_keypairs,
output_value=output_value)
preview_dlg.show()
tx = conf_dlg.run()
if tx is None:
# user cancelled
return
cancelled, is_send, password, tx = conf_dlg.run()
if cancelled:
is_preview = conf_dlg.is_preview
if is_preview:
self.window.show_transaction(tx)
return
if is_send:
self.save_pending_invoice()
def sign_done(success):
if success:
self.window.broadcast_or_show(tx)
self.window.sign_tx_with_password(
tx,
callback=sign_done,
password=password,
external_keypairs=external_keypairs,
)
else:
preview_dlg = PreviewTxDialog(
window=self.window,
make_tx=make_tx,
external_keypairs=external_keypairs,
output_value=output_value)
preview_dlg.show()
self.save_pending_invoice()
def sign_done(success):
if success:
self.window.broadcast_or_show(tx)
else:
raise
self.window.sign_tx(
tx,
callback=sign_done,
external_keypairs=external_keypairs)
def get_text_not_enough_funds_mentioning_frozen(self) -> str:
text = _("Not enough funds")

8
electrum/gui/qt/settings_dialog.py

@ -276,13 +276,6 @@ class SettingsDialog(QDialog, QtEventListener):
filelogging_cb.stateChanged.connect(on_set_filelogging)
filelogging_cb.setToolTip(_('Debug logs can be persisted to disk. These are useful for troubleshooting.'))
preview_cb = QCheckBox(_('Advanced preview'))
preview_cb.setChecked(bool(self.config.get('advanced_preview', False)))
preview_cb.setToolTip(_("Open advanced transaction preview dialog when 'Pay' is clicked."))
def on_preview(x):
self.config.set_key('advanced_preview', x == Qt.Checked)
preview_cb.stateChanged.connect(on_preview)
usechange_cb = QCheckBox(_('Use change addresses'))
usechange_cb.setChecked(self.wallet.use_change)
if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
@ -494,7 +487,6 @@ class SettingsDialog(QDialog, QtEventListener):
tx_widgets = []
tx_widgets.append((usechange_cb, None))
tx_widgets.append((batch_rbf_cb, None))
tx_widgets.append((preview_cb, None))
tx_widgets.append((unconf_cb, None))
tx_widgets.append((multiple_cb, None))
tx_widgets.append((outrounding_cb, None))

269
electrum/gui/qt/transaction_dialog.py

@ -43,6 +43,7 @@ from qrcode import exceptions
from electrum.simple_config import SimpleConfig
from electrum.util import quantize_feerate
from electrum import bitcoin
from electrum.bitcoin import base_encode, NLOCKTIME_BLOCKHEIGHT_MAX
from electrum.i18n import _
from electrum.plugin import run_hook
@ -59,10 +60,6 @@ from .util import (MessageBoxMixin, read_QIcon, Buttons, icon_path,
BlockingWaitingDialog, getSaveFileName, ColorSchemeItem,
get_iconname_qrcode)
from .fee_slider import FeeSlider, FeeComboBox
from .confirm_tx_dialog import TxEditor
from .amountedit import FeerateEdit, BTCAmountEdit
from .locktimeedit import LockTimeEdit
if TYPE_CHECKING:
from .main_window import ElectrumWindow
@ -353,6 +350,7 @@ class TxInOutWidget(QWidget):
menu.exec_(global_pos)
def show_transaction(tx: Transaction, *, parent: 'ElectrumWindow', desc=None, prompt_if_unsaved=False):
try:
d = TxDialog(tx, parent=parent, desc=desc, prompt_if_unsaved=prompt_if_unsaved)
@ -428,9 +426,6 @@ class BaseTxDialog(QDialog, MessageBoxMixin):
self.export_actions_button.setMenu(export_actions_menu)
self.export_actions_button.setPopupMode(QToolButton.InstantPopup)
self.finalize_button = QPushButton(_('Finalize'))
self.finalize_button.clicked.connect(self.on_finalize)
partial_tx_actions_menu = QMenu()
ptx_merge_sigs_action = QAction(_("Merge signatures from"), self)
ptx_merge_sigs_action.triggered.connect(self.merge_sigs)
@ -447,11 +442,11 @@ class BaseTxDialog(QDialog, MessageBoxMixin):
# Action buttons
self.buttons = [self.partial_tx_actions_button, self.sign_button, self.broadcast_button, self.cancel_button]
# Transaction sharing buttons
self.sharing_buttons = [self.finalize_button, self.export_actions_button, self.save_button]
self.sharing_buttons = [self.export_actions_button, self.save_button]
run_hook('transaction_dialog', self)
if not self.finalized:
self.create_fee_controls()
vbox.addWidget(self.feecontrol_fields)
#if not self.finalized:
# self.create_fee_controls()
# vbox.addWidget(self.feecontrol_fields)
self.hbox = hbox = QHBoxLayout()
hbox.addLayout(Buttons(*self.sharing_buttons))
hbox.addStretch(1)
@ -464,8 +459,6 @@ class BaseTxDialog(QDialog, MessageBoxMixin):
def set_buttons_visibility(self):
for b in [self.export_actions_button, self.save_button, self.sign_button, self.broadcast_button, self.partial_tx_actions_button]:
b.setVisible(self.finalized)
for b in [self.finalize_button]:
b.setVisible(not self.finalized)
def set_tx(self, tx: 'Transaction'):
# Take a copy; it might get updated in the main window by
@ -659,9 +652,6 @@ class BaseTxDialog(QDialog, MessageBoxMixin):
self.update()
def update(self):
if not self.finalized:
self.update_fee_fields()
self.finalize_button.setEnabled(self.can_finalize())
if self.tx is None:
return
self.io_widget.update(self.tx)
@ -723,8 +713,7 @@ class BaseTxDialog(QDialog, MessageBoxMixin):
else:
locktime_final_str = f"LockTime: {self.tx.locktime} ({datetime.datetime.fromtimestamp(self.tx.locktime)})"
self.locktime_final_label.setText(locktime_final_str)
if self.locktime_e.get_locktime() is None:
self.locktime_e.set_locktime(self.tx.locktime)
self.rbf_label.setText(_('Replace by fee') + f": {not self.tx.is_final()}")
if tx_mined_status.header_hash:
@ -768,10 +757,7 @@ class BaseTxDialog(QDialog, MessageBoxMixin):
fee_rate = Decimal(fee) / size # sat/byte
fee_str += ' ( %s ) ' % self.main_window.format_fee_rate(fee_rate * 1000)
if isinstance(self.tx, PartialTransaction):
if isinstance(self, PreviewTxDialog):
invoice_amt = self.tx.output_value() if self.output_value == '!' else self.output_value
else:
invoice_amt = amount
invoice_amt = amount
fee_warning_tuple = self.wallet.get_tx_fee_warning(
invoice_amt=invoice_amt, tx_size=size, fee=fee)
if fee_warning_tuple:
@ -865,19 +851,6 @@ class BaseTxDialog(QDialog, MessageBoxMixin):
self.locktime_final_label = TxDetailLabel()
vbox_right.addWidget(self.locktime_final_label)
locktime_setter_hbox = QHBoxLayout()
locktime_setter_hbox.setContentsMargins(0, 0, 0, 0)
locktime_setter_hbox.setSpacing(0)
locktime_setter_label = TxDetailLabel()
locktime_setter_label.setText("LockTime: ")
self.locktime_e = LockTimeEdit(self)
locktime_setter_hbox.addWidget(locktime_setter_label)
locktime_setter_hbox.addWidget(self.locktime_e)
locktime_setter_hbox.addStretch(1)
self.locktime_setter_widget = QWidget()
self.locktime_setter_widget.setLayout(locktime_setter_hbox)
vbox_right.addWidget(self.locktime_setter_widget)
self.block_height_label = TxDetailLabel()
vbox_right.addWidget(self.block_height_label)
vbox_right.addStretch(1)
@ -892,7 +865,6 @@ class BaseTxDialog(QDialog, MessageBoxMixin):
# set visibility after parenting can be determined by Qt
self.rbf_label.setVisible(self.finalized)
self.locktime_final_label.setVisible(self.finalized)
self.locktime_setter_widget.setVisible(not self.finalized)
def set_title(self):
self.setWindowTitle(_("Create transaction") if not self.finalized else _("Transaction"))
@ -947,228 +919,3 @@ class TxDialog(BaseTxDialog):
self.update()
class PreviewTxDialog(BaseTxDialog, TxEditor):
def __init__(
self,
*,
make_tx,
external_keypairs,
window: 'ElectrumWindow',
output_value: Union[int, str],
):
TxEditor.__init__(
self,
window=window,
make_tx=make_tx,
is_sweep=bool(external_keypairs),
output_value=output_value,
)
BaseTxDialog.__init__(self, parent=window, desc='', prompt_if_unsaved=False,
finalized=False, external_keypairs=external_keypairs)
BlockingWaitingDialog(window, _("Preparing transaction..."),
lambda: self.update_tx(fallback_to_zero_fee=True))
self.update()
def create_fee_controls(self):
self.size_e = TxSizeLabel()
self.size_e.setAlignment(Qt.AlignCenter)
self.size_e.setAmount(0)
self.size_e.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet())
self.fiat_fee_label = TxFiatLabel()
self.fiat_fee_label.setAlignment(Qt.AlignCenter)
self.fiat_fee_label.setAmount(0)
self.fiat_fee_label.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet())
self.feerate_e = FeerateEdit(lambda: 0)
self.feerate_e.setAmount(self.config.fee_per_byte())
self.feerate_e.textEdited.connect(partial(self.on_fee_or_feerate, self.feerate_e, False))
self.feerate_e.editingFinished.connect(partial(self.on_fee_or_feerate, self.feerate_e, True))
self.fee_e = BTCAmountEdit(self.main_window.get_decimal_point)
self.fee_e.textEdited.connect(partial(self.on_fee_or_feerate, self.fee_e, False))
self.fee_e.editingFinished.connect(partial(self.on_fee_or_feerate, self.fee_e, True))
self.fee_e.textChanged.connect(self.entry_changed)
self.feerate_e.textChanged.connect(self.entry_changed)
self.fee_slider = FeeSlider(self, self.config, self.fee_slider_callback)
self.fee_combo = FeeComboBox(self.fee_slider)
self.fee_slider.setFixedWidth(self.fee_e.width())
def feerounding_onclick():
text = (self.feerounding_text + '\n\n' +
_('To somewhat protect your privacy, Electrum tries to create change with similar precision to other outputs.') + ' ' +
_('At most 100 satoshis might be lost due to this rounding.') + ' ' +
_("You can disable this setting in '{}'.").format(_('Preferences')) + '\n' +
_('Also, dust is not kept as change, but added to the fee.') + '\n' +
_('Also, when batching RBF transactions, BIP 125 imposes a lower bound on the fee.'))
self.show_message(title=_('Fee rounding'), msg=text)
self.feerounding_icon = QToolButton()
self.feerounding_icon.setIcon(read_QIcon('info.png'))
self.feerounding_icon.setAutoRaise(True)
self.feerounding_icon.clicked.connect(feerounding_onclick)
self.feerounding_icon.setVisible(False)
self.feecontrol_fields = QWidget()
hbox = QHBoxLayout(self.feecontrol_fields)
hbox.setContentsMargins(0, 0, 0, 0)
grid = QGridLayout()
grid.addWidget(QLabel(_("Target fee:")), 0, 0)
grid.addWidget(self.feerate_e, 0, 1)
grid.addWidget(self.size_e, 0, 2)
grid.addWidget(self.fee_e, 0, 3)
grid.addWidget(self.feerounding_icon, 0, 4)
grid.addWidget(self.fiat_fee_label, 0, 5)
grid.addWidget(self.fee_slider, 1, 1)
grid.addWidget(self.fee_combo, 1, 2)
hbox.addLayout(grid)
hbox.addStretch(1)
def fee_slider_callback(self, dyn, pos, fee_rate):
super().fee_slider_callback(dyn, pos, fee_rate)
self.fee_slider.activate()
if fee_rate:
fee_rate = Decimal(fee_rate)
self.feerate_e.setAmount(quantize_feerate(fee_rate / 1000))
else:
self.feerate_e.setAmount(None)
self.fee_e.setModified(False)
def on_fee_or_feerate(self, edit_changed, editing_finished):
edit_other = self.feerate_e if edit_changed == self.fee_e else self.fee_e
if editing_finished:
if edit_changed.get_amount() is None:
# This is so that when the user blanks the fee and moves on,
# we go back to auto-calculate mode and put a fee back.
edit_changed.setModified(False)
else:
# edit_changed was edited just now, so make sure we will
# freeze the correct fee setting (this)
edit_other.setModified(False)
self.fee_slider.deactivate()
self.update()
def is_send_fee_frozen(self):
return self.fee_e.isVisible() and self.fee_e.isModified() \
and (self.fee_e.text() or self.fee_e.hasFocus())
def is_send_feerate_frozen(self):
return self.feerate_e.isVisible() and self.feerate_e.isModified() \
and (self.feerate_e.text() or self.feerate_e.hasFocus())
def set_feerounding_text(self, num_satoshis_added):
self.feerounding_text = (_('Additional {} satoshis are going to be added.')
.format(num_satoshis_added))
def get_fee_estimator(self):
if self.is_send_fee_frozen() and self.fee_e.get_amount() is not None:
fee_estimator = self.fee_e.get_amount()
elif self.is_send_feerate_frozen() and self.feerate_e.get_amount() is not None:
amount = self.feerate_e.get_amount() # sat/byte feerate
amount = 0 if amount is None else amount * 1000 # sat/kilobyte feerate
fee_estimator = partial(
SimpleConfig.estimate_fee_for_feerate, amount)
else:
fee_estimator = None
return fee_estimator
def entry_changed(self):
# blue color denotes auto-filled values
text = ""
fee_color = ColorScheme.DEFAULT
feerate_color = ColorScheme.DEFAULT
if self.not_enough_funds:
fee_color = ColorScheme.RED
feerate_color = ColorScheme.RED
elif self.fee_e.isModified():
feerate_color = ColorScheme.BLUE
elif self.feerate_e.isModified():
fee_color = ColorScheme.BLUE
else:
fee_color = ColorScheme.BLUE
feerate_color = ColorScheme.BLUE
self.fee_e.setStyleSheet(fee_color.as_stylesheet())
self.feerate_e.setStyleSheet(feerate_color.as_stylesheet())
#
self.needs_update = True
def update_fee_fields(self):
freeze_fee = self.is_send_fee_frozen()
freeze_feerate = self.is_send_feerate_frozen()
tx = self.tx
if self.no_dynfee_estimates and tx:
size = tx.estimated_size()
self.size_e.setAmount(size)
if self.not_enough_funds or self.no_dynfee_estimates:
if not freeze_fee:
self.fee_e.setAmount(None)
if not freeze_feerate:
self.feerate_e.setAmount(None)
self.feerounding_icon.setVisible(False)
return
assert tx is not None
size = tx.estimated_size()
fee = tx.get_fee()
self.size_e.setAmount(size)
fiat_fee = self.main_window.format_fiat_and_units(fee)
self.fiat_fee_label.setAmount(fiat_fee)
# Displayed fee/fee_rate values are set according to user input.
# Due to rounding or dropping dust in CoinChooser,
# actual fees often differ somewhat.
if freeze_feerate or self.fee_slider.is_active():
displayed_feerate = self.feerate_e.get_amount()
if displayed_feerate is not None:
displayed_feerate = quantize_feerate(displayed_feerate)
elif self.fee_slider.is_active():
# fallback to actual fee
displayed_feerate = quantize_feerate(fee / size) if fee is not None else None
self.feerate_e.setAmount(displayed_feerate)
displayed_fee = round(displayed_feerate * size) if displayed_feerate is not None else None
self.fee_e.setAmount(displayed_fee)
else:
if freeze_fee:
displayed_fee = self.fee_e.get_amount()
else:
# fallback to actual fee if nothing is frozen
displayed_fee = fee
self.fee_e.setAmount(displayed_fee)
displayed_fee = displayed_fee if displayed_fee else 0
displayed_feerate = quantize_feerate(displayed_fee / size) if displayed_fee is not None else None
self.feerate_e.setAmount(displayed_feerate)
# show/hide fee rounding icon
feerounding = (fee - displayed_fee) if (fee and displayed_fee is not None) else 0
self.set_feerounding_text(int(feerounding))
self.feerounding_icon.setToolTip(self.feerounding_text)
self.feerounding_icon.setVisible(abs(feerounding) >= 1)
def can_finalize(self):
return (self.tx is not None
and not self.not_enough_funds)
def on_finalize(self):
if not self.can_finalize():
return
assert self.tx
self.finalized = True
self.stop_editor_updates()
self.tx.set_rbf(True)
locktime = self.locktime_e.get_locktime()
if locktime is not None:
self.tx.locktime = locktime
for widget in [self.fee_slider, self.fee_combo, self.feecontrol_fields,
self.locktime_setter_widget, self.locktime_e]:
widget.setEnabled(False)
widget.setVisible(False)
for widget in [self.rbf_label, self.locktime_final_label]:
widget.setVisible(True)
self.set_title()
self.set_buttons_visibility()
self.update()

2
electrum/wallet.py

@ -2770,7 +2770,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
fee: int) -> Optional[Tuple[bool, str, str]]:
feerate = Decimal(fee) / tx_size # sat/byte
fee_ratio = Decimal(fee) / invoice_amt if invoice_amt else 1
fee_ratio = Decimal(fee) / invoice_amt if invoice_amt else 0
long_warning = None
short_warning = None
allow_send = True

Loading…
Cancel
Save