Browse Source

Merge pull request #8197 from spesmilo/new_tx_flow

Qt: new onchain tx creation flow:
master
ThomasV 3 years ago committed by GitHub
parent
commit
bf16919a74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 625
      electrum/gui/qt/confirm_tx_dialog.py
  2. 21
      electrum/gui/qt/fee_slider.py
  3. 9
      electrum/gui/qt/locktimeedit.py
  4. 15
      electrum/gui/qt/main_window.py
  5. 173
      electrum/gui/qt/rbf_dialog.py
  6. 50
      electrum/gui/qt/send_tab.py
  7. 8
      electrum/gui/qt/settings_dialog.py
  8. 269
      electrum/gui/qt/transaction_dialog.py
  9. 2
      electrum/wallet.py

625
electrum/gui/qt/confirm_tx_dialog.py

@ -24,53 +24,106 @@
# 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
self.tx = None # type: Optional[PartialTransaction]
self.message = '' # set by side effect
self.error = '' # set by side effect
self.config = window.config
self.wallet = window.wallet
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_e.valueEdited.connect(self.trigger_update)
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_tx_fee_details', False))
self.set_locktime_visible(self.config.get('show_tx_locktime', False))
self.set_preview_visible(self.config.get('show_tx_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()
self.update()
self.needs_update = False
def update(self):
self.update_tx()
self.set_locktime()
self._update_widgets()
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 +131,423 @@ 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):
# expected to set self.tx, self.message and self.error
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)
self.fee_combo.setFocusPolicy(Qt.NoFocus)
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.setStyleSheet("background-color: rgba(255, 255, 255, 0); ")
self.feerounding_icon.setAutoRaise(True)
self.feerounding_icon.clicked.connect(feerounding_onclick)
self.set_feerounding_visibility(False)
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.message = ''
self.error = ''
self._update_widgets()
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()
# do not call trigger_update on editing_finished,
# because that event is emitted when we press OK
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 set_feerounding_visibility(self, b:bool):
# we do not use setVisible because it affects the layout
self.feerounding_icon.setIcon(read_QIcon('info.png') if b else QIcon())
self.feerounding_icon.setEnabled(b)
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.set_feerounding_visibility(False)
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.set_feerounding_visibility(abs(feerounding) >= 1)
# feerate_label needs to be updated from feerate_e
self.update_feerate_label()
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)
self.pref_button.setFocusPolicy(Qt.NoFocus)
hbox = QHBoxLayout()
hbox.addWidget(QLabel(text))
hbox.addStretch()
hbox.addWidget(self.pref_button)
return hbox
def resize_to_fit_content(self):
# fixme: calling resize once is not enough...
size = self.layout().sizeHint()
self.resize(size)
self.resize(size)
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_to_fit_content()
def toggle_fee_details(self):
b = not self.config.get('show_tx_fee_details', False)
self.config.set_key('show_tx_fee_details', b)
self.set_fee_edit_visible(b)
self.resize_to_fit_content()
def toggle_locktime(self):
b = not self.config.get('show_tx_locktime', False)
self.config.set_key('show_tx_locktime', b)
self.set_locktime_visible(b)
self.resize_to_fit_content()
def toggle_preview_button(self):
b = not self.config.get('show_tx_preview_button', False)
self.config.set_key('show_tx_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 _update_widgets(self):
self._update_amount_label()
if self.not_enough_funds:
self.error = self.main_window.send_tab.get_text_not_enough_funds_mentioning_frozen()
if not self.tx:
self.set_feerounding_visibility(False)
else:
self.check_tx_fee_warning()
self.update_fee_fields()
if self.locktime_e.get_locktime() is None:
self.locktime_e.set_locktime(self.tx.locktime)
self.io_widget.update(self.tx)
self.fee_label.setText(self.main_window.config.format_amount_and_units(self.tx.get_fee()))
self._update_extra_fees()
self._update_send_button()
self._update_message()
def check_tx_fee_warning(self):
# side effects: self.error, self.message
fee = self.tx.get_fee()
assert fee is not None
amount = self.tx.output_value() if self.output_value == '!' else self.output_value
tx_size = self.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
if not allow_send:
self.error = long_warning
else:
# note: this may overrride existing message
self.message = long_warning
def set_locktime(self):
if not self.tx:
return
locktime = self.locktime_e.get_locktime()
if locktime is not None:
self.tx.locktime = locktime
def _update_amount_label(self):
pass
def _update_extra_fees(self):
pass
def _update_message(self):
style = ColorScheme.RED if self.error else ColorScheme.BLUE
self.message_label.setStyleSheet(style.as_stylesheet())
self.message_label.setText(self.error or self.message)
def _update_send_button(self):
enabled = bool(self.tx) and not self.error
self.preview_button.setEnabled(enabled)
self.ok_button.setEnabled(enabled)
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)
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()
@ -89,6 +555,8 @@ class TxEditor:
self.tx = self.make_tx(fee_estimator)
self.not_enough_funds = False
self.no_dynfee_estimates = False
error = ''
message = ''
except NotEnoughFunds:
self.not_enough_funds = True
self.tx = None
@ -116,6 +584,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 +592,47 @@ 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)
grid.addWidget(self.extra_fee_label, 5, 0)
grid.addWidget(self.extra_fee_value, 5, 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
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()
return grid
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)
def _update_extra_fees(self):
x_fee = run_hook('get_tx_extra_fee', self.wallet, self.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)

21
electrum/gui/qt/fee_slider.py

@ -41,12 +41,16 @@ class FeeSlider(QSlider):
self.valueChanged.connect(self.moved)
self._active = True
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 +64,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()

9
electrum/gui/qt/locktimeedit.py

@ -6,7 +6,7 @@ import time
from datetime import datetime
from typing import Optional, Any
from PyQt5.QtCore import Qt, QDateTime
from PyQt5.QtCore import Qt, QDateTime, pyqtSignal
from PyQt5.QtGui import QPalette, QPainter
from PyQt5.QtWidgets import (QWidget, QLineEdit, QStyle, QStyleOptionFrame, QComboBox,
QHBoxLayout, QDateTimeEdit)
@ -19,6 +19,8 @@ from .util import char_width_in_lineedit, ColorScheme
class LockTimeEdit(QWidget):
valueEdited = pyqtSignal()
def __init__(self, parent=None):
QWidget.__init__(self, parent)
@ -63,6 +65,11 @@ class LockTimeEdit(QWidget):
hbox.addWidget(w)
hbox.addStretch(1)
self.locktime_height_e.textEdited.connect(self.valueEdited.emit)
self.locktime_raw_e.textEdited.connect(self.valueEdited.emit)
self.locktime_date_e.dateTimeChanged.connect(self.valueEdited.emit)
self.combo.currentIndexChanged.connect(self.valueEdited.emit)
def get_locktime(self) -> Optional[int]:
return self.editor.get_locktime()

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():

173
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,132 +33,104 @@ 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.update()
self.fee_slider.deactivate()
# 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.trigger_update)
self.method_combo.setFocusPolicy(Qt.NoFocus)
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.message = ''
self.tx = None
self.error = _('No fee rate')
elif fee_rate <= self.old_fee_rate:
self.new_tx = None
self.message = _("The new fee rate needs to be higher than the old fee rate.")
self.tx = None
self.error = _("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.message = str(e)
if not self.new_tx:
self.tx = None
self.error = str(e)
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)
self.ok_button.setEnabled(enabled)
if enabled:
style = ColorScheme.BLUE.as_stylesheet()
else:
style = ColorScheme.RED.as_stylesheet()
self.message_label.setStyleSheet(style)
self.message_label.setText(self.message)
class BumpFeeDialog(_BaseRBFDialog):
@ -177,10 +152,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 +181,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