Browse Source

Merge pull request #7874 from SomberNight/202206_qt_split_sendtab

qt: (refactor) split "send tab" out from main_window.py
patch-4
ThomasV 3 years ago
committed by GitHub
parent
commit
798df1fd53
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      electrum/gui/qt/__init__.py
  2. 2
      electrum/gui/qt/confirm_tx_dialog.py
  3. 52
      electrum/gui/qt/invoice_list.py
  4. 753
      electrum/gui/qt/main_window.py
  5. 31
      electrum/gui/qt/paytoedit.py
  6. 773
      electrum/gui/qt/send_tab.py
  7. 2
      electrum/gui/qt/transaction_dialog.py

4
electrum/gui/qt/__init__.py

@ -28,7 +28,7 @@ import signal
import sys import sys
import traceback import traceback
import threading import threading
from typing import Optional, TYPE_CHECKING, List from typing import Optional, TYPE_CHECKING, List, Sequence
from electrum import GuiImportError from electrum import GuiImportError
@ -80,7 +80,7 @@ if TYPE_CHECKING:
class OpenFileEventFilter(QObject): class OpenFileEventFilter(QObject):
def __init__(self, windows): def __init__(self, windows: Sequence[ElectrumWindow]):
self.windows = windows self.windows = windows
super(OpenFileEventFilter, self).__init__() super(OpenFileEventFilter, self).__init__()

2
electrum/gui/qt/confirm_tx_dialog.py

@ -238,7 +238,7 @@ class ConfirmTxDialog(TxEditor, WindowModalDialog):
self._update_amount_label() self._update_amount_label()
if self.not_enough_funds: if self.not_enough_funds:
text = self.main_window.get_text_not_enough_funds_mentioning_frozen() text = self.main_window.send_tab.get_text_not_enough_funds_mentioning_frozen()
self.toggle_send_button(False, message=text) self.toggle_send_button(False, message=text)
return return

52
electrum/gui/qt/invoice_list.py

@ -24,7 +24,7 @@
# SOFTWARE. # SOFTWARE.
from enum import IntEnum from enum import IntEnum
from typing import Sequence from typing import Sequence, TYPE_CHECKING
from PyQt5.QtCore import Qt, QItemSelectionModel from PyQt5.QtCore import Qt, QItemSelectionModel
from PyQt5.QtGui import QStandardItemModel, QStandardItem from PyQt5.QtGui import QStandardItemModel, QStandardItem
@ -40,6 +40,9 @@ from .util import MyTreeView, read_QIcon, MySortModel, pr_icons
from .util import CloseButton, Buttons from .util import CloseButton, Buttons
from .util import WindowModalDialog from .util import WindowModalDialog
if TYPE_CHECKING:
from .main_window import ElectrumWindow
from .send_tab import SendTab
ROLE_REQUEST_TYPE = Qt.UserRole ROLE_REQUEST_TYPE = Qt.UserRole
@ -64,8 +67,9 @@ class InvoiceList(MyTreeView):
} }
filter_columns = [Columns.DATE, Columns.DESCRIPTION, Columns.AMOUNT] filter_columns = [Columns.DATE, Columns.DESCRIPTION, Columns.AMOUNT]
def __init__(self, parent): def __init__(self, send_tab: 'SendTab'):
super().__init__(parent, self.create_menu, window = send_tab.window
super().__init__(window, self.create_menu,
stretch_column=self.Columns.DESCRIPTION) stretch_column=self.Columns.DESCRIPTION)
self.std_model = QStandardItemModel(self) self.std_model = QStandardItemModel(self)
self.proxy = MySortModel(self, sort_role=ROLE_SORT_ORDER) self.proxy = MySortModel(self, sort_role=ROLE_SORT_ORDER)
@ -74,17 +78,20 @@ class InvoiceList(MyTreeView):
self.setSortingEnabled(True) self.setSortingEnabled(True)
self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.send_tab = send_tab
self.wallet = window.wallet
def refresh_row(self, key, row): def refresh_row(self, key, row):
assert row is not None assert row is not None
invoice = self.parent.wallet.invoices.get(key) invoice = self.wallet.invoices.get(key)
if invoice is None: if invoice is None:
return return
model = self.std_model model = self.std_model
status_item = model.item(row, self.Columns.STATUS) status_item = model.item(row, self.Columns.STATUS)
status = self.parent.wallet.get_invoice_status(invoice) status = self.wallet.get_invoice_status(invoice)
status_str = invoice.get_status_str(status) status_str = invoice.get_status_str(status)
if self.parent.wallet.lnworker: if self.wallet.lnworker:
log = self.parent.wallet.lnworker.logs.get(key) log = self.wallet.lnworker.logs.get(key)
if log and status == PR_INFLIGHT: if log and status == PR_INFLIGHT:
status_str += '... (%d)'%len(log) status_str += '... (%d)'%len(log)
status_item.setText(status_str) status_item.setText(status_str)
@ -95,15 +102,15 @@ class InvoiceList(MyTreeView):
self.proxy.setDynamicSortFilter(False) # temp. disable re-sorting after every change self.proxy.setDynamicSortFilter(False) # temp. disable re-sorting after every change
self.std_model.clear() self.std_model.clear()
self.update_headers(self.__class__.headers) self.update_headers(self.__class__.headers)
for idx, item in enumerate(self.parent.wallet.get_unpaid_invoices()): for idx, item in enumerate(self.wallet.get_unpaid_invoices()):
key = self.parent.wallet.get_key_for_outgoing_invoice(item) key = self.wallet.get_key_for_outgoing_invoice(item)
if item.is_lightning(): if item.is_lightning():
icon_name = 'lightning.png' icon_name = 'lightning.png'
else: else:
icon_name = 'bitcoin.png' icon_name = 'bitcoin.png'
if item.bip70: if item.bip70:
icon_name = 'seal.png' icon_name = 'seal.png'
status = self.parent.wallet.get_invoice_status(item) status = self.wallet.get_invoice_status(item)
status_str = item.get_status_str(status) status_str = item.get_status_str(status)
message = item.message message = item.message
amount = item.get_amount_sat() amount = item.get_amount_sat()
@ -128,10 +135,10 @@ class InvoiceList(MyTreeView):
def hide_if_empty(self): def hide_if_empty(self):
b = self.std_model.rowCount() > 0 b = self.std_model.rowCount() > 0
self.setVisible(b) self.setVisible(b)
self.parent.invoices_label.setVisible(b) self.send_tab.invoices_label.setVisible(b)
def create_menu(self, position): def create_menu(self, position):
wallet = self.parent.wallet wallet = self.wallet
items = self.selected_in_column(0) items = self.selected_in_column(0)
if len(items)>1: if len(items)>1:
keys = [item.data(ROLE_REQUEST_ID) for item in items] keys = [item.data(ROLE_REQUEST_ID) for item in items]
@ -139,8 +146,8 @@ class InvoiceList(MyTreeView):
can_batch_pay = all([not i.is_lightning() and wallet.get_invoice_status(i) == PR_UNPAID for i in invoices]) can_batch_pay = all([not i.is_lightning() and wallet.get_invoice_status(i) == PR_UNPAID for i in invoices])
menu = QMenu(self) menu = QMenu(self)
if can_batch_pay: if can_batch_pay:
menu.addAction(_("Batch pay invoices") + "...", lambda: self.parent.pay_multiple_invoices(invoices)) menu.addAction(_("Batch pay invoices") + "...", lambda: self.send_tab.pay_multiple_invoices(invoices))
menu.addAction(_("Delete invoices"), lambda: self.parent.delete_invoices(keys)) menu.addAction(_("Delete invoices"), lambda: self.delete_invoices(keys))
menu.exec_(self.viewport().mapToGlobal(position)) menu.exec_(self.viewport().mapToGlobal(position))
return return
idx = self.indexAt(position) idx = self.indexAt(position)
@ -149,7 +156,7 @@ class InvoiceList(MyTreeView):
if not item or not item_col0: if not item or not item_col0:
return return
key = item_col0.data(ROLE_REQUEST_ID) key = item_col0.data(ROLE_REQUEST_ID)
invoice = self.parent.wallet.get_invoice(key) invoice = self.wallet.get_invoice(key)
menu = QMenu(self) menu = QMenu(self)
self.add_copy_menu(menu, idx) self.add_copy_menu(menu, idx)
if invoice.is_lightning(): if invoice.is_lightning():
@ -160,14 +167,14 @@ class InvoiceList(MyTreeView):
menu.addAction(_("Details"), lambda: self.parent.show_onchain_invoice(invoice)) menu.addAction(_("Details"), lambda: self.parent.show_onchain_invoice(invoice))
status = wallet.get_invoice_status(invoice) status = wallet.get_invoice_status(invoice)
if status == PR_UNPAID: if status == PR_UNPAID:
menu.addAction(_("Pay") + "...", lambda: self.parent.do_pay_invoice(invoice)) menu.addAction(_("Pay") + "...", lambda: self.send_tab.do_pay_invoice(invoice))
if status == PR_FAILED: if status == PR_FAILED:
menu.addAction(_("Retry"), lambda: self.parent.do_pay_invoice(invoice)) menu.addAction(_("Retry"), lambda: self.send_tab.do_pay_invoice(invoice))
if self.parent.wallet.lnworker: if self.wallet.lnworker:
log = self.parent.wallet.lnworker.logs.get(key) log = self.wallet.lnworker.logs.get(key)
if log: if log:
menu.addAction(_("View log"), lambda: self.show_log(key, log)) menu.addAction(_("View log"), lambda: self.show_log(key, log))
menu.addAction(_("Delete"), lambda: self.parent.delete_invoices([key])) menu.addAction(_("Delete"), lambda: self.delete_invoices([key]))
menu.exec_(self.viewport().mapToGlobal(position)) menu.exec_(self.viewport().mapToGlobal(position))
def show_log(self, key, log: Sequence[HtlcLog]): def show_log(self, key, log: Sequence[HtlcLog]):
@ -185,3 +192,8 @@ class InvoiceList(MyTreeView):
vbox.addWidget(log_w) vbox.addWidget(log_w)
vbox.addLayout(Buttons(CloseButton(d))) vbox.addLayout(Buttons(CloseButton(d)))
d.exec_() d.exec_()
def delete_invoices(self, keys):
for key in keys:
self.wallet.delete_invoice(key)
self.delete_item(key)

753
electrum/gui/qt/main_window.py

@ -99,6 +99,7 @@ from .util import (read_QIcon, ColorScheme, text_dialog, icon_path, WaitingDialo
TRANSACTION_FILE_EXTENSION_FILTER_ANY, MONOSPACE_FONT, TRANSACTION_FILE_EXTENSION_FILTER_ANY, MONOSPACE_FONT,
getOpenFileName, getSaveFileName, BlockingWaitingDialog) getOpenFileName, getSaveFileName, BlockingWaitingDialog)
from .util import ButtonsTextEdit, ButtonsLineEdit from .util import ButtonsTextEdit, ButtonsLineEdit
from .util import QtEventListener, qt_event_listener, event_listener
from .installwizard import WIF_HELP_TEXT from .installwizard import WIF_HELP_TEXT
from .history_list import HistoryList, HistoryModel from .history_list import HistoryList, HistoryModel
from .update_checker import UpdateCheck, UpdateCheckThread from .update_checker import UpdateCheck, UpdateCheckThread
@ -195,23 +196,13 @@ def protected(func):
return func(self, *args, **kwargs) return func(self, *args, **kwargs)
return request_password return request_password
from .util import QtEventListener, qt_event_listener, event_listener
class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
payment_request_ok_signal = pyqtSignal()
payment_request_error_signal = pyqtSignal()
lnurl6_round1_signal = pyqtSignal(object, object)
lnurl6_round2_signal = pyqtSignal(object)
clear_send_tab_signal = pyqtSignal()
#ln_payment_attempt_signal = pyqtSignal(str)
computing_privkeys_signal = pyqtSignal() computing_privkeys_signal = pyqtSignal()
show_privkeys_signal = pyqtSignal() show_privkeys_signal = pyqtSignal()
show_error_signal = pyqtSignal(str) show_error_signal = pyqtSignal(str)
payment_request: Optional[paymentrequest.PaymentRequest]
_lnurl_data: Optional[LNURL6Data] = None
def __init__(self, gui_object: 'ElectrumGui', wallet: Abstract_Wallet): def __init__(self, gui_object: 'ElectrumGui', wallet: Abstract_Wallet):
QMainWindow.__init__(self) QMainWindow.__init__(self)
self.gui_object = gui_object self.gui_object = gui_object
@ -230,14 +221,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
self.tray = gui_object.tray self.tray = gui_object.tray
self.app = gui_object.app self.app = gui_object.app
self._cleaned_up = False self._cleaned_up = False
self.payment_request = None # type: Optional[paymentrequest.PaymentRequest]
self.payto_URI = None
self.checking_accounts = False
self.qr_window = None self.qr_window = None
self.pluginsdialog = None self.pluginsdialog = None
self.showing_cert_mismatch_error = False self.showing_cert_mismatch_error = False
self.tl_windows = [] self.tl_windows = []
self.pending_invoice = None
Logger.__init__(self) Logger.__init__(self)
self._coroutines_scheduled = {} # type: Dict[concurrent.futures.Future, str] self._coroutines_scheduled = {} # type: Dict[concurrent.futures.Future, str]
@ -313,12 +300,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
self.app.update_status_signal.connect(self.update_status) self.app.update_status_signal.connect(self.update_status)
self.app.update_fiat_signal.connect(self.update_fiat) self.app.update_fiat_signal.connect(self.update_fiat)
self.payment_request_ok_signal.connect(self.payment_request_ok)
self.payment_request_error_signal.connect(self.payment_request_error)
self.lnurl6_round1_signal.connect(self.on_lnurl6_round1)
self.lnurl6_round2_signal.connect(self.on_lnurl6_round2)
self.clear_send_tab_signal.connect(self.do_clear)
self.show_error_signal.connect(self.show_error) self.show_error_signal.connect(self.show_error)
self.history_list.setFocus(True) self.history_list.setFocus(True)
@ -382,7 +363,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
def on_fx_quotes(self): def on_fx_quotes(self):
self.update_status() self.update_status()
# Refresh edits with the new rate # Refresh edits with the new rate
edit = self.fiat_send_e if self.fiat_send_e.is_last_edited else self.amount_e edit = self.send_tab.fiat_send_e if self.send_tab.fiat_send_e.is_last_edited else self.send_tab.amount_e
edit.textEdited.emit(edit.text()) edit.textEdited.emit(edit.text())
edit = self.fiat_receive_e if self.fiat_receive_e.is_last_edited else self.receive_amount_e edit = self.fiat_receive_e if self.fiat_receive_e.is_last_edited else self.receive_amount_e
edit.textEdited.emit(edit.text()) edit.textEdited.emit(edit.text())
@ -801,7 +782,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
tools_menu.addAction(_("&Encrypt/decrypt message"), self.encrypt_message) tools_menu.addAction(_("&Encrypt/decrypt message"), self.encrypt_message)
tools_menu.addSeparator() tools_menu.addSeparator()
paytomany_menu = tools_menu.addAction(_("&Pay to many"), self.paytomany) paytomany_menu = tools_menu.addAction(_("&Pay to many"), self.send_tab.paytomany)
tools_menu.addAction(_("&Show QR code in separate window"), self.toggle_qr_window) tools_menu.addAction(_("&Show QR code in separate window"), self.toggle_qr_window)
raw_transaction_menu = tools_menu.addMenu(_("&Load transaction")) raw_transaction_menu = tools_menu.addMenu(_("&Load transaction"))
@ -913,7 +894,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
def timer_actions(self): def timer_actions(self):
# refresh invoices and requests because they show ETA # refresh invoices and requests because they show ETA
self.request_list.refresh_all() self.request_list.refresh_all()
self.invoice_list.refresh_all() self.send_tab.invoice_list.refresh_all()
# Note this runs in the GUI thread # Note this runs in the GUI thread
if self.need_update.is_set(): if self.need_update.is_set():
self.need_update.clear() self.need_update.clear()
@ -923,7 +904,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
self.update_status() self.update_status()
# resolve aliases # resolve aliases
# FIXME this might do blocking network calls that has a timeout of several seconds # FIXME this might do blocking network calls that has a timeout of several seconds
self.payto_e.on_timer_check_text() self.send_tab.payto_e.on_timer_check_text()
self.notify_transactions() self.notify_transactions()
def format_amount(self, amount_sat, is_diff=False, whitespaces=False) -> str: def format_amount(self, amount_sat, is_diff=False, whitespaces=False) -> str:
@ -1085,7 +1066,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
self.history_model.refresh('update_tabs') self.history_model.refresh('update_tabs')
self.request_list.update() self.request_list.update()
self.update_current_request() self.update_current_request()
self.invoice_list.update() self.send_tab.invoice_list.update()
self.address_list.update() self.address_list.update()
self.utxo_list.update() self.utxo_list.update()
self.contact_list.update() self.contact_list.update()
@ -1095,7 +1076,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
def refresh_tabs(self, wallet=None): def refresh_tabs(self, wallet=None):
self.history_model.refresh('refresh_tabs') self.history_model.refresh('refresh_tabs')
self.request_list.refresh_all() self.request_list.refresh_all()
self.invoice_list.refresh_all() self.send_tab.invoice_list.refresh_all()
self.address_list.refresh_all() self.address_list.refresh_all()
self.utxo_list.refresh_all() self.utxo_list.refresh_all()
self.contact_list.refresh_all() self.contact_list.refresh_all()
@ -1167,7 +1148,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
grid.addWidget(self.fiat_receive_e, 1, 2, Qt.AlignLeft) grid.addWidget(self.fiat_receive_e, 1, 2, Qt.AlignLeft)
self.connect_fields(self, self.receive_amount_e, self.fiat_receive_e, None) self.connect_fields(self, self.receive_amount_e, self.fiat_receive_e, None)
self.connect_fields(self, self.amount_e, self.fiat_send_e, None) self.connect_fields(self, self.send_tab.amount_e, self.send_tab.fiat_send_e, None)
self.expires_combo = QComboBox() self.expires_combo = QComboBox()
evl = sorted(pr_expiration_values.items()) evl = sorted(pr_expiration_values.items())
@ -1525,146 +1506,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
self.receive_address_e.setToolTip("") self.receive_address_e.setToolTip("")
def create_send_tab(self): def create_send_tab(self):
# A 4-column grid layout. All the stretch is in the last column. from .send_tab import SendTab
# The exchange rate plugin adds a fiat widget in column 2 return SendTab(self)
self.send_grid = grid = QGridLayout()
grid.setSpacing(8)
grid.setColumnStretch(3, 1)
from .paytoedit import PayToEdit
self.amount_e = BTCAmountEdit(self.get_decimal_point)
self.payto_e = PayToEdit(self)
msg = (_("Recipient of the funds.") + "\n\n"
+ _("You may enter a Bitcoin address, a label from your list of contacts "
"(a list of completions will be proposed), "
"or an alias (email-like address that forwards to a Bitcoin address)") + ". "
+ _("Lightning invoices are also supported.") + "\n\n"
+ _("You can also pay to many outputs in a single transaction, "
"specifying one output per line.") + "\n" + _("Format: address, amount") + "\n"
+ _("To set the amount to 'max', use the '!' special character.") + "\n"
+ _("Integers weights can also be used in conjunction with '!', "
"e.g. set one amount to '2!' and another to '3!' to split your coins 40-60."))
payto_label = HelpLabel(_('Pay to'), msg)
grid.addWidget(payto_label, 1, 0)
grid.addWidget(self.payto_e, 1, 1, 1, -1)
completer = QCompleter()
completer.setCaseSensitivity(False)
self.payto_e.set_completer(completer)
completer.setModel(self.completions)
msg = _('Description of the transaction (not mandatory).') + '\n\n'\
+ _('The description is not sent to the recipient of the funds. It is stored in your wallet file, and displayed in the \'History\' tab.')
description_label = HelpLabel(_('Description'), msg)
grid.addWidget(description_label, 2, 0)
self.message_e = SizedFreezableLineEdit(width=700)
grid.addWidget(self.message_e, 2, 1, 1, -1)
msg = (_('The amount to be received by the recipient.') + ' '
+ _('Fees are paid by the sender.') + '\n\n'
+ _('The amount will be displayed in red if you do not have enough funds in your wallet.') + ' '
+ _('Note that if you have frozen some of your addresses, the available funds will be lower than your total balance.') + '\n\n'
+ _('Keyboard shortcut: type "!" to send all your coins.'))
amount_label = HelpLabel(_('Amount'), msg)
grid.addWidget(amount_label, 3, 0)
grid.addWidget(self.amount_e, 3, 1)
self.fiat_send_e = AmountEdit(self.fx.get_currency if self.fx else '')
if not self.fx or not self.fx.is_enabled():
self.fiat_send_e.setVisible(False)
grid.addWidget(self.fiat_send_e, 3, 2)
self.amount_e.frozen.connect(
lambda: self.fiat_send_e.setFrozen(self.amount_e.isReadOnly()))
self.max_button = EnterButton(_("Max"), self.spend_max)
self.max_button.setFixedWidth(100)
self.max_button.setCheckable(True)
grid.addWidget(self.max_button, 3, 3)
self.save_button = EnterButton(_("Save"), self.do_save_invoice)
self.send_button = EnterButton(_("Pay") + "...", self.do_pay_or_get_invoice)
self.clear_button = EnterButton(_("Clear"), self.do_clear)
buttons = QHBoxLayout()
buttons.addStretch(1)
buttons.addWidget(self.clear_button)
buttons.addWidget(self.save_button)
buttons.addWidget(self.send_button)
grid.addLayout(buttons, 6, 1, 1, 4)
self.amount_e.shortcut.connect(self.spend_max)
def reset_max(text):
self.max_button.setChecked(False)
enable = not bool(text) and not self.amount_e.isReadOnly()
#self.max_button.setEnabled(enable)
self.amount_e.textEdited.connect(reset_max)
self.fiat_send_e.textEdited.connect(reset_max)
self.set_onchain(False)
self.invoices_label = QLabel(_('Send queue'))
from .invoice_list import InvoiceList
self.invoice_list = InvoiceList(self)
vbox0 = QVBoxLayout()
vbox0.addLayout(grid)
hbox = QHBoxLayout()
hbox.addLayout(vbox0)
hbox.addStretch(1)
w = QWidget()
vbox = QVBoxLayout(w)
vbox.addLayout(hbox)
vbox.addStretch(1)
vbox.addWidget(self.invoices_label)
vbox.addWidget(self.invoice_list)
vbox.setStretchFactor(self.invoice_list, 1000)
w.searchable_list = self.invoice_list
self.invoice_list.update() # after parented and put into a layout, can update without flickering
run_hook('create_send_tab', grid)
return w
def spend_max(self):
if run_hook('abort_send', self):
return
outputs = self.payto_e.get_outputs(True)
if not outputs:
return
make_tx = lambda fee_est: self.wallet.make_unsigned_transaction(
coins=self.get_coins(),
outputs=outputs,
fee=fee_est,
is_sweep=False)
try:
try:
tx = make_tx(None)
except (NotEnoughFunds, NoDynamicFeeEstimates) as e:
# Check if we had enough funds excluding fees,
# if so, still provide opportunity to set lower fees.
tx = make_tx(0)
except NotEnoughFunds as e:
self.max_button.setChecked(False)
text = self.get_text_not_enough_funds_mentioning_frozen()
self.show_error(text)
return
self.max_button.setChecked(True)
amount = tx.output_value()
__, x_fee_amount = run_hook('get_tx_extra_fee', self.wallet, tx) or (None, 0)
amount_after_all_fees = amount - x_fee_amount
self.amount_e.setAmount(amount_after_all_fees)
# show tooltip explaining max amount
mining_fee = tx.get_fee()
mining_fee_str = self.format_amount_and_units(mining_fee)
msg = _("Mining fee: {} (can be adjusted on next screen)").format(mining_fee_str)
if x_fee_amount:
twofactor_fee_str = self.format_amount_and_units(x_fee_amount)
msg += "\n" + _("2fa fee: {} (for the next batch of transactions)").format(twofactor_fee_str)
frozen_bal = self.get_frozen_balance_str()
if frozen_bal:
msg += "\n" + _("Some coins are frozen: {} (can be unfrozen in the Addresses or in the Coins tab)").format(frozen_bal)
QToolTip.showText(self.max_button.mapToGlobal(QPoint(0, 0)), msg)
def get_contact_payto(self, key): def get_contact_payto(self, key):
_type, label = self.contacts.get(key) _type, label = self.contacts.get(key)
@ -1678,136 +1521,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
def protect(self, func, args, password): def protect(self, func, args, password):
return func(*args, password) return func(*args, password)
def read_outputs(self) -> List[PartialTxOutput]:
if self.payment_request:
outputs = self.payment_request.get_outputs()
else:
outputs = self.payto_e.get_outputs(self.max_button.isChecked())
return outputs
def check_send_tab_onchain_outputs_and_show_errors(self, outputs: List[PartialTxOutput]) -> bool:
"""Returns whether there are errors with outputs.
Also shows error dialog to user if so.
"""
if not outputs:
self.show_error(_('No outputs'))
return True
for o in outputs:
if o.scriptpubkey is None:
self.show_error(_('Bitcoin Address is None'))
return True
if o.value is None:
self.show_error(_('Invalid Amount'))
return True
return False # no errors
def check_send_tab_payto_line_and_show_errors(self) -> bool:
"""Returns whether there are errors.
Also shows error dialog to user if so.
"""
pr = self.payment_request
if pr:
if pr.has_expired():
self.show_error(_('Payment request has expired'))
return True
if not pr:
errors = self.payto_e.get_errors()
if errors:
if len(errors) == 1 and not errors[0].is_multiline:
err = errors[0]
self.show_warning(_("Failed to parse 'Pay to' line") + ":\n" +
f"{err.line_content[:40]}...\n\n"
f"{err.exc!r}")
else:
self.show_warning(_("Invalid Lines found:") + "\n\n" +
'\n'.join([_("Line #") +
f"{err.idx+1}: {err.line_content[:40]}... ({err.exc!r})"
for err in errors]))
return True
if self.payto_e.is_alias and self.payto_e.validated is False:
alias = self.payto_e.toPlainText()
msg = _('WARNING: the alias "{}" could not be validated via an additional '
'security check, DNSSEC, and thus may not be correct.').format(alias) + '\n'
msg += _('Do you wish to continue?')
if not self.question(msg):
return True
return False # no errors
def pay_lightning_invoice(self, invoice: Invoice):
amount_sat = invoice.get_amount_sat()
key = self.wallet.get_key_for_outgoing_invoice(invoice)
if amount_sat is None:
raise Exception("missing amount for LN invoice")
if not self.wallet.lnworker.can_pay_invoice(invoice):
num_sats_can_send = int(self.wallet.lnworker.num_sats_can_send())
lightning_needed = amount_sat - num_sats_can_send
lightning_needed += (lightning_needed // 20) # operational safety margin
coins = self.get_coins(nonlocal_only=True)
can_pay_onchain = invoice.get_address() and self.wallet.can_pay_onchain(invoice.get_outputs(), coins=coins)
can_pay_with_new_channel = self.wallet.lnworker.suggest_funding_amount(amount_sat, coins=coins)
can_pay_with_swap = self.wallet.lnworker.suggest_swap_to_send(amount_sat, coins=coins)
rebalance_suggestion = self.wallet.lnworker.suggest_rebalance_to_send(amount_sat)
can_rebalance = bool(rebalance_suggestion) and self.num_tasks() == 0
choices = {}
if can_rebalance:
msg = ''.join([
_('Rebalance existing channels'), '\n',
_('Move funds between your channels in order to increase your sending capacity.')
])
choices[0] = msg
if can_pay_with_new_channel:
msg = ''.join([
_('Open a new channel'), '\n',
_('You will be able to pay once the channel is open.')
])
choices[1] = msg
if can_pay_with_swap:
msg = ''.join([
_('Swap onchain funds for lightning funds'), '\n',
_('You will be able to pay once the swap is confirmed.')
])
choices[2] = msg
if can_pay_onchain:
msg = ''.join([
_('Pay onchain'), '\n',
_('Funds will be sent to the invoice fallback address.')
])
choices[3] = msg
if not choices:
raise NotEnoughFunds()
msg = _('You cannot pay that invoice using Lightning.')
if self.wallet.lnworker.channels:
msg += '\n' + _('Your channels can send {}.').format(self.format_amount(num_sats_can_send) + self.base_unit())
r = self.query_choice(msg, choices)
if r is not None:
self.save_pending_invoice()
if r == 0:
chan1, chan2, delta = rebalance_suggestion
self.rebalance_dialog(chan1, chan2, amount_sat=delta)
elif r == 1:
amount_sat, min_amount_sat = can_pay_with_new_channel
self.channels_list.new_channel_dialog(amount_sat=amount_sat, min_amount_sat=min_amount_sat)
elif r == 2:
chan, swap_recv_amount_sat = can_pay_with_swap
self.run_swap_dialog(is_reverse=False, recv_amount_sat=swap_recv_amount_sat, channels=[chan])
elif r == 3:
self.pay_onchain_dialog(coins, invoice.get_outputs())
return
# FIXME this is currently lying to user as we truncate to satoshis
amount_msat = invoice.get_amount_msat()
msg = _("Pay lightning invoice?") + '\n\n' + _("This will send {}?").format(self.format_amount_and_units(Decimal(amount_msat)/1000))
if not self.question(msg):
return
self.save_pending_invoice()
coro = self.wallet.lnworker.pay_invoice(invoice.lightning_invoice, amount_msat=amount_msat)
self.run_coroutine_from_thread(coro, _('Sending payment'))
def run_swap_dialog(self, is_reverse=None, recv_amount_sat=None, channels=None): def run_swap_dialog(self, is_reverse=None, recv_amount_sat=None, channels=None):
if not self.network: if not self.network:
self.show_error(_("You are offline.")) self.show_error(_("You are offline."))
@ -1848,9 +1561,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
return return
status = self.wallet.get_invoice_status(invoice) status = self.wallet.get_invoice_status(invoice)
if status == PR_PAID: if status == PR_PAID:
self.invoice_list.delete_item(key) self.send_tab.invoice_list.delete_item(key)
else: else:
self.invoice_list.refresh_item(key) self.send_tab.invoice_list.refresh_item(key)
@qt_event_listener @qt_event_listener
def on_event_payment_succeeded(self, wallet, key): def on_event_payment_succeeded(self, wallet, key):
@ -1868,111 +1581,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
invoice = self.wallet.get_invoice(key) invoice = self.wallet.get_invoice(key)
if invoice and invoice.is_lightning() and invoice.get_address(): if invoice and invoice.is_lightning() and invoice.get_address():
if self.question(_('Payment failed') + '\n\n' + reason + '\n\n'+ 'Fallback to onchain payment?'): if self.question(_('Payment failed') + '\n\n' + reason + '\n\n'+ 'Fallback to onchain payment?'):
self.pay_onchain_dialog(self.get_coins(), invoice.get_outputs()) self.send_tab.pay_onchain_dialog(self.get_coins(), invoice.get_outputs())
else: else:
self.show_error(_('Payment failed') + '\n\n' + reason) self.show_error(_('Payment failed') + '\n\n' + reason)
def read_invoice(self):
if self.check_send_tab_payto_line_and_show_errors():
return
try:
if not self._is_onchain:
invoice_str = self.payto_e.lightning_invoice
if not invoice_str:
return
if not self.wallet.has_lightning():
self.show_error(_('Lightning is disabled'))
return
invoice = Invoice.from_bech32(invoice_str)
if invoice.amount_msat is None:
amount_sat = self.amount_e.get_amount()
if amount_sat:
invoice.amount_msat = int(amount_sat * 1000)
else:
self.show_error(_('No amount'))
return
return invoice
else:
outputs = self.read_outputs()
if self.check_send_tab_onchain_outputs_and_show_errors(outputs):
return
message = self.message_e.text()
return self.wallet.create_invoice(
outputs=outputs,
message=message,
pr=self.payment_request,
URI=self.payto_URI)
except InvoiceError as e:
self.show_error(_('Error creating payment') + ':\n' + str(e))
def do_save_invoice(self):
self.pending_invoice = self.read_invoice()
if not self.pending_invoice:
return
self.save_pending_invoice()
def save_pending_invoice(self):
if not self.pending_invoice:
return
self.do_clear()
self.wallet.save_invoice(self.pending_invoice)
self.invoice_list.update()
self.pending_invoice = None
def _lnurl_get_invoice(self) -> None:
assert self._lnurl_data
amount = self.amount_e.get_amount()
if not (self._lnurl_data.min_sendable_sat <= amount <= self._lnurl_data.max_sendable_sat):
self.show_error(f'Amount must be between {self._lnurl_data.min_sendable_sat} and {self._lnurl_data.max_sendable_sat} sat.')
return
async def f():
try:
invoice_data = await callback_lnurl(
self._lnurl_data.callback_url,
params={'amount': self.amount_e.get_amount() * 1000},
)
except LNURLError as e:
self.show_error_signal.emit(f"LNURL request encountered error: {e}")
self.clear_send_tab_signal.emit()
return
invoice = invoice_data.get('pr')
self.lnurl6_round2_signal.emit(invoice)
asyncio.run_coroutine_threadsafe(f(), get_asyncio_loop()) # TODO should be cancellable
self.prepare_for_send_tab_network_lookup()
def on_lnurl6_round2(self, bolt11_invoice: str):
self.set_bolt11(bolt11_invoice)
self.payto_e.setFrozen(True)
self.amount_e.setEnabled(False)
self.fiat_send_e.setEnabled(False)
for btn in [self.send_button, self.clear_button, self.save_button]:
btn.setEnabled(True)
self.send_button.restore_original_text()
self._lnurl_data = None
def do_pay_or_get_invoice(self):
if self._lnurl_data:
self._lnurl_get_invoice()
return
self.pending_invoice = self.read_invoice()
if not self.pending_invoice:
return
self.do_pay_invoice(self.pending_invoice)
def pay_multiple_invoices(self, invoices):
outputs = []
for invoice in invoices:
outputs += invoice.outputs
self.pay_onchain_dialog(self.get_coins(), outputs)
def do_pay_invoice(self, invoice: 'Invoice'):
if invoice.is_lightning():
self.pay_lightning_invoice(invoice)
else:
self.pay_onchain_dialog(self.get_coins(), invoice.outputs)
def get_coins(self, *, nonlocal_only=False) -> Sequence[PartialTxInput]: def get_coins(self, *, nonlocal_only=False) -> Sequence[PartialTxInput]:
coins = self.get_manually_selected_coins() coins = self.get_manually_selected_coins()
if coins is not None: if coins is not None:
@ -1987,76 +1599,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
""" """
return self.utxo_list.get_spend_list() return self.utxo_list.get_spend_list()
def get_text_not_enough_funds_mentioning_frozen(self) -> str:
text = _("Not enough funds")
frozen_str = self.get_frozen_balance_str()
if frozen_str:
text += " ({} {})".format(
frozen_str, _("are frozen")
)
return text
def get_frozen_balance_str(self) -> Optional[str]:
frozen_bal = sum(self.wallet.get_frozen_balance())
if not frozen_bal:
return None
return self.format_amount_and_units(frozen_bal)
def pay_onchain_dialog(
self, inputs: Sequence[PartialTxInput],
outputs: List[PartialTxOutput], *,
external_keypairs=None) -> None:
# trustedcoin requires this
if run_hook('abort_send', self):
return
is_sweep = bool(external_keypairs)
make_tx = lambda fee_est: self.wallet.make_unsigned_transaction(
coins=inputs,
outputs=outputs,
fee=fee_est,
is_sweep=is_sweep)
output_values = [x.value for x in outputs]
if any(parse_max_spend(outval) for outval in output_values):
output_value = '!'
else:
output_value = sum(output_values)
conf_dlg = ConfirmTxDialog(window=self, make_tx=make_tx, output_value=output_value, is_sweep=is_sweep)
if conf_dlg.not_enough_funds:
# Check if we had enough funds excluding fees,
# if so, still provide opportunity to set lower fees.
if not conf_dlg.have_enough_funds_assuming_zero_fees():
text = self.get_text_not_enough_funds_mentioning_frozen()
self.show_message(text)
return
# shortcut to advanced preview (after "enough funds" check!)
if self.config.get('advanced_preview'):
preview_dlg = PreviewTxDialog(
window=self,
make_tx=make_tx,
external_keypairs=external_keypairs,
output_value=output_value)
preview_dlg.show()
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.broadcast_or_show(tx)
self.sign_tx_with_password(tx, callback=sign_done, password=password,
external_keypairs=external_keypairs)
else:
preview_dlg = PreviewTxDialog(
window=self,
make_tx=make_tx,
external_keypairs=external_keypairs,
output_value=output_value)
preview_dlg.show()
def broadcast_or_show(self, tx: Transaction): def broadcast_or_show(self, tx: Transaction):
if not tx.is_complete(): if not tx.is_complete():
self.show_transaction(tx) self.show_transaction(tx)
@ -2067,6 +1609,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
return return
self.broadcast_transaction(tx) self.broadcast_transaction(tx)
def broadcast_transaction(self, tx: Transaction):
self.send_tab.broadcast_transaction(tx)
@protected @protected
def sign_tx(self, tx, *, callback, external_keypairs, password): def sign_tx(self, tx, *, callback, external_keypairs, password):
self.sign_tx_with_password(tx, callback=callback, password=password, external_keypairs=external_keypairs) self.sign_tx_with_password(tx, callback=callback, password=password, external_keypairs=external_keypairs)
@ -2089,48 +1634,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
msg = _('Signing transaction...') msg = _('Signing transaction...')
WaitingDialog(self, msg, task, on_success, on_failure) WaitingDialog(self, msg, task, on_success, on_failure)
def broadcast_transaction(self, tx: Transaction):
def broadcast_thread():
# non-GUI thread
pr = self.payment_request
if pr and pr.has_expired():
self.payment_request = None
return False, _("Invoice has expired")
try:
self.network.run_from_another_thread(self.network.broadcast_transaction(tx))
except TxBroadcastError as e:
return False, e.get_message_for_gui()
except BestEffortRequestFailed as e:
return False, repr(e)
# success
txid = tx.txid()
if pr:
self.payment_request = None
refund_address = self.wallet.get_receiving_address()
coro = pr.send_payment_and_receive_paymentack(tx.serialize(), refund_address)
fut = asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop)
ack_status, ack_msg = fut.result(timeout=20)
self.logger.info(f"Payment ACK: {ack_status}. Ack message: {ack_msg}")
return True, txid
# Capture current TL window; override might be removed on return
parent = self.top_level_window(lambda win: isinstance(win, MessageBoxMixin))
def broadcast_done(result):
# GUI thread
if result:
success, msg = result
if success:
parent.show_message(_('Payment sent.') + '\n' + msg)
self.invoice_list.update()
else:
msg = msg or ''
parent.show_error(msg)
WaitingDialog(self, _('Broadcasting transaction...'),
broadcast_thread, broadcast_done, self.on_error)
def mktx_for_open_channel(self, *, funding_sat, node_id): def mktx_for_open_channel(self, *, funding_sat, node_id):
coins = self.get_coins(nonlocal_only=True) coins = self.get_coins(nonlocal_only=True)
make_tx = lambda fee_est: self.wallet.lnworker.mktx_for_open_channel( make_tx = lambda fee_est: self.wallet.lnworker.mktx_for_open_channel(
@ -2212,199 +1715,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
return None return None
return clayout.selected_index() return clayout.selected_index()
def lock_amount(self, b: bool) -> None: def handle_payment_identifier(self, *args, **kwargs):
self.amount_e.setFrozen(b) self.send_tab.handle_payment_identifier(*args, **kwargs)
self.max_button.setEnabled(not b)
def prepare_for_send_tab_network_lookup(self):
self.show_send_tab()
self.payto_e.disable_checks = True
for e in [self.payto_e, self.message_e]:
e.setFrozen(True)
self.lock_amount(True)
for btn in [self.save_button, self.send_button, self.clear_button]:
btn.setEnabled(False)
self.payto_e.setTextNoCheck(_("please wait..."))
def delete_invoices(self, keys):
for key in keys:
self.wallet.delete_invoice(key)
self.invoice_list.delete_item(key)
def payment_request_ok(self):
pr = self.payment_request
if not pr:
return
invoice = Invoice.from_bip70_payreq(pr, height=0)
if self.wallet.get_invoice_status(invoice) == PR_PAID:
self.show_message("invoice already paid")
self.do_clear()
self.payment_request = None
return
self.payto_e.disable_checks = True
if not pr.has_expired():
self.payto_e.setGreen()
else:
self.payto_e.setExpired()
self.payto_e.setTextNoCheck(pr.get_requestor())
self.amount_e.setAmount(pr.get_amount())
self.message_e.setText(pr.get_memo())
self.set_onchain(True)
self.max_button.setEnabled(False)
for btn in [self.send_button, self.clear_button]:
btn.setEnabled(True)
# signal to set fee
self.amount_e.textEdited.emit("")
def payment_request_error(self):
pr = self.payment_request
if not pr:
return
self.show_message(pr.error)
self.payment_request = None
self.do_clear()
def on_pr(self, request: 'paymentrequest.PaymentRequest'):
self.payment_request = request
if self.payment_request.verify(self.contacts):
self.payment_request_ok_signal.emit()
else:
self.payment_request_error_signal.emit()
def set_lnurl6(self, lnurl: str, *, can_use_network: bool = True):
try:
url = decode_lnurl(lnurl)
except LnInvoiceException as e:
self.show_error(_("Error parsing Lightning invoice") + f":\n{e}")
return
if not can_use_network:
return
async def f():
try:
lnurl_data = await request_lnurl(url)
except LNURLError as e:
self.show_error_signal.emit(f"LNURL request encountered error: {e}")
self.clear_send_tab_signal.emit()
return
self.lnurl6_round1_signal.emit(lnurl_data, url)
asyncio.run_coroutine_threadsafe(f(), get_asyncio_loop()) # TODO should be cancellable
self.prepare_for_send_tab_network_lookup()
def on_lnurl6_round1(self, lnurl_data: LNURL6Data, url: str):
self._lnurl_data = lnurl_data
domain = urlparse(url).netloc
self.payto_e.setTextNoCheck(f"invoice from lnurl")
self.message_e.setText(f"lnurl: {domain}: {lnurl_data.metadata_plaintext}")
self.amount_e.setAmount(lnurl_data.min_sendable_sat)
self.amount_e.setFrozen(False)
self.send_button.setText(_('Get Invoice'))
for btn in [self.send_button, self.clear_button]:
btn.setEnabled(True)
self.set_onchain(False)
def set_bolt11(self, invoice: str):
"""Parse ln invoice, and prepare the send tab for it."""
try:
lnaddr = lndecode(invoice)
except LnInvoiceException as e:
self.show_error(_("Error parsing Lightning invoice") + f":\n{e}")
return
pubkey = bh2u(lnaddr.pubkey.serialize())
for k,v in lnaddr.tags:
if k == 'd':
description = v
break
else:
description = ''
self.payto_e.setFrozen(True)
self.payto_e.setTextNoCheck(pubkey)
self.payto_e.lightning_invoice = invoice
if not self.message_e.text():
self.message_e.setText(description)
if lnaddr.get_amount_sat() is not None:
self.amount_e.setAmount(lnaddr.get_amount_sat())
self.set_onchain(False)
def set_onchain(self, b):
self._is_onchain = b
self.max_button.setEnabled(b)
def set_bip21(self, text: str, *, can_use_network: bool = True):
on_bip70_pr = self.on_pr if can_use_network else None
try:
out = util.parse_URI(text, on_bip70_pr)
except InvalidBitcoinURI as e:
self.show_error(_("Error parsing URI") + f":\n{e}")
return
self.payto_URI = out
r = out.get('r')
sig = out.get('sig')
name = out.get('name')
if (r or (name and sig)) and can_use_network:
self.prepare_for_send_tab_network_lookup()
return
address = out.get('address')
amount = out.get('amount')
label = out.get('label')
message = out.get('message')
lightning = out.get('lightning')
if lightning:
self.handle_payment_identifier(lightning, can_use_network=can_use_network)
return
# use label as description (not BIP21 compliant)
if label and not message:
message = label
if address:
self.payto_e.setText(address)
if message:
self.message_e.setText(message)
if amount:
self.amount_e.setAmount(amount)
def handle_payment_identifier(self, text: str, *, can_use_network: bool = True):
"""Takes
Lightning identifiers:
* lightning-URI (containing bolt11 or lnurl)
* bolt11 invoice
* lnurl
Bitcoin identifiers:
* bitcoin-URI
and sets the sending screen.
"""
text = text.strip()
if not text:
return
if invoice_or_lnurl := maybe_extract_lightning_payment_identifier(text):
if invoice_or_lnurl.startswith('lnurl'):
self.set_lnurl6(invoice_or_lnurl, can_use_network=can_use_network)
else:
self.set_bolt11(invoice_or_lnurl)
elif text.lower().startswith(util.BITCOIN_BIP21_URI_SCHEME + ':'):
self.set_bip21(text, can_use_network=can_use_network)
else:
raise ValueError("Could not handle payment identifier.")
# update fiat amount
self.amount_e.textEdited.emit("")
self.show_send_tab()
def do_clear(self):
self._lnurl_data = None
self.send_button.restore_original_text()
self.max_button.setChecked(False)
self.payment_request = None
self.payto_URI = None
self.payto_e.do_clear()
self.set_onchain(False)
for e in [self.message_e, self.amount_e]:
e.setText('')
e.setFrozen(False)
for e in [self.send_button, self.save_button, self.clear_button, self.amount_e, self.fiat_send_e]:
e.setEnabled(True)
self.update_status()
run_hook('do_clear', self)
def set_frozen_state_of_addresses(self, addrs, freeze: bool): def set_frozen_state_of_addresses(self, addrs, freeze: bool):
self.wallet.set_frozen_state_of_addresses(addrs, freeze) self.wallet.set_frozen_state_of_addresses(addrs, freeze)
@ -2460,27 +1772,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
self.need_update.set() # history, addresses, coins self.need_update.set() # history, addresses, coins
self.clear_receive_tab() self.clear_receive_tab()
def paytomany(self):
self.show_send_tab()
self.payto_e.paytomany()
msg = '\n'.join([
_('Enter a list of outputs in the \'Pay to\' field.'),
_('One output per line.'),
_('Format: address, amount'),
_('You may load a CSV file using the file icon.')
])
self.show_message(msg, title=_('Pay to many'))
def payto_contacts(self, labels): def payto_contacts(self, labels):
paytos = [self.get_contact_payto(label) for label in labels] self.send_tab.payto_contacts(labels)
self.show_send_tab()
if len(paytos) == 1:
self.payto_e.setText(paytos[0])
self.amount_e.setFocus()
else:
text = "\n".join([payto + ", 0" for payto in paytos])
self.payto_e.setText(text)
self.payto_e.setFocus()
def set_contact(self, label, address): def set_contact(self, label, address):
if not is_address(address): if not is_address(address):
@ -3424,7 +2717,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
export_meta_gui(self, _('labels'), self.wallet.export_labels) export_meta_gui(self, _('labels'), self.wallet.export_labels)
def import_invoices(self): def import_invoices(self):
import_meta_gui(self, _('invoices'), self.wallet.import_invoices, self.invoice_list.update) import_meta_gui(self, _('invoices'), self.wallet.import_invoices, self.send_tab.invoice_list.update)
def export_invoices(self): def export_invoices(self):
export_meta_gui(self, _('invoices'), self.wallet.export_invoices) export_meta_gui(self, _('invoices'), self.wallet.export_invoices)
@ -3506,7 +2799,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
coins, keypairs = result coins, keypairs = result
outputs = [PartialTxOutput.from_address_and_value(addr, value='!')] outputs = [PartialTxOutput.from_address_and_value(addr, value='!')]
self.warn_if_watching_only() self.warn_if_watching_only()
self.pay_onchain_dialog(coins, outputs, external_keypairs=keypairs) self.send_tab.pay_onchain_dialog(coins, outputs, external_keypairs=keypairs)
def on_failure(exc_info): def on_failure(exc_info):
self.on_error(exc_info) self.on_error(exc_info)
msg = _('Preparing sweep transaction...') msg = _('Preparing sweep transaction...')
@ -3557,14 +2850,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
self._do_import(title, header_layout, lambda x: self.wallet.import_private_keys(x, password)) self._do_import(title, header_layout, lambda x: self.wallet.import_private_keys(x, password))
def refresh_amount_edits(self): def refresh_amount_edits(self):
edits = self.amount_e, self.receive_amount_e edits = self.send_tab.amount_e, self.receive_amount_e
amounts = [edit.get_amount() for edit in edits] amounts = [edit.get_amount() for edit in edits]
for edit, amount in zip(edits, amounts): for edit, amount in zip(edits, amounts):
edit.setAmount(amount) edit.setAmount(amount)
def update_fiat(self): def update_fiat(self):
b = self.fx and self.fx.is_enabled() b = self.fx and self.fx.is_enabled()
self.fiat_send_e.setVisible(b) self.send_tab.fiat_send_e.setVisible(b)
self.fiat_receive_e.setVisible(b) self.fiat_receive_e.setVisible(b)
self.history_model.refresh('update_fiat') self.history_model.refresh('update_fiat')
self.history_list.update() self.history_list.update()

31
electrum/gui/qt/paytoedit.py

@ -29,6 +29,7 @@ from decimal import Decimal
from typing import NamedTuple, Sequence, Optional, List, TYPE_CHECKING from typing import NamedTuple, Sequence, Optional, List, TYPE_CHECKING
from PyQt5.QtGui import QFontMetrics, QFont from PyQt5.QtGui import QFontMetrics, QFont
from PyQt5.QtWidgets import QApplication
from electrum import bitcoin from electrum import bitcoin
from electrum.util import bfh, parse_max_spend from electrum.util import bfh, parse_max_spend
@ -44,6 +45,7 @@ from .util import MONOSPACE_FONT
if TYPE_CHECKING: if TYPE_CHECKING:
from .main_window import ElectrumWindow from .main_window import ElectrumWindow
from .send_tab import SendTab
RE_ALIAS = r'(.*?)\s*\<([0-9A-Za-z]{1,})\>' RE_ALIAS = r'(.*?)\s*\<([0-9A-Za-z]{1,})\>'
@ -61,13 +63,14 @@ class PayToLineError(NamedTuple):
class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger): class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
def __init__(self, win: 'ElectrumWindow'): def __init__(self, send_tab: 'SendTab'):
CompletionTextEdit.__init__(self) CompletionTextEdit.__init__(self)
ScanQRTextEdit.__init__(self, config=win.config, setText=self._on_input_btn) ScanQRTextEdit.__init__(self, config=send_tab.config, setText=self._on_input_btn)
Logger.__init__(self) Logger.__init__(self)
self.win = win self.send_tab = send_tab
self.app = win.app self.win = send_tab.window
self.amount_edit = win.amount_e self.app = QApplication.instance()
self.amount_edit = self.send_tab.amount_e
self.setFont(QFont(MONOSPACE_FONT)) self.setFont(QFont(MONOSPACE_FONT))
document = self.document() document = self.document()
document.contentsChanged.connect(self.update_size) document.contentsChanged.connect(self.update_size)
@ -205,10 +208,10 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
if len(lines) == 1: if len(lines) == 1:
data = lines[0] data = lines[0]
try: try:
self.win.handle_payment_identifier(data, can_use_network=full_check) self.send_tab.handle_payment_identifier(data, can_use_network=full_check)
except LNURLError as e: except LNURLError as e:
self.logger.exception("") self.logger.exception("")
self.win.show_error(e) self.send_tab.show_error(e)
except ValueError: except ValueError:
pass pass
else: else:
@ -226,8 +229,8 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
except Exception as e: except Exception as e:
self.errors.append(PayToLineError(line_content=data, exc=e)) self.errors.append(PayToLineError(line_content=data, exc=e))
else: else:
self.win.set_onchain(True) self.send_tab.set_onchain(True)
self.win.lock_amount(False) self.send_tab.lock_amount(False)
return return
if full_check: # network requests # FIXME blocking GUI thread if full_check: # network requests # FIXME blocking GUI thread
# try openalias # try openalias
@ -259,17 +262,17 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
else: else:
total += output.value total += output.value
if outputs: if outputs:
self.win.set_onchain(True) self.send_tab.set_onchain(True)
self.win.max_button.setChecked(is_max) self.send_tab.max_button.setChecked(is_max)
self.outputs = outputs self.outputs = outputs
self.payto_scriptpubkey = None self.payto_scriptpubkey = None
if self.win.max_button.isChecked(): if self.send_tab.max_button.isChecked():
self.win.spend_max() self.send_tab.spend_max()
else: else:
self.amount_edit.setAmount(total if outputs else None) self.amount_edit.setAmount(total if outputs else None)
self.win.lock_amount(self.win.max_button.isChecked() or bool(outputs)) self.send_tab.lock_amount(self.send_tab.max_button.isChecked() or bool(outputs))
def get_errors(self) -> Sequence[PayToLineError]: def get_errors(self) -> Sequence[PayToLineError]:
return self.errors return self.errors

773
electrum/gui/qt/send_tab.py

@ -0,0 +1,773 @@
# Copyright (C) 2022 The Electrum developers
# Distributed under the MIT software license, see the accompanying
# file LICENCE or http://www.opensource.org/licenses/mit-license.php
import asyncio
from decimal import Decimal
from typing import Optional, TYPE_CHECKING, Sequence, List
from urllib.parse import urlparse
from PyQt5.QtCore import pyqtSignal, QPoint
from PyQt5.QtWidgets import (QLabel, QVBoxLayout, QGridLayout,
QHBoxLayout, QCompleter, QWidget, QToolTip)
from electrum import util, paymentrequest
from electrum.plugin import run_hook
from electrum.i18n import _
from electrum.util import (get_asyncio_loop, bh2u,
InvalidBitcoinURI, maybe_extract_lightning_payment_identifier, NotEnoughFunds,
NoDynamicFeeEstimates, InvoiceError, parse_max_spend)
from electrum.invoices import PR_PAID, Invoice
from electrum.transaction import Transaction, PartialTxInput, PartialTransaction, PartialTxOutput
from electrum.network import TxBroadcastError, BestEffortRequestFailed
from electrum.logging import Logger
from electrum.lnaddr import lndecode, LnInvoiceException
from electrum.lnurl import decode_lnurl, request_lnurl, callback_lnurl, LNURLError, LNURL6Data
from .amountedit import AmountEdit, BTCAmountEdit, SizedFreezableLineEdit
from .util import WaitingDialog, HelpLabel, MessageBoxMixin, EnterButton
from .confirm_tx_dialog import ConfirmTxDialog
from .transaction_dialog import PreviewTxDialog
if TYPE_CHECKING:
from .main_window import ElectrumWindow
class SendTab(QWidget, MessageBoxMixin, Logger):
payment_request_ok_signal = pyqtSignal()
payment_request_error_signal = pyqtSignal()
lnurl6_round1_signal = pyqtSignal(object, object)
lnurl6_round2_signal = pyqtSignal(object)
clear_send_tab_signal = pyqtSignal()
show_error_signal = pyqtSignal(str)
payment_request: Optional[paymentrequest.PaymentRequest]
_lnurl_data: Optional[LNURL6Data] = None
def __init__(self, window: 'ElectrumWindow'):
QWidget.__init__(self, window)
Logger.__init__(self)
self.window = window
self.wallet = window.wallet
self.fx = window.fx
self.config = window.config
self.network = window.network
self.format_amount_and_units = window.format_amount_and_units
self.format_amount = window.format_amount
self.base_unit = window.base_unit
self.payto_URI = None
self.payment_request = None # type: Optional[paymentrequest.PaymentRequest]
self.pending_invoice = None
# A 4-column grid layout. All the stretch is in the last column.
# The exchange rate plugin adds a fiat widget in column 2
self.send_grid = grid = QGridLayout()
grid.setSpacing(8)
grid.setColumnStretch(3, 1)
from .paytoedit import PayToEdit
self.amount_e = BTCAmountEdit(self.window.get_decimal_point)
self.payto_e = PayToEdit(self)
msg = (_("Recipient of the funds.") + "\n\n"
+ _("You may enter a Bitcoin address, a label from your list of contacts "
"(a list of completions will be proposed), "
"or an alias (email-like address that forwards to a Bitcoin address)") + ". "
+ _("Lightning invoices are also supported.") + "\n\n"
+ _("You can also pay to many outputs in a single transaction, "
"specifying one output per line.") + "\n" + _("Format: address, amount") + "\n"
+ _("To set the amount to 'max', use the '!' special character.") + "\n"
+ _("Integers weights can also be used in conjunction with '!', "
"e.g. set one amount to '2!' and another to '3!' to split your coins 40-60."))
payto_label = HelpLabel(_('Pay to'), msg)
grid.addWidget(payto_label, 1, 0)
grid.addWidget(self.payto_e, 1, 1, 1, -1)
completer = QCompleter()
completer.setCaseSensitivity(False)
self.payto_e.set_completer(completer)
completer.setModel(self.window.completions)
msg = _('Description of the transaction (not mandatory).') + '\n\n' \
+ _(
'The description is not sent to the recipient of the funds. It is stored in your wallet file, and displayed in the \'History\' tab.')
description_label = HelpLabel(_('Description'), msg)
grid.addWidget(description_label, 2, 0)
self.message_e = SizedFreezableLineEdit(width=700)
grid.addWidget(self.message_e, 2, 1, 1, -1)
msg = (_('The amount to be received by the recipient.') + ' '
+ _('Fees are paid by the sender.') + '\n\n'
+ _('The amount will be displayed in red if you do not have enough funds in your wallet.') + ' '
+ _('Note that if you have frozen some of your addresses, the available funds will be lower than your total balance.') + '\n\n'
+ _('Keyboard shortcut: type "!" to send all your coins.'))
amount_label = HelpLabel(_('Amount'), msg)
grid.addWidget(amount_label, 3, 0)
grid.addWidget(self.amount_e, 3, 1)
self.fiat_send_e = AmountEdit(self.fx.get_currency if self.fx else '')
if not self.fx or not self.fx.is_enabled():
self.fiat_send_e.setVisible(False)
grid.addWidget(self.fiat_send_e, 3, 2)
self.amount_e.frozen.connect(
lambda: self.fiat_send_e.setFrozen(self.amount_e.isReadOnly()))
self.max_button = EnterButton(_("Max"), self.spend_max)
self.max_button.setFixedWidth(100)
self.max_button.setCheckable(True)
grid.addWidget(self.max_button, 3, 3)
self.save_button = EnterButton(_("Save"), self.do_save_invoice)
self.send_button = EnterButton(_("Pay") + "...", self.do_pay_or_get_invoice)
self.clear_button = EnterButton(_("Clear"), self.do_clear)
buttons = QHBoxLayout()
buttons.addStretch(1)
buttons.addWidget(self.clear_button)
buttons.addWidget(self.save_button)
buttons.addWidget(self.send_button)
grid.addLayout(buttons, 6, 1, 1, 4)
self.amount_e.shortcut.connect(self.spend_max)
def reset_max(text):
self.max_button.setChecked(False)
enable = not bool(text) and not self.amount_e.isReadOnly()
# self.max_button.setEnabled(enable)
self.amount_e.textEdited.connect(reset_max)
self.fiat_send_e.textEdited.connect(reset_max)
self.set_onchain(False)
self.invoices_label = QLabel(_('Send queue'))
from .invoice_list import InvoiceList
self.invoice_list = InvoiceList(self)
vbox0 = QVBoxLayout()
vbox0.addLayout(grid)
hbox = QHBoxLayout()
hbox.addLayout(vbox0)
hbox.addStretch(1)
vbox = QVBoxLayout(self)
vbox.addLayout(hbox)
vbox.addStretch(1)
vbox.addWidget(self.invoices_label)
vbox.addWidget(self.invoice_list)
vbox.setStretchFactor(self.invoice_list, 1000)
self.searchable_list = self.invoice_list
self.invoice_list.update() # after parented and put into a layout, can update without flickering
run_hook('create_send_tab', grid)
self.payment_request_ok_signal.connect(self.payment_request_ok)
self.payment_request_error_signal.connect(self.payment_request_error)
self.lnurl6_round1_signal.connect(self.on_lnurl6_round1)
self.lnurl6_round2_signal.connect(self.on_lnurl6_round2)
self.clear_send_tab_signal.connect(self.do_clear)
self.show_error_signal.connect(self.show_error)
def spend_max(self):
if run_hook('abort_send', self):
return
outputs = self.payto_e.get_outputs(True)
if not outputs:
return
make_tx = lambda fee_est: self.wallet.make_unsigned_transaction(
coins=self.window.get_coins(),
outputs=outputs,
fee=fee_est,
is_sweep=False)
try:
try:
tx = make_tx(None)
except (NotEnoughFunds, NoDynamicFeeEstimates) as e:
# Check if we had enough funds excluding fees,
# if so, still provide opportunity to set lower fees.
tx = make_tx(0)
except NotEnoughFunds as e:
self.max_button.setChecked(False)
text = self.get_text_not_enough_funds_mentioning_frozen()
self.show_error(text)
return
self.max_button.setChecked(True)
amount = tx.output_value()
__, x_fee_amount = run_hook('get_tx_extra_fee', self.wallet, tx) or (None, 0)
amount_after_all_fees = amount - x_fee_amount
self.amount_e.setAmount(amount_after_all_fees)
# show tooltip explaining max amount
mining_fee = tx.get_fee()
mining_fee_str = self.format_amount_and_units(mining_fee)
msg = _("Mining fee: {} (can be adjusted on next screen)").format(mining_fee_str)
if x_fee_amount:
twofactor_fee_str = self.format_amount_and_units(x_fee_amount)
msg += "\n" + _("2fa fee: {} (for the next batch of transactions)").format(twofactor_fee_str)
frozen_bal = self.get_frozen_balance_str()
if frozen_bal:
msg += "\n" + _("Some coins are frozen: {} (can be unfrozen in the Addresses or in the Coins tab)").format(frozen_bal)
QToolTip.showText(self.max_button.mapToGlobal(QPoint(0, 0)), msg)
def pay_onchain_dialog(
self, inputs: Sequence[PartialTxInput],
outputs: List[PartialTxOutput], *,
external_keypairs=None) -> None:
# trustedcoin requires this
if run_hook('abort_send', self):
return
is_sweep = bool(external_keypairs)
make_tx = lambda fee_est: self.wallet.make_unsigned_transaction(
coins=inputs,
outputs=outputs,
fee=fee_est,
is_sweep=is_sweep)
output_values = [x.value for x in outputs]
if any(parse_max_spend(outval) for outval in output_values):
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)
if conf_dlg.not_enough_funds:
# Check if we had enough funds excluding fees,
# if so, still provide opportunity to set lower fees.
if not conf_dlg.have_enough_funds_assuming_zero_fees():
text = self.get_text_not_enough_funds_mentioning_frozen()
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()
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()
def get_text_not_enough_funds_mentioning_frozen(self) -> str:
text = _("Not enough funds")
frozen_str = self.get_frozen_balance_str()
if frozen_str:
text += " ({} {})".format(
frozen_str, _("are frozen")
)
return text
def get_frozen_balance_str(self) -> Optional[str]:
frozen_bal = sum(self.wallet.get_frozen_balance())
if not frozen_bal:
return None
return self.format_amount_and_units(frozen_bal)
def do_clear(self):
self._lnurl_data = None
self.send_button.restore_original_text()
self.max_button.setChecked(False)
self.payment_request = None
self.payto_URI = None
self.payto_e.do_clear()
self.set_onchain(False)
for e in [self.message_e, self.amount_e]:
e.setText('')
e.setFrozen(False)
for e in [self.send_button, self.save_button, self.clear_button, self.amount_e, self.fiat_send_e]:
e.setEnabled(True)
self.window.update_status()
run_hook('do_clear', self)
def set_onchain(self, b):
self._is_onchain = b
self.max_button.setEnabled(b)
def lock_amount(self, b: bool) -> None:
self.amount_e.setFrozen(b)
self.max_button.setEnabled(not b)
def prepare_for_send_tab_network_lookup(self):
self.window.show_send_tab()
self.payto_e.disable_checks = True
for e in [self.payto_e, self.message_e]:
e.setFrozen(True)
self.lock_amount(True)
for btn in [self.save_button, self.send_button, self.clear_button]:
btn.setEnabled(False)
self.payto_e.setTextNoCheck(_("please wait..."))
def payment_request_ok(self):
pr = self.payment_request
if not pr:
return
invoice = Invoice.from_bip70_payreq(pr, height=0)
if self.wallet.get_invoice_status(invoice) == PR_PAID:
self.show_message("invoice already paid")
self.do_clear()
self.payment_request = None
return
self.payto_e.disable_checks = True
if not pr.has_expired():
self.payto_e.setGreen()
else:
self.payto_e.setExpired()
self.payto_e.setTextNoCheck(pr.get_requestor())
self.amount_e.setAmount(pr.get_amount())
self.message_e.setText(pr.get_memo())
self.set_onchain(True)
self.max_button.setEnabled(False)
for btn in [self.send_button, self.clear_button]:
btn.setEnabled(True)
# signal to set fee
self.amount_e.textEdited.emit("")
def payment_request_error(self):
pr = self.payment_request
if not pr:
return
self.show_message(pr.error)
self.payment_request = None
self.do_clear()
def on_pr(self, request: 'paymentrequest.PaymentRequest'):
self.payment_request = request
if self.payment_request.verify(self.window.contacts):
self.payment_request_ok_signal.emit()
else:
self.payment_request_error_signal.emit()
def set_lnurl6(self, lnurl: str, *, can_use_network: bool = True):
try:
url = decode_lnurl(lnurl)
except LnInvoiceException as e:
self.show_error(_("Error parsing Lightning invoice") + f":\n{e}")
return
if not can_use_network:
return
async def f():
try:
lnurl_data = await request_lnurl(url)
except LNURLError as e:
self.show_error_signal.emit(f"LNURL request encountered error: {e}")
self.clear_send_tab_signal.emit()
return
self.lnurl6_round1_signal.emit(lnurl_data, url)
asyncio.run_coroutine_threadsafe(f(), get_asyncio_loop()) # TODO should be cancellable
self.prepare_for_send_tab_network_lookup()
def on_lnurl6_round1(self, lnurl_data: LNURL6Data, url: str):
self._lnurl_data = lnurl_data
domain = urlparse(url).netloc
self.payto_e.setTextNoCheck(f"invoice from lnurl")
self.message_e.setText(f"lnurl: {domain}: {lnurl_data.metadata_plaintext}")
self.amount_e.setAmount(lnurl_data.min_sendable_sat)
self.amount_e.setFrozen(False)
self.send_button.setText(_('Get Invoice'))
for btn in [self.send_button, self.clear_button]:
btn.setEnabled(True)
self.set_onchain(False)
def set_bolt11(self, invoice: str):
"""Parse ln invoice, and prepare the send tab for it."""
try:
lnaddr = lndecode(invoice)
except LnInvoiceException as e:
self.show_error(_("Error parsing Lightning invoice") + f":\n{e}")
return
pubkey = bh2u(lnaddr.pubkey.serialize())
for k,v in lnaddr.tags:
if k == 'd':
description = v
break
else:
description = ''
self.payto_e.setFrozen(True)
self.payto_e.setTextNoCheck(pubkey)
self.payto_e.lightning_invoice = invoice
if not self.message_e.text():
self.message_e.setText(description)
if lnaddr.get_amount_sat() is not None:
self.amount_e.setAmount(lnaddr.get_amount_sat())
self.set_onchain(False)
def set_bip21(self, text: str, *, can_use_network: bool = True):
on_bip70_pr = self.on_pr if can_use_network else None
try:
out = util.parse_URI(text, on_bip70_pr)
except InvalidBitcoinURI as e:
self.show_error(_("Error parsing URI") + f":\n{e}")
return
self.payto_URI = out
r = out.get('r')
sig = out.get('sig')
name = out.get('name')
if (r or (name and sig)) and can_use_network:
self.prepare_for_send_tab_network_lookup()
return
address = out.get('address')
amount = out.get('amount')
label = out.get('label')
message = out.get('message')
lightning = out.get('lightning')
if lightning:
self.handle_payment_identifier(lightning, can_use_network=can_use_network)
return
# use label as description (not BIP21 compliant)
if label and not message:
message = label
if address:
self.payto_e.setText(address)
if message:
self.message_e.setText(message)
if amount:
self.amount_e.setAmount(amount)
def handle_payment_identifier(self, text: str, *, can_use_network: bool = True):
"""Takes
Lightning identifiers:
* lightning-URI (containing bolt11 or lnurl)
* bolt11 invoice
* lnurl
Bitcoin identifiers:
* bitcoin-URI
and sets the sending screen.
"""
text = text.strip()
if not text:
return
if invoice_or_lnurl := maybe_extract_lightning_payment_identifier(text):
if invoice_or_lnurl.startswith('lnurl'):
self.set_lnurl6(invoice_or_lnurl, can_use_network=can_use_network)
else:
self.set_bolt11(invoice_or_lnurl)
elif text.lower().startswith(util.BITCOIN_BIP21_URI_SCHEME + ':'):
self.set_bip21(text, can_use_network=can_use_network)
else:
raise ValueError("Could not handle payment identifier.")
# update fiat amount
self.amount_e.textEdited.emit("")
self.window.show_send_tab()
def read_invoice(self):
if self.check_send_tab_payto_line_and_show_errors():
return
try:
if not self._is_onchain:
invoice_str = self.payto_e.lightning_invoice
if not invoice_str:
return
if not self.wallet.has_lightning():
self.show_error(_('Lightning is disabled'))
return
invoice = Invoice.from_bech32(invoice_str)
if invoice.amount_msat is None:
amount_sat = self.amount_e.get_amount()
if amount_sat:
invoice.amount_msat = int(amount_sat * 1000)
else:
self.show_error(_('No amount'))
return
return invoice
else:
outputs = self.read_outputs()
if self.check_send_tab_onchain_outputs_and_show_errors(outputs):
return
message = self.message_e.text()
return self.wallet.create_invoice(
outputs=outputs,
message=message,
pr=self.payment_request,
URI=self.payto_URI)
except InvoiceError as e:
self.show_error(_('Error creating payment') + ':\n' + str(e))
def do_save_invoice(self):
self.pending_invoice = self.read_invoice()
if not self.pending_invoice:
return
self.save_pending_invoice()
def save_pending_invoice(self):
if not self.pending_invoice:
return
self.do_clear()
self.wallet.save_invoice(self.pending_invoice)
self.invoice_list.update()
self.pending_invoice = None
def _lnurl_get_invoice(self) -> None:
assert self._lnurl_data
amount = self.amount_e.get_amount()
if not (self._lnurl_data.min_sendable_sat <= amount <= self._lnurl_data.max_sendable_sat):
self.show_error(f'Amount must be between {self._lnurl_data.min_sendable_sat} and {self._lnurl_data.max_sendable_sat} sat.')
return
async def f():
try:
invoice_data = await callback_lnurl(
self._lnurl_data.callback_url,
params={'amount': self.amount_e.get_amount() * 1000},
)
except LNURLError as e:
self.show_error_signal.emit(f"LNURL request encountered error: {e}")
self.clear_send_tab_signal.emit()
return
invoice = invoice_data.get('pr')
self.lnurl6_round2_signal.emit(invoice)
asyncio.run_coroutine_threadsafe(f(), get_asyncio_loop()) # TODO should be cancellable
self.prepare_for_send_tab_network_lookup()
def on_lnurl6_round2(self, bolt11_invoice: str):
self.set_bolt11(bolt11_invoice)
self.payto_e.setFrozen(True)
self.amount_e.setEnabled(False)
self.fiat_send_e.setEnabled(False)
for btn in [self.send_button, self.clear_button, self.save_button]:
btn.setEnabled(True)
self.send_button.restore_original_text()
self._lnurl_data = None
def do_pay_or_get_invoice(self):
if self._lnurl_data:
self._lnurl_get_invoice()
return
self.pending_invoice = self.read_invoice()
if not self.pending_invoice:
return
self.do_pay_invoice(self.pending_invoice)
def pay_multiple_invoices(self, invoices):
outputs = []
for invoice in invoices:
outputs += invoice.outputs
self.pay_onchain_dialog(self.window.get_coins(), outputs)
def do_pay_invoice(self, invoice: 'Invoice'):
if invoice.is_lightning():
self.pay_lightning_invoice(invoice)
else:
self.pay_onchain_dialog(self.window.get_coins(), invoice.outputs)
def read_outputs(self) -> List[PartialTxOutput]:
if self.payment_request:
outputs = self.payment_request.get_outputs()
else:
outputs = self.payto_e.get_outputs(self.max_button.isChecked())
return outputs
def check_send_tab_onchain_outputs_and_show_errors(self, outputs: List[PartialTxOutput]) -> bool:
"""Returns whether there are errors with outputs.
Also shows error dialog to user if so.
"""
if not outputs:
self.show_error(_('No outputs'))
return True
for o in outputs:
if o.scriptpubkey is None:
self.show_error(_('Bitcoin Address is None'))
return True
if o.value is None:
self.show_error(_('Invalid Amount'))
return True
return False # no errors
def check_send_tab_payto_line_and_show_errors(self) -> bool:
"""Returns whether there are errors.
Also shows error dialog to user if so.
"""
pr = self.payment_request
if pr:
if pr.has_expired():
self.show_error(_('Payment request has expired'))
return True
if not pr:
errors = self.payto_e.get_errors()
if errors:
if len(errors) == 1 and not errors[0].is_multiline:
err = errors[0]
self.show_warning(_("Failed to parse 'Pay to' line") + ":\n" +
f"{err.line_content[:40]}...\n\n"
f"{err.exc!r}")
else:
self.show_warning(_("Invalid Lines found:") + "\n\n" +
'\n'.join([_("Line #") +
f"{err.idx+1}: {err.line_content[:40]}... ({err.exc!r})"
for err in errors]))
return True
if self.payto_e.is_alias and self.payto_e.validated is False:
alias = self.payto_e.toPlainText()
msg = _('WARNING: the alias "{}" could not be validated via an additional '
'security check, DNSSEC, and thus may not be correct.').format(alias) + '\n'
msg += _('Do you wish to continue?')
if not self.question(msg):
return True
return False # no errors
def pay_lightning_invoice(self, invoice: Invoice):
amount_sat = invoice.get_amount_sat()
key = self.wallet.get_key_for_outgoing_invoice(invoice)
if amount_sat is None:
raise Exception("missing amount for LN invoice")
if not self.wallet.lnworker.can_pay_invoice(invoice):
num_sats_can_send = int(self.wallet.lnworker.num_sats_can_send())
lightning_needed = amount_sat - num_sats_can_send
lightning_needed += (lightning_needed // 20) # operational safety margin
coins = self.window.get_coins(nonlocal_only=True)
can_pay_onchain = invoice.get_address() and self.wallet.can_pay_onchain(invoice.get_outputs(), coins=coins)
can_pay_with_new_channel = self.wallet.lnworker.suggest_funding_amount(amount_sat, coins=coins)
can_pay_with_swap = self.wallet.lnworker.suggest_swap_to_send(amount_sat, coins=coins)
rebalance_suggestion = self.wallet.lnworker.suggest_rebalance_to_send(amount_sat)
can_rebalance = bool(rebalance_suggestion) and self.window.num_tasks() == 0
choices = {}
if can_rebalance:
msg = ''.join([
_('Rebalance existing channels'), '\n',
_('Move funds between your channels in order to increase your sending capacity.')
])
choices[0] = msg
if can_pay_with_new_channel:
msg = ''.join([
_('Open a new channel'), '\n',
_('You will be able to pay once the channel is open.')
])
choices[1] = msg
if can_pay_with_swap:
msg = ''.join([
_('Swap onchain funds for lightning funds'), '\n',
_('You will be able to pay once the swap is confirmed.')
])
choices[2] = msg
if can_pay_onchain:
msg = ''.join([
_('Pay onchain'), '\n',
_('Funds will be sent to the invoice fallback address.')
])
choices[3] = msg
if not choices:
raise NotEnoughFunds()
msg = _('You cannot pay that invoice using Lightning.')
if self.wallet.lnworker.channels:
msg += '\n' + _('Your channels can send {}.').format(self.format_amount(num_sats_can_send) + self.base_unit())
r = self.window.query_choice(msg, choices)
if r is not None:
self.save_pending_invoice()
if r == 0:
chan1, chan2, delta = rebalance_suggestion
self.window.rebalance_dialog(chan1, chan2, amount_sat=delta)
elif r == 1:
amount_sat, min_amount_sat = can_pay_with_new_channel
self.window.channels_list.new_channel_dialog(amount_sat=amount_sat, min_amount_sat=min_amount_sat)
elif r == 2:
chan, swap_recv_amount_sat = can_pay_with_swap
self.window.run_swap_dialog(is_reverse=False, recv_amount_sat=swap_recv_amount_sat, channels=[chan])
elif r == 3:
self.pay_onchain_dialog(coins, invoice.get_outputs())
return
# FIXME this is currently lying to user as we truncate to satoshis
amount_msat = invoice.get_amount_msat()
msg = _("Pay lightning invoice?") + '\n\n' + _("This will send {}?").format(self.format_amount_and_units(Decimal(amount_msat)/1000))
if not self.question(msg):
return
self.save_pending_invoice()
coro = self.wallet.lnworker.pay_invoice(invoice.lightning_invoice, amount_msat=amount_msat)
self.window.run_coroutine_from_thread(coro, _('Sending payment'))
def broadcast_transaction(self, tx: Transaction):
def broadcast_thread():
# non-GUI thread
pr = self.payment_request
if pr and pr.has_expired():
self.payment_request = None
return False, _("Invoice has expired")
try:
self.network.run_from_another_thread(self.network.broadcast_transaction(tx))
except TxBroadcastError as e:
return False, e.get_message_for_gui()
except BestEffortRequestFailed as e:
return False, repr(e)
# success
txid = tx.txid()
if pr:
self.payment_request = None
refund_address = self.wallet.get_receiving_address()
coro = pr.send_payment_and_receive_paymentack(tx.serialize(), refund_address)
fut = asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop)
ack_status, ack_msg = fut.result(timeout=20)
self.logger.info(f"Payment ACK: {ack_status}. Ack message: {ack_msg}")
return True, txid
# Capture current TL window; override might be removed on return
parent = self.window.top_level_window(lambda win: isinstance(win, MessageBoxMixin))
def broadcast_done(result):
# GUI thread
if result:
success, msg = result
if success:
parent.show_message(_('Payment sent.') + '\n' + msg)
self.invoice_list.update()
else:
msg = msg or ''
parent.show_error(msg)
WaitingDialog(self, _('Broadcasting transaction...'),
broadcast_thread, broadcast_done, self.window.on_error)
def paytomany(self):
self.window.show_send_tab()
self.payto_e.paytomany()
msg = '\n'.join([
_('Enter a list of outputs in the \'Pay to\' field.'),
_('One output per line.'),
_('Format: address, amount'),
_('You may load a CSV file using the file icon.')
])
self.show_message(msg, title=_('Pay to many'))
def payto_contacts(self, labels):
paytos = [self.window.get_contact_payto(label) for label in labels]
self.window.show_send_tab()
if len(paytos) == 1:
self.payto_e.setText(paytos[0])
self.amount_e.setFocus()
else:
text = "\n".join([payto + ", 0" for payto in paytos])
self.payto_e.setText(text)
self.payto_e.setFocus()

2
electrum/gui/qt/transaction_dialog.py

@ -242,7 +242,7 @@ class BaseTxDialog(QDialog, MessageBoxMixin):
def do_broadcast(self): def do_broadcast(self):
self.main_window.push_top_level_window(self) self.main_window.push_top_level_window(self)
self.main_window.save_pending_invoice() self.main_window.send_tab.save_pending_invoice()
try: try:
self.main_window.broadcast_transaction(self.tx) self.main_window.broadcast_transaction(self.tx)
finally: finally:

Loading…
Cancel
Save