1
0
Files
electrum/electrum/gui/qt/confirm_tx_dialog.py
f321x 1b28e6bf73 TxEditor: move swap request to TxEditor
Moves the logic requesting the forward swap into the TxEditor so it can
use the open transport and doesn't have to reconnect to the relays
again.

Also disables the "Preview" button in the TxEditor when the transaction will
send change to lightning.
This should prevent the user from saving the transaction to history and
broadcasting it later or exporting it and broadcasting it through some
external way.
Broadcasting needs to happen directly after the TxEditor so we can send
the second rpc call to the swapserver and await the incoming htlcs
before broadcasting the (funding-) transaction.
2026-01-22 10:09:29 +01:00

1241 lines
56 KiB
Python

#!/usr/bin/env python
#
# Electrum - lightweight Bitcoin client
# Copyright (2019) The Electrum Developers
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import asyncio
from decimal import Decimal
from functools import partial
from typing import TYPE_CHECKING, Optional, Union
from concurrent.futures import Future
from enum import Enum, auto
from PyQt6.QtCore import Qt, QTimer, pyqtSlot, pyqtSignal
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import (QHBoxLayout, QVBoxLayout, QLabel, QGridLayout, QPushButton, QToolButton,
QComboBox, QTabWidget, QWidget, QStackedWidget)
from electrum.i18n import _
from electrum.util import (UserCancelled, quantize_feerate, profiler, NotEnoughFunds, NoDynamicFeeEstimates,
get_asyncio_loop, wait_for2, UserFacingException)
from electrum.plugin import run_hook
from electrum.transaction import PartialTransaction, PartialTxOutput
from electrum.wallet import InternalAddressCorruption
from electrum.bitcoin import DummyAddress
from electrum.fee_policy import FeePolicy, FixedFeePolicy, FeeMethod
from electrum.logging import Logger
from electrum.submarine_swaps import NostrTransport, HttpTransport, SwapServerTransport, SwapServerError
from electrum.gui.messages import MSG_SUBMARINE_PAYMENT_HELP_TEXT
from .util import (WindowModalDialog, ColorScheme, HelpLabel, Buttons, CancelButton, WWLabel,
read_QIcon, qt_event_listener, QtEventListener, IconLabel,
HelpButton, RunCoroutineDialog)
from .transaction_dialog import TxSizeLabel, TxFiatLabel, TxInOutWidget
from .fee_slider import FeeSlider, FeeComboBox
from .amountedit import FeerateEdit, BTCAmountEdit
from .locktimeedit import LockTimeEdit
from .my_treeview import QMenuWithConfig
from .swap_dialog import SwapProvidersButton
if TYPE_CHECKING:
from .main_window import ElectrumWindow
class TxEditorContext(Enum):
"""
Context for which the TxEditor gets launched.
Allows to enable/disable certain features.
"""
PAYMENT = auto()
CHANNEL_FUNDING = auto()
class TxEditor(WindowModalDialog, QtEventListener, Logger):
swap_availability_changed = pyqtSignal()
def __init__(
self, *, title='',
window: 'ElectrumWindow',
make_tx,
output_value: Union[int, str],
payee_outputs: Optional[list[PartialTxOutput]] = None,
context: TxEditorContext = TxEditorContext.PAYMENT,
batching_candidates=None,
):
WindowModalDialog.__init__(self, window, title=title)
Logger.__init__(self)
self.main_window = window
self.make_tx = make_tx
self.output_value = output_value
# used only for submarine payments as they construct tx independently of make_tx
self.payee_outputs = payee_outputs
self.tx = None # type: Optional[PartialTransaction]
self.messages = []
self.error = '' # set by side effect
self.config = window.config
self.network = window.network
self.fee_policy = FeePolicy(self.config.FEE_POLICY)
self.wallet = window.wallet
self.feerounding_sats = 0
self.not_enough_funds = False
self.no_dynfee_estimates = False
self.needs_update = False
self.context = context
self.is_preview = False
self._base_tx = None # for batching
self.batching_candidates = batching_candidates
self.swap_manager = self.wallet.lnworker.swap_manager if self.wallet.has_lightning() else None
self.swap_transport = None # type: Optional[SwapServerTransport]
self.swap_availability_changed.connect(self.on_swap_availability_changed, Qt.ConnectionType.QueuedConnection)
self.ongoing_swap_transport_connection_attempt = None # type: Optional[Future]
self.did_swap = False # used to clear the PI on send tab
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()
onchain_vbox = QVBoxLayout()
onchain_top = self.create_top_bar(self.help_text)
onchain_grid = self.create_grid()
onchain_vbox.addLayout(onchain_top)
onchain_vbox.addLayout(onchain_grid)
onchain_vbox.addWidget(self.io_widget)
self.message_label = WWLabel('')
self.message_label.setMinimumHeight(70)
onchain_vbox.addWidget(self.message_label)
onchain_buttons = self.create_buttons_bar()
onchain_vbox.addStretch(1)
onchain_vbox.addLayout(onchain_buttons)
# onchain tab is the main tab and the content is also shown if tabs are disabled
self.onchain_tab = QWidget()
self.onchain_tab.setContentsMargins(0,0,0,0)
self.onchain_tab.setLayout(onchain_vbox)
# optional submarine payment tab, the tab is only shown if the option is enabled
self.submarine_payment_tab = self.create_submarine_payment_tab()
self.tab_widget = QTabWidget()
self.tab_widget.setTabBarAutoHide(True) # hides the tab bar if there is only one tab
self.tab_widget.setContentsMargins(0, 0, 0, 0)
self.tab_widget.currentChanged.connect(self.on_tab_changed)
self.main_layout = QVBoxLayout()
self.main_layout.addWidget(self.tab_widget)
self.main_layout.setContentsMargins(6, 6, 6, 6) # reduce outermost margins a bit
self.setLayout(self.main_layout)
self.set_io_visible()
self.set_fee_edit_visible()
self.set_locktime_visible()
self.update_fee_target()
self.update_tab_visibility()
self.resize_to_fit_content()
self.timer = QTimer(self)
self.timer.setInterval(500)
self.timer.setSingleShot(False)
self.timer.timeout.connect(self.timer_actions)
self.timer.start()
self.register_callbacks()
# debug_widget_layouts(self) # enable to show red lines around all elements
def accept(self):
self._cleanup()
super().accept()
def reject(self):
self._cleanup()
super().reject()
def closeEvent(self, event):
self._cleanup()
super().closeEvent(event)
def _cleanup(self):
self.unregister_callbacks()
if self.ongoing_swap_transport_connection_attempt:
self.ongoing_swap_transport_connection_attempt.cancel()
if isinstance(self.swap_transport, NostrTransport):
asyncio.run_coroutine_threadsafe(self.swap_transport.stop(), get_asyncio_loop())
self.swap_transport = None # HTTPTransport doesn't need to be closed
def on_tab_changed(self, index):
if self.tab_widget.widget(index) == self.submarine_payment_tab:
self.prepare_swap_transport()
self.update_submarine_payment_tab()
else:
self.update()
def is_batching(self) -> bool:
return self._base_tx is not None
def timer_actions(self):
if self.needs_update:
self.update()
self.needs_update = False
def update(self):
self.update_tx()
self.set_locktime()
self._update_widgets()
def stop_editor_updates(self):
self.timer.stop()
def update_tx(self, *, fallback_to_zero_fee: bool = False):
# expected to set self.tx, self.message and self.error
raise NotImplementedError()
def create_grid(self) -> QGridLayout:
raise NotImplementedError()
@property
def help_text(self) -> str:
raise NotImplementedError()
def update_fee_target(self):
if self.fee_slider.is_active():
text = self.fee_policy.get_target_text()
else:
text = ""
self.fee_target.setText(text)
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.TextInteractionFlag.TextSelectableByMouse)
self.size_label = TxSizeLabel()
self.size_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.size_label.setAmount(0)
self.size_label.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet())
self.feerate_label = QLabel('')
self.feerate_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
self.fiat_fee_label = TxFiatLabel()
self.fiat_fee_label.setAlignment(Qt.AlignmentFlag.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.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)
if self.fee_policy.method != FeeMethod.FIXED:
self.feerate_e.setAmount(self.fee_policy.fee_per_byte(self.network))
else:
self.fee_e.setAmount(self.fee_policy.value)
self.fee_e.textChanged.connect(self.entry_changed)
self.feerate_e.textChanged.connect(self.entry_changed)
self.fee_target = QLabel('')
self.fee_slider = FeeSlider(parent=self, network=self.network, fee_policy=self.fee_policy, callback=self.fee_slider_callback)
self.fee_combo = FeeComboBox(self.fee_slider)
self.fee_combo.setFocusPolicy(Qt.FocusPolicy.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 update_tab_visibility(self):
"""Update self.tab_widget to show all tabs that are enabled."""
# first remove all tabs
while self.tab_widget.count() > 0:
self.tab_widget.removeTab(0)
# always show onchain payment tab
self.tab_widget.addTab(self.onchain_tab, _('Onchain Transaction'))
allow_swaps = self.context == TxEditorContext.PAYMENT and self.payee_outputs and self.swap_manager
if self.config.WALLET_ENABLE_SUBMARINE_PAYMENTS and allow_swaps:
i = self.tab_widget.addTab(self.submarine_payment_tab, _('Submarine Payment'))
tooltip = self.config.cv.WALLET_ENABLE_SUBMARINE_PAYMENTS.get_long_desc()
if len(self.payee_outputs) > 1:
self.tab_widget.setTabEnabled(i, False)
tooltip = _("Submarine Payments don't support multiple outputs (Pay-to-many).")
elif self.payee_outputs[0].value == '!':
self.tab_widget.setTabEnabled(i, False)
self.submarine_payment_tab.setEnabled(False)
tooltip = _("Submarine Payments don't support 'Max' value spends.")
self.tab_widget.tabBar().setTabToolTip(i, tooltip)
# enable document mode if there is only one tab to hide the frame
self.tab_widget.setDocumentMode(self.tab_widget.count() < 2)
self.resize_to_fit_content()
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.messages = []
self.error = ''
self._update_widgets()
self.needs_update = True
def fee_slider_callback(self, 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) -> bool:
return self.fee_e.isVisible() and self.fee_e.isModified() \
and (bool(self.fee_e.text()) or self.fee_e.hasFocus())
def is_send_feerate_frozen(self) -> bool:
return self.feerate_e.isVisible() and self.feerate_e.isModified() \
and (bool(self.feerate_e.text()) or self.feerate_e.hasFocus())
def feerounding_text(self):
return (_('Additional {} satoshis are going to be added.').format(self.feerounding_sats))
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_policy(self):
feerate = self.feerate_e.get_amount()
fee_amount = self.fee_e.get_amount()
if self.is_send_fee_frozen() and fee_amount is not None:
fee_policy = FixedFeePolicy(fee_amount)
elif self.is_send_feerate_frozen() and feerate is not None:
feerate_per_kb = int(feerate * 1000)
fee_policy = FeePolicy(f'feerate:{feerate_per_kb}')
else:
fee_policy = self.fee_slider.get_policy()
return fee_policy
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)
if displayed_feerate is not None:
displayed_fee = FeePolicy.estimate_fee_for_feerate(fee_per_kb=displayed_feerate * 1000, size=size)
else:
displayed_fee = 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.feerounding_sats = 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()
self.update_fee_target()
def create_buttons_bar(self):
self.change_to_ln_swap_providers_button = SwapProvidersButton(lambda: self.swap_transport, self.config, self.main_window)
self.preview_button = QPushButton(_('Preview'))
self.preview_button.clicked.connect(self.on_preview)
self.preview_button.setVisible(self.context != TxEditorContext.CHANNEL_FUNDING)
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)
buttons.insertWidget(0, self.change_to_ln_swap_providers_button)
if self.batching_candidates is not None and len(self.batching_candidates) > 0:
batching_combo = QComboBox()
batching_combo.addItems([_('Do not batch')] + [_('Batch with') + ' ' + tx.txid()[0:10] for tx in self.batching_candidates])
buttons.insertWidget(0, batching_combo)
def on_batching_combo(x):
self._base_tx = self.batching_candidates[x - 1] if x > 0 else None
self.trigger_update()
batching_combo.currentIndexChanged.connect(on_batching_combo)
return buttons
def create_top_bar(self, text):
self.pref_menu = QMenuWithConfig(self.config)
def cb():
self.set_io_visible()
self.resize_to_fit_content()
self.pref_menu.addConfig(self.config.cv.GUI_QT_TX_EDITOR_SHOW_IO, callback=cb)
def cb():
self.set_fee_edit_visible()
self.resize_to_fit_content()
self.pref_menu.addConfig(self.config.cv.GUI_QT_TX_EDITOR_SHOW_FEE_DETAILS, callback=cb)
def cb():
self.set_locktime_visible()
self.resize_to_fit_content()
self.pref_menu.addConfig(self.config.cv.GUI_QT_TX_EDITOR_SHOW_LOCKTIME, callback=cb)
self.pref_menu.addSeparator()
can_have_lightning = self.wallet.can_have_lightning()
send_ch_to_ln = self.pref_menu.addConfig(
self.config.cv.WALLET_SEND_CHANGE_TO_LIGHTNING,
callback=lambda: (self.prepare_swap_transport(), self.trigger_update()), # type: ignore
checked=False if not can_have_lightning else None,
)
sub_payments = self.pref_menu.addConfig(
self.config.cv.WALLET_ENABLE_SUBMARINE_PAYMENTS,
callback=self.update_tab_visibility,
checked=False if not can_have_lightning else None,
)
if not can_have_lightning: # disable the buttons and override tooltip
ln_unavailable_msg = _("Not available for this wallet.") \
+ "\n" + _("Requires a wallet with Lightning network support.")
for ln_conf in (send_ch_to_ln, sub_payments):
ln_conf.setEnabled(False)
ln_conf.setToolTip(ln_unavailable_msg)
self.pref_menu.addToggle(
_('Use change addresses'),
self.toggle_use_change,
default_state=self.wallet.use_change,
tooltip=_('Using change addresses makes it more difficult for other people to track your transactions.'))
self.use_multi_change_menu = self.pref_menu.addToggle(
_('Use multiple change addresses'),
self.toggle_multiple_change,
default_state=self.wallet.multiple_change,
tooltip='\n'.join([
_('In some cases, use up to 3 change addresses in order to break '
'up large coin amounts and obfuscate the recipient address.'),
_('This may result in higher transactions fees.')
]))
self.use_multi_change_menu.setEnabled(self.wallet.use_change)
# fixme: some of these options (WALLET_SEND_CHANGE_TO_LIGHTNING, WALLET_MERGE_DUPLICATE_OUTPUTS)
# only make sense when we create a new tx, and should not be visible/enabled in rbf dialog
self.pref_menu.addConfig(self.config.cv.WALLET_MERGE_DUPLICATE_OUTPUTS, callback=self.trigger_update)
self.pref_menu.addConfig(self.config.cv.WALLET_SPEND_CONFIRMED_ONLY, callback=self.trigger_update)
self.pref_menu.addConfig(self.config.cv.WALLET_COIN_CHOOSER_OUTPUT_ROUNDING, callback=self.trigger_update)
self.pref_button = QToolButton()
self.pref_button.setIcon(read_QIcon("preferences.png"))
self.pref_button.setText(_('Tools'))
self.pref_button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
self.pref_button.setMenu(self.pref_menu)
self.pref_button.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
self.pref_button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
hbox = QHBoxLayout()
hbox.addWidget(QLabel(text))
hbox.addStretch()
hbox.addWidget(self.pref_button)
return hbox
@profiler(min_threshold=0.02)
def resize_to_fit_content(self):
# update all geometries so the updated size hints are used for size adjustment
for widget in self.findChildren(QWidget):
widget.updateGeometry()
self.adjustSize()
def toggle_use_change(self):
self.wallet.use_change = not self.wallet.use_change
self.wallet.db.put('use_change', self.wallet.use_change)
self.use_multi_change_menu.setEnabled(self.wallet.use_change)
self.trigger_update()
def toggle_multiple_change(self):
self.wallet.multiple_change = not self.wallet.multiple_change
self.wallet.db.put('multiple_change', self.wallet.multiple_change)
self.trigger_update()
def set_io_visible(self):
self.io_widget.setVisible(self.config.GUI_QT_TX_EDITOR_SHOW_IO)
def set_fee_edit_visible(self):
b = self.config.GUI_QT_TX_EDITOR_SHOW_FEE_DETAILS
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()
def set_locktime_visible(self):
b = self.config.GUI_QT_TX_EDITOR_SHOW_LOCKTIME
for w in [
self.locktime_e,
self.locktime_label]:
w.setVisible(b)
def run(self):
if self.config.WALLET_SEND_CHANGE_TO_LIGHTNING:
# if disabled but submarine payments are enabled we only connect once the other tab gets opened
self.prepare_swap_transport()
cancelled = not self.exec()
self.stop_editor_updates()
self.deleteLater() # see #3956
return self.tx if not cancelled else None
def on_send(self):
if self.tx and self.tx.get_dummy_output(DummyAddress.SWAP):
if not self.request_forward_swap():
return
self.accept()
def on_preview(self):
assert not self.tx.get_dummy_output(DummyAddress.SWAP), "no preview when sending change to ln"
self.is_preview = True
self.accept()
def _update_widgets(self):
# side effect: self.error
self._update_amount_label()
if self.not_enough_funds:
self.error = _('Not enough funds.')
confirmed_only = self.config.WALLET_SPEND_CONFIRMED_ONLY
if confirmed_only and self.can_pay_assuming_zero_fees(confirmed_only=False):
self.error += ' ' + _('Change your settings to allow spending unconfirmed coins.')
elif self.can_pay_assuming_zero_fees(confirmed_only=confirmed_only):
self.error += ' ' + _('You need to set a lower fee.')
elif frozen_bal := self.wallet.get_frozen_balance_str():
self.error = self.wallet.get_text_not_enough_funds_mentioning_frozen(
for_amount=self.output_value,
hint=_('Can be unfrozen in the Addresses or in the Coins tab')
)
if not self.tx:
if self.not_enough_funds:
self.io_widget.update(None)
self.set_feerounding_visibility(False)
self.messages = [_('Preparing transaction...')]
else:
self.messages = self.get_messages()
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()
if self.config.WALLET_SEND_CHANGE_TO_LIGHTNING:
self.change_to_ln_swap_providers_button.setVisible(True)
self.change_to_ln_swap_providers_button.fetching = bool(self.ongoing_swap_transport_connection_attempt)
self.change_to_ln_swap_providers_button.update()
else:
self.change_to_ln_swap_providers_button.setVisible(False)
self._update_send_button()
self._update_message()
def get_messages(self):
# side effect: self.error
messages = []
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, txid=self.tx.txid())
if fee_warning_tuple:
allow_send, long_warning, short_warning = fee_warning_tuple
if not allow_send:
self.error = long_warning
else:
messages.append(long_warning)
if self.no_dynfee_estimates:
self.error = _('Fee estimates not available. Please set a fixed fee or feerate.')
if dummy_output := self.tx.get_dummy_output(DummyAddress.SWAP):
swap_msg = _('Will send change to lightning')
swap_fee_msg = "."
if self.swap_manager and self.swap_manager.is_initialized.is_set() and isinstance(dummy_output.value, int):
ln_amount_we_recv = self.swap_manager.get_recv_amount(send_amount=dummy_output.value, is_reverse=False)
if ln_amount_we_recv:
swap_fees = dummy_output.value - ln_amount_we_recv
swap_fee_msg = " [" + _("Swap fees:") + " " + self.main_window.format_amount_and_units(swap_fees) + "]."
messages.append(swap_msg + swap_fee_msg)
elif self.config.WALLET_SEND_CHANGE_TO_LIGHTNING \
and not self.ongoing_swap_transport_connection_attempt \
and self.tx.has_change():
swap_msg = _('Will not send change to Lightning')
swap_msg_reason = None
change_amount = sum(c.value for c in self.tx.get_change_outputs() if isinstance(c.value, int))
if not self.wallet.has_lightning():
swap_msg_reason = _('Lightning is not enabled.')
elif change_amount > int(self.wallet.lnworker.num_sats_can_receive()):
swap_msg_reason = _("Your channels cannot receive this amount.")
elif self.wallet.lnworker.swap_manager.is_initialized.is_set():
min_amount = self.wallet.lnworker.swap_manager.get_min_amount()
max_amount = self.wallet.lnworker.swap_manager.get_provider_max_reverse_amount()
if change_amount < min_amount:
swap_msg_reason = _("Below the swap providers minimum value of {}.").format(
self.main_window.format_amount_and_units(min_amount)
)
else:
swap_msg_reason = _('Change amount exceeds the swap providers maximum value of {}.').format(
self.main_window.format_amount_and_units(max_amount)
)
messages.append(swap_msg + (f": {swap_msg_reason}" if swap_msg_reason else '.'))
elif self.ongoing_swap_transport_connection_attempt:
messages.append(_("Fetching submarine swap providers..."))
# warn if spending unconf
if any((txin.block_height is not None and txin.block_height<=0) for txin in self.tx.inputs()):
messages.append(_('This transaction will spend unconfirmed coins.'))
# warn if a reserve utxo was added
if reserve_sats := self.wallet.tx_keeps_ln_utxo_reserve(self.tx, gui_spend_max=bool(self.output_value == '!')):
reserve_str = self.main_window.config.format_amount_and_units(reserve_sats)
messages.append(_('Could not spend max: a security reserve of {} was kept for your Lightning channels.').format(reserve_str))
# warn if we merge from mempool
if self.is_batching():
messages.append(_('This payment will be merged with another existing transaction.'))
# warn if we use multiple change outputs
num_change = sum(int(o.is_change) for o in self.tx.outputs())
num_ismine = sum(int(o.is_mine) for o in self.tx.outputs())
if num_change > 1:
messages.append(_('This transaction has {} change outputs.'.format(num_change)))
# warn if there is no ismine output, as it might be problematic to RBF the tx later.
# (though RBF is still possible by adding new inputs, if the wallet has more utxos)
if num_ismine == 0:
messages.append(_('Make sure you pay enough mining fees; you will not be able to bump the fee later.'))
# TODO: warn if we send change back to input address
return messages
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
message_str = '\n'.join(self.messages) if self.messages else ''
self.message_label.setStyleSheet(style.as_stylesheet())
self.message_label.setText(self.error or message_str)
def _update_send_button(self):
# disable preview button when sending change to lightning to prevent the user from saving or
# exporting the transaction and broadcasting it later somehow.
send_change_to_ln = self.tx and self.tx.get_dummy_output(DummyAddress.SWAP)
enabled = bool(self.tx) and not self.error
self.preview_button.setEnabled(enabled and not send_change_to_ln)
self.preview_button.setToolTip(_("Can't show preview when sending change to lightning") if send_change_to_ln else "")
self.ok_button.setEnabled(enabled)
def can_pay_assuming_zero_fees(self, confirmed_only: bool) -> bool:
raise NotImplementedError
### --- Shared functionality for submarine swaps (change to ln and submarine payments) ---
def prepare_swap_transport(self):
if not self.swap_manager:
return # no swaps possible, lightning disabled
if self.swap_transport is not None and self.swap_transport.is_connected.is_set():
# we already have a connected transport, no need to create a new one
return
if self.ongoing_swap_transport_connection_attempt:
# another task is currently trying to connect
return
# there should only be a connected transport.
# a useless transport should get cleaned up and not stored.
assert self.swap_transport is None, "swap transport wasn't cleaned up properly"
new_swap_transport = self.main_window.create_sm_transport()
if not new_swap_transport:
# user declined to enable Nostr and has no http server configured
self.swap_availability_changed.emit()
return
async def _initialize_transport(transport):
try:
if isinstance(transport, NostrTransport):
asyncio.create_task(transport.main_loop())
else:
assert isinstance(transport, HttpTransport)
asyncio.create_task(transport.get_pairs_just_once())
if not await self.wait_for_swap_transport(transport):
return
self.swap_transport = transport
except Exception:
self.logger.exception("failed to create swap transport")
finally:
self.ongoing_swap_transport_connection_attempt = None
self.swap_availability_changed.emit()
# this task will get cancelled if the TxEditor gets closed
self.ongoing_swap_transport_connection_attempt = asyncio.run_coroutine_threadsafe(
_initialize_transport(new_swap_transport),
get_asyncio_loop(),
)
async def wait_for_swap_transport(self, new_swap_transport: Union[HttpTransport, NostrTransport]) -> bool:
"""
Wait until we found the announcement event of the configured swap server.
If it is not found but the relay connection is established return True anyway,
the user will then need to select a different swap server.
"""
timeout = new_swap_transport.connect_timeout + 1
try:
# swap_manager.is_initialized gets set once we got pairs of the configured swap server
await wait_for2(self.swap_manager.is_initialized.wait(), timeout)
except asyncio.TimeoutError:
self.logger.debug(f"swap transport initialization timed out after {timeout} sec")
if self.swap_manager.is_initialized.is_set():
return True
# timed out above
if self.config.SWAPSERVER_URL:
# http swapserver didn't return pairs
self.logger.error(f"couldn't request pairs from {self.config.SWAPSERVER_URL=}")
return False
elif new_swap_transport.is_connected.is_set():
assert isinstance(new_swap_transport, NostrTransport)
# couldn't find announcement of configured swapserver, maybe it is gone.
# update_submarine_payment_tab will tell the user to select a different swap server.
return True
# we couldn't even connect to the relays, this transport is useless. maybe network issues.
return False
@qt_event_listener
def on_event_swap_provider_changed(self):
self.swap_availability_changed.emit()
@qt_event_listener
def on_event_channel(self, wallet, _channel):
# useful e.g. if the user quickly opens the tab after startup before the channels are initialized
if wallet == self.wallet and self.swap_manager and self.swap_manager.is_initialized.is_set():
self.swap_availability_changed.emit()
@qt_event_listener
def on_event_swap_offers_changed(self, _):
self.change_to_ln_swap_providers_button.update()
self.submarine_payment_provider_button.update()
if self.ongoing_swap_transport_connection_attempt:
return
self.swap_availability_changed.emit()
@pyqtSlot()
def on_swap_availability_changed(self):
# uses a signal/slot to update the gui so we can schedule an update from the asyncio thread
if self.tab_widget.currentWidget() == self.submarine_payment_tab:
self.update_submarine_payment_tab()
else:
self.update()
### --- Functionality for reverse submarine swaps to external address ---
def create_submarine_payment_tab(self) -> QWidget:
"""Returns widget for submarine payment functionality to be added as tab"""
tab_widget = QWidget()
vbox = QVBoxLayout(tab_widget)
# stack two views, a warning view and the regular one. The warning view is shown if
# the swap cannot be performed, e.g. due to missing liquidity.
self.submarine_stacked_widget = QStackedWidget()
# Normal layout page
normal_page = QWidget()
h = QGridLayout(normal_page)
help_button = HelpButton(MSG_SUBMARINE_PAYMENT_HELP_TEXT)
self.submarine_lightning_send_amount_label = QLabel()
self.submarine_onchain_send_amount_label = QLabel()
self.submarine_claim_mining_fee_label = QLabel()
self.submarine_server_fee_label = QLabel()
self.submarine_we_send_label = IconLabel(text=_('You send')+':')
self.submarine_we_send_label.setIcon(read_QIcon('lightning.png'))
self.submarine_they_receive_label = IconLabel(text=_('They receive')+':')
self.submarine_they_receive_label.setIcon(read_QIcon('bitcoin.png'))
# column 0 (labels)
h.addWidget(self.submarine_we_send_label, 0, 0)
h.addWidget(self.submarine_they_receive_label, 1, 0)
h.addWidget(QLabel(_('Swap fee')+':'), 2, 0)
h.addWidget(QLabel(_('Mining fee')+':'), 3, 0)
# column 1 (spacing)
h.setColumnStretch(1, 1)
# column 2 (amounts)
h.addWidget(self.submarine_lightning_send_amount_label, 0, 2)
h.addWidget(self.submarine_onchain_send_amount_label, 1, 2)
h.addWidget(self.submarine_server_fee_label, 2, 2, 1, 2)
h.addWidget(self.submarine_claim_mining_fee_label, 3, 2, 1, 2)
# column 3 (spacing)
h.setColumnStretch(3, 1)
# column 4 (help button)
h.addWidget(help_button, 0, 4)
# Warning layout page
warning_page = QWidget()
warning_layout = QVBoxLayout(warning_page)
self.submarine_warning_label = QLabel('')
warning_layout.addWidget(self.submarine_warning_label)
self.submarine_stacked_widget.addWidget(normal_page)
self.submarine_stacked_widget.addWidget(warning_page)
vbox.addWidget(self.submarine_stacked_widget)
vbox.addStretch(1)
self.submarine_payment_provider_button = SwapProvidersButton(lambda: self.swap_transport, self.config, self.main_window)
self.submarine_ok_button = QPushButton(_('OK'))
self.submarine_ok_button.setDefault(True)
self.submarine_ok_button.setEnabled(False)
# pay button must not self.accept() as this triggers closing the transport
self.submarine_ok_button.clicked.connect(self.start_submarine_payment)
buttons = Buttons(CancelButton(self), self.submarine_ok_button)
buttons.insertWidget(0, self.submarine_payment_provider_button)
vbox.addLayout(buttons)
return tab_widget
def show_swap_transport_connection_message(self):
self.submarine_stacked_widget.setCurrentIndex(1)
self.submarine_warning_label.setText(_("Connecting, please wait..."))
self.submarine_ok_button.setEnabled(False)
def start_submarine_payment(self):
assert self.payee_outputs and len(self.payee_outputs) == 1
payee_output = self.payee_outputs[0]
assert self.expected_onchain_amount_sat is not None
assert self.lightning_send_amount_sat is not None
assert self.last_server_mining_fee_sat is not None
assert self.swap_transport.is_connected.is_set()
assert self.swap_manager.is_initialized.is_set()
self.tx = None # prevent broadcasting
self.submarine_ok_button.setEnabled(False)
coro = self.swap_manager.reverse_swap(
transport=self.swap_transport,
lightning_amount_sat=self.lightning_send_amount_sat,
expected_onchain_amount_sat=self.expected_onchain_amount_sat,
prepayment_sat=2 * self.last_server_mining_fee_sat,
claim_to_output=payee_output,
)
try:
funding_txid = self.main_window.run_coroutine_dialog(coro, _('Initiating Submarine Payment...'))
except Exception as e:
self.close()
self.main_window.show_error(_("Submarine Payment failed:") + "\n" + str(e))
return
self.did_swap = True
# accepting closes the swap transport, so it needs to happen after the swap
self.accept()
self.main_window.on_swap_result(funding_txid, is_reverse=True)
def update_submarine_payment_tab(self):
assert self.tab_widget.currentWidget() == self.submarine_payment_tab
assert self.payee_outputs, "Opened submarine payment tab without outputs?"
assert len(self.payee_outputs) == \
len([o for o in self.payee_outputs if not o.is_change and not isinstance(o.value, str)])
f = self.main_window.format_amount_and_units
self.logger.debug(f"TxEditor updating submarine payment tab")
if not self.swap_manager:
self.set_submarine_payment_tab_warning(_("Enable Lightning in the 'Channels' tab to use Submarine Swaps."))
return
if not self.swap_manager.is_initialized.is_set() \
and self.ongoing_swap_transport_connection_attempt:
self.show_swap_transport_connection_message()
return
if not self.swap_transport:
# couldn't connect to nostr relays or http server didn't respond
self.set_submarine_payment_tab_warning(_("Submarine swap provider unavailable."))
return
# Update the swapserver selection button text
self.submarine_payment_provider_button.update()
if not self.swap_manager.is_initialized.is_set():
# connected to nostr relays but couldn't find swapserver announcement
assert isinstance(self.swap_transport, NostrTransport), "HTTPTransport shouldn't get set if it cannot fetch pairs"
assert self.swap_transport.is_connected.is_set(), "closed transport wasn't cleaned up"
if self.config.SWAPSERVER_NPUB:
msg = _("Couldn't connect to your swap provider. Please select a different provider.")
else:
msg = _('Please select a submarine swap provider.')
self.set_submarine_payment_tab_warning(msg)
return
# update values
self.lightning_send_amount_sat = self.swap_manager.get_send_amount(
self.payee_outputs[0].value, # claim tx fee reserve gets added in get_send_amount
is_reverse=True,
)
self.last_server_mining_fee_sat = self.swap_manager.mining_fee
self.expected_onchain_amount_sat = (
self.payee_outputs[0].value + self.swap_manager.get_fee_for_txbatcher()
)
# get warning
warning_text = self.get_swap_warning()
if warning_text:
self.set_submarine_payment_tab_warning(warning_text)
return
# There is no warning, show the normal view (amounts etc.)
self.submarine_stacked_widget.setCurrentIndex(0)
# label showing the payment amount (the amount the user entered in SendTab)
self.submarine_onchain_send_amount_label.setText(f(self.payee_outputs[0].value))
# the fee we pay to claim the funding output to the onchain address, shown as "Mining Fee"
claim_tx_mining_fee = self.swap_manager.get_fee_for_txbatcher()
self.submarine_claim_mining_fee_label.setText(f(claim_tx_mining_fee))
assert self.lightning_send_amount_sat is not None
self.submarine_lightning_send_amount_label.setText(f(self.lightning_send_amount_sat))
# complete fee we pay to the server
server_fee = self.lightning_send_amount_sat - self.expected_onchain_amount_sat
self.submarine_server_fee_label.setText(f(server_fee))
self.submarine_ok_button.setEnabled(True)
def get_swap_warning(self) -> Optional[str]:
f = self.main_window.format_amount_and_units
ln_can_send = int(self.wallet.lnworker.num_sats_can_send())
if self.expected_onchain_amount_sat < self.swap_manager.get_min_amount():
return '\n'.join([
_("Payment amount below the minimum possible swap amount."),
_("You need to send a higher amount to be able to do a Submarine Payment."),
_("Minimum amount: {}").format(f(self.swap_manager.get_min_amount())),
])
too_low_outbound_liquidity_msg = '\n'.join([
_("You don't have enough outgoing capacity in your lightning channels."),
_("To add outgoing capacity you can open a new lightning channel or do a submarine swap."),
_("Your lightning channels can send: {}").format(f(ln_can_send)),
])
# prioritize showing the swap provider liquidity warning before the channel liquidity warning
# as it could be annoying for the user to be told to open a new channel just to come back to
# notice there is no provider supporting their swap amount
if self.lightning_send_amount_sat is None:
provider_liquidity = self.swap_manager.get_provider_max_forward_amount()
if provider_liquidity < self.swap_manager.get_min_amount():
provider_liquidity = 0
msg = [
_("The selected swap provider is unable to offer a forward swap of this value."),
_("In order to continue select a different provider or try to send a smaller amount."),
_("Available liquidity") + f": {f(provider_liquidity)}",
]
# we don't know exactly how much we need to send on ln yet, so we can assume 0 provider fees
probably_too_low_outbound_liquidity = self.expected_onchain_amount_sat > ln_can_send
if probably_too_low_outbound_liquidity:
msg.extend([
"",
"Please also note:",
too_low_outbound_liquidity_msg,
])
return "\n".join(msg)
# if we have lightning_send_amount_sat our provider has enough liquidity, so we know the exact
# amount we need to send including the providers fees
too_low_outbound_liquidity = self.lightning_send_amount_sat > ln_can_send
if too_low_outbound_liquidity:
return too_low_outbound_liquidity_msg
return None
def set_submarine_payment_tab_warning(self, warning: str):
msg = _('Submarine Payment not possible:') + '\n' + warning
self.submarine_warning_label.setText(msg)
self.submarine_stacked_widget.setCurrentIndex(1)
self.submarine_ok_button.setEnabled(False)
# --- send change to lightning swap functionality ---
def request_forward_swap(self):
swap_dummy_output = self.tx.get_dummy_output(DummyAddress.SWAP)
sm, transport = self.swap_manager, self.swap_transport
assert sm and transport and swap_dummy_output and isinstance(swap_dummy_output.value, int)
coro = sm.request_swap_for_amount(transport=transport, onchain_amount=int(swap_dummy_output.value))
coro_dialog = RunCoroutineDialog(self, _('Requesting swap invoice...'), coro)
try:
swap, swap_invoice = coro_dialog.run()
except (SwapServerError, UserFacingException) as e:
self.show_error(str(e))
return False
except UserCancelled:
return False
self.tx.replace_output_address(DummyAddress.SWAP, swap.lockup_address)
assert self.tx.get_dummy_output(DummyAddress.SWAP) is None
self.tx.swap_invoice = swap_invoice
self.tx.swap_payment_hash = swap.payment_hash
return True
class ConfirmTxDialog(TxEditor):
help_text = '' #_('Set the mining fee of your transaction')
def __init__(
self, *,
window: 'ElectrumWindow',
make_tx,
output_value: Union[int, str],
payee_outputs: Optional[list[PartialTxOutput]] = None,
context: TxEditorContext = TxEditorContext.PAYMENT,
batching_candidates=None,
):
TxEditor.__init__(
self,
window=window,
make_tx=make_tx,
output_value=output_value,
payee_outputs=payee_outputs,
title=_("New Transaction"), # todo: adapt title for channel funding tx, swaps
context=context,
batching_candidates=batching_candidates,
)
self.trigger_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):
self.fee_policy = fee_policy = self.get_fee_policy()
if fee_policy.method != FeeMethod.FIXED:
self.config.FEE_POLICY = fee_policy.get_descriptor()
confirmed_only = self.config.WALLET_SPEND_CONFIRMED_ONLY
base_tx = self._base_tx
try:
self.tx = self.make_tx(fee_policy, confirmed_only=confirmed_only, base_tx=base_tx)
self.not_enough_funds = False
self.no_dynfee_estimates = False
except NotEnoughFunds:
self.not_enough_funds = True
self.tx = None
if fallback_to_zero_fee:
try:
self.tx = self.make_tx(FixedFeePolicy(0), confirmed_only=confirmed_only, base_tx=base_tx)
except BaseException:
return
else:
return
except NoDynamicFeeEstimates:
# is this still needed?
self.no_dynfee_estimates = True
self.tx = None
try:
self.tx = self.make_tx(FixedFeePolicy(0), confirmed_only=confirmed_only, base_tx=base_tx)
except NotEnoughFunds:
self.not_enough_funds = True
return
except BaseException:
return
except InternalAddressCorruption as e:
self.tx = None
self.main_window.show_error(str(e))
raise
self.tx.set_rbf(True)
def can_pay_assuming_zero_fees(self, confirmed_only: bool) -> bool:
# called in send_tab.py
try:
tx = self.make_tx(FixedFeePolicy(0), confirmed_only=confirmed_only, base_tx=None)
except NotEnoughFunds:
return False
else:
return True
def create_grid(self):
grid = QGridLayout()
msg = (_('The amount to be received by the recipient.') + ' '
+ _('Fees are paid by the sender.'))
self.amount_label = QLabel('')
self.amount_label.setTextInteractionFlags(Qt.TextInteractionFlag.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.')
grid.addWidget(HelpLabel(_("Mining Fee") + ": ", msg), 1, 0)
grid.addLayout(self.fee_hbox, 1, 1, 1, 3)
grid.addWidget(HelpLabel(_("Fee policy") + ": ", 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.TextInteractionFlag.TextSelectableByMouse)
self.extra_fee_value.setVisible(False)
grid.addWidget(self.extra_fee_label, 5, 0)
grid.addWidget(self.extra_fee_value, 5, 1)
# locktime editor
grid.addWidget(self.locktime_label, 6, 0)
grid.addWidget(self.locktime_e, 6, 1, 1, 2)
return grid
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))