diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index 994aafa64..329083301 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -28,7 +28,7 @@ import signal import sys import traceback import threading -from typing import Optional, TYPE_CHECKING, List +from typing import Optional, TYPE_CHECKING, List, Sequence from electrum import GuiImportError @@ -80,7 +80,7 @@ if TYPE_CHECKING: class OpenFileEventFilter(QObject): - def __init__(self, windows): + def __init__(self, windows: Sequence[ElectrumWindow]): self.windows = windows super(OpenFileEventFilter, self).__init__() diff --git a/electrum/gui/qt/confirm_tx_dialog.py b/electrum/gui/qt/confirm_tx_dialog.py index 21ee9927b..a12dd9c5c 100644 --- a/electrum/gui/qt/confirm_tx_dialog.py +++ b/electrum/gui/qt/confirm_tx_dialog.py @@ -238,7 +238,7 @@ class ConfirmTxDialog(TxEditor, WindowModalDialog): self._update_amount_label() 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) return diff --git a/electrum/gui/qt/invoice_list.py b/electrum/gui/qt/invoice_list.py index 821316dbb..096346118 100644 --- a/electrum/gui/qt/invoice_list.py +++ b/electrum/gui/qt/invoice_list.py @@ -24,7 +24,7 @@ # SOFTWARE. from enum import IntEnum -from typing import Sequence +from typing import Sequence, TYPE_CHECKING from PyQt5.QtCore import Qt, QItemSelectionModel 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 WindowModalDialog +if TYPE_CHECKING: + from .main_window import ElectrumWindow + from .send_tab import SendTab ROLE_REQUEST_TYPE = Qt.UserRole @@ -64,8 +67,9 @@ class InvoiceList(MyTreeView): } filter_columns = [Columns.DATE, Columns.DESCRIPTION, Columns.AMOUNT] - def __init__(self, parent): - super().__init__(parent, self.create_menu, + def __init__(self, send_tab: 'SendTab'): + window = send_tab.window + super().__init__(window, self.create_menu, stretch_column=self.Columns.DESCRIPTION) self.std_model = QStandardItemModel(self) self.proxy = MySortModel(self, sort_role=ROLE_SORT_ORDER) @@ -74,17 +78,20 @@ class InvoiceList(MyTreeView): self.setSortingEnabled(True) self.setSelectionMode(QAbstractItemView.ExtendedSelection) + self.send_tab = send_tab + self.wallet = window.wallet + def refresh_row(self, key, row): assert row is not None - invoice = self.parent.wallet.invoices.get(key) + invoice = self.wallet.invoices.get(key) if invoice is None: return model = self.std_model 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) - if self.parent.wallet.lnworker: - log = self.parent.wallet.lnworker.logs.get(key) + if self.wallet.lnworker: + log = self.wallet.lnworker.logs.get(key) if log and status == PR_INFLIGHT: status_str += '... (%d)'%len(log) status_item.setText(status_str) @@ -95,15 +102,15 @@ class InvoiceList(MyTreeView): self.proxy.setDynamicSortFilter(False) # temp. disable re-sorting after every change self.std_model.clear() self.update_headers(self.__class__.headers) - for idx, item in enumerate(self.parent.wallet.get_unpaid_invoices()): - key = self.parent.wallet.get_key_for_outgoing_invoice(item) + for idx, item in enumerate(self.wallet.get_unpaid_invoices()): + key = self.wallet.get_key_for_outgoing_invoice(item) if item.is_lightning(): icon_name = 'lightning.png' else: icon_name = 'bitcoin.png' if item.bip70: 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) message = item.message amount = item.get_amount_sat() @@ -128,10 +135,10 @@ class InvoiceList(MyTreeView): def hide_if_empty(self): b = self.std_model.rowCount() > 0 self.setVisible(b) - self.parent.invoices_label.setVisible(b) + self.send_tab.invoices_label.setVisible(b) def create_menu(self, position): - wallet = self.parent.wallet + wallet = self.wallet items = self.selected_in_column(0) if len(items)>1: 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]) menu = QMenu(self) if can_batch_pay: - menu.addAction(_("Batch pay invoices") + "...", lambda: self.parent.pay_multiple_invoices(invoices)) - menu.addAction(_("Delete invoices"), lambda: self.parent.delete_invoices(keys)) + menu.addAction(_("Batch pay invoices") + "...", lambda: self.send_tab.pay_multiple_invoices(invoices)) + menu.addAction(_("Delete invoices"), lambda: self.delete_invoices(keys)) menu.exec_(self.viewport().mapToGlobal(position)) return idx = self.indexAt(position) @@ -149,7 +156,7 @@ class InvoiceList(MyTreeView): if not item or not item_col0: return key = item_col0.data(ROLE_REQUEST_ID) - invoice = self.parent.wallet.get_invoice(key) + invoice = self.wallet.get_invoice(key) menu = QMenu(self) self.add_copy_menu(menu, idx) if invoice.is_lightning(): @@ -160,14 +167,14 @@ class InvoiceList(MyTreeView): menu.addAction(_("Details"), lambda: self.parent.show_onchain_invoice(invoice)) status = wallet.get_invoice_status(invoice) 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: - menu.addAction(_("Retry"), lambda: self.parent.do_pay_invoice(invoice)) - if self.parent.wallet.lnworker: - log = self.parent.wallet.lnworker.logs.get(key) + menu.addAction(_("Retry"), lambda: self.send_tab.do_pay_invoice(invoice)) + if self.wallet.lnworker: + log = self.wallet.lnworker.logs.get(key) if 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)) def show_log(self, key, log: Sequence[HtlcLog]): @@ -185,3 +192,8 @@ class InvoiceList(MyTreeView): vbox.addWidget(log_w) vbox.addLayout(Buttons(CloseButton(d))) d.exec_() + + def delete_invoices(self, keys): + for key in keys: + self.wallet.delete_invoice(key) + self.delete_item(key) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index a12e30f11..6d47d64d7 100644 --- a/electrum/gui/qt/main_window.py +++ b/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, getOpenFileName, getSaveFileName, BlockingWaitingDialog) from .util import ButtonsTextEdit, ButtonsLineEdit +from .util import QtEventListener, qt_event_listener, event_listener from .installwizard import WIF_HELP_TEXT from .history_list import HistoryList, HistoryModel from .update_checker import UpdateCheck, UpdateCheckThread @@ -195,23 +196,13 @@ def protected(func): return func(self, *args, **kwargs) return request_password -from .util import QtEventListener, qt_event_listener, event_listener 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() show_privkeys_signal = pyqtSignal() show_error_signal = pyqtSignal(str) - payment_request: Optional[paymentrequest.PaymentRequest] - _lnurl_data: Optional[LNURL6Data] = None - def __init__(self, gui_object: 'ElectrumGui', wallet: Abstract_Wallet): QMainWindow.__init__(self) self.gui_object = gui_object @@ -230,14 +221,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): self.tray = gui_object.tray self.app = gui_object.app 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.pluginsdialog = None self.showing_cert_mismatch_error = False self.tl_windows = [] - self.pending_invoice = None Logger.__init__(self) 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_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.history_list.setFocus(True) @@ -382,7 +363,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): def on_fx_quotes(self): self.update_status() # 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 = self.fiat_receive_e if self.fiat_receive_e.is_last_edited else self.receive_amount_e 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.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) raw_transaction_menu = tools_menu.addMenu(_("&Load transaction")) @@ -913,7 +894,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): def timer_actions(self): # refresh invoices and requests because they show ETA self.request_list.refresh_all() - self.invoice_list.refresh_all() + self.send_tab.invoice_list.refresh_all() # Note this runs in the GUI thread if self.need_update.is_set(): self.need_update.clear() @@ -923,7 +904,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): self.update_status() # resolve aliases # 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() 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.request_list.update() self.update_current_request() - self.invoice_list.update() + self.send_tab.invoice_list.update() self.address_list.update() self.utxo_list.update() self.contact_list.update() @@ -1095,7 +1076,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): def refresh_tabs(self, wallet=None): self.history_model.refresh('refresh_tabs') self.request_list.refresh_all() - self.invoice_list.refresh_all() + self.send_tab.invoice_list.refresh_all() self.address_list.refresh_all() self.utxo_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) 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() evl = sorted(pr_expiration_values.items()) @@ -1525,146 +1506,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): self.receive_address_e.setToolTip("") def create_send_tab(self): - # 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.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) + from .send_tab import SendTab + return SendTab(self) def get_contact_payto(self, key): _type, label = self.contacts.get(key) @@ -1678,136 +1521,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): def protect(self, 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): if not self.network: self.show_error(_("You are offline.")) @@ -1848,9 +1561,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): return status = self.wallet.get_invoice_status(invoice) if status == PR_PAID: - self.invoice_list.delete_item(key) + self.send_tab.invoice_list.delete_item(key) else: - self.invoice_list.refresh_item(key) + self.send_tab.invoice_list.refresh_item(key) @qt_event_listener def on_event_payment_succeeded(self, wallet, key): @@ -1868,111 +1581,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): invoice = self.wallet.get_invoice(key) if invoice and invoice.is_lightning() and invoice.get_address(): 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: 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]: coins = self.get_manually_selected_coins() if coins is not None: @@ -1987,76 +1599,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): """ 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): if not tx.is_complete(): self.show_transaction(tx) @@ -2067,6 +1609,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): return self.broadcast_transaction(tx) + def broadcast_transaction(self, tx: Transaction): + self.send_tab.broadcast_transaction(tx) + @protected def sign_tx(self, tx, *, callback, external_keypairs, password): 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...') 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): coins = self.get_coins(nonlocal_only=True) 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 clayout.selected_index() - 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.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 handle_payment_identifier(self, *args, **kwargs): + self.send_tab.handle_payment_identifier(*args, **kwargs) def set_frozen_state_of_addresses(self, addrs, freeze: bool): 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.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): - paytos = [self.get_contact_payto(label) for label in 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() + self.send_tab.payto_contacts(labels) def set_contact(self, label, 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) 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): export_meta_gui(self, _('invoices'), self.wallet.export_invoices) @@ -3506,7 +2799,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): coins, keypairs = result outputs = [PartialTxOutput.from_address_and_value(addr, value='!')] 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): self.on_error(exc_info) 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)) 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] for edit, amount in zip(edits, amounts): edit.setAmount(amount) def update_fiat(self): 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.history_model.refresh('update_fiat') self.history_list.update() diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py index 876c96c0e..84305c3d7 100644 --- a/electrum/gui/qt/paytoedit.py +++ b/electrum/gui/qt/paytoedit.py @@ -29,6 +29,7 @@ from decimal import Decimal from typing import NamedTuple, Sequence, Optional, List, TYPE_CHECKING from PyQt5.QtGui import QFontMetrics, QFont +from PyQt5.QtWidgets import QApplication from electrum import bitcoin from electrum.util import bfh, parse_max_spend @@ -44,6 +45,7 @@ from .util import MONOSPACE_FONT if TYPE_CHECKING: from .main_window import ElectrumWindow + from .send_tab import SendTab RE_ALIAS = r'(.*?)\s*\<([0-9A-Za-z]{1,})\>' @@ -61,13 +63,14 @@ class PayToLineError(NamedTuple): class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger): - def __init__(self, win: 'ElectrumWindow'): + def __init__(self, send_tab: 'SendTab'): 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) - self.win = win - self.app = win.app - self.amount_edit = win.amount_e + self.send_tab = send_tab + self.win = send_tab.window + self.app = QApplication.instance() + self.amount_edit = self.send_tab.amount_e self.setFont(QFont(MONOSPACE_FONT)) document = self.document() document.contentsChanged.connect(self.update_size) @@ -205,10 +208,10 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger): if len(lines) == 1: data = lines[0] 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: self.logger.exception("") - self.win.show_error(e) + self.send_tab.show_error(e) except ValueError: pass else: @@ -226,8 +229,8 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger): except Exception as e: self.errors.append(PayToLineError(line_content=data, exc=e)) else: - self.win.set_onchain(True) - self.win.lock_amount(False) + self.send_tab.set_onchain(True) + self.send_tab.lock_amount(False) return if full_check: # network requests # FIXME blocking GUI thread # try openalias @@ -259,17 +262,17 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger): else: total += output.value 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.payto_scriptpubkey = None - if self.win.max_button.isChecked(): - self.win.spend_max() + if self.send_tab.max_button.isChecked(): + self.send_tab.spend_max() else: 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]: return self.errors diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py new file mode 100644 index 000000000..763dc78be --- /dev/null +++ b/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() + + diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index 5824db372..301e712f8 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -242,7 +242,7 @@ class BaseTxDialog(QDialog, MessageBoxMixin): def do_broadcast(self): self.main_window.push_top_level_window(self) - self.main_window.save_pending_invoice() + self.main_window.send_tab.save_pending_invoice() try: self.main_window.broadcast_transaction(self.tx) finally: