Merge pull request #8197 from spesmilo/new_tx_flow
Qt: new onchain tx creation flow:
This commit is contained in:
@@ -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:
|
||||
class TxEditor(WindowModalDialog):
|
||||
|
||||
def __init__(self, *, window: 'ElectrumWindow', make_tx,
|
||||
output_value: Union[int, str] = None, is_sweep: bool):
|
||||
def __init__(self, *, title='',
|
||||
window: 'ElectrumWindow',
|
||||
make_tx,
|
||||
output_value: Union[int, str] = None,
|
||||
allow_preview=True):
|
||||
|
||||
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)
|
||||
# locktime editor
|
||||
grid.addWidget(self.locktime_label, 6, 0)
|
||||
grid.addWidget(self.locktime_e, 6, 1, 1, 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
|
||||
return grid
|
||||
|
||||
def default_message(self):
|
||||
return _('Enter your password to proceed') if self.password_required else _('Click Send to proceed')
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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.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.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)
|
||||
TxEditor.__init__(
|
||||
self,
|
||||
window=main_window,
|
||||
title=title,
|
||||
make_tx=self.rbf_func)
|
||||
|
||||
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.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)
|
||||
|
||||
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??
|
||||
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(self):
|
||||
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)
|
||||
|
||||
+19
-31
@@ -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
|
||||
is_preview = conf_dlg.is_preview
|
||||
if is_preview:
|
||||
self.window.show_transaction(tx)
|
||||
return
|
||||
|
||||
cancelled, is_send, password, tx = conf_dlg.run()
|
||||
if cancelled:
|
||||
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")
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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()
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user