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