electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

main_window.py (141783B)


      1 #!/usr/bin/env python
      2 #
      3 # Electrum - lightweight Bitcoin client
      4 # Copyright (C) 2012 thomasv@gitorious
      5 #
      6 # Permission is hereby granted, free of charge, to any person
      7 # obtaining a copy of this software and associated documentation files
      8 # (the "Software"), to deal in the Software without restriction,
      9 # including without limitation the rights to use, copy, modify, merge,
     10 # publish, distribute, sublicense, and/or sell copies of the Software,
     11 # and to permit persons to whom the Software is furnished to do so,
     12 # subject to the following conditions:
     13 #
     14 # The above copyright notice and this permission notice shall be
     15 # included in all copies or substantial portions of the Software.
     16 #
     17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
     18 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
     19 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
     20 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
     21 # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
     22 # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
     23 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
     24 # SOFTWARE.
     25 import sys
     26 import time
     27 import threading
     28 import os
     29 import traceback
     30 import json
     31 import shutil
     32 import weakref
     33 import csv
     34 from decimal import Decimal
     35 import base64
     36 from functools import partial
     37 import queue
     38 import asyncio
     39 from typing import Optional, TYPE_CHECKING, Sequence, List, Union
     40 
     41 from PyQt5.QtGui import QPixmap, QKeySequence, QIcon, QCursor, QFont
     42 from PyQt5.QtCore import Qt, QRect, QStringListModel, QSize, pyqtSignal
     43 from PyQt5.QtCore import QTimer
     44 from PyQt5.QtWidgets import (QMessageBox, QComboBox, QSystemTrayIcon, QTabWidget,
     45                              QMenuBar, QFileDialog, QCheckBox, QLabel,
     46                              QVBoxLayout, QGridLayout, QLineEdit,
     47                              QHBoxLayout, QPushButton, QScrollArea, QTextEdit,
     48                              QShortcut, QMainWindow, QCompleter, QInputDialog,
     49                              QWidget, QSizePolicy, QStatusBar, QToolTip, QDialog,
     50                              QMenu, QAction, QStackedWidget, QToolButton)
     51 
     52 import electrum
     53 from electrum import (keystore, ecc, constants, util, bitcoin, commands,
     54                       paymentrequest, lnutil)
     55 from electrum.bitcoin import COIN, is_address
     56 from electrum.plugin import run_hook, BasePlugin
     57 from electrum.i18n import _
     58 from electrum.util import (format_time,
     59                            UserCancelled, profiler,
     60                            bh2u, bfh, InvalidPassword,
     61                            UserFacingException,
     62                            get_new_wallet_name, send_exception_to_crash_reporter,
     63                            InvalidBitcoinURI, maybe_extract_bolt11_invoice, NotEnoughFunds,
     64                            NoDynamicFeeEstimates, MultipleSpendMaxTxOutputs,
     65                            AddTransactionException, BITCOIN_BIP21_URI_SCHEME)
     66 from electrum.invoices import PR_TYPE_ONCHAIN, PR_TYPE_LN, PR_DEFAULT_EXPIRATION_WHEN_CREATING, Invoice
     67 from electrum.invoices import PR_PAID, PR_FAILED, pr_expiration_values, LNInvoice, OnchainInvoice
     68 from electrum.transaction import (Transaction, PartialTxInput,
     69                                   PartialTransaction, PartialTxOutput)
     70 from electrum.wallet import (Multisig_Wallet, CannotBumpFee, Abstract_Wallet,
     71                              sweep_preparations, InternalAddressCorruption,
     72                              CannotDoubleSpendTx, CannotCPFP)
     73 from electrum.version import ELECTRUM_VERSION
     74 from electrum.network import (Network, TxBroadcastError, BestEffortRequestFailed,
     75                               UntrustedServerReturnedError, NetworkException)
     76 from electrum.exchange_rate import FxThread
     77 from electrum.simple_config import SimpleConfig
     78 from electrum.logging import Logger
     79 from electrum.lnutil import ln_dummy_address, extract_nodeid, ConnStringFormatError
     80 from electrum.lnaddr import lndecode, LnDecodeException
     81 
     82 from .exception_window import Exception_Hook
     83 from .amountedit import AmountEdit, BTCAmountEdit, FreezableLineEdit, FeerateEdit
     84 from .qrcodewidget import QRCodeWidget, QRDialog
     85 from .qrtextedit import ShowQRTextEdit, ScanQRTextEdit
     86 from .transaction_dialog import show_transaction
     87 from .fee_slider import FeeSlider, FeeComboBox
     88 from .util import (read_QIcon, ColorScheme, text_dialog, icon_path, WaitingDialog,
     89                    WindowModalDialog, ChoicesLayout, HelpLabel, Buttons,
     90                    OkButton, InfoButton, WWLabel, TaskThread, CancelButton,
     91                    CloseButton, HelpButton, MessageBoxMixin, EnterButton,
     92                    import_meta_gui, export_meta_gui,
     93                    filename_field, address_field, char_width_in_lineedit, webopen,
     94                    TRANSACTION_FILE_EXTENSION_FILTER_ANY, MONOSPACE_FONT,
     95                    getOpenFileName, getSaveFileName, BlockingWaitingDialog)
     96 from .util import ButtonsTextEdit, ButtonsLineEdit
     97 from .installwizard import WIF_HELP_TEXT
     98 from .history_list import HistoryList, HistoryModel
     99 from .update_checker import UpdateCheck, UpdateCheckThread
    100 from .channels_list import ChannelsList
    101 from .confirm_tx_dialog import ConfirmTxDialog
    102 from .transaction_dialog import PreviewTxDialog
    103 from .rbf_dialog import BumpFeeDialog, DSCancelDialog
    104 
    105 if TYPE_CHECKING:
    106     from . import ElectrumGui
    107 
    108 
    109 LN_NUM_PAYMENT_ATTEMPTS = 10
    110 
    111 
    112 class StatusBarButton(QToolButton):
    113     # note: this class has a custom stylesheet applied in stylesheet_patcher.py
    114     def __init__(self, icon, tooltip, func):
    115         QToolButton.__init__(self)
    116         self.setText('')
    117         self.setIcon(icon)
    118         self.setToolTip(tooltip)
    119         self.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
    120         self.setAutoRaise(True)
    121         self.setMaximumWidth(25)
    122         self.clicked.connect(self.onPress)
    123         self.func = func
    124         self.setIconSize(QSize(25,25))
    125         self.setCursor(QCursor(Qt.PointingHandCursor))
    126 
    127     def onPress(self, checked=False):
    128         '''Drops the unwanted PyQt5 "checked" argument'''
    129         self.func()
    130 
    131     def keyPressEvent(self, e):
    132         if e.key() in [ Qt.Key_Return, Qt.Key_Enter ]:
    133             self.func()
    134 
    135 
    136 def protected(func):
    137     '''Password request wrapper.  The password is passed to the function
    138         as the 'password' named argument.  "None" indicates either an
    139         unencrypted wallet, or the user cancelled the password request.
    140         An empty input is passed as the empty string.'''
    141     def request_password(self, *args, **kwargs):
    142         parent = self.top_level_window()
    143         password = None
    144         while self.wallet.has_keystore_encryption():
    145             password = self.password_dialog(parent=parent)
    146             if password is None:
    147                 # User cancelled password input
    148                 return
    149             try:
    150                 self.wallet.check_password(password)
    151                 break
    152             except Exception as e:
    153                 self.show_error(str(e), parent=parent)
    154                 continue
    155 
    156         kwargs['password'] = password
    157         return func(self, *args, **kwargs)
    158     return request_password
    159 
    160 
    161 class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
    162 
    163     payment_request_ok_signal = pyqtSignal()
    164     payment_request_error_signal = pyqtSignal()
    165     network_signal = pyqtSignal(str, object)
    166     #ln_payment_attempt_signal = pyqtSignal(str)
    167     alias_received_signal = pyqtSignal()
    168     computing_privkeys_signal = pyqtSignal()
    169     show_privkeys_signal = pyqtSignal()
    170     show_error_signal = pyqtSignal(str)
    171 
    172     payment_request: Optional[paymentrequest.PaymentRequest]
    173 
    174     def __init__(self, gui_object: 'ElectrumGui', wallet: Abstract_Wallet):
    175         QMainWindow.__init__(self)
    176 
    177         self.gui_object = gui_object
    178         self.config = config = gui_object.config  # type: SimpleConfig
    179         self.gui_thread = gui_object.gui_thread
    180         assert wallet, "no wallet"
    181         self.wallet = wallet
    182         if wallet.has_lightning():
    183             self.wallet.config.set_key('show_channels_tab', True)
    184 
    185         self.setup_exception_hook()
    186 
    187         self.network = gui_object.daemon.network  # type: Network
    188         self.fx = gui_object.daemon.fx  # type: FxThread
    189         self.contacts = wallet.contacts
    190         self.tray = gui_object.tray
    191         self.app = gui_object.app
    192         self.cleaned_up = False
    193         self.payment_request = None  # type: Optional[paymentrequest.PaymentRequest]
    194         self.payto_URI = None
    195         self.checking_accounts = False
    196         self.qr_window = None
    197         self.pluginsdialog = None
    198         self.showing_cert_mismatch_error = False
    199         self.tl_windows = []
    200         self.pending_invoice = None
    201         Logger.__init__(self)
    202 
    203         self.tx_notification_queue = queue.Queue()
    204         self.tx_notification_last_time = 0
    205 
    206         self.create_status_bar()
    207         self.need_update = threading.Event()
    208 
    209         self.completions = QStringListModel()
    210 
    211         coincontrol_sb = self.create_coincontrol_statusbar()
    212 
    213         self.tabs = tabs = QTabWidget(self)
    214         self.send_tab = self.create_send_tab()
    215         self.receive_tab = self.create_receive_tab()
    216         self.addresses_tab = self.create_addresses_tab()
    217         self.utxo_tab = self.create_utxo_tab()
    218         self.console_tab = self.create_console_tab()
    219         self.contacts_tab = self.create_contacts_tab()
    220         self.channels_tab = self.create_channels_tab()
    221         tabs.addTab(self.create_history_tab(), read_QIcon("tab_history.png"), _('History'))
    222         tabs.addTab(self.send_tab, read_QIcon("tab_send.png"), _('Send'))
    223         tabs.addTab(self.receive_tab, read_QIcon("tab_receive.png"), _('Receive'))
    224 
    225         def add_optional_tab(tabs, tab, icon, description, name):
    226             tab.tab_icon = icon
    227             tab.tab_description = description
    228             tab.tab_pos = len(tabs)
    229             tab.tab_name = name
    230             if self.config.get('show_{}_tab'.format(name), False):
    231                 tabs.addTab(tab, icon, description.replace("&", ""))
    232 
    233         add_optional_tab(tabs, self.addresses_tab, read_QIcon("tab_addresses.png"), _("&Addresses"), "addresses")
    234         add_optional_tab(tabs, self.channels_tab, read_QIcon("lightning.png"), _("Channels"), "channels")
    235         add_optional_tab(tabs, self.utxo_tab, read_QIcon("tab_coins.png"), _("Co&ins"), "utxo")
    236         add_optional_tab(tabs, self.contacts_tab, read_QIcon("tab_contacts.png"), _("Con&tacts"), "contacts")
    237         add_optional_tab(tabs, self.console_tab, read_QIcon("tab_console.png"), _("Con&sole"), "console")
    238 
    239         tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
    240 
    241         central_widget = QWidget()
    242         vbox = QVBoxLayout(central_widget)
    243         vbox.setContentsMargins(0, 0, 0, 0)
    244         vbox.addWidget(tabs)
    245         vbox.addWidget(coincontrol_sb)
    246 
    247         self.setCentralWidget(central_widget)
    248 
    249         if self.config.get("is_maximized"):
    250             self.showMaximized()
    251 
    252         self.setWindowIcon(read_QIcon("electrum.png"))
    253         self.init_menubar()
    254 
    255         wrtabs = weakref.proxy(tabs)
    256         QShortcut(QKeySequence("Ctrl+W"), self, self.close)
    257         QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
    258         QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet)
    259         QShortcut(QKeySequence("F5"), self, self.update_wallet)
    260         QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: wrtabs.setCurrentIndex((wrtabs.currentIndex() - 1)%wrtabs.count()))
    261         QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: wrtabs.setCurrentIndex((wrtabs.currentIndex() + 1)%wrtabs.count()))
    262 
    263         for i in range(wrtabs.count()):
    264             QShortcut(QKeySequence("Alt+" + str(i + 1)), self, lambda i=i: wrtabs.setCurrentIndex(i))
    265 
    266         self.payment_request_ok_signal.connect(self.payment_request_ok)
    267         self.payment_request_error_signal.connect(self.payment_request_error)
    268         self.show_error_signal.connect(self.show_error)
    269         self.history_list.setFocus(True)
    270 
    271         # network callbacks
    272         if self.network:
    273             self.network_signal.connect(self.on_network_qt)
    274             interests = ['wallet_updated', 'network_updated', 'blockchain_updated',
    275                          'new_transaction', 'status',
    276                          'banner', 'verified', 'fee', 'fee_histogram', 'on_quotes',
    277                          'on_history', 'channel', 'channels_updated',
    278                          'payment_failed', 'payment_succeeded',
    279                          'invoice_status', 'request_status', 'ln_gossip_sync_progress',
    280                          'cert_mismatch', 'gossip_db_loaded']
    281             # To avoid leaking references to "self" that prevent the
    282             # window from being GC-ed when closed, callbacks should be
    283             # methods of this class only, and specifically not be
    284             # partials, lambdas or methods of subobjects.  Hence...
    285             util.register_callback(self.on_network, interests)
    286             # set initial message
    287             self.console.showMessage(self.network.banner)
    288 
    289         # update fee slider in case we missed the callback
    290         #self.fee_slider.update()
    291         self.load_wallet(wallet)
    292         gui_object.timer.timeout.connect(self.timer_actions)
    293         self.fetch_alias()
    294 
    295         # If the option hasn't been set yet
    296         if config.get('check_updates') is None:
    297             choice = self.question(title="Electrum - " + _("Enable update check"),
    298                                    msg=_("For security reasons we advise that you always use the latest version of Electrum.") + " " +
    299                                        _("Would you like to be notified when there is a newer version of Electrum available?"))
    300             config.set_key('check_updates', bool(choice), save=True)
    301 
    302         if config.get('check_updates', False):
    303             # The references to both the thread and the window need to be stored somewhere
    304             # to prevent GC from getting in our way.
    305             def on_version_received(v):
    306                 if UpdateCheck.is_newer(v):
    307                     self.update_check_button.setText(_("Update to Electrum {} is available").format(v))
    308                     self.update_check_button.clicked.connect(lambda: self.show_update_check(v))
    309                     self.update_check_button.show()
    310             self._update_check_thread = UpdateCheckThread()
    311             self._update_check_thread.checked.connect(on_version_received)
    312             self._update_check_thread.start()
    313 
    314     def setup_exception_hook(self):
    315         Exception_Hook.maybe_setup(config=self.config,
    316                                    wallet=self.wallet)
    317 
    318     def run_coroutine_from_thread(self, coro, on_result=None):
    319         def task():
    320             try:
    321                 f = asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop)
    322                 r = f.result()
    323                 if on_result:
    324                     on_result(r)
    325             except Exception as e:
    326                 self.logger.exception("exception in coro scheduled via window.wallet")
    327                 self.show_error_signal.emit(str(e))
    328         self.wallet.thread.add(task)
    329 
    330     def on_fx_history(self):
    331         self.history_model.refresh('fx_history')
    332         self.address_list.update()
    333 
    334     def on_fx_quotes(self):
    335         self.update_status()
    336         # Refresh edits with the new rate
    337         edit = self.fiat_send_e if self.fiat_send_e.is_last_edited else self.amount_e
    338         edit.textEdited.emit(edit.text())
    339         edit = self.fiat_receive_e if self.fiat_receive_e.is_last_edited else self.receive_amount_e
    340         edit.textEdited.emit(edit.text())
    341         # History tab needs updating if it used spot
    342         if self.fx.history_used_spot:
    343             self.history_model.refresh('fx_quotes')
    344         self.address_list.update()
    345 
    346     def toggle_tab(self, tab):
    347         show = not self.config.get('show_{}_tab'.format(tab.tab_name), False)
    348         self.config.set_key('show_{}_tab'.format(tab.tab_name), show)
    349         item_text = (_("Hide {}") if show else _("Show {}")).format(tab.tab_description)
    350         tab.menu_action.setText(item_text)
    351         if show:
    352             # Find out where to place the tab
    353             index = len(self.tabs)
    354             for i in range(len(self.tabs)):
    355                 try:
    356                     if tab.tab_pos < self.tabs.widget(i).tab_pos:
    357                         index = i
    358                         break
    359                 except AttributeError:
    360                     pass
    361             self.tabs.insertTab(index, tab, tab.tab_icon, tab.tab_description.replace("&", ""))
    362         else:
    363             i = self.tabs.indexOf(tab)
    364             self.tabs.removeTab(i)
    365 
    366     def push_top_level_window(self, window):
    367         '''Used for e.g. tx dialog box to ensure new dialogs are appropriately
    368         parented.  This used to be done by explicitly providing the parent
    369         window, but that isn't something hardware wallet prompts know.'''
    370         self.tl_windows.append(window)
    371 
    372     def pop_top_level_window(self, window):
    373         self.tl_windows.remove(window)
    374 
    375     def top_level_window(self, test_func=None):
    376         '''Do the right thing in the presence of tx dialog windows'''
    377         override = self.tl_windows[-1] if self.tl_windows else None
    378         if override and test_func and not test_func(override):
    379             override = None  # only override if ok for test_func
    380         return self.top_level_window_recurse(override, test_func)
    381 
    382     def diagnostic_name(self):
    383         #return '{}:{}'.format(self.__class__.__name__, self.wallet.diagnostic_name())
    384         return self.wallet.diagnostic_name()
    385 
    386     def is_hidden(self):
    387         return self.isMinimized() or self.isHidden()
    388 
    389     def show_or_hide(self):
    390         if self.is_hidden():
    391             self.bring_to_top()
    392         else:
    393             self.hide()
    394 
    395     def bring_to_top(self):
    396         self.show()
    397         self.raise_()
    398 
    399     def on_error(self, exc_info):
    400         e = exc_info[1]
    401         if isinstance(e, UserCancelled):
    402             pass
    403         elif isinstance(e, UserFacingException):
    404             self.show_error(str(e))
    405         else:
    406             # TODO would be nice if we just sent these to the crash reporter...
    407             #      anything we don't want to send there, we should explicitly catch
    408             # send_exception_to_crash_reporter(e)
    409             try:
    410                 self.logger.error("on_error", exc_info=exc_info)
    411             except OSError:
    412                 pass  # see #4418
    413             self.show_error(repr(e))
    414 
    415     def on_network(self, event, *args):
    416         # Handle in GUI thread
    417         self.network_signal.emit(event, args)
    418 
    419     def on_network_qt(self, event, args=None):
    420         # Handle a network message in the GUI thread
    421         # note: all windows get events from all wallets!
    422         if event == 'wallet_updated':
    423             wallet = args[0]
    424             if wallet == self.wallet:
    425                 self.need_update.set()
    426         elif event == 'network_updated':
    427             self.gui_object.network_updated_signal_obj.network_updated_signal \
    428                 .emit(event, args)
    429             self.network_signal.emit('status', None)
    430         elif event == 'blockchain_updated':
    431             # to update number of confirmations in history
    432             self.need_update.set()
    433         elif event == 'new_transaction':
    434             wallet, tx = args
    435             if wallet == self.wallet:
    436                 self.tx_notification_queue.put(tx)
    437         elif event == 'on_quotes':
    438             self.on_fx_quotes()
    439         elif event == 'on_history':
    440             self.on_fx_history()
    441         elif event == 'gossip_db_loaded':
    442             self.channels_list.gossip_db_loaded.emit(*args)
    443         elif event == 'channels_updated':
    444             wallet = args[0]
    445             if wallet == self.wallet:
    446                 self.channels_list.update_rows.emit(*args)
    447         elif event == 'channel':
    448             wallet = args[0]
    449             if wallet == self.wallet:
    450                 self.channels_list.update_single_row.emit(*args)
    451                 self.update_status()
    452         elif event == 'request_status':
    453             self.on_request_status(*args)
    454         elif event == 'invoice_status':
    455             self.on_invoice_status(*args)
    456         elif event == 'payment_succeeded':
    457             wallet = args[0]
    458             if wallet == self.wallet:
    459                 self.on_payment_succeeded(*args)
    460         elif event == 'payment_failed':
    461             wallet = args[0]
    462             if wallet == self.wallet:
    463                 self.on_payment_failed(*args)
    464         elif event == 'status':
    465             self.update_status()
    466         elif event == 'banner':
    467             self.console.showMessage(args[0])
    468         elif event == 'verified':
    469             wallet, tx_hash, tx_mined_status = args
    470             if wallet == self.wallet:
    471                 self.history_model.update_tx_mined_status(tx_hash, tx_mined_status)
    472         elif event == 'fee':
    473             pass
    474         elif event == 'fee_histogram':
    475             self.history_model.on_fee_histogram()
    476         elif event == 'ln_gossip_sync_progress':
    477             self.update_lightning_icon()
    478         elif event == 'cert_mismatch':
    479             self.show_cert_mismatch_error()
    480         else:
    481             self.logger.info(f"unexpected network event: {event} {args}")
    482 
    483     def fetch_alias(self):
    484         self.alias_info = None
    485         alias = self.config.get('alias')
    486         if alias:
    487             alias = str(alias)
    488             def f():
    489                 self.alias_info = self.contacts.resolve_openalias(alias)
    490                 self.alias_received_signal.emit()
    491             t = threading.Thread(target=f)
    492             t.setDaemon(True)
    493             t.start()
    494 
    495     def close_wallet(self):
    496         if self.wallet:
    497             self.logger.info(f'close_wallet {self.wallet.storage.path}')
    498             self.wallet.thread = None
    499         run_hook('close_wallet', self.wallet)
    500 
    501     @profiler
    502     def load_wallet(self, wallet: Abstract_Wallet):
    503         wallet.thread = TaskThread(self, self.on_error)
    504         self.update_recently_visited(wallet.storage.path)
    505         if wallet.has_lightning():
    506             util.trigger_callback('channels_updated', wallet)
    507         self.need_update.set()
    508         # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
    509         # update menus
    510         self.seed_menu.setEnabled(self.wallet.has_seed())
    511         self.update_lock_icon()
    512         self.update_buttons_on_seed()
    513         self.update_console()
    514         self.clear_receive_tab()
    515         self.request_list.update()
    516         self.channels_list.update()
    517         self.tabs.show()
    518         self.init_geometry()
    519         if self.config.get('hide_gui') and self.gui_object.tray.isVisible():
    520             self.hide()
    521         else:
    522             self.show()
    523         self.watching_only_changed()
    524         run_hook('load_wallet', wallet, self)
    525         try:
    526             wallet.try_detecting_internal_addresses_corruption()
    527         except InternalAddressCorruption as e:
    528             self.show_error(str(e))
    529             send_exception_to_crash_reporter(e)
    530 
    531     def init_geometry(self):
    532         winpos = self.wallet.db.get("winpos-qt")
    533         try:
    534             screen = self.app.desktop().screenGeometry()
    535             assert screen.contains(QRect(*winpos))
    536             self.setGeometry(*winpos)
    537         except:
    538             self.logger.info("using default geometry")
    539             self.setGeometry(100, 100, 840, 400)
    540 
    541     def watching_only_changed(self):
    542         name = "Electrum Testnet" if constants.net.TESTNET else "Electrum"
    543         title = '%s %s  -  %s' % (name, ELECTRUM_VERSION,
    544                                         self.wallet.basename())
    545         extra = [self.wallet.db.get('wallet_type', '?')]
    546         if self.wallet.is_watching_only():
    547             extra.append(_('watching only'))
    548         title += '  [%s]'% ', '.join(extra)
    549         self.setWindowTitle(title)
    550         self.password_menu.setEnabled(self.wallet.may_have_password())
    551         self.import_privkey_menu.setVisible(self.wallet.can_import_privkey())
    552         self.import_address_menu.setVisible(self.wallet.can_import_address())
    553         self.export_menu.setEnabled(self.wallet.can_export())
    554 
    555     def warn_if_watching_only(self):
    556         if self.wallet.is_watching_only():
    557             msg = ' '.join([
    558                 _("This wallet is watching-only."),
    559                 _("This means you will not be able to spend Bitcoins with it."),
    560                 _("Make sure you own the seed phrase or the private keys, before you request Bitcoins to be sent to this wallet.")
    561             ])
    562             self.show_warning(msg, title=_('Watch-only wallet'))
    563 
    564     def warn_if_testnet(self):
    565         if not constants.net.TESTNET:
    566             return
    567         # user might have opted out already
    568         if self.config.get('dont_show_testnet_warning', False):
    569             return
    570         # only show once per process lifecycle
    571         if getattr(self.gui_object, '_warned_testnet', False):
    572             return
    573         self.gui_object._warned_testnet = True
    574         msg = ''.join([
    575             _("You are in testnet mode."), ' ',
    576             _("Testnet coins are worthless."), '\n',
    577             _("Testnet is separate from the main Bitcoin network. It is used for testing.")
    578         ])
    579         cb = QCheckBox(_("Don't show this again."))
    580         cb_checked = False
    581         def on_cb(x):
    582             nonlocal cb_checked
    583             cb_checked = x == Qt.Checked
    584         cb.stateChanged.connect(on_cb)
    585         self.show_warning(msg, title=_('Testnet'), checkbox=cb)
    586         if cb_checked:
    587             self.config.set_key('dont_show_testnet_warning', True)
    588 
    589     def open_wallet(self):
    590         try:
    591             wallet_folder = self.get_wallet_folder()
    592         except FileNotFoundError as e:
    593             self.show_error(str(e))
    594             return
    595         filename, __ = QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder)
    596         if not filename:
    597             return
    598         self.gui_object.new_window(filename)
    599 
    600     def select_backup_dir(self, b):
    601         name = self.config.get('backup_dir', '')
    602         dirname = QFileDialog.getExistingDirectory(self, "Select your wallet backup directory", name)
    603         if dirname:
    604             self.config.set_key('backup_dir', dirname)
    605             self.backup_dir_e.setText(dirname)
    606 
    607     def backup_wallet(self):
    608         d = WindowModalDialog(self, _("File Backup"))
    609         vbox = QVBoxLayout(d)
    610         grid = QGridLayout()
    611         backup_help = ""
    612         backup_dir = self.config.get('backup_dir')
    613         backup_dir_label = HelpLabel(_('Backup directory') + ':', backup_help)
    614         msg = _('Please select a backup directory')
    615         if self.wallet.has_lightning() and self.wallet.lnworker.channels:
    616             msg += '\n\n' + ' '.join([
    617                 _("Note that lightning channels will be converted to channel backups."),
    618                 _("You cannot use channel backups to perform lightning payments."),
    619                 _("Channel backups can only be used to request your channels to be closed.")
    620             ])
    621         self.backup_dir_e = QPushButton(backup_dir)
    622         self.backup_dir_e.clicked.connect(self.select_backup_dir)
    623         grid.addWidget(backup_dir_label, 1, 0)
    624         grid.addWidget(self.backup_dir_e, 1, 1)
    625         vbox.addLayout(grid)
    626         vbox.addWidget(WWLabel(msg))
    627         vbox.addLayout(Buttons(CancelButton(d), OkButton(d)))
    628         if not d.exec_():
    629             return
    630         try:
    631             new_path = self.wallet.save_backup()
    632         except BaseException as reason:
    633             self.show_critical(_("Electrum was unable to copy your wallet file to the specified location.") + "\n" + str(reason), title=_("Unable to create backup"))
    634             return
    635         if new_path:
    636             msg = _("A copy of your wallet file was created in")+" '%s'" % str(new_path)
    637             self.show_message(msg, title=_("Wallet backup created"))
    638         else:
    639             self.show_message(_("You need to configure a backup directory in your preferences"), title=_("Backup not created"))
    640 
    641     def update_recently_visited(self, filename):
    642         recent = self.config.get('recently_open', [])
    643         try:
    644             sorted(recent)
    645         except:
    646             recent = []
    647         if filename in recent:
    648             recent.remove(filename)
    649         recent.insert(0, filename)
    650         recent = [path for path in recent if os.path.exists(path)]
    651         recent = recent[:5]
    652         self.config.set_key('recently_open', recent)
    653         self.recently_visited_menu.clear()
    654         for i, k in enumerate(sorted(recent)):
    655             b = os.path.basename(k)
    656             def loader(k):
    657                 return lambda: self.gui_object.new_window(k)
    658             self.recently_visited_menu.addAction(b, loader(k)).setShortcut(QKeySequence("Ctrl+%d"%(i+1)))
    659         self.recently_visited_menu.setEnabled(len(recent))
    660 
    661     def get_wallet_folder(self):
    662         return os.path.dirname(os.path.abspath(self.wallet.storage.path))
    663 
    664     def new_wallet(self):
    665         try:
    666             wallet_folder = self.get_wallet_folder()
    667         except FileNotFoundError as e:
    668             self.show_error(str(e))
    669             return
    670         filename = get_new_wallet_name(wallet_folder)
    671         full_path = os.path.join(wallet_folder, filename)
    672         self.gui_object.start_new_window(full_path, None)
    673 
    674     def init_menubar(self):
    675         menubar = QMenuBar()
    676 
    677         file_menu = menubar.addMenu(_("&File"))
    678         self.recently_visited_menu = file_menu.addMenu(_("&Recently open"))
    679         file_menu.addAction(_("&Open"), self.open_wallet).setShortcut(QKeySequence.Open)
    680         file_menu.addAction(_("&New/Restore"), self.new_wallet).setShortcut(QKeySequence.New)
    681         file_menu.addAction(_("&Save backup"), self.backup_wallet).setShortcut(QKeySequence.SaveAs)
    682         file_menu.addAction(_("Delete"), self.remove_wallet)
    683         file_menu.addSeparator()
    684         file_menu.addAction(_("&Quit"), self.close)
    685 
    686         wallet_menu = menubar.addMenu(_("&Wallet"))
    687         wallet_menu.addAction(_("&Information"), self.show_wallet_info)
    688         wallet_menu.addSeparator()
    689         self.password_menu = wallet_menu.addAction(_("&Password"), self.change_password_dialog)
    690         self.seed_menu = wallet_menu.addAction(_("&Seed"), self.show_seed_dialog)
    691         self.private_keys_menu = wallet_menu.addMenu(_("&Private keys"))
    692         self.private_keys_menu.addAction(_("&Sweep"), self.sweep_key_dialog)
    693         self.import_privkey_menu = self.private_keys_menu.addAction(_("&Import"), self.do_import_privkey)
    694         self.export_menu = self.private_keys_menu.addAction(_("&Export"), self.export_privkeys_dialog)
    695         self.import_address_menu = wallet_menu.addAction(_("Import addresses"), self.import_addresses)
    696         wallet_menu.addSeparator()
    697 
    698         addresses_menu = wallet_menu.addMenu(_("&Addresses"))
    699         addresses_menu.addAction(_("&Filter"), lambda: self.address_list.toggle_toolbar(self.config))
    700         labels_menu = wallet_menu.addMenu(_("&Labels"))
    701         labels_menu.addAction(_("&Import"), self.do_import_labels)
    702         labels_menu.addAction(_("&Export"), self.do_export_labels)
    703         history_menu = wallet_menu.addMenu(_("&History"))
    704         history_menu.addAction(_("&Filter"), lambda: self.history_list.toggle_toolbar(self.config))
    705         history_menu.addAction(_("&Summary"), self.history_list.show_summary)
    706         history_menu.addAction(_("&Plot"), self.history_list.plot_history_dialog)
    707         history_menu.addAction(_("&Export"), self.history_list.export_history_dialog)
    708         contacts_menu = wallet_menu.addMenu(_("Contacts"))
    709         contacts_menu.addAction(_("&New"), self.new_contact_dialog)
    710         contacts_menu.addAction(_("Import"), lambda: self.import_contacts())
    711         contacts_menu.addAction(_("Export"), lambda: self.export_contacts())
    712         invoices_menu = wallet_menu.addMenu(_("Invoices"))
    713         invoices_menu.addAction(_("Import"), lambda: self.import_invoices())
    714         invoices_menu.addAction(_("Export"), lambda: self.export_invoices())
    715         requests_menu = wallet_menu.addMenu(_("Requests"))
    716         requests_menu.addAction(_("Import"), lambda: self.import_requests())
    717         requests_menu.addAction(_("Export"), lambda: self.export_requests())
    718 
    719         wallet_menu.addSeparator()
    720         wallet_menu.addAction(_("Find"), self.toggle_search).setShortcut(QKeySequence("Ctrl+F"))
    721 
    722         def add_toggle_action(view_menu, tab):
    723             is_shown = self.config.get('show_{}_tab'.format(tab.tab_name), False)
    724             item_name = (_("Hide") if is_shown else _("Show")) + " " + tab.tab_description
    725             tab.menu_action = view_menu.addAction(item_name, lambda: self.toggle_tab(tab))
    726 
    727         view_menu = menubar.addMenu(_("&View"))
    728         add_toggle_action(view_menu, self.addresses_tab)
    729         add_toggle_action(view_menu, self.utxo_tab)
    730         add_toggle_action(view_menu, self.channels_tab)
    731         add_toggle_action(view_menu, self.contacts_tab)
    732         add_toggle_action(view_menu, self.console_tab)
    733 
    734         tools_menu = menubar.addMenu(_("&Tools"))  # type: QMenu
    735         preferences_action = tools_menu.addAction(_("Preferences"), self.settings_dialog)  # type: QAction
    736         if sys.platform == 'darwin':
    737             # "Settings"/"Preferences" are all reserved keywords in macOS.
    738             # preferences_action will get picked up based on name (and put into a standardized location,
    739             # and given a standard reserved hotkey)
    740             # Hence, this menu item will be at a "uniform location re macOS processes"
    741             preferences_action.setMenuRole(QAction.PreferencesRole)  # make sure OS recognizes it as preferences
    742             # Add another preferences item, to also have a "uniform location for Electrum between different OSes"
    743             tools_menu.addAction(_("Electrum preferences"), self.settings_dialog)
    744 
    745         tools_menu.addAction(_("&Network"), self.gui_object.show_network_dialog).setEnabled(bool(self.network))
    746         tools_menu.addAction(_("Local &Watchtower"), self.gui_object.show_watchtower_dialog).setEnabled(bool(self.network and self.network.local_watchtower))
    747         tools_menu.addAction(_("&Plugins"), self.plugins_dialog)
    748         tools_menu.addSeparator()
    749         tools_menu.addAction(_("&Sign/verify message"), self.sign_verify_message)
    750         tools_menu.addAction(_("&Encrypt/decrypt message"), self.encrypt_message)
    751         tools_menu.addSeparator()
    752 
    753         paytomany_menu = tools_menu.addAction(_("&Pay to many"), self.paytomany)
    754 
    755         raw_transaction_menu = tools_menu.addMenu(_("&Load transaction"))
    756         raw_transaction_menu.addAction(_("&From file"), self.do_process_from_file)
    757         raw_transaction_menu.addAction(_("&From text"), self.do_process_from_text)
    758         raw_transaction_menu.addAction(_("&From the blockchain"), self.do_process_from_txid)
    759         raw_transaction_menu.addAction(_("&From QR code"), self.read_tx_from_qrcode)
    760         self.raw_transaction_menu = raw_transaction_menu
    761         run_hook('init_menubar_tools', self, tools_menu)
    762 
    763         help_menu = menubar.addMenu(_("&Help"))
    764         help_menu.addAction(_("&About"), self.show_about)
    765         help_menu.addAction(_("&Check for updates"), self.show_update_check)
    766         help_menu.addAction(_("&Official website"), lambda: webopen("https://electrum.org"))
    767         help_menu.addSeparator()
    768         help_menu.addAction(_("&Documentation"), lambda: webopen("http://docs.electrum.org/")).setShortcut(QKeySequence.HelpContents)
    769         if not constants.net.TESTNET:
    770             help_menu.addAction(_("&Bitcoin Paper"), self.show_bitcoin_paper)
    771         help_menu.addAction(_("&Report Bug"), self.show_report_bug)
    772         help_menu.addSeparator()
    773         help_menu.addAction(_("&Donate to server"), self.donate_to_server)
    774 
    775         self.setMenuBar(menubar)
    776 
    777     def donate_to_server(self):
    778         d = self.network.get_donation_address()
    779         if d:
    780             host = self.network.get_parameters().server.host
    781             self.pay_to_URI('bitcoin:%s?message=donation for %s'%(d, host))
    782         else:
    783             self.show_error(_('No donation address for this server'))
    784 
    785     def show_about(self):
    786         QMessageBox.about(self, "Electrum",
    787                           (_("Version")+" %s" % ELECTRUM_VERSION + "\n\n" +
    788                            _("Electrum's focus is speed, with low resource usage and simplifying Bitcoin.") + " " +
    789                            _("You do not need to perform regular backups, because your wallet can be "
    790                               "recovered from a secret phrase that you can memorize or write on paper.") + " " +
    791                            _("Startup times are instant because it operates in conjunction with high-performance "
    792                               "servers that handle the most complicated parts of the Bitcoin system.") + "\n\n" +
    793                            _("Uses icons from the Icons8 icon pack (icons8.com).")))
    794 
    795     def show_bitcoin_paper(self):
    796         filename = os.path.join(self.config.path, 'bitcoin.pdf')
    797         if not os.path.exists(filename):
    798             s = self._fetch_tx_from_network("54e48e5f5c656b26c3bca14a8c95aa583d07ebe84dde3b7dd4a78f4e4186e713")
    799             if not s:
    800                 return
    801             s = s.split("0100000000000000")[1:-1]
    802             out = ''.join(x[6:136] + x[138:268] + x[270:400] if len(x) > 136 else x[6:] for x in s)[16:-20]
    803             with open(filename, 'wb') as f:
    804                 f.write(bytes.fromhex(out))
    805         webopen('file:///' + filename)
    806 
    807     def show_update_check(self, version=None):
    808         self.gui_object._update_check = UpdateCheck(latest_version=version)
    809 
    810     def show_report_bug(self):
    811         msg = ' '.join([
    812             _("Please report any bugs as issues on github:<br/>"),
    813             f'''<a href="{constants.GIT_REPO_ISSUES_URL}">{constants.GIT_REPO_ISSUES_URL}</a><br/><br/>''',
    814             _("Before reporting a bug, upgrade to the most recent version of Electrum (latest release or git HEAD), and include the version number in your report."),
    815             _("Try to explain not only what the bug is, but how it occurs.")
    816          ])
    817         self.show_message(msg, title="Electrum - " + _("Reporting Bugs"), rich_text=True)
    818 
    819     def notify_transactions(self):
    820         if self.tx_notification_queue.qsize() == 0:
    821             return
    822         if not self.wallet.up_to_date:
    823             return  # no notifications while syncing
    824         now = time.time()
    825         rate_limit = 20  # seconds
    826         if self.tx_notification_last_time + rate_limit > now:
    827             return
    828         self.tx_notification_last_time = now
    829         self.logger.info("Notifying GUI about new transactions")
    830         txns = []
    831         while True:
    832             try:
    833                 txns.append(self.tx_notification_queue.get_nowait())
    834             except queue.Empty:
    835                 break
    836         # Combine the transactions if there are at least three
    837         if len(txns) >= 3:
    838             total_amount = 0
    839             for tx in txns:
    840                 tx_wallet_delta = self.wallet.get_wallet_delta(tx)
    841                 if not tx_wallet_delta.is_relevant:
    842                     continue
    843                 total_amount += tx_wallet_delta.delta
    844             self.notify(_("{} new transactions: Total amount received in the new transactions {}")
    845                         .format(len(txns), self.format_amount_and_units(total_amount)))
    846         else:
    847             for tx in txns:
    848                 tx_wallet_delta = self.wallet.get_wallet_delta(tx)
    849                 if not tx_wallet_delta.is_relevant:
    850                     continue
    851                 self.notify(_("New transaction: {}").format(self.format_amount_and_units(tx_wallet_delta.delta)))
    852 
    853     def notify(self, message):
    854         if self.tray:
    855             try:
    856                 # this requires Qt 5.9
    857                 self.tray.showMessage("Electrum", message, read_QIcon("electrum_dark_icon"), 20000)
    858             except TypeError:
    859                 self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
    860 
    861     def timer_actions(self):
    862         self.request_list.refresh_status()
    863         # Note this runs in the GUI thread
    864         if self.need_update.is_set():
    865             self.need_update.clear()
    866             self.update_wallet()
    867         elif not self.wallet.up_to_date:
    868             # this updates "synchronizing" progress
    869             self.update_status()
    870         # resolve aliases
    871         # FIXME this is a blocking network call that has a timeout of 5 sec
    872         self.payto_e.resolve()
    873         self.notify_transactions()
    874 
    875     def format_amount(self, amount_sat, is_diff=False, whitespaces=False) -> str:
    876         """Formats amount as string, converting to desired unit.
    877         E.g. 500_000 -> '0.005'
    878         """
    879         return self.config.format_amount(amount_sat, is_diff=is_diff, whitespaces=whitespaces)
    880 
    881     def format_amount_and_units(self, amount_sat) -> str:
    882         """Returns string with both bitcoin and fiat amounts, in desired units.
    883         E.g. 500_000 -> '0.005 BTC (191.42 EUR)'
    884         """
    885         text = self.config.format_amount_and_units(amount_sat)
    886         x = self.fx.format_amount_and_units(amount_sat) if self.fx else None
    887         if text and x:
    888             text += ' (%s)'%x
    889         return text
    890 
    891     def format_fiat_and_units(self, amount_sat) -> str:
    892         """Returns string of FX fiat amount, in desired units.
    893         E.g. 500_000 -> '191.42 EUR'
    894         """
    895         return self.fx.format_amount_and_units(amount_sat) if self.fx else ''
    896 
    897     def format_fee_rate(self, fee_rate):
    898         return self.config.format_fee_rate(fee_rate)
    899 
    900     def get_decimal_point(self):
    901         return self.config.get_decimal_point()
    902 
    903     def base_unit(self):
    904         return self.config.get_base_unit()
    905 
    906     def connect_fields(self, window, btc_e, fiat_e, fee_e):
    907 
    908         def edit_changed(edit):
    909             if edit.follows:
    910                 return
    911             edit.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet())
    912             fiat_e.is_last_edited = (edit == fiat_e)
    913             amount = edit.get_amount()
    914             rate = self.fx.exchange_rate() if self.fx else Decimal('NaN')
    915             if rate.is_nan() or amount is None:
    916                 if edit is fiat_e:
    917                     btc_e.setText("")
    918                     if fee_e:
    919                         fee_e.setText("")
    920                 else:
    921                     fiat_e.setText("")
    922             else:
    923                 if edit is fiat_e:
    924                     btc_e.follows = True
    925                     btc_e.setAmount(int(amount / Decimal(rate) * COIN))
    926                     btc_e.setStyleSheet(ColorScheme.BLUE.as_stylesheet())
    927                     btc_e.follows = False
    928                     if fee_e:
    929                         window.update_fee()
    930                 else:
    931                     fiat_e.follows = True
    932                     fiat_e.setText(self.fx.ccy_amount_str(
    933                         amount * Decimal(rate) / COIN, False))
    934                     fiat_e.setStyleSheet(ColorScheme.BLUE.as_stylesheet())
    935                     fiat_e.follows = False
    936 
    937         btc_e.follows = False
    938         fiat_e.follows = False
    939         fiat_e.textChanged.connect(partial(edit_changed, fiat_e))
    940         btc_e.textChanged.connect(partial(edit_changed, btc_e))
    941         fiat_e.is_last_edited = False
    942 
    943     def update_status(self):
    944         if not self.wallet:
    945             return
    946 
    947         if self.network is None:
    948             text = _("Offline")
    949             icon = read_QIcon("status_disconnected.png")
    950 
    951         elif self.network.is_connected():
    952             server_height = self.network.get_server_height()
    953             server_lag = self.network.get_local_height() - server_height
    954             fork_str = "_fork" if len(self.network.get_blockchains())>1 else ""
    955             # Server height can be 0 after switching to a new server
    956             # until we get a headers subscription request response.
    957             # Display the synchronizing message in that case.
    958             if not self.wallet.up_to_date or server_height == 0:
    959                 num_sent, num_answered = self.wallet.get_history_sync_state_details()
    960                 text = ("{} ({}/{})"
    961                         .format(_("Synchronizing..."), num_answered, num_sent))
    962                 icon = read_QIcon("status_waiting.png")
    963             elif server_lag > 1:
    964                 text = _("Server is lagging ({} blocks)").format(server_lag)
    965                 icon = read_QIcon("status_lagging%s.png"%fork_str)
    966             else:
    967                 c, u, x = self.wallet.get_balance()
    968                 text =  _("Balance" ) + ": %s "%(self.format_amount_and_units(c))
    969                 if u:
    970                     text +=  " [%s unconfirmed]"%(self.format_amount(u, is_diff=True).strip())
    971                 if x:
    972                     text +=  " [%s unmatured]"%(self.format_amount(x, is_diff=True).strip())
    973                 if self.wallet.has_lightning():
    974                     l = self.wallet.lnworker.get_balance()
    975                     text += u'    \U000026a1 %s'%(self.format_amount_and_units(l).strip())
    976                 # append fiat balance and price
    977                 if self.fx.is_enabled():
    978                     text += self.fx.get_fiat_status_text(c + u + x,
    979                         self.base_unit(), self.get_decimal_point()) or ''
    980                 if not self.network.proxy:
    981                     icon = read_QIcon("status_connected%s.png"%fork_str)
    982                 else:
    983                     icon = read_QIcon("status_connected_proxy%s.png"%fork_str)
    984         else:
    985             if self.network.proxy:
    986                 text = "{} ({})".format(_("Not connected"), _("proxy enabled"))
    987             else:
    988                 text = _("Not connected")
    989             icon = read_QIcon("status_disconnected.png")
    990 
    991         self.tray.setToolTip("%s (%s)" % (text, self.wallet.basename()))
    992         self.balance_label.setText(text)
    993         if self.status_button:
    994             self.status_button.setIcon( icon )
    995 
    996     def update_wallet(self):
    997         self.update_status()
    998         if self.wallet.up_to_date or not self.network or not self.network.is_connected():
    999             self.update_tabs()
   1000 
   1001     def update_tabs(self, wallet=None):
   1002         if wallet is None:
   1003             wallet = self.wallet
   1004         if wallet != self.wallet:
   1005             return
   1006         self.history_model.refresh('update_tabs')
   1007         self.request_list.update()
   1008         self.address_list.update()
   1009         self.utxo_list.update()
   1010         self.contact_list.update()
   1011         self.invoice_list.update()
   1012         self.channels_list.update_rows.emit(wallet)
   1013         self.update_completions()
   1014 
   1015     def create_channels_tab(self):
   1016         self.channels_list = ChannelsList(self)
   1017         t = self.channels_list.get_toolbar()
   1018         return self.create_list_tab(self.channels_list, t)
   1019 
   1020     def create_history_tab(self):
   1021         self.history_model = HistoryModel(self)
   1022         self.history_list = l = HistoryList(self, self.history_model)
   1023         self.history_model.set_view(self.history_list)
   1024         l.searchable_list = l
   1025         toolbar = l.create_toolbar(self.config)
   1026         toolbar_shown = bool(self.config.get('show_toolbar_history', False))
   1027         l.show_toolbar(toolbar_shown)
   1028         return self.create_list_tab(l, toolbar)
   1029 
   1030     def show_address(self, addr):
   1031         from . import address_dialog
   1032         d = address_dialog.AddressDialog(self, addr)
   1033         d.exec_()
   1034 
   1035     def show_channel(self, channel_id):
   1036         from . import channel_details
   1037         channel_details.ChannelDetailsDialog(self, channel_id).show()
   1038 
   1039     def show_transaction(self, tx, *, tx_desc=None):
   1040         '''tx_desc is set only for txs created in the Send tab'''
   1041         show_transaction(tx, parent=self, desc=tx_desc)
   1042 
   1043     def show_lightning_transaction(self, tx_item):
   1044         from .lightning_tx_dialog import LightningTxDialog
   1045         d = LightningTxDialog(self, tx_item)
   1046         d.show()
   1047 
   1048     def create_receive_tab(self):
   1049         # A 4-column grid layout.  All the stretch is in the last column.
   1050         # The exchange rate plugin adds a fiat widget in column 2
   1051         self.receive_grid = grid = QGridLayout()
   1052         grid.setSpacing(8)
   1053         grid.setColumnStretch(3, 1)
   1054 
   1055         self.receive_message_e = QLineEdit()
   1056         grid.addWidget(QLabel(_('Description')), 0, 0)
   1057         grid.addWidget(self.receive_message_e, 0, 1, 1, 4)
   1058         self.receive_message_e.textChanged.connect(self.update_receive_qr)
   1059 
   1060         self.receive_amount_e = BTCAmountEdit(self.get_decimal_point)
   1061         grid.addWidget(QLabel(_('Requested amount')), 1, 0)
   1062         grid.addWidget(self.receive_amount_e, 1, 1)
   1063         self.receive_amount_e.textChanged.connect(self.update_receive_qr)
   1064 
   1065         self.fiat_receive_e = AmountEdit(self.fx.get_currency if self.fx else '')
   1066         if not self.fx or not self.fx.is_enabled():
   1067             self.fiat_receive_e.setVisible(False)
   1068         grid.addWidget(self.fiat_receive_e, 1, 2, Qt.AlignLeft)
   1069 
   1070         self.connect_fields(self, self.receive_amount_e, self.fiat_receive_e, None)
   1071         self.connect_fields(self, self.amount_e, self.fiat_send_e, None)
   1072 
   1073         self.expires_combo = QComboBox()
   1074         evl = sorted(pr_expiration_values.items())
   1075         evl_keys = [i[0] for i in evl]
   1076         evl_values = [i[1] for i in evl]
   1077         default_expiry = self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING)
   1078         try:
   1079             i = evl_keys.index(default_expiry)
   1080         except ValueError:
   1081             i = 0
   1082         self.expires_combo.addItems(evl_values)
   1083         self.expires_combo.setCurrentIndex(i)
   1084         self.expires_combo.setFixedWidth(self.receive_amount_e.width())
   1085         def on_expiry(i):
   1086             self.config.set_key('request_expiry', evl_keys[i])
   1087         self.expires_combo.currentIndexChanged.connect(on_expiry)
   1088         msg = ' '.join([
   1089             _('Expiration date of your request.'),
   1090             _('This information is seen by the recipient if you send them a signed payment request.'),
   1091             _('Expired requests have to be deleted manually from your list, in order to free the corresponding Bitcoin addresses.'),
   1092             _('The bitcoin address never expires and will always be part of this electrum wallet.'),
   1093         ])
   1094         grid.addWidget(HelpLabel(_('Expires after'), msg), 2, 0)
   1095         grid.addWidget(self.expires_combo, 2, 1)
   1096         self.expires_label = QLineEdit('')
   1097         self.expires_label.setReadOnly(1)
   1098         self.expires_label.setFocusPolicy(Qt.NoFocus)
   1099         self.expires_label.hide()
   1100         grid.addWidget(self.expires_label, 2, 1)
   1101 
   1102         self.clear_invoice_button = QPushButton(_('Clear'))
   1103         self.clear_invoice_button.clicked.connect(self.clear_receive_tab)
   1104         self.create_invoice_button = QPushButton(_('New Address'))
   1105         self.create_invoice_button.setIcon(read_QIcon("bitcoin.png"))
   1106         self.create_invoice_button.setToolTip('Create on-chain request')
   1107         self.create_invoice_button.clicked.connect(lambda: self.create_invoice(False))
   1108         self.receive_buttons = buttons = QHBoxLayout()
   1109         buttons.addStretch(1)
   1110         buttons.addWidget(self.clear_invoice_button)
   1111         buttons.addWidget(self.create_invoice_button)
   1112         if self.wallet.has_lightning():
   1113             self.create_invoice_button.setText(_('New Address'))
   1114             self.create_lightning_invoice_button = QPushButton(_('Lightning'))
   1115             self.create_lightning_invoice_button.setToolTip('Create lightning request')
   1116             self.create_lightning_invoice_button.setIcon(read_QIcon("lightning.png"))
   1117             self.create_lightning_invoice_button.clicked.connect(lambda: self.create_invoice(True))
   1118             buttons.addWidget(self.create_lightning_invoice_button)
   1119         grid.addLayout(buttons, 4, 3, 1, 2)
   1120 
   1121         self.receive_payreq_e = ButtonsTextEdit()
   1122         self.receive_payreq_e.setFont(QFont(MONOSPACE_FONT))
   1123         self.receive_payreq_e.addCopyButton(self.app)
   1124         self.receive_payreq_e.setReadOnly(True)
   1125         self.receive_payreq_e.textChanged.connect(self.update_receive_qr)
   1126         self.receive_payreq_e.setFocusPolicy(Qt.ClickFocus)
   1127 
   1128         self.receive_qr = QRCodeWidget(fixedSize=220)
   1129         self.receive_qr.mouseReleaseEvent = lambda x: self.toggle_qr_window()
   1130         self.receive_qr.enterEvent = lambda x: self.app.setOverrideCursor(QCursor(Qt.PointingHandCursor))
   1131         self.receive_qr.leaveEvent = lambda x: self.app.setOverrideCursor(QCursor(Qt.ArrowCursor))
   1132 
   1133         self.receive_address_e = ButtonsTextEdit()
   1134         self.receive_address_e.setFont(QFont(MONOSPACE_FONT))
   1135         self.receive_address_e.addCopyButton(self.app)
   1136         self.receive_address_e.setReadOnly(True)
   1137         self.receive_address_e.textChanged.connect(self.update_receive_address_styling)
   1138 
   1139         qr_show = lambda: self.show_qrcode(str(self.receive_address_e.text()), _('Receiving address'), parent=self)
   1140         qr_icon = "qrcode_white.png" if ColorScheme.dark_scheme else "qrcode.png"
   1141         self.receive_address_e.addButton(qr_icon, qr_show, _("Show as QR code"))
   1142 
   1143         self.receive_requests_label = QLabel(_('Receive queue'))
   1144 
   1145         from .request_list import RequestList
   1146         self.request_list = RequestList(self)
   1147 
   1148         receive_tabs = QTabWidget()
   1149         receive_tabs.addTab(self.receive_address_e, _('Address'))
   1150         receive_tabs.addTab(self.receive_payreq_e, _('Request'))
   1151         receive_tabs.addTab(self.receive_qr, _('QR Code'))
   1152         receive_tabs.setCurrentIndex(self.config.get('receive_tabs_index', 0))
   1153         receive_tabs.currentChanged.connect(lambda i: self.config.set_key('receive_tabs_index', i))
   1154         receive_tabs_sp = receive_tabs.sizePolicy()
   1155         receive_tabs_sp.setRetainSizeWhenHidden(True)
   1156         receive_tabs.setSizePolicy(receive_tabs_sp)
   1157 
   1158         def maybe_hide_receive_tabs():
   1159             receive_tabs.setVisible(bool(self.receive_payreq_e.text()))
   1160         self.receive_payreq_e.textChanged.connect(maybe_hide_receive_tabs)
   1161         maybe_hide_receive_tabs()
   1162 
   1163         # layout
   1164         vbox_g = QVBoxLayout()
   1165         vbox_g.addLayout(grid)
   1166         vbox_g.addStretch()
   1167         hbox = QHBoxLayout()
   1168         hbox.addLayout(vbox_g)
   1169         hbox.addStretch()
   1170         hbox.addWidget(receive_tabs)
   1171 
   1172         w = QWidget()
   1173         w.searchable_list = self.request_list
   1174         vbox = QVBoxLayout(w)
   1175         vbox.addLayout(hbox)
   1176 
   1177         vbox.addStretch(1)
   1178         vbox.addWidget(self.receive_requests_label)
   1179         vbox.addWidget(self.request_list)
   1180         vbox.setStretchFactor(self.request_list, 1000)
   1181 
   1182         return w
   1183 
   1184     def delete_requests(self, keys):
   1185         for key in keys:
   1186             self.wallet.delete_request(key)
   1187         self.request_list.update()
   1188         self.clear_receive_tab()
   1189 
   1190     def delete_lightning_payreq(self, payreq_key):
   1191         self.wallet.lnworker.delete_invoice(payreq_key)
   1192         self.request_list.update()
   1193         self.invoice_list.update()
   1194         self.clear_receive_tab()
   1195 
   1196     def sign_payment_request(self, addr):
   1197         alias = self.config.get('alias')
   1198         if alias and self.alias_info:
   1199             alias_addr, alias_name, validated = self.alias_info
   1200             if alias_addr:
   1201                 if self.wallet.is_mine(alias_addr):
   1202                     msg = _('This payment request will be signed.') + '\n' + _('Please enter your password')
   1203                     password = None
   1204                     if self.wallet.has_keystore_encryption():
   1205                         password = self.password_dialog(msg)
   1206                         if not password:
   1207                             return
   1208                     try:
   1209                         self.wallet.sign_payment_request(addr, alias, alias_addr, password)
   1210                     except Exception as e:
   1211                         self.show_error(repr(e))
   1212                         return
   1213                 else:
   1214                     return
   1215 
   1216     def create_invoice(self, is_lightning):
   1217         amount = self.receive_amount_e.get_amount()
   1218         message = self.receive_message_e.text()
   1219         expiry = self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING)
   1220         if is_lightning:
   1221             if not self.wallet.lnworker.channels:
   1222                 self.show_error(_("You need to open a Lightning channel first."))
   1223                 return
   1224             # TODO maybe show a warning if amount exceeds lnworker.num_sats_can_receive (as in kivy)
   1225             key = self.wallet.lnworker.add_request(amount, message, expiry)
   1226         else:
   1227             key = self.create_bitcoin_request(amount, message, expiry)
   1228             if not key:
   1229                 return
   1230             self.address_list.update()
   1231         assert key is not None
   1232         self.request_list.update()
   1233         self.request_list.select_key(key)
   1234         # clear request fields
   1235         self.receive_amount_e.setText('')
   1236         self.receive_message_e.setText('')
   1237         # copy to clipboard
   1238         r = self.wallet.get_request(key)
   1239         content = r.invoice if r.is_lightning() else r.get_address()
   1240         title = _('Invoice') if is_lightning else _('Address')
   1241         self.do_copy(content, title=title)
   1242 
   1243     def create_bitcoin_request(self, amount, message, expiration) -> Optional[str]:
   1244         addr = self.wallet.get_unused_address()
   1245         if addr is None:
   1246             if not self.wallet.is_deterministic():  # imported wallet
   1247                 msg = [
   1248                     _('No more addresses in your wallet.'), ' ',
   1249                     _('You are using a non-deterministic wallet, which cannot create new addresses.'), ' ',
   1250                     _('If you want to create new addresses, use a deterministic wallet instead.'), '\n\n',
   1251                     _('Creating a new payment request will reuse one of your addresses and overwrite an existing request. Continue anyway?'),
   1252                    ]
   1253                 if not self.question(''.join(msg)):
   1254                     return
   1255                 addr = self.wallet.get_receiving_address()
   1256             else:  # deterministic wallet
   1257                 if not self.question(_("Warning: The next address will not be recovered automatically if you restore your wallet from seed; you may need to add it manually.\n\nThis occurs because you have too many unused addresses in your wallet. To avoid this situation, use the existing addresses first.\n\nCreate anyway?")):
   1258                     return
   1259                 addr = self.wallet.create_new_address(False)
   1260         req = self.wallet.make_payment_request(addr, amount, message, expiration)
   1261         try:
   1262             self.wallet.add_payment_request(req)
   1263         except Exception as e:
   1264             self.logger.exception('Error adding payment request')
   1265             self.show_error(_('Error adding payment request') + ':\n' + repr(e))
   1266         else:
   1267             self.sign_payment_request(addr)
   1268         return addr
   1269 
   1270     def do_copy(self, content: str, *, title: str = None) -> None:
   1271         self.app.clipboard().setText(content)
   1272         if title is None:
   1273             tooltip_text = _("Text copied to clipboard").format(title)
   1274         else:
   1275             tooltip_text = _("{} copied to clipboard").format(title)
   1276         QToolTip.showText(QCursor.pos(), tooltip_text, self)
   1277 
   1278     def clear_receive_tab(self):
   1279         self.receive_payreq_e.setText('')
   1280         self.receive_address_e.setText('')
   1281         self.receive_message_e.setText('')
   1282         self.receive_amount_e.setAmount(None)
   1283         self.expires_label.hide()
   1284         self.expires_combo.show()
   1285         self.request_list.clearSelection()
   1286 
   1287     def toggle_qr_window(self):
   1288         from . import qrwindow
   1289         if not self.qr_window:
   1290             self.qr_window = qrwindow.QR_Window(self)
   1291             self.qr_window.setVisible(True)
   1292             self.qr_window_geometry = self.qr_window.geometry()
   1293         else:
   1294             if not self.qr_window.isVisible():
   1295                 self.qr_window.setVisible(True)
   1296                 self.qr_window.setGeometry(self.qr_window_geometry)
   1297             else:
   1298                 self.qr_window_geometry = self.qr_window.geometry()
   1299                 self.qr_window.setVisible(False)
   1300         self.update_receive_qr()
   1301 
   1302     def show_send_tab(self):
   1303         self.tabs.setCurrentIndex(self.tabs.indexOf(self.send_tab))
   1304 
   1305     def show_receive_tab(self):
   1306         self.tabs.setCurrentIndex(self.tabs.indexOf(self.receive_tab))
   1307 
   1308     def update_receive_qr(self):
   1309         uri = str(self.receive_payreq_e.text())
   1310         if maybe_extract_bolt11_invoice(uri):
   1311             # encode lightning invoices as uppercase so QR encoding can use
   1312             # alphanumeric mode; resulting in smaller QR codes
   1313             uri = uri.upper()
   1314         self.receive_qr.setData(uri)
   1315         if self.qr_window and self.qr_window.isVisible():
   1316             self.qr_window.qrw.setData(uri)
   1317 
   1318     def update_receive_address_styling(self):
   1319         addr = str(self.receive_address_e.text())
   1320         if is_address(addr) and self.wallet.is_used(addr):
   1321             self.receive_address_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True))
   1322             self.receive_address_e.setToolTip(_("This address has already been used. "
   1323                                                 "For better privacy, do not reuse it for new payments."))
   1324         else:
   1325             self.receive_address_e.setStyleSheet("")
   1326             self.receive_address_e.setToolTip("")
   1327 
   1328     def create_send_tab(self):
   1329         # A 4-column grid layout.  All the stretch is in the last column.
   1330         # The exchange rate plugin adds a fiat widget in column 2
   1331         self.send_grid = grid = QGridLayout()
   1332         grid.setSpacing(8)
   1333         grid.setColumnStretch(3, 1)
   1334 
   1335         from .paytoedit import PayToEdit
   1336         self.amount_e = BTCAmountEdit(self.get_decimal_point)
   1337         self.payto_e = PayToEdit(self)
   1338         self.payto_e.addPasteButton(self.app)
   1339         msg = _('Recipient of the funds.') + '\n\n'\
   1340               + _('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)')
   1341         payto_label = HelpLabel(_('Pay to'), msg)
   1342         grid.addWidget(payto_label, 1, 0)
   1343         grid.addWidget(self.payto_e, 1, 1, 1, -1)
   1344 
   1345         completer = QCompleter()
   1346         completer.setCaseSensitivity(False)
   1347         self.payto_e.set_completer(completer)
   1348         completer.setModel(self.completions)
   1349 
   1350         msg = _('Description of the transaction (not mandatory).') + '\n\n'\
   1351               + _('The description is not sent to the recipient of the funds. It is stored in your wallet file, and displayed in the \'History\' tab.')
   1352         description_label = HelpLabel(_('Description'), msg)
   1353         grid.addWidget(description_label, 2, 0)
   1354         self.message_e = FreezableLineEdit()
   1355         self.message_e.setMinimumWidth(700)
   1356         grid.addWidget(self.message_e, 2, 1, 1, -1)
   1357 
   1358         msg = _('Amount to be sent.') + '\n\n' \
   1359               + _('The amount will be displayed in red if you do not have enough funds in your wallet.') + ' ' \
   1360               + _('Note that if you have frozen some of your addresses, the available funds will be lower than your total balance.') + '\n\n' \
   1361               + _('Keyboard shortcut: type "!" to send all your coins.')
   1362         amount_label = HelpLabel(_('Amount'), msg)
   1363         grid.addWidget(amount_label, 3, 0)
   1364         grid.addWidget(self.amount_e, 3, 1)
   1365 
   1366         self.fiat_send_e = AmountEdit(self.fx.get_currency if self.fx else '')
   1367         if not self.fx or not self.fx.is_enabled():
   1368             self.fiat_send_e.setVisible(False)
   1369         grid.addWidget(self.fiat_send_e, 3, 2)
   1370         self.amount_e.frozen.connect(
   1371             lambda: self.fiat_send_e.setFrozen(self.amount_e.isReadOnly()))
   1372 
   1373         self.max_button = EnterButton(_("Max"), self.spend_max)
   1374         self.max_button.setFixedWidth(100)
   1375         self.max_button.setCheckable(True)
   1376         grid.addWidget(self.max_button, 3, 3)
   1377 
   1378         self.save_button = EnterButton(_("Save"), self.do_save_invoice)
   1379         self.send_button = EnterButton(_("Pay") + "...", self.do_pay)
   1380         self.clear_button = EnterButton(_("Clear"), self.do_clear)
   1381 
   1382         buttons = QHBoxLayout()
   1383         buttons.addStretch(1)
   1384         buttons.addWidget(self.clear_button)
   1385         buttons.addWidget(self.save_button)
   1386         buttons.addWidget(self.send_button)
   1387         grid.addLayout(buttons, 6, 1, 1, 4)
   1388 
   1389         self.amount_e.shortcut.connect(self.spend_max)
   1390 
   1391         def reset_max(text):
   1392             self.max_button.setChecked(False)
   1393             enable = not bool(text) and not self.amount_e.isReadOnly()
   1394             #self.max_button.setEnabled(enable)
   1395         self.amount_e.textEdited.connect(reset_max)
   1396         self.fiat_send_e.textEdited.connect(reset_max)
   1397 
   1398         self.set_onchain(False)
   1399 
   1400         self.invoices_label = QLabel(_('Send queue'))
   1401         from .invoice_list import InvoiceList
   1402         self.invoice_list = InvoiceList(self)
   1403 
   1404         vbox0 = QVBoxLayout()
   1405         vbox0.addLayout(grid)
   1406         hbox = QHBoxLayout()
   1407         hbox.addLayout(vbox0)
   1408         hbox.addStretch(1)
   1409         w = QWidget()
   1410         vbox = QVBoxLayout(w)
   1411         vbox.addLayout(hbox)
   1412         vbox.addStretch(1)
   1413         vbox.addWidget(self.invoices_label)
   1414         vbox.addWidget(self.invoice_list)
   1415         vbox.setStretchFactor(self.invoice_list, 1000)
   1416         w.searchable_list = self.invoice_list
   1417         run_hook('create_send_tab', grid)
   1418         return w
   1419 
   1420     def spend_max(self):
   1421         if run_hook('abort_send', self):
   1422             return
   1423         outputs = self.payto_e.get_outputs(True)
   1424         if not outputs:
   1425             return
   1426         make_tx = lambda fee_est: self.wallet.make_unsigned_transaction(
   1427             coins=self.get_coins(),
   1428             outputs=outputs,
   1429             fee=fee_est,
   1430             is_sweep=False)
   1431 
   1432         try:
   1433             try:
   1434                 tx = make_tx(None)
   1435             except (NotEnoughFunds, NoDynamicFeeEstimates) as e:
   1436                 # Check if we had enough funds excluding fees,
   1437                 # if so, still provide opportunity to set lower fees.
   1438                 tx = make_tx(0)
   1439         except MultipleSpendMaxTxOutputs as e:
   1440             self.max_button.setChecked(False)
   1441             self.show_error(str(e))
   1442             return
   1443         except NotEnoughFunds as e:
   1444             self.max_button.setChecked(False)
   1445             text = self.get_text_not_enough_funds_mentioning_frozen()
   1446             self.show_error(text)
   1447             return
   1448 
   1449         self.max_button.setChecked(True)
   1450         amount = tx.output_value()
   1451         __, x_fee_amount = run_hook('get_tx_extra_fee', self.wallet, tx) or (None, 0)
   1452         amount_after_all_fees = amount - x_fee_amount
   1453         self.amount_e.setAmount(amount_after_all_fees)
   1454 
   1455     def get_contact_payto(self, key):
   1456         _type, label = self.contacts.get(key)
   1457         return label + '  <' + key + '>' if _type == 'address' else key
   1458 
   1459     def update_completions(self):
   1460         l = [self.get_contact_payto(key) for key in self.contacts.keys()]
   1461         self.completions.setStringList(l)
   1462 
   1463     @protected
   1464     def protect(self, func, args, password):
   1465         return func(*args, password)
   1466 
   1467     def read_outputs(self) -> List[PartialTxOutput]:
   1468         if self.payment_request:
   1469             outputs = self.payment_request.get_outputs()
   1470         else:
   1471             outputs = self.payto_e.get_outputs(self.max_button.isChecked())
   1472         return outputs
   1473 
   1474     def check_send_tab_onchain_outputs_and_show_errors(self, outputs: List[PartialTxOutput]) -> bool:
   1475         """Returns whether there are errors with outputs.
   1476         Also shows error dialog to user if so.
   1477         """
   1478         if not outputs:
   1479             self.show_error(_('No outputs'))
   1480             return True
   1481 
   1482         for o in outputs:
   1483             if o.scriptpubkey is None:
   1484                 self.show_error(_('Bitcoin Address is None'))
   1485                 return True
   1486             if o.value is None:
   1487                 self.show_error(_('Invalid Amount'))
   1488                 return True
   1489 
   1490         return False  # no errors
   1491 
   1492     def check_send_tab_payto_line_and_show_errors(self) -> bool:
   1493         """Returns whether there are errors.
   1494         Also shows error dialog to user if so.
   1495         """
   1496         pr = self.payment_request
   1497         if pr:
   1498             if pr.has_expired():
   1499                 self.show_error(_('Payment request has expired'))
   1500                 return True
   1501 
   1502         if not pr:
   1503             errors = self.payto_e.get_errors()
   1504             if errors:
   1505                 if len(errors) == 1 and not errors[0].is_multiline:
   1506                     err = errors[0]
   1507                     self.show_warning(_("Failed to parse 'Pay to' line") + ":\n" +
   1508                                       f"{err.line_content[:40]}...\n\n"
   1509                                       f"{err.exc!r}")
   1510                 else:
   1511                     self.show_warning(_("Invalid Lines found:") + "\n\n" +
   1512                                       '\n'.join([_("Line #") +
   1513                                                  f"{err.idx+1}: {err.line_content[:40]}... ({err.exc!r})"
   1514                                                  for err in errors]))
   1515                 return True
   1516 
   1517             if self.payto_e.is_alias and self.payto_e.validated is False:
   1518                 alias = self.payto_e.toPlainText()
   1519                 msg = _('WARNING: the alias "{}" could not be validated via an additional '
   1520                         'security check, DNSSEC, and thus may not be correct.').format(alias) + '\n'
   1521                 msg += _('Do you wish to continue?')
   1522                 if not self.question(msg):
   1523                     return True
   1524 
   1525         return False  # no errors
   1526 
   1527     def pay_lightning_invoice(self, invoice: str, *, amount_msat: Optional[int]):
   1528         if amount_msat is None:
   1529             raise Exception("missing amount for LN invoice")
   1530         amount_sat = Decimal(amount_msat) / 1000
   1531         # FIXME this is currently lying to user as we truncate to satoshis
   1532         msg = _("Pay lightning invoice?") + '\n\n' + _("This will send {}?").format(self.format_amount_and_units(amount_sat))
   1533         if not self.question(msg):
   1534             return
   1535         self.save_pending_invoice()
   1536         def task():
   1537             coro = self.wallet.lnworker.pay_invoice(invoice, amount_msat=amount_msat, attempts=LN_NUM_PAYMENT_ATTEMPTS)
   1538             fut = asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop)
   1539             return fut.result()
   1540         self.wallet.thread.add(task)
   1541 
   1542     def on_request_status(self, wallet, key, status):
   1543         if wallet != self.wallet:
   1544             return
   1545         req = self.wallet.receive_requests.get(key)
   1546         if req is None:
   1547             return
   1548         if status == PR_PAID:
   1549             self.notify(_('Payment received') + '\n' + key)
   1550             self.need_update.set()
   1551         else:
   1552             self.request_list.update_item(key, req)
   1553 
   1554     def on_invoice_status(self, wallet, key):
   1555         if wallet != self.wallet:
   1556             return
   1557         invoice = self.wallet.get_invoice(key)
   1558         if invoice is None:
   1559             return
   1560         status = self.wallet.get_invoice_status(invoice)
   1561         if status == PR_PAID:
   1562             self.invoice_list.update()
   1563         else:
   1564             self.invoice_list.update_item(key, invoice)
   1565 
   1566     def on_payment_succeeded(self, wallet, key):
   1567         description = self.wallet.get_label(key)
   1568         self.notify(_('Payment succeeded') + '\n\n' + description)
   1569         self.need_update.set()
   1570 
   1571     def on_payment_failed(self, wallet, key, reason):
   1572         self.show_error(_('Payment failed') + '\n\n' + reason)
   1573 
   1574     def read_invoice(self):
   1575         if self.check_send_tab_payto_line_and_show_errors():
   1576             return
   1577         if not self._is_onchain:
   1578             invoice_str = self.payto_e.lightning_invoice
   1579             if not invoice_str:
   1580                 return
   1581             if not self.wallet.has_lightning():
   1582                 self.show_error(_('Lightning is disabled'))
   1583                 return
   1584             invoice = LNInvoice.from_bech32(invoice_str)
   1585             if invoice.get_amount_msat() is None:
   1586                 amount_sat = self.amount_e.get_amount()
   1587                 if amount_sat:
   1588                     invoice.amount_msat = int(amount_sat * 1000)
   1589                 else:
   1590                     self.show_error(_('No amount'))
   1591                     return
   1592             return invoice
   1593         else:
   1594             outputs = self.read_outputs()
   1595             if self.check_send_tab_onchain_outputs_and_show_errors(outputs):
   1596                 return
   1597             message = self.message_e.text()
   1598             return self.wallet.create_invoice(
   1599                 outputs=outputs,
   1600                 message=message,
   1601                 pr=self.payment_request,
   1602                 URI=self.payto_URI)
   1603 
   1604     def do_save_invoice(self):
   1605         self.pending_invoice = self.read_invoice()
   1606         if not self.pending_invoice:
   1607             return
   1608         self.save_pending_invoice()
   1609 
   1610     def save_pending_invoice(self):
   1611         if not self.pending_invoice:
   1612             return
   1613         self.do_clear()
   1614         self.wallet.save_invoice(self.pending_invoice)
   1615         self.invoice_list.update()
   1616         self.pending_invoice = None
   1617 
   1618     def do_pay(self):
   1619         self.pending_invoice = self.read_invoice()
   1620         if not self.pending_invoice:
   1621             return
   1622         self.do_pay_invoice(self.pending_invoice)
   1623 
   1624     def pay_multiple_invoices(self, invoices):
   1625         outputs = []
   1626         for invoice in invoices:
   1627             outputs += invoice.outputs
   1628         self.pay_onchain_dialog(self.get_coins(), outputs)
   1629 
   1630     def do_pay_invoice(self, invoice: 'Invoice'):
   1631         if invoice.type == PR_TYPE_LN:
   1632             assert isinstance(invoice, LNInvoice)
   1633             self.pay_lightning_invoice(invoice.invoice, amount_msat=invoice.get_amount_msat())
   1634         elif invoice.type == PR_TYPE_ONCHAIN:
   1635             assert isinstance(invoice, OnchainInvoice)
   1636             self.pay_onchain_dialog(self.get_coins(), invoice.outputs)
   1637         else:
   1638             raise Exception('unknown invoice type')
   1639 
   1640     def get_coins(self, *, nonlocal_only=False) -> Sequence[PartialTxInput]:
   1641         coins = self.get_manually_selected_coins()
   1642         if coins is not None:
   1643             return coins
   1644         else:
   1645             return self.wallet.get_spendable_coins(None, nonlocal_only=nonlocal_only)
   1646 
   1647     def get_manually_selected_coins(self) -> Optional[Sequence[PartialTxInput]]:
   1648         """Return a list of selected coins or None.
   1649         Note: None means selection is not being used,
   1650               while an empty sequence means the user specifically selected that.
   1651         """
   1652         return self.utxo_list.get_spend_list()
   1653 
   1654     def get_text_not_enough_funds_mentioning_frozen(self) -> str:
   1655         text = _("Not enough funds")
   1656         frozen_bal = sum(self.wallet.get_frozen_balance())
   1657         if frozen_bal:
   1658             text += " ({} {} {})".format(
   1659                 self.format_amount(frozen_bal).strip(), self.base_unit(), _("are frozen")
   1660             )
   1661         return text
   1662 
   1663     def pay_onchain_dialog(
   1664             self, inputs: Sequence[PartialTxInput],
   1665             outputs: List[PartialTxOutput], *,
   1666             external_keypairs=None) -> None:
   1667         # trustedcoin requires this
   1668         if run_hook('abort_send', self):
   1669             return
   1670         is_sweep = bool(external_keypairs)
   1671         make_tx = lambda fee_est: self.wallet.make_unsigned_transaction(
   1672             coins=inputs,
   1673             outputs=outputs,
   1674             fee=fee_est,
   1675             is_sweep=is_sweep)
   1676         output_values = [x.value for x in outputs]
   1677         if output_values.count('!') > 1:
   1678             self.show_error(_("More than one output set to spend max"))
   1679             return
   1680 
   1681         output_value = '!' if '!' in output_values else sum(output_values)
   1682         conf_dlg = ConfirmTxDialog(window=self, make_tx=make_tx, output_value=output_value, is_sweep=is_sweep)
   1683         if conf_dlg.not_enough_funds:
   1684             # Check if we had enough funds excluding fees,
   1685             # if so, still provide opportunity to set lower fees.
   1686             if not conf_dlg.have_enough_funds_assuming_zero_fees():
   1687                 text = self.get_text_not_enough_funds_mentioning_frozen()
   1688                 self.show_message(text)
   1689                 return
   1690 
   1691         # shortcut to advanced preview (after "enough funds" check!)
   1692         if self.config.get('advanced_preview'):
   1693             preview_dlg = PreviewTxDialog(
   1694                 window=self,
   1695                 make_tx=make_tx,
   1696                 external_keypairs=external_keypairs,
   1697                 output_value=output_value)
   1698             preview_dlg.show()
   1699             return
   1700 
   1701         cancelled, is_send, password, tx = conf_dlg.run()
   1702         if cancelled:
   1703             return
   1704         if is_send:
   1705             self.save_pending_invoice()
   1706             def sign_done(success):
   1707                 if success:
   1708                     self.broadcast_or_show(tx)
   1709             self.sign_tx_with_password(tx, callback=sign_done, password=password,
   1710                                        external_keypairs=external_keypairs)
   1711         else:
   1712             preview_dlg = PreviewTxDialog(
   1713                 window=self,
   1714                 make_tx=make_tx,
   1715                 external_keypairs=external_keypairs,
   1716                 output_value=output_value)
   1717             preview_dlg.show()
   1718 
   1719     def broadcast_or_show(self, tx: Transaction):
   1720         if not tx.is_complete():
   1721             self.show_transaction(tx)
   1722             return
   1723         if not self.network:
   1724             self.show_error(_("You can't broadcast a transaction without a live network connection."))
   1725             self.show_transaction(tx)
   1726             return
   1727         self.broadcast_transaction(tx)
   1728 
   1729     @protected
   1730     def sign_tx(self, tx, *, callback, external_keypairs, password):
   1731         self.sign_tx_with_password(tx, callback=callback, password=password, external_keypairs=external_keypairs)
   1732 
   1733     def sign_tx_with_password(self, tx: PartialTransaction, *, callback, password, external_keypairs=None):
   1734         '''Sign the transaction in a separate thread.  When done, calls
   1735         the callback with a success code of True or False.
   1736         '''
   1737         def on_success(result):
   1738             callback(True)
   1739         def on_failure(exc_info):
   1740             self.on_error(exc_info)
   1741             callback(False)
   1742         on_success = run_hook('tc_sign_wrapper', self.wallet, tx, on_success, on_failure) or on_success
   1743         if external_keypairs:
   1744             # can sign directly
   1745             task = partial(tx.sign, external_keypairs)
   1746         else:
   1747             task = partial(self.wallet.sign_transaction, tx, password)
   1748         msg = _('Signing transaction...')
   1749         WaitingDialog(self, msg, task, on_success, on_failure)
   1750 
   1751     def broadcast_transaction(self, tx: Transaction):
   1752 
   1753         def broadcast_thread():
   1754             # non-GUI thread
   1755             pr = self.payment_request
   1756             if pr and pr.has_expired():
   1757                 self.payment_request = None
   1758                 return False, _("Invoice has expired")
   1759             try:
   1760                 self.network.run_from_another_thread(self.network.broadcast_transaction(tx))
   1761             except TxBroadcastError as e:
   1762                 return False, e.get_message_for_gui()
   1763             except BestEffortRequestFailed as e:
   1764                 return False, repr(e)
   1765             # success
   1766             txid = tx.txid()
   1767             if pr:
   1768                 self.payment_request = None
   1769                 refund_address = self.wallet.get_receiving_address()
   1770                 coro = pr.send_payment_and_receive_paymentack(tx.serialize(), refund_address)
   1771                 fut = asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop)
   1772                 ack_status, ack_msg = fut.result(timeout=20)
   1773                 self.logger.info(f"Payment ACK: {ack_status}. Ack message: {ack_msg}")
   1774             return True, txid
   1775 
   1776         # Capture current TL window; override might be removed on return
   1777         parent = self.top_level_window(lambda win: isinstance(win, MessageBoxMixin))
   1778 
   1779         def broadcast_done(result):
   1780             # GUI thread
   1781             if result:
   1782                 success, msg = result
   1783                 if success:
   1784                     parent.show_message(_('Payment sent.') + '\n' + msg)
   1785                     self.invoice_list.update()
   1786                 else:
   1787                     msg = msg or ''
   1788                     parent.show_error(msg)
   1789 
   1790         WaitingDialog(self, _('Broadcasting transaction...'),
   1791                       broadcast_thread, broadcast_done, self.on_error)
   1792 
   1793     def mktx_for_open_channel(self, funding_sat):
   1794         coins = self.get_coins(nonlocal_only=True)
   1795         make_tx = lambda fee_est: self.wallet.lnworker.mktx_for_open_channel(coins=coins,
   1796                                                                              funding_sat=funding_sat,
   1797                                                                              fee_est=fee_est)
   1798         return make_tx
   1799 
   1800     def open_channel(self, connect_str, funding_sat, push_amt):
   1801         try:
   1802             extract_nodeid(connect_str)
   1803         except ConnStringFormatError as e:
   1804             self.show_error(str(e))
   1805             return
   1806         # use ConfirmTxDialog
   1807         # we need to know the fee before we broadcast, because the txid is required
   1808         make_tx = self.mktx_for_open_channel(funding_sat)
   1809         d = ConfirmTxDialog(window=self, make_tx=make_tx, output_value=funding_sat, is_sweep=False)
   1810         # disable preview button because the user must not broadcast tx before establishment_flow
   1811         d.preview_button.setEnabled(False)
   1812         cancelled, is_send, password, funding_tx = d.run()
   1813         if not is_send:
   1814             return
   1815         if cancelled:
   1816             return
   1817         # read funding_sat from tx; converts '!' to int value
   1818         funding_sat = funding_tx.output_value_for_address(ln_dummy_address())
   1819         def task():
   1820             return self.wallet.lnworker.open_channel(connect_str=connect_str,
   1821                                                      funding_tx=funding_tx,
   1822                                                      funding_sat=funding_sat,
   1823                                                      push_amt_sat=push_amt,
   1824                                                      password=password)
   1825         def on_success(args):
   1826             chan, funding_tx = args
   1827             n = chan.constraints.funding_txn_minimum_depth
   1828             message = '\n'.join([
   1829                 _('Channel established.'),
   1830                 _('Remote peer ID') + ':' + chan.node_id.hex(),
   1831                 _('This channel will be usable after {} confirmations').format(n)
   1832             ])
   1833             if not funding_tx.is_complete():
   1834                 message += '\n\n' + _('Please sign and broadcast the funding transaction')
   1835             self.show_message(message)
   1836             if not funding_tx.is_complete():
   1837                 self.show_transaction(funding_tx)
   1838 
   1839         def on_failure(exc_info):
   1840             type_, e, traceback = exc_info
   1841             self.show_error(_('Could not open channel: {}').format(repr(e)))
   1842         WaitingDialog(self, _('Opening channel...'), task, on_success, on_failure)
   1843 
   1844     def query_choice(self, msg, choices):
   1845         # Needed by QtHandler for hardware wallets
   1846         dialog = WindowModalDialog(self.top_level_window())
   1847         clayout = ChoicesLayout(msg, choices)
   1848         vbox = QVBoxLayout(dialog)
   1849         vbox.addLayout(clayout.layout())
   1850         vbox.addLayout(Buttons(OkButton(dialog)))
   1851         if not dialog.exec_():
   1852             return None
   1853         return clayout.selected_index()
   1854 
   1855     def lock_amount(self, b: bool) -> None:
   1856         self.amount_e.setFrozen(b)
   1857         self.max_button.setEnabled(not b)
   1858 
   1859     def prepare_for_payment_request(self):
   1860         self.show_send_tab()
   1861         self.payto_e.is_pr = True
   1862         for e in [self.payto_e, self.message_e]:
   1863             e.setFrozen(True)
   1864         self.lock_amount(True)
   1865         self.payto_e.setText(_("please wait..."))
   1866         return True
   1867 
   1868     def delete_invoices(self, keys):
   1869         for key in keys:
   1870             self.wallet.delete_invoice(key)
   1871         self.invoice_list.update()
   1872 
   1873     def payment_request_ok(self):
   1874         pr = self.payment_request
   1875         if not pr:
   1876             return
   1877         key = pr.get_id()
   1878         invoice = self.wallet.get_invoice(key)
   1879         if invoice and self.wallet.get_invoice_status(invoice) == PR_PAID:
   1880             self.show_message("invoice already paid")
   1881             self.do_clear()
   1882             self.payment_request = None
   1883             return
   1884         self.payto_e.is_pr = True
   1885         if not pr.has_expired():
   1886             self.payto_e.setGreen()
   1887         else:
   1888             self.payto_e.setExpired()
   1889         self.payto_e.setText(pr.get_requestor())
   1890         self.amount_e.setAmount(pr.get_amount())
   1891         self.message_e.setText(pr.get_memo())
   1892         # signal to set fee
   1893         self.amount_e.textEdited.emit("")
   1894 
   1895     def payment_request_error(self):
   1896         pr = self.payment_request
   1897         if not pr:
   1898             return
   1899         self.show_message(pr.error)
   1900         self.payment_request = None
   1901         self.do_clear()
   1902 
   1903     def on_pr(self, request: 'paymentrequest.PaymentRequest'):
   1904         self.set_onchain(True)
   1905         self.payment_request = request
   1906         if self.payment_request.verify(self.contacts):
   1907             self.payment_request_ok_signal.emit()
   1908         else:
   1909             self.payment_request_error_signal.emit()
   1910 
   1911     def parse_lightning_invoice(self, invoice):
   1912         """Parse ln invoice, and prepare the send tab for it."""
   1913         try:
   1914             lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
   1915         except Exception as e:
   1916             raise LnDecodeException(e) from e
   1917         pubkey = bh2u(lnaddr.pubkey.serialize())
   1918         for k,v in lnaddr.tags:
   1919             if k == 'd':
   1920                 description = v
   1921                 break
   1922         else:
   1923              description = ''
   1924         self.payto_e.setFrozen(True)
   1925         self.payto_e.setText(pubkey)
   1926         self.message_e.setText(description)
   1927         if lnaddr.get_amount_sat() is not None:
   1928             self.amount_e.setAmount(lnaddr.get_amount_sat())
   1929         #self.amount_e.textEdited.emit("")
   1930         self.set_onchain(False)
   1931 
   1932     def set_onchain(self, b):
   1933         self._is_onchain = b
   1934         self.max_button.setEnabled(b)
   1935 
   1936     def pay_to_URI(self, URI):
   1937         if not URI:
   1938             return
   1939         try:
   1940             out = util.parse_URI(URI, self.on_pr)
   1941         except InvalidBitcoinURI as e:
   1942             self.show_error(_("Error parsing URI") + f":\n{e}")
   1943             return
   1944         self.show_send_tab()
   1945         self.payto_URI = out
   1946         r = out.get('r')
   1947         sig = out.get('sig')
   1948         name = out.get('name')
   1949         if r or (name and sig):
   1950             self.prepare_for_payment_request()
   1951             return
   1952         address = out.get('address')
   1953         amount = out.get('amount')
   1954         label = out.get('label')
   1955         message = out.get('message')
   1956         # use label as description (not BIP21 compliant)
   1957         if label and not message:
   1958             message = label
   1959         if address:
   1960             self.payto_e.setText(address)
   1961         if message:
   1962             self.message_e.setText(message)
   1963         if amount:
   1964             self.amount_e.setAmount(amount)
   1965             self.amount_e.textEdited.emit("")
   1966 
   1967 
   1968     def do_clear(self):
   1969         self.max_button.setChecked(False)
   1970         self.payment_request = None
   1971         self.payto_URI = None
   1972         self.payto_e.is_pr = False
   1973         self.set_onchain(False)
   1974         for e in [self.payto_e, self.message_e, self.amount_e]:
   1975             e.setText('')
   1976             e.setFrozen(False)
   1977         self.update_status()
   1978         run_hook('do_clear', self)
   1979 
   1980     def set_frozen_state_of_addresses(self, addrs, freeze: bool):
   1981         self.wallet.set_frozen_state_of_addresses(addrs, freeze)
   1982         self.address_list.update()
   1983         self.utxo_list.update()
   1984 
   1985     def set_frozen_state_of_coins(self, utxos: Sequence[PartialTxInput], freeze: bool):
   1986         utxos_str = {utxo.prevout.to_str() for utxo in utxos}
   1987         self.wallet.set_frozen_state_of_coins(utxos_str, freeze)
   1988         self.utxo_list.update()
   1989 
   1990     def create_list_tab(self, l, toolbar=None):
   1991         w = QWidget()
   1992         w.searchable_list = l
   1993         vbox = QVBoxLayout()
   1994         w.setLayout(vbox)
   1995         #vbox.setContentsMargins(0, 0, 0, 0)
   1996         #vbox.setSpacing(0)
   1997         if toolbar:
   1998             vbox.addLayout(toolbar)
   1999         vbox.addWidget(l)
   2000         return w
   2001 
   2002     def create_addresses_tab(self):
   2003         from .address_list import AddressList
   2004         self.address_list = l = AddressList(self)
   2005         toolbar = l.create_toolbar(self.config)
   2006         toolbar_shown = bool(self.config.get('show_toolbar_addresses', False))
   2007         l.show_toolbar(toolbar_shown)
   2008         return self.create_list_tab(l, toolbar)
   2009 
   2010     def create_utxo_tab(self):
   2011         from .utxo_list import UTXOList
   2012         self.utxo_list = UTXOList(self)
   2013         return self.create_list_tab(self.utxo_list)
   2014 
   2015     def create_contacts_tab(self):
   2016         from .contact_list import ContactList
   2017         self.contact_list = l = ContactList(self)
   2018         return self.create_list_tab(l)
   2019 
   2020     def remove_address(self, addr):
   2021         if not self.question(_("Do you want to remove {} from your wallet?").format(addr)):
   2022             return
   2023         try:
   2024             self.wallet.delete_address(addr)
   2025         except UserFacingException as e:
   2026             self.show_error(str(e))
   2027         else:
   2028             self.need_update.set()  # history, addresses, coins
   2029             self.clear_receive_tab()
   2030 
   2031     def paytomany(self):
   2032         self.show_send_tab()
   2033         self.payto_e.paytomany()
   2034         msg = '\n'.join([
   2035             _('Enter a list of outputs in the \'Pay to\' field.'),
   2036             _('One output per line.'),
   2037             _('Format: address, amount'),
   2038             _('You may load a CSV file using the file icon.')
   2039         ])
   2040         self.show_message(msg, title=_('Pay to many'))
   2041 
   2042     def payto_contacts(self, labels):
   2043         paytos = [self.get_contact_payto(label) for label in labels]
   2044         self.show_send_tab()
   2045         if len(paytos) == 1:
   2046             self.payto_e.setText(paytos[0])
   2047             self.amount_e.setFocus()
   2048         else:
   2049             text = "\n".join([payto + ", 0" for payto in paytos])
   2050             self.payto_e.setText(text)
   2051             self.payto_e.setFocus()
   2052 
   2053     def set_contact(self, label, address):
   2054         if not is_address(address):
   2055             self.show_error(_('Invalid Address'))
   2056             self.contact_list.update()  # Displays original unchanged value
   2057             return False
   2058         self.contacts[address] = ('address', label)
   2059         self.contact_list.update()
   2060         self.history_list.update()
   2061         self.update_completions()
   2062         return True
   2063 
   2064     def delete_contacts(self, labels):
   2065         if not self.question(_("Remove {} from your list of contacts?")
   2066                              .format(" + ".join(labels))):
   2067             return
   2068         for label in labels:
   2069             self.contacts.pop(label)
   2070         self.history_list.update()
   2071         self.contact_list.update()
   2072         self.update_completions()
   2073 
   2074     def show_onchain_invoice(self, invoice: OnchainInvoice):
   2075         amount_str = self.format_amount(invoice.amount_sat) + ' ' + self.base_unit()
   2076         d = WindowModalDialog(self, _("Onchain Invoice"))
   2077         vbox = QVBoxLayout(d)
   2078         grid = QGridLayout()
   2079         grid.addWidget(QLabel(_("Amount") + ':'), 1, 0)
   2080         grid.addWidget(QLabel(amount_str), 1, 1)
   2081         if len(invoice.outputs) == 1:
   2082             grid.addWidget(QLabel(_("Address") + ':'), 2, 0)
   2083             grid.addWidget(QLabel(invoice.get_address()), 2, 1)
   2084         else:
   2085             outputs_str = '\n'.join(map(lambda x: x.address + ' : ' + self.format_amount(x.value)+ self.base_unit(), invoice.outputs))
   2086             grid.addWidget(QLabel(_("Outputs") + ':'), 2, 0)
   2087             grid.addWidget(QLabel(outputs_str), 2, 1)
   2088         grid.addWidget(QLabel(_("Description") + ':'), 3, 0)
   2089         grid.addWidget(QLabel(invoice.message), 3, 1)
   2090         if invoice.exp:
   2091             grid.addWidget(QLabel(_("Expires") + ':'), 4, 0)
   2092             grid.addWidget(QLabel(format_time(invoice.exp + invoice.time)), 4, 1)
   2093         if invoice.bip70:
   2094             pr = paymentrequest.PaymentRequest(bytes.fromhex(invoice.bip70))
   2095             pr.verify(self.contacts)
   2096             grid.addWidget(QLabel(_("Requestor") + ':'), 5, 0)
   2097             grid.addWidget(QLabel(pr.get_requestor()), 5, 1)
   2098             grid.addWidget(QLabel(_("Signature") + ':'), 6, 0)
   2099             grid.addWidget(QLabel(pr.get_verify_status()), 6, 1)
   2100             def do_export():
   2101                 key = pr.get_id()
   2102                 name = str(key) + '.bip70'
   2103                 fn = getSaveFileName(
   2104                     parent=self,
   2105                     title=_("Save invoice to file"),
   2106                     filename=name,
   2107                     filter="*.bip70",
   2108                     config=self.config,
   2109                 )
   2110                 if not fn:
   2111                     return
   2112                 with open(fn, 'wb') as f:
   2113                     data = f.write(pr.raw)
   2114                 self.show_message(_('BIP70 invoice saved as {}').format(fn))
   2115             exportButton = EnterButton(_('Export'), do_export)
   2116             buttons = Buttons(exportButton, CloseButton(d))
   2117         else:
   2118             buttons = Buttons(CloseButton(d))
   2119         vbox.addLayout(grid)
   2120         vbox.addLayout(buttons)
   2121         d.exec_()
   2122 
   2123     def show_lightning_invoice(self, invoice: LNInvoice):
   2124         lnaddr = lndecode(invoice.invoice, expected_hrp=constants.net.SEGWIT_HRP)
   2125         d = WindowModalDialog(self, _("Lightning Invoice"))
   2126         vbox = QVBoxLayout(d)
   2127         grid = QGridLayout()
   2128         grid.addWidget(QLabel(_("Node ID") + ':'), 0, 0)
   2129         grid.addWidget(QLabel(lnaddr.pubkey.serialize().hex()), 0, 1)
   2130         grid.addWidget(QLabel(_("Amount") + ':'), 1, 0)
   2131         amount_str = self.format_amount(invoice.get_amount_sat()) + ' ' + self.base_unit()
   2132         grid.addWidget(QLabel(amount_str), 1, 1)
   2133         grid.addWidget(QLabel(_("Description") + ':'), 2, 0)
   2134         grid.addWidget(QLabel(invoice.message), 2, 1)
   2135         grid.addWidget(QLabel(_("Hash") + ':'), 3, 0)
   2136         payhash_e = ButtonsLineEdit(lnaddr.paymenthash.hex())
   2137         payhash_e.addCopyButton(self.app)
   2138         payhash_e.setReadOnly(True)
   2139         vbox.addWidget(payhash_e)
   2140         grid.addWidget(payhash_e, 3, 1)
   2141         if invoice.exp:
   2142             grid.addWidget(QLabel(_("Expires") + ':'), 4, 0)
   2143             grid.addWidget(QLabel(format_time(invoice.time + invoice.exp)), 4, 1)
   2144         vbox.addLayout(grid)
   2145         invoice_e = ShowQRTextEdit(config=self.config)
   2146         invoice_e.addCopyButton(self.app)
   2147         invoice_e.setText(invoice.invoice)
   2148         vbox.addWidget(invoice_e)
   2149         vbox.addLayout(Buttons(CloseButton(d),))
   2150         d.exec_()
   2151 
   2152     def create_console_tab(self):
   2153         from .console import Console
   2154         self.console = console = Console()
   2155         return console
   2156 
   2157     def update_console(self):
   2158         console = self.console
   2159         console.history = self.wallet.db.get("qt-console-history", [])
   2160         console.history_index = len(console.history)
   2161 
   2162         console.updateNamespace({
   2163             'wallet': self.wallet,
   2164             'network': self.network,
   2165             'plugins': self.gui_object.plugins,
   2166             'window': self,
   2167             'config': self.config,
   2168             'electrum': electrum,
   2169             'daemon': self.gui_object.daemon,
   2170             'util': util,
   2171             'bitcoin': bitcoin,
   2172             'lnutil': lnutil,
   2173         })
   2174 
   2175         c = commands.Commands(
   2176             config=self.config,
   2177             daemon=self.gui_object.daemon,
   2178             network=self.network,
   2179             callback=lambda: self.console.set_json(True))
   2180         methods = {}
   2181         def mkfunc(f, method):
   2182             return lambda *args, **kwargs: f(method,
   2183                                              args,
   2184                                              self.password_dialog,
   2185                                              **{**kwargs, 'wallet': self.wallet})
   2186         for m in dir(c):
   2187             if m[0]=='_' or m in ['network','wallet','config','daemon']: continue
   2188             methods[m] = mkfunc(c._run, m)
   2189 
   2190         console.updateNamespace(methods)
   2191 
   2192     def create_status_bar(self):
   2193 
   2194         sb = QStatusBar()
   2195         sb.setFixedHeight(35)
   2196 
   2197         self.balance_label = QLabel("Loading wallet...")
   2198         self.balance_label.setTextInteractionFlags(Qt.TextSelectableByMouse)
   2199         self.balance_label.setStyleSheet("""QLabel { padding: 0 }""")
   2200         sb.addWidget(self.balance_label)
   2201 
   2202         self.search_box = QLineEdit()
   2203         self.search_box.textChanged.connect(self.do_search)
   2204         self.search_box.hide()
   2205         sb.addPermanentWidget(self.search_box)
   2206 
   2207         self.update_check_button = QPushButton("")
   2208         self.update_check_button.setFlat(True)
   2209         self.update_check_button.setCursor(QCursor(Qt.PointingHandCursor))
   2210         self.update_check_button.setIcon(read_QIcon("update.png"))
   2211         self.update_check_button.hide()
   2212         sb.addPermanentWidget(self.update_check_button)
   2213 
   2214         self.password_button = StatusBarButton(QIcon(), _("Password"), self.change_password_dialog )
   2215         sb.addPermanentWidget(self.password_button)
   2216 
   2217         sb.addPermanentWidget(StatusBarButton(read_QIcon("preferences.png"), _("Preferences"), self.settings_dialog ) )
   2218         self.seed_button = StatusBarButton(read_QIcon("seed.png"), _("Seed"), self.show_seed_dialog )
   2219         sb.addPermanentWidget(self.seed_button)
   2220         self.lightning_button = None
   2221         if self.wallet.has_lightning():
   2222             self.lightning_button = StatusBarButton(read_QIcon("lightning.png"), _("Lightning Network"), self.gui_object.show_lightning_dialog)
   2223             self.update_lightning_icon()
   2224             sb.addPermanentWidget(self.lightning_button)
   2225         self.status_button = None
   2226         if self.network:
   2227             self.status_button = StatusBarButton(read_QIcon("status_disconnected.png"), _("Network"), self.gui_object.show_network_dialog)
   2228             sb.addPermanentWidget(self.status_button)
   2229         run_hook('create_status_bar', sb)
   2230         self.setStatusBar(sb)
   2231 
   2232     def create_coincontrol_statusbar(self):
   2233         self.coincontrol_sb = sb = QStatusBar()
   2234         sb.setSizeGripEnabled(False)
   2235         #sb.setFixedHeight(3 * char_width_in_lineedit())
   2236         sb.setStyleSheet('QStatusBar::item {border: None;} '
   2237                          + ColorScheme.GREEN.as_stylesheet(True))
   2238 
   2239         self.coincontrol_label = QLabel()
   2240         self.coincontrol_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
   2241         self.coincontrol_label.setTextInteractionFlags(Qt.TextSelectableByMouse)
   2242         sb.addWidget(self.coincontrol_label)
   2243 
   2244         clear_cc_button = EnterButton(_('Reset'), lambda: self.utxo_list.set_spend_list(None))
   2245         clear_cc_button.setStyleSheet("margin-right: 5px;")
   2246         sb.addPermanentWidget(clear_cc_button)
   2247 
   2248         sb.setVisible(False)
   2249         return sb
   2250 
   2251     def set_coincontrol_msg(self, msg: Optional[str]) -> None:
   2252         if not msg:
   2253             self.coincontrol_label.setText("")
   2254             self.coincontrol_sb.setVisible(False)
   2255             return
   2256         self.coincontrol_label.setText(msg)
   2257         self.coincontrol_sb.setVisible(True)
   2258 
   2259     def update_lightning_icon(self):
   2260         if self.lightning_button is None:
   2261             return
   2262         if self.network is None or self.network.channel_db is None:
   2263             self.lightning_button.setVisible(False)
   2264             return
   2265 
   2266         self.lightning_button.setVisible(True)
   2267 
   2268         cur, total, progress_percent = self.network.lngossip.get_sync_progress_estimate()
   2269         # self.logger.debug(f"updating lngossip sync progress estimate: cur={cur}, total={total}")
   2270         progress_str = "??%"
   2271         if progress_percent is not None:
   2272             progress_str = f"{progress_percent}%"
   2273         if progress_percent and progress_percent >= 100:
   2274             self.lightning_button.setMaximumWidth(25)
   2275             self.lightning_button.setText('')
   2276             self.lightning_button.setToolTip(_("The Lightning Network graph is fully synced."))
   2277         else:
   2278             self.lightning_button.setMaximumWidth(25 + 5 * char_width_in_lineedit())
   2279             self.lightning_button.setText(progress_str)
   2280             self.lightning_button.setToolTip(_("The Lightning Network graph is syncing...\n"
   2281                                                "Payments are more likely to succeed with a more complete graph."))
   2282 
   2283     def update_lock_icon(self):
   2284         icon = read_QIcon("lock.png") if self.wallet.has_password() else read_QIcon("unlock.png")
   2285         self.password_button.setIcon(icon)
   2286 
   2287     def update_buttons_on_seed(self):
   2288         self.seed_button.setVisible(self.wallet.has_seed())
   2289         self.password_button.setVisible(self.wallet.may_have_password())
   2290 
   2291     def change_password_dialog(self):
   2292         from electrum.storage import StorageEncryptionVersion
   2293         if self.wallet.get_available_storage_encryption_version() == StorageEncryptionVersion.XPUB_PASSWORD:
   2294             from .password_dialog import ChangePasswordDialogForHW
   2295             d = ChangePasswordDialogForHW(self, self.wallet)
   2296             ok, encrypt_file = d.run()
   2297             if not ok:
   2298                 return
   2299 
   2300             try:
   2301                 hw_dev_pw = self.wallet.keystore.get_password_for_storage_encryption()
   2302             except UserCancelled:
   2303                 return
   2304             except BaseException as e:
   2305                 self.logger.exception('')
   2306                 self.show_error(repr(e))
   2307                 return
   2308             old_password = hw_dev_pw if self.wallet.has_password() else None
   2309             new_password = hw_dev_pw if encrypt_file else None
   2310         else:
   2311             from .password_dialog import ChangePasswordDialogForSW
   2312             d = ChangePasswordDialogForSW(self, self.wallet)
   2313             ok, old_password, new_password, encrypt_file = d.run()
   2314 
   2315         if not ok:
   2316             return
   2317         try:
   2318             self.wallet.update_password(old_password, new_password, encrypt_storage=encrypt_file)
   2319         except InvalidPassword as e:
   2320             self.show_error(str(e))
   2321             return
   2322         except BaseException:
   2323             self.logger.exception('Failed to update password')
   2324             self.show_error(_('Failed to update password'))
   2325             return
   2326         msg = _('Password was updated successfully') if self.wallet.has_password() else _('Password is disabled, this wallet is not protected')
   2327         self.show_message(msg, title=_("Success"))
   2328         self.update_lock_icon()
   2329 
   2330     def toggle_search(self):
   2331         self.search_box.setHidden(not self.search_box.isHidden())
   2332         if not self.search_box.isHidden():
   2333             self.search_box.setFocus(1)
   2334         else:
   2335             self.do_search('')
   2336 
   2337     def do_search(self, t):
   2338         tab = self.tabs.currentWidget()
   2339         if hasattr(tab, 'searchable_list'):
   2340             tab.searchable_list.filter(t)
   2341 
   2342     def new_contact_dialog(self):
   2343         d = WindowModalDialog(self, _("New Contact"))
   2344         vbox = QVBoxLayout(d)
   2345         vbox.addWidget(QLabel(_('New Contact') + ':'))
   2346         grid = QGridLayout()
   2347         line1 = QLineEdit()
   2348         line1.setFixedWidth(32 * char_width_in_lineedit())
   2349         line2 = QLineEdit()
   2350         line2.setFixedWidth(32 * char_width_in_lineedit())
   2351         grid.addWidget(QLabel(_("Address")), 1, 0)
   2352         grid.addWidget(line1, 1, 1)
   2353         grid.addWidget(QLabel(_("Name")), 2, 0)
   2354         grid.addWidget(line2, 2, 1)
   2355         vbox.addLayout(grid)
   2356         vbox.addLayout(Buttons(CancelButton(d), OkButton(d)))
   2357         if d.exec_():
   2358             self.set_contact(line2.text(), line1.text())
   2359 
   2360     def show_wallet_info(self):
   2361         dialog = WindowModalDialog(self, _("Wallet Information"))
   2362         dialog.setMinimumSize(500, 100)
   2363         vbox = QVBoxLayout()
   2364         wallet_type = self.wallet.db.get('wallet_type', '')
   2365         if self.wallet.is_watching_only():
   2366             wallet_type += ' [{}]'.format(_('watching-only'))
   2367         seed_available = _('True') if self.wallet.has_seed() else _('False')
   2368         keystore_types = [k.get_type_text() for k in self.wallet.get_keystores()]
   2369         grid = QGridLayout()
   2370         basename = os.path.basename(self.wallet.storage.path)
   2371         grid.addWidget(QLabel(_("Wallet name")+ ':'), 0, 0)
   2372         grid.addWidget(QLabel(basename), 0, 1)
   2373         grid.addWidget(QLabel(_("Wallet type")+ ':'), 1, 0)
   2374         grid.addWidget(QLabel(wallet_type), 1, 1)
   2375         grid.addWidget(QLabel(_("Script type")+ ':'), 2, 0)
   2376         grid.addWidget(QLabel(self.wallet.txin_type), 2, 1)
   2377         grid.addWidget(QLabel(_("Seed available") + ':'), 3, 0)
   2378         grid.addWidget(QLabel(str(seed_available)), 3, 1)
   2379         if len(keystore_types) <= 1:
   2380             grid.addWidget(QLabel(_("Keystore type") + ':'), 4, 0)
   2381             ks_type = str(keystore_types[0]) if keystore_types else _('No keystore')
   2382             grid.addWidget(QLabel(ks_type), 4, 1)
   2383         # lightning
   2384         grid.addWidget(QLabel(_('Lightning') + ':'), 5, 0)
   2385         if self.wallet.can_have_lightning():
   2386             grid.addWidget(QLabel(_('Enabled')), 5, 1)
   2387             local_nodeid = QLabel(bh2u(self.wallet.lnworker.node_keypair.pubkey))
   2388             local_nodeid.setTextInteractionFlags(Qt.TextSelectableByMouse)
   2389             grid.addWidget(QLabel(_('Lightning Node ID:')), 6, 0)
   2390             grid.addWidget(local_nodeid, 6, 1, 1, 3)
   2391         else:
   2392             grid.addWidget(QLabel(_("Not available for this wallet.")), 5, 1)
   2393             grid.addWidget(HelpButton(_("Lightning is currently restricted to HD wallets with p2wpkh addresses.")), 5, 2)
   2394         vbox.addLayout(grid)
   2395 
   2396         labels_clayout = None
   2397 
   2398         if self.wallet.is_deterministic():
   2399             keystores = self.wallet.get_keystores()
   2400 
   2401             ks_stack = QStackedWidget()
   2402 
   2403             def select_ks(index):
   2404                 ks_stack.setCurrentIndex(index)
   2405 
   2406             # only show the combobox in case multiple accounts are available
   2407             if len(keystores) > 1:
   2408                 def label(idx, ks):
   2409                     if isinstance(self.wallet, Multisig_Wallet) and hasattr(ks, 'label'):
   2410                         return _("cosigner") + f' {idx+1}: {ks.get_type_text()} {ks.label}'
   2411                     else:
   2412                         return _("keystore") + f' {idx+1}'
   2413 
   2414                 labels = [label(idx, ks) for idx, ks in enumerate(self.wallet.get_keystores())]
   2415 
   2416                 on_click = lambda clayout: select_ks(clayout.selected_index())
   2417                 labels_clayout = ChoicesLayout(_("Select keystore"), labels, on_click)
   2418                 vbox.addLayout(labels_clayout.layout())
   2419 
   2420             for ks in keystores:
   2421                 ks_w = QWidget()
   2422                 ks_vbox = QVBoxLayout()
   2423                 ks_vbox.setContentsMargins(0, 0, 0, 0)
   2424                 ks_w.setLayout(ks_vbox)
   2425 
   2426                 mpk_text = ShowQRTextEdit(ks.get_master_public_key(), config=self.config)
   2427                 mpk_text.setMaximumHeight(150)
   2428                 mpk_text.addCopyButton(self.app)
   2429                 run_hook('show_xpub_button', mpk_text, ks)
   2430 
   2431                 der_path_hbox = QHBoxLayout()
   2432                 der_path_hbox.setContentsMargins(0, 0, 0, 0)
   2433 
   2434                 der_path_hbox.addWidget(QLabel(_("Derivation path") + ':'))
   2435                 der_path_text = QLabel(ks.get_derivation_prefix() or _("unknown"))
   2436                 der_path_text.setTextInteractionFlags(Qt.TextSelectableByMouse)
   2437                 der_path_hbox.addWidget(der_path_text)
   2438                 der_path_hbox.addStretch()
   2439 
   2440                 ks_vbox.addWidget(QLabel(_("Master Public Key")))
   2441                 ks_vbox.addWidget(mpk_text)
   2442                 ks_vbox.addLayout(der_path_hbox)
   2443 
   2444                 ks_stack.addWidget(ks_w)
   2445 
   2446             select_ks(0)
   2447             vbox.addWidget(ks_stack)
   2448 
   2449         vbox.addStretch(1)
   2450         btn_export_info = run_hook('wallet_info_buttons', self, dialog)
   2451         btn_close = CloseButton(dialog)
   2452         btns = Buttons(btn_export_info, btn_close)
   2453         vbox.addLayout(btns)
   2454         dialog.setLayout(vbox)
   2455         dialog.exec_()
   2456 
   2457     def remove_wallet(self):
   2458         if self.question('\n'.join([
   2459                 _('Delete wallet file?'),
   2460                 "%s"%self.wallet.storage.path,
   2461                 _('If your wallet contains funds, make sure you have saved its seed.')])):
   2462             self._delete_wallet()
   2463 
   2464     @protected
   2465     def _delete_wallet(self, password):
   2466         wallet_path = self.wallet.storage.path
   2467         basename = os.path.basename(wallet_path)
   2468         r = self.gui_object.daemon.delete_wallet(wallet_path)
   2469         self.close()
   2470         if r:
   2471             self.show_error(_("Wallet removed: {}").format(basename))
   2472         else:
   2473             self.show_error(_("Wallet file not found: {}").format(basename))
   2474 
   2475     @protected
   2476     def show_seed_dialog(self, password):
   2477         if not self.wallet.has_seed():
   2478             self.show_message(_('This wallet has no seed'))
   2479             return
   2480         keystore = self.wallet.get_keystore()
   2481         try:
   2482             seed = keystore.get_seed(password)
   2483             passphrase = keystore.get_passphrase(password)
   2484         except BaseException as e:
   2485             self.show_error(repr(e))
   2486             return
   2487         from .seed_dialog import SeedDialog
   2488         d = SeedDialog(self, seed, passphrase, config=self.config)
   2489         d.exec_()
   2490 
   2491     def show_qrcode(self, data, title = _("QR code"), parent=None, *,
   2492                     help_text=None, show_copy_text_btn=False):
   2493         if not data:
   2494             return
   2495         d = QRDialog(
   2496             data=data,
   2497             parent=parent or self,
   2498             title=title,
   2499             help_text=help_text,
   2500             show_copy_text_btn=show_copy_text_btn,
   2501             config=self.config,
   2502         )
   2503         d.exec_()
   2504 
   2505     @protected
   2506     def show_private_key(self, address, password):
   2507         if not address:
   2508             return
   2509         try:
   2510             pk = self.wallet.export_private_key(address, password)
   2511         except Exception as e:
   2512             self.logger.exception('')
   2513             self.show_message(repr(e))
   2514             return
   2515         xtype = bitcoin.deserialize_privkey(pk)[0]
   2516         d = WindowModalDialog(self, _("Private key"))
   2517         d.setMinimumSize(600, 150)
   2518         vbox = QVBoxLayout()
   2519         vbox.addWidget(QLabel(_("Address") + ': ' + address))
   2520         vbox.addWidget(QLabel(_("Script type") + ': ' + xtype))
   2521         vbox.addWidget(QLabel(_("Private key") + ':'))
   2522         keys_e = ShowQRTextEdit(text=pk, config=self.config)
   2523         keys_e.addCopyButton(self.app)
   2524         vbox.addWidget(keys_e)
   2525         vbox.addLayout(Buttons(CloseButton(d)))
   2526         d.setLayout(vbox)
   2527         d.exec_()
   2528 
   2529     msg_sign = _("Signing with an address actually means signing with the corresponding "
   2530                 "private key, and verifying with the corresponding public key. The "
   2531                 "address you have entered does not have a unique public key, so these "
   2532                 "operations cannot be performed.") + '\n\n' + \
   2533                _('The operation is undefined. Not just in Electrum, but in general.')
   2534 
   2535     @protected
   2536     def do_sign(self, address, message, signature, password):
   2537         address  = address.text().strip()
   2538         message = message.toPlainText().strip()
   2539         if not bitcoin.is_address(address):
   2540             self.show_message(_('Invalid Bitcoin address.'))
   2541             return
   2542         if self.wallet.is_watching_only():
   2543             self.show_message(_('This is a watching-only wallet.'))
   2544             return
   2545         if not self.wallet.is_mine(address):
   2546             self.show_message(_('Address not in wallet.'))
   2547             return
   2548         txin_type = self.wallet.get_txin_type(address)
   2549         if txin_type not in ['p2pkh', 'p2wpkh', 'p2wpkh-p2sh']:
   2550             self.show_message(_('Cannot sign messages with this type of address:') + \
   2551                               ' ' + txin_type + '\n\n' + self.msg_sign)
   2552             return
   2553         task = partial(self.wallet.sign_message, address, message, password)
   2554 
   2555         def show_signed_message(sig):
   2556             try:
   2557                 signature.setText(base64.b64encode(sig).decode('ascii'))
   2558             except RuntimeError:
   2559                 # (signature) wrapped C/C++ object has been deleted
   2560                 pass
   2561 
   2562         self.wallet.thread.add(task, on_success=show_signed_message)
   2563 
   2564     def do_verify(self, address, message, signature):
   2565         address  = address.text().strip()
   2566         message = message.toPlainText().strip().encode('utf-8')
   2567         if not bitcoin.is_address(address):
   2568             self.show_message(_('Invalid Bitcoin address.'))
   2569             return
   2570         try:
   2571             # This can throw on invalid base64
   2572             sig = base64.b64decode(str(signature.toPlainText()))
   2573             verified = ecc.verify_message_with_address(address, sig, message)
   2574         except Exception as e:
   2575             verified = False
   2576         if verified:
   2577             self.show_message(_("Signature verified"))
   2578         else:
   2579             self.show_error(_("Wrong signature"))
   2580 
   2581     def sign_verify_message(self, address=''):
   2582         d = WindowModalDialog(self, _('Sign/verify Message'))
   2583         d.setMinimumSize(610, 290)
   2584 
   2585         layout = QGridLayout(d)
   2586 
   2587         message_e = QTextEdit()
   2588         message_e.setAcceptRichText(False)
   2589         layout.addWidget(QLabel(_('Message')), 1, 0)
   2590         layout.addWidget(message_e, 1, 1)
   2591         layout.setRowStretch(2,3)
   2592 
   2593         address_e = QLineEdit()
   2594         address_e.setText(address)
   2595         layout.addWidget(QLabel(_('Address')), 2, 0)
   2596         layout.addWidget(address_e, 2, 1)
   2597 
   2598         signature_e = QTextEdit()
   2599         signature_e.setAcceptRichText(False)
   2600         layout.addWidget(QLabel(_('Signature')), 3, 0)
   2601         layout.addWidget(signature_e, 3, 1)
   2602         layout.setRowStretch(3,1)
   2603 
   2604         hbox = QHBoxLayout()
   2605 
   2606         b = QPushButton(_("Sign"))
   2607         b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
   2608         hbox.addWidget(b)
   2609 
   2610         b = QPushButton(_("Verify"))
   2611         b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
   2612         hbox.addWidget(b)
   2613 
   2614         b = QPushButton(_("Close"))
   2615         b.clicked.connect(d.accept)
   2616         hbox.addWidget(b)
   2617         layout.addLayout(hbox, 4, 1)
   2618         d.exec_()
   2619 
   2620     @protected
   2621     def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
   2622         if self.wallet.is_watching_only():
   2623             self.show_message(_('This is a watching-only wallet.'))
   2624             return
   2625         cyphertext = encrypted_e.toPlainText()
   2626         task = partial(self.wallet.decrypt_message, pubkey_e.text(), cyphertext, password)
   2627 
   2628         def setText(text):
   2629             try:
   2630                 message_e.setText(text.decode('utf-8'))
   2631             except RuntimeError:
   2632                 # (message_e) wrapped C/C++ object has been deleted
   2633                 pass
   2634 
   2635         self.wallet.thread.add(task, on_success=setText)
   2636 
   2637     def do_encrypt(self, message_e, pubkey_e, encrypted_e):
   2638         message = message_e.toPlainText()
   2639         message = message.encode('utf-8')
   2640         try:
   2641             public_key = ecc.ECPubkey(bfh(pubkey_e.text()))
   2642         except BaseException as e:
   2643             self.logger.exception('Invalid Public key')
   2644             self.show_warning(_('Invalid Public key'))
   2645             return
   2646         encrypted = public_key.encrypt_message(message)
   2647         encrypted_e.setText(encrypted.decode('ascii'))
   2648 
   2649     def encrypt_message(self, address=''):
   2650         d = WindowModalDialog(self, _('Encrypt/decrypt Message'))
   2651         d.setMinimumSize(610, 490)
   2652 
   2653         layout = QGridLayout(d)
   2654 
   2655         message_e = QTextEdit()
   2656         message_e.setAcceptRichText(False)
   2657         layout.addWidget(QLabel(_('Message')), 1, 0)
   2658         layout.addWidget(message_e, 1, 1)
   2659         layout.setRowStretch(2,3)
   2660 
   2661         pubkey_e = QLineEdit()
   2662         if address:
   2663             pubkey = self.wallet.get_public_key(address)
   2664             pubkey_e.setText(pubkey)
   2665         layout.addWidget(QLabel(_('Public key')), 2, 0)
   2666         layout.addWidget(pubkey_e, 2, 1)
   2667 
   2668         encrypted_e = QTextEdit()
   2669         encrypted_e.setAcceptRichText(False)
   2670         layout.addWidget(QLabel(_('Encrypted')), 3, 0)
   2671         layout.addWidget(encrypted_e, 3, 1)
   2672         layout.setRowStretch(3,1)
   2673 
   2674         hbox = QHBoxLayout()
   2675         b = QPushButton(_("Encrypt"))
   2676         b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
   2677         hbox.addWidget(b)
   2678 
   2679         b = QPushButton(_("Decrypt"))
   2680         b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
   2681         hbox.addWidget(b)
   2682 
   2683         b = QPushButton(_("Close"))
   2684         b.clicked.connect(d.accept)
   2685         hbox.addWidget(b)
   2686 
   2687         layout.addLayout(hbox, 4, 1)
   2688         d.exec_()
   2689 
   2690     def password_dialog(self, msg=None, parent=None):
   2691         from .password_dialog import PasswordDialog
   2692         parent = parent or self
   2693         d = PasswordDialog(parent, msg)
   2694         return d.run()
   2695 
   2696     def tx_from_text(self, data: Union[str, bytes]) -> Union[None, 'PartialTransaction', 'Transaction']:
   2697         from electrum.transaction import tx_from_any
   2698         try:
   2699             return tx_from_any(data)
   2700         except BaseException as e:
   2701             self.show_critical(_("Electrum was unable to parse your transaction") + ":\n" + repr(e))
   2702             return
   2703 
   2704     def import_channel_backup(self, encrypted: str):
   2705         if not self.question('Import channel backup?'):
   2706             return
   2707         try:
   2708             self.wallet.lnbackups.import_channel_backup(encrypted)
   2709         except Exception as e:
   2710             self.show_error("failed to import backup" + '\n' + str(e))
   2711             return
   2712 
   2713     def read_tx_from_qrcode(self):
   2714         from electrum import qrscanner
   2715         try:
   2716             data = qrscanner.scan_barcode(self.config.get_video_device())
   2717         except UserFacingException as e:
   2718             self.show_error(e)
   2719             return
   2720         except BaseException as e:
   2721             self.logger.exception('camera error')
   2722             self.show_error(repr(e))
   2723             return
   2724         if not data:
   2725             return
   2726         # if the user scanned a bitcoin URI
   2727         if data.lower().startswith(BITCOIN_BIP21_URI_SCHEME + ':'):
   2728             self.pay_to_URI(data)
   2729             return
   2730         if data.lower().startswith('channel_backup:'):
   2731             self.import_channel_backup(data)
   2732             return
   2733         # else if the user scanned an offline signed tx
   2734         tx = self.tx_from_text(data)
   2735         if not tx:
   2736             return
   2737         self.show_transaction(tx)
   2738 
   2739     def read_tx_from_file(self) -> Optional[Transaction]:
   2740         fileName = getOpenFileName(
   2741             parent=self,
   2742             title=_("Select your transaction file"),
   2743             filter=TRANSACTION_FILE_EXTENSION_FILTER_ANY,
   2744             config=self.config,
   2745         )
   2746         if not fileName:
   2747             return
   2748         try:
   2749             with open(fileName, "rb") as f:
   2750                 file_content = f.read()  # type: Union[str, bytes]
   2751         except (ValueError, IOError, os.error) as reason:
   2752             self.show_critical(_("Electrum was unable to open your transaction file") + "\n" + str(reason),
   2753                                title=_("Unable to read file or no transaction found"))
   2754             return
   2755         return self.tx_from_text(file_content)
   2756 
   2757     def do_process_from_text(self):
   2758         text = text_dialog(
   2759             parent=self,
   2760             title=_('Input raw transaction'),
   2761             header_layout=_("Transaction:"),
   2762             ok_label=_("Load transaction"),
   2763             config=self.config,
   2764         )
   2765         if not text:
   2766             return
   2767         tx = self.tx_from_text(text)
   2768         if tx:
   2769             self.show_transaction(tx)
   2770 
   2771     def do_process_from_text_channel_backup(self):
   2772         text = text_dialog(
   2773             parent=self,
   2774             title=_('Input channel backup'),
   2775             header_layout=_("Channel Backup:"),
   2776             ok_label=_("Load backup"),
   2777             config=self.config,
   2778         )
   2779         if not text:
   2780             return
   2781         if text.startswith('channel_backup:'):
   2782             self.import_channel_backup(text)
   2783 
   2784     def do_process_from_file(self):
   2785         tx = self.read_tx_from_file()
   2786         if tx:
   2787             self.show_transaction(tx)
   2788 
   2789     def do_process_from_txid(self):
   2790         from electrum import transaction
   2791         txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
   2792         if ok and txid:
   2793             txid = str(txid).strip()
   2794             raw_tx = self._fetch_tx_from_network(txid)
   2795             if not raw_tx:
   2796                 return
   2797             tx = transaction.Transaction(raw_tx)
   2798             self.show_transaction(tx)
   2799 
   2800     def _fetch_tx_from_network(self, txid: str) -> Optional[str]:
   2801         if not self.network:
   2802             self.show_message(_("You are offline."))
   2803             return
   2804         try:
   2805             raw_tx = self.network.run_from_another_thread(
   2806                 self.network.get_transaction(txid, timeout=10))
   2807         except UntrustedServerReturnedError as e:
   2808             self.logger.info(f"Error getting transaction from network: {repr(e)}")
   2809             self.show_message(_("Error getting transaction from network") + ":\n" + e.get_message_for_gui())
   2810             return
   2811         except Exception as e:
   2812             self.show_message(_("Error getting transaction from network") + ":\n" + repr(e))
   2813             return
   2814         return raw_tx
   2815 
   2816     @protected
   2817     def export_privkeys_dialog(self, password):
   2818         if self.wallet.is_watching_only():
   2819             self.show_message(_("This is a watching-only wallet"))
   2820             return
   2821 
   2822         if isinstance(self.wallet, Multisig_Wallet):
   2823             self.show_message(_('WARNING: This is a multi-signature wallet.') + '\n' +
   2824                               _('It cannot be "backed up" by simply exporting these private keys.'))
   2825 
   2826         d = WindowModalDialog(self, _('Private keys'))
   2827         d.setMinimumSize(980, 300)
   2828         vbox = QVBoxLayout(d)
   2829 
   2830         msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),
   2831                               _("Exposing a single private key can compromise your entire wallet!"),
   2832                               _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
   2833         vbox.addWidget(QLabel(msg))
   2834 
   2835         e = QTextEdit()
   2836         e.setReadOnly(True)
   2837         vbox.addWidget(e)
   2838 
   2839         defaultname = 'electrum-private-keys.csv'
   2840         select_msg = _('Select file to export your private keys to')
   2841         hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
   2842         vbox.addLayout(hbox)
   2843 
   2844         b = OkButton(d, _('Export'))
   2845         b.setEnabled(False)
   2846         vbox.addLayout(Buttons(CancelButton(d), b))
   2847 
   2848         private_keys = {}
   2849         addresses = self.wallet.get_addresses()
   2850         done = False
   2851         cancelled = False
   2852         def privkeys_thread():
   2853             for addr in addresses:
   2854                 time.sleep(0.1)
   2855                 if done or cancelled:
   2856                     break
   2857                 privkey = self.wallet.export_private_key(addr, password)
   2858                 private_keys[addr] = privkey
   2859                 self.computing_privkeys_signal.emit()
   2860             if not cancelled:
   2861                 self.computing_privkeys_signal.disconnect()
   2862                 self.show_privkeys_signal.emit()
   2863 
   2864         def show_privkeys():
   2865             s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
   2866             e.setText(s)
   2867             b.setEnabled(True)
   2868             self.show_privkeys_signal.disconnect()
   2869             nonlocal done
   2870             done = True
   2871 
   2872         def on_dialog_closed(*args):
   2873             nonlocal done
   2874             nonlocal cancelled
   2875             if not done:
   2876                 cancelled = True
   2877                 self.computing_privkeys_signal.disconnect()
   2878                 self.show_privkeys_signal.disconnect()
   2879 
   2880         self.computing_privkeys_signal.connect(lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
   2881         self.show_privkeys_signal.connect(show_privkeys)
   2882         d.finished.connect(on_dialog_closed)
   2883         threading.Thread(target=privkeys_thread).start()
   2884 
   2885         if not d.exec_():
   2886             done = True
   2887             return
   2888 
   2889         filename = filename_e.text()
   2890         if not filename:
   2891             return
   2892 
   2893         try:
   2894             self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
   2895         except (IOError, os.error) as reason:
   2896             txt = "\n".join([
   2897                 _("Electrum was unable to produce a private key-export."),
   2898                 str(reason)
   2899             ])
   2900             self.show_critical(txt, title=_("Unable to create csv"))
   2901 
   2902         except Exception as e:
   2903             self.show_message(repr(e))
   2904             return
   2905 
   2906         self.show_message(_("Private keys exported."))
   2907 
   2908     def do_export_privkeys(self, fileName, pklist, is_csv):
   2909         with open(fileName, "w+") as f:
   2910             os.chmod(fileName, 0o600)
   2911             if is_csv:
   2912                 transaction = csv.writer(f)
   2913                 transaction.writerow(["address", "private_key"])
   2914                 for addr, pk in pklist.items():
   2915                     transaction.writerow(["%34s"%addr,pk])
   2916             else:
   2917                 f.write(json.dumps(pklist, indent = 4))
   2918 
   2919     def do_import_labels(self):
   2920         def on_import():
   2921             self.need_update.set()
   2922         import_meta_gui(self, _('labels'), self.wallet.import_labels, on_import)
   2923 
   2924     def do_export_labels(self):
   2925         export_meta_gui(self, _('labels'), self.wallet.export_labels)
   2926 
   2927     def import_invoices(self):
   2928         import_meta_gui(self, _('invoices'), self.wallet.import_invoices, self.invoice_list.update)
   2929 
   2930     def export_invoices(self):
   2931         export_meta_gui(self, _('invoices'), self.wallet.export_invoices)
   2932 
   2933     def import_requests(self):
   2934         import_meta_gui(self, _('requests'), self.wallet.import_requests, self.request_list.update)
   2935 
   2936     def export_requests(self):
   2937         export_meta_gui(self, _('requests'), self.wallet.export_requests)
   2938 
   2939     def import_contacts(self):
   2940         import_meta_gui(self, _('contacts'), self.contacts.import_file, self.contact_list.update)
   2941 
   2942     def export_contacts(self):
   2943         export_meta_gui(self, _('contacts'), self.contacts.export_file)
   2944 
   2945 
   2946     def sweep_key_dialog(self):
   2947         d = WindowModalDialog(self, title=_('Sweep private keys'))
   2948         d.setMinimumSize(600, 300)
   2949         vbox = QVBoxLayout(d)
   2950         hbox_top = QHBoxLayout()
   2951         hbox_top.addWidget(QLabel(_("Enter private keys:")))
   2952         hbox_top.addWidget(InfoButton(WIF_HELP_TEXT), alignment=Qt.AlignRight)
   2953         vbox.addLayout(hbox_top)
   2954         keys_e = ScanQRTextEdit(allow_multi=True, config=self.config)
   2955         keys_e.setTabChangesFocus(True)
   2956         vbox.addWidget(keys_e)
   2957 
   2958         addresses = self.wallet.get_unused_addresses()
   2959         if not addresses:
   2960             try:
   2961                 addresses = self.wallet.get_receiving_addresses()
   2962             except AttributeError:
   2963                 addresses = self.wallet.get_addresses()
   2964         h, address_e = address_field(addresses)
   2965         vbox.addLayout(h)
   2966 
   2967         vbox.addStretch(1)
   2968         button = OkButton(d, _('Sweep'))
   2969         vbox.addLayout(Buttons(CancelButton(d), button))
   2970         button.setEnabled(False)
   2971 
   2972         def get_address():
   2973             addr = str(address_e.text()).strip()
   2974             if bitcoin.is_address(addr):
   2975                 return addr
   2976 
   2977         def get_pk(*, raise_on_error=False):
   2978             text = str(keys_e.toPlainText())
   2979             return keystore.get_private_keys(text, raise_on_error=raise_on_error)
   2980 
   2981         def on_edit():
   2982             valid_privkeys = False
   2983             try:
   2984                 valid_privkeys = get_pk(raise_on_error=True) is not None
   2985             except Exception as e:
   2986                 button.setToolTip(f'{_("Error")}: {repr(e)}')
   2987             else:
   2988                 button.setToolTip('')
   2989             button.setEnabled(get_address() is not None and valid_privkeys)
   2990         on_address = lambda text: address_e.setStyleSheet((ColorScheme.DEFAULT if get_address() else ColorScheme.RED).as_stylesheet())
   2991         keys_e.textChanged.connect(on_edit)
   2992         address_e.textChanged.connect(on_edit)
   2993         address_e.textChanged.connect(on_address)
   2994         on_address(str(address_e.text()))
   2995         if not d.exec_():
   2996             return
   2997         # user pressed "sweep"
   2998         addr = get_address()
   2999         try:
   3000             self.wallet.check_address_for_corruption(addr)
   3001         except InternalAddressCorruption as e:
   3002             self.show_error(str(e))
   3003             raise
   3004         privkeys = get_pk()
   3005 
   3006         def on_success(result):
   3007             coins, keypairs = result
   3008             outputs = [PartialTxOutput.from_address_and_value(addr, value='!')]
   3009             self.warn_if_watching_only()
   3010             self.pay_onchain_dialog(coins, outputs, external_keypairs=keypairs)
   3011         def on_failure(exc_info):
   3012             self.on_error(exc_info)
   3013         msg = _('Preparing sweep transaction...')
   3014         task = lambda: self.network.run_from_another_thread(
   3015             sweep_preparations(privkeys, self.network))
   3016         WaitingDialog(self, msg, task, on_success, on_failure)
   3017 
   3018     def _do_import(self, title, header_layout, func):
   3019         text = text_dialog(
   3020             parent=self,
   3021             title=title,
   3022             header_layout=header_layout,
   3023             ok_label=_('Import'),
   3024             allow_multi=True,
   3025             config=self.config,
   3026         )
   3027         if not text:
   3028             return
   3029         keys = str(text).split()
   3030         good_inputs, bad_inputs = func(keys)
   3031         if good_inputs:
   3032             msg = '\n'.join(good_inputs[:10])
   3033             if len(good_inputs) > 10: msg += '\n...'
   3034             self.show_message(_("The following addresses were added")
   3035                               + f' ({len(good_inputs)}):\n' + msg)
   3036         if bad_inputs:
   3037             msg = "\n".join(f"{key[:10]}... ({msg})" for key, msg in bad_inputs[:10])
   3038             if len(bad_inputs) > 10: msg += '\n...'
   3039             self.show_error(_("The following inputs could not be imported")
   3040                             + f' ({len(bad_inputs)}):\n' + msg)
   3041         self.address_list.update()
   3042         self.history_list.update()
   3043 
   3044     def import_addresses(self):
   3045         if not self.wallet.can_import_address():
   3046             return
   3047         title, msg = _('Import addresses'), _("Enter addresses")+':'
   3048         self._do_import(title, msg, self.wallet.import_addresses)
   3049 
   3050     @protected
   3051     def do_import_privkey(self, password):
   3052         if not self.wallet.can_import_privkey():
   3053             return
   3054         title = _('Import private keys')
   3055         header_layout = QHBoxLayout()
   3056         header_layout.addWidget(QLabel(_("Enter private keys")+':'))
   3057         header_layout.addWidget(InfoButton(WIF_HELP_TEXT), alignment=Qt.AlignRight)
   3058         self._do_import(title, header_layout, lambda x: self.wallet.import_private_keys(x, password))
   3059 
   3060     def update_fiat(self):
   3061         b = self.fx and self.fx.is_enabled()
   3062         self.fiat_send_e.setVisible(b)
   3063         self.fiat_receive_e.setVisible(b)
   3064         self.history_list.update()
   3065         self.address_list.refresh_headers()
   3066         self.address_list.update()
   3067         self.update_status()
   3068 
   3069     def settings_dialog(self):
   3070         from .settings_dialog import SettingsDialog
   3071         d = SettingsDialog(self, self.config)
   3072         self.alias_received_signal.connect(d.set_alias_color)
   3073         d.exec_()
   3074         self.alias_received_signal.disconnect(d.set_alias_color)
   3075         if self.fx:
   3076             self.fx.trigger_update()
   3077         run_hook('close_settings_dialog')
   3078         if d.need_restart:
   3079             self.show_warning(_('Please restart Electrum to activate the new GUI settings'), title=_('Success'))
   3080 
   3081     def closeEvent(self, event):
   3082         # It seems in some rare cases this closeEvent() is called twice
   3083         if not self.cleaned_up:
   3084             self.cleaned_up = True
   3085             self.clean_up()
   3086         event.accept()
   3087 
   3088     def clean_up(self):
   3089         self.wallet.thread.stop()
   3090         util.unregister_callback(self.on_network)
   3091         self.config.set_key("is_maximized", self.isMaximized())
   3092         if not self.isMaximized():
   3093             g = self.geometry()
   3094             self.wallet.db.put("winpos-qt", [g.left(),g.top(),
   3095                                                   g.width(),g.height()])
   3096         self.wallet.db.put("qt-console-history", self.console.history[-50:])
   3097         if self.qr_window:
   3098             self.qr_window.close()
   3099         self.close_wallet()
   3100 
   3101         self.gui_object.timer.timeout.disconnect(self.timer_actions)
   3102         self.gui_object.close_window(self)
   3103 
   3104     def plugins_dialog(self):
   3105         self.pluginsdialog = d = WindowModalDialog(self, _('Electrum Plugins'))
   3106 
   3107         plugins = self.gui_object.plugins
   3108 
   3109         vbox = QVBoxLayout(d)
   3110 
   3111         # plugins
   3112         scroll = QScrollArea()
   3113         scroll.setEnabled(True)
   3114         scroll.setWidgetResizable(True)
   3115         scroll.setMinimumSize(400,250)
   3116         vbox.addWidget(scroll)
   3117 
   3118         w = QWidget()
   3119         scroll.setWidget(w)
   3120         w.setMinimumHeight(plugins.count() * 35)
   3121 
   3122         grid = QGridLayout()
   3123         grid.setColumnStretch(0,1)
   3124         w.setLayout(grid)
   3125 
   3126         settings_widgets = {}
   3127 
   3128         def enable_settings_widget(p: Optional['BasePlugin'], name: str, i: int):
   3129             widget = settings_widgets.get(name)  # type: Optional[QWidget]
   3130             if widget and not p:
   3131                 # plugin got disabled, rm widget
   3132                 grid.removeWidget(widget)
   3133                 widget.setParent(None)
   3134                 settings_widgets.pop(name)
   3135             elif widget is None and p and p.requires_settings() and p.is_enabled():
   3136                 # plugin got enabled, add widget
   3137                 widget = settings_widgets[name] = p.settings_widget(d)
   3138                 grid.addWidget(widget, i, 1)
   3139 
   3140         def do_toggle(cb, name, i):
   3141             p = plugins.toggle(name)
   3142             cb.setChecked(bool(p))
   3143             enable_settings_widget(p, name, i)
   3144             # note: all enabled plugins will receive this hook:
   3145             run_hook('init_qt', self.gui_object)
   3146 
   3147         for i, descr in enumerate(plugins.descriptions.values()):
   3148             full_name = descr['__name__']
   3149             prefix, _separator, name = full_name.rpartition('.')
   3150             p = plugins.get(name)
   3151             if descr.get('registers_keystore'):
   3152                 continue
   3153             try:
   3154                 cb = QCheckBox(descr['fullname'])
   3155                 plugin_is_loaded = p is not None
   3156                 cb_enabled = (not plugin_is_loaded and plugins.is_available(name, self.wallet)
   3157                               or plugin_is_loaded and p.can_user_disable())
   3158                 cb.setEnabled(cb_enabled)
   3159                 cb.setChecked(plugin_is_loaded and p.is_enabled())
   3160                 grid.addWidget(cb, i, 0)
   3161                 enable_settings_widget(p, name, i)
   3162                 cb.clicked.connect(partial(do_toggle, cb, name, i))
   3163                 msg = descr['description']
   3164                 if descr.get('requires'):
   3165                     msg += '\n\n' + _('Requires') + ':\n' + '\n'.join(map(lambda x: x[1], descr.get('requires')))
   3166                 grid.addWidget(HelpButton(msg), i, 2)
   3167             except Exception:
   3168                 self.logger.exception(f"cannot display plugin {name}")
   3169         grid.setRowStretch(len(plugins.descriptions.values()), 1)
   3170         vbox.addLayout(Buttons(CloseButton(d)))
   3171         d.exec_()
   3172 
   3173     def cpfp_dialog(self, parent_tx: Transaction) -> None:
   3174         new_tx = self.wallet.cpfp(parent_tx, 0)
   3175         total_size = parent_tx.estimated_size() + new_tx.estimated_size()
   3176         parent_txid = parent_tx.txid()
   3177         assert parent_txid
   3178         parent_fee = self.wallet.get_tx_fee(parent_txid)
   3179         if parent_fee is None:
   3180             self.show_error(_("Can't CPFP: unknown fee for parent transaction."))
   3181             return
   3182         d = WindowModalDialog(self, _('Child Pays for Parent'))
   3183         vbox = QVBoxLayout(d)
   3184         msg = (
   3185             "A CPFP is a transaction that sends an unconfirmed output back to "
   3186             "yourself, with a high fee. The goal is to have miners confirm "
   3187             "the parent transaction in order to get the fee attached to the "
   3188             "child transaction.")
   3189         vbox.addWidget(WWLabel(_(msg)))
   3190         msg2 = ("The proposed fee is computed using your "
   3191             "fee/kB settings, applied to the total size of both child and "
   3192             "parent transactions. After you broadcast a CPFP transaction, "
   3193             "it is normal to see a new unconfirmed transaction in your history.")
   3194         vbox.addWidget(WWLabel(_(msg2)))
   3195         grid = QGridLayout()
   3196         grid.addWidget(QLabel(_('Total size') + ':'), 0, 0)
   3197         grid.addWidget(QLabel('%d bytes'% total_size), 0, 1)
   3198         max_fee = new_tx.output_value()
   3199         grid.addWidget(QLabel(_('Input amount') + ':'), 1, 0)
   3200         grid.addWidget(QLabel(self.format_amount(max_fee) + ' ' + self.base_unit()), 1, 1)
   3201         output_amount = QLabel('')
   3202         grid.addWidget(QLabel(_('Output amount') + ':'), 2, 0)
   3203         grid.addWidget(output_amount, 2, 1)
   3204         fee_e = BTCAmountEdit(self.get_decimal_point)
   3205         # FIXME with dyn fees, without estimates, there are all kinds of crashes here
   3206         combined_fee = QLabel('')
   3207         combined_feerate = QLabel('')
   3208         def on_fee_edit(x):
   3209             fee_for_child = fee_e.get_amount()
   3210             if fee_for_child is None:
   3211                 return
   3212             out_amt = max_fee - fee_for_child
   3213             out_amt_str = (self.format_amount(out_amt) + ' ' + self.base_unit()) if out_amt else ''
   3214             output_amount.setText(out_amt_str)
   3215             comb_fee = parent_fee + fee_for_child
   3216             comb_fee_str = (self.format_amount(comb_fee) + ' ' + self.base_unit()) if comb_fee else ''
   3217             combined_fee.setText(comb_fee_str)
   3218             comb_feerate = comb_fee / total_size * 1000
   3219             comb_feerate_str = self.format_fee_rate(comb_feerate) if comb_feerate else ''
   3220             combined_feerate.setText(comb_feerate_str)
   3221         fee_e.textChanged.connect(on_fee_edit)
   3222         def get_child_fee_from_total_feerate(fee_per_kb):
   3223             fee = fee_per_kb * total_size / 1000 - parent_fee
   3224             fee = min(max_fee, fee)
   3225             fee = max(total_size, fee)  # pay at least 1 sat/byte for combined size
   3226             return fee
   3227         suggested_feerate = self.config.fee_per_kb()
   3228         if suggested_feerate is None:
   3229             self.show_error(f'''{_("Can't CPFP'")}: {_('Dynamic fee estimates not available')}''')
   3230             return
   3231         fee = get_child_fee_from_total_feerate(suggested_feerate)
   3232         fee_e.setAmount(fee)
   3233         grid.addWidget(QLabel(_('Fee for child') + ':'), 3, 0)
   3234         grid.addWidget(fee_e, 3, 1)
   3235         def on_rate(dyn, pos, fee_rate):
   3236             fee = get_child_fee_from_total_feerate(fee_rate)
   3237             fee_e.setAmount(fee)
   3238         fee_slider = FeeSlider(self, self.config, on_rate)
   3239         fee_combo = FeeComboBox(fee_slider)
   3240         fee_slider.update()
   3241         grid.addWidget(fee_slider, 4, 1)
   3242         grid.addWidget(fee_combo, 4, 2)
   3243         grid.addWidget(QLabel(_('Total fee') + ':'), 5, 0)
   3244         grid.addWidget(combined_fee, 5, 1)
   3245         grid.addWidget(QLabel(_('Total feerate') + ':'), 6, 0)
   3246         grid.addWidget(combined_feerate, 6, 1)
   3247         vbox.addLayout(grid)
   3248         vbox.addLayout(Buttons(CancelButton(d), OkButton(d)))
   3249         if not d.exec_():
   3250             return
   3251         fee = fee_e.get_amount()
   3252         if fee is None:
   3253             return  # fee left empty, treat is as "cancel"
   3254         if fee > max_fee:
   3255             self.show_error(_('Max fee exceeded'))
   3256             return
   3257         try:
   3258             new_tx = self.wallet.cpfp(parent_tx, fee)
   3259         except CannotCPFP as e:
   3260             self.show_error(str(e))
   3261             return
   3262         self.show_transaction(new_tx)
   3263 
   3264     def _add_info_to_tx_from_wallet_and_network(self, tx: PartialTransaction) -> bool:
   3265         """Returns whether successful."""
   3266         # note side-effect: tx is being mutated
   3267         assert isinstance(tx, PartialTransaction)
   3268         try:
   3269             # note: this might download input utxos over network
   3270             BlockingWaitingDialog(
   3271                 self,
   3272                 _("Adding info to tx, from wallet and network..."),
   3273                 lambda: tx.add_info_from_wallet(self.wallet, ignore_network_issues=False),
   3274             )
   3275         except NetworkException as e:
   3276             self.show_error(repr(e))
   3277             return False
   3278         return True
   3279 
   3280     def bump_fee_dialog(self, tx: Transaction):
   3281         txid = tx.txid()
   3282         if not isinstance(tx, PartialTransaction):
   3283             tx = PartialTransaction.from_tx(tx)
   3284         if not self._add_info_to_tx_from_wallet_and_network(tx):
   3285             return
   3286         d = BumpFeeDialog(main_window=self, tx=tx, txid=txid)
   3287         d.run()
   3288 
   3289     def dscancel_dialog(self, tx: Transaction):
   3290         txid = tx.txid()
   3291         if not isinstance(tx, PartialTransaction):
   3292             tx = PartialTransaction.from_tx(tx)
   3293         if not self._add_info_to_tx_from_wallet_and_network(tx):
   3294             return
   3295         d = DSCancelDialog(main_window=self, tx=tx, txid=txid)
   3296         d.run()
   3297 
   3298     def save_transaction_into_wallet(self, tx: Transaction):
   3299         win = self.top_level_window()
   3300         try:
   3301             if not self.wallet.add_transaction(tx):
   3302                 win.show_error(_("Transaction could not be saved.") + "\n" +
   3303                                _("It conflicts with current history."))
   3304                 return False
   3305         except AddTransactionException as e:
   3306             win.show_error(e)
   3307             return False
   3308         else:
   3309             self.wallet.save_db()
   3310             # need to update at least: history_list, utxo_list, address_list
   3311             self.need_update.set()
   3312             msg = (_("Transaction added to wallet history.") + '\n\n' +
   3313                    _("Note: this is an offline transaction, if you want the network "
   3314                      "to see it, you need to broadcast it."))
   3315             win.msg_box(QPixmap(icon_path("offline_tx.png")), None, _('Success'), msg)
   3316             return True
   3317 
   3318     def show_cert_mismatch_error(self):
   3319         if self.showing_cert_mismatch_error:
   3320             return
   3321         self.showing_cert_mismatch_error = True
   3322         self.show_critical(title=_("Certificate mismatch"),
   3323                            msg=_("The SSL certificate provided by the main server did not match the fingerprint passed in with the --serverfingerprint option.") + "\n\n" +
   3324                                _("Electrum will now exit."))
   3325         self.showing_cert_mismatch_error = False
   3326         self.close()