electrum

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

__init__.py (15724B)


      1 #!/usr/bin/env python
      2 #
      3 # Electrum - lightweight Bitcoin client
      4 # Copyright (C) 2012 thomasv@gitorious
      5 #
      6 # Permission is hereby granted, free of charge, to any person
      7 # obtaining a copy of this software and associated documentation files
      8 # (the "Software"), to deal in the Software without restriction,
      9 # including without limitation the rights to use, copy, modify, merge,
     10 # publish, distribute, sublicense, and/or sell copies of the Software,
     11 # and to permit persons to whom the Software is furnished to do so,
     12 # subject to the following conditions:
     13 #
     14 # The above copyright notice and this permission notice shall be
     15 # included in all copies or substantial portions of the Software.
     16 #
     17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
     18 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
     19 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
     20 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
     21 # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
     22 # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
     23 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
     24 # SOFTWARE.
     25 
     26 import os
     27 import signal
     28 import sys
     29 import traceback
     30 import threading
     31 from typing import Optional, TYPE_CHECKING, List
     32 
     33 
     34 try:
     35     import PyQt5
     36 except Exception:
     37     sys.exit("Error: Could not import PyQt5 on Linux systems, you may try 'sudo apt-get install python3-pyqt5'")
     38 
     39 from PyQt5.QtGui import QGuiApplication
     40 from PyQt5.QtWidgets import (QApplication, QSystemTrayIcon, QWidget, QMenu,
     41                              QMessageBox)
     42 from PyQt5.QtCore import QObject, pyqtSignal, QTimer
     43 import PyQt5.QtCore as QtCore
     44 
     45 from electrum.i18n import _, set_language
     46 from electrum.plugin import run_hook
     47 from electrum.base_wizard import GoBack
     48 from electrum.util import (UserCancelled, profiler,
     49                            WalletFileException, BitcoinException, get_new_wallet_name)
     50 from electrum.wallet import Wallet, Abstract_Wallet
     51 from electrum.wallet_db import WalletDB
     52 from electrum.logging import Logger
     53 
     54 from .installwizard import InstallWizard, WalletAlreadyOpenInMemory
     55 from .util import get_default_language, read_QIcon, ColorScheme, custom_message_box
     56 from .main_window import ElectrumWindow
     57 from .network_dialog import NetworkDialog
     58 from .stylesheet_patcher import patch_qt_stylesheet
     59 from .lightning_dialog import LightningDialog
     60 from .watchtower_dialog import WatchtowerDialog
     61 
     62 if TYPE_CHECKING:
     63     from electrum.daemon import Daemon
     64     from electrum.simple_config import SimpleConfig
     65     from electrum.plugin import Plugins
     66 
     67 
     68 class OpenFileEventFilter(QObject):
     69     def __init__(self, windows):
     70         self.windows = windows
     71         super(OpenFileEventFilter, self).__init__()
     72 
     73     def eventFilter(self, obj, event):
     74         if event.type() == QtCore.QEvent.FileOpen:
     75             if len(self.windows) >= 1:
     76                 self.windows[0].pay_to_URI(event.url().toString())
     77                 return True
     78         return False
     79 
     80 
     81 class QElectrumApplication(QApplication):
     82     new_window_signal = pyqtSignal(str, object)
     83 
     84 
     85 class QNetworkUpdatedSignalObject(QObject):
     86     network_updated_signal = pyqtSignal(str, object)
     87 
     88 
     89 class ElectrumGui(Logger):
     90 
     91     @profiler
     92     def __init__(self, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugins'):
     93         set_language(config.get('language', get_default_language()))
     94         Logger.__init__(self)
     95         self.logger.info(f"Qt GUI starting up... Qt={QtCore.QT_VERSION_STR}, PyQt={QtCore.PYQT_VERSION_STR}")
     96         # Uncomment this call to verify objects are being properly
     97         # GC-ed when windows are closed
     98         #network.add_jobs([DebugMem([Abstract_Wallet, SPV, Synchronizer,
     99         #                            ElectrumWindow], interval=5)])
    100         QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads)
    101         if hasattr(QtCore.Qt, "AA_ShareOpenGLContexts"):
    102             QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts)
    103         if hasattr(QGuiApplication, 'setDesktopFileName'):
    104             QGuiApplication.setDesktopFileName('electrum.desktop')
    105         self.gui_thread = threading.current_thread()
    106         self.config = config
    107         self.daemon = daemon
    108         self.plugins = plugins
    109         self.windows = []  # type: List[ElectrumWindow]
    110         self.efilter = OpenFileEventFilter(self.windows)
    111         self.app = QElectrumApplication(sys.argv)
    112         self.app.installEventFilter(self.efilter)
    113         self.app.setWindowIcon(read_QIcon("electrum.png"))
    114         # timer
    115         self.timer = QTimer(self.app)
    116         self.timer.setSingleShot(False)
    117         self.timer.setInterval(500)  # msec
    118 
    119         self.network_dialog = None
    120         self.lightning_dialog = None
    121         self.watchtower_dialog = None
    122         self.network_updated_signal_obj = QNetworkUpdatedSignalObject()
    123         self._num_wizards_in_progress = 0
    124         self._num_wizards_lock = threading.Lock()
    125         # init tray
    126         self.dark_icon = self.config.get("dark_icon", False)
    127         self.tray = QSystemTrayIcon(self.tray_icon(), None)
    128         self.tray.setToolTip('Electrum')
    129         self.tray.activated.connect(self.tray_activated)
    130         self.build_tray_menu()
    131         self.tray.show()
    132         self.app.new_window_signal.connect(self.start_new_window)
    133         self.set_dark_theme_if_needed()
    134         run_hook('init_qt', self)
    135 
    136     def set_dark_theme_if_needed(self):
    137         use_dark_theme = self.config.get('qt_gui_color_theme', 'default') == 'dark'
    138         if use_dark_theme:
    139             try:
    140                 import qdarkstyle
    141                 self.app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5())
    142             except BaseException as e:
    143                 use_dark_theme = False
    144                 self.logger.warning(f'Error setting dark theme: {repr(e)}')
    145         # Apply any necessary stylesheet patches
    146         patch_qt_stylesheet(use_dark_theme=use_dark_theme)
    147         # Even if we ourselves don't set the dark theme,
    148         # the OS/window manager/etc might set *a dark theme*.
    149         # Hence, try to choose colors accordingly:
    150         ColorScheme.update_from_widget(QWidget(), force_dark=use_dark_theme)
    151 
    152     def build_tray_menu(self):
    153         # Avoid immediate GC of old menu when window closed via its action
    154         if self.tray.contextMenu() is None:
    155             m = QMenu()
    156             self.tray.setContextMenu(m)
    157         else:
    158             m = self.tray.contextMenu()
    159             m.clear()
    160         network = self.daemon.network
    161         m.addAction(_("Network"), self.show_network_dialog)
    162         if network and network.lngossip:
    163             m.addAction(_("Lightning Network"), self.show_lightning_dialog)
    164         if network and network.local_watchtower:
    165             m.addAction(_("Local Watchtower"), self.show_watchtower_dialog)
    166         for window in self.windows:
    167             name = window.wallet.basename()
    168             submenu = m.addMenu(name)
    169             submenu.addAction(_("Show/Hide"), window.show_or_hide)
    170             submenu.addAction(_("Close"), window.close)
    171         m.addAction(_("Dark/Light"), self.toggle_tray_icon)
    172         m.addSeparator()
    173         m.addAction(_("Exit Electrum"), self.close)
    174 
    175     def tray_icon(self):
    176         if self.dark_icon:
    177             return read_QIcon('electrum_dark_icon.png')
    178         else:
    179             return read_QIcon('electrum_light_icon.png')
    180 
    181     def toggle_tray_icon(self):
    182         self.dark_icon = not self.dark_icon
    183         self.config.set_key("dark_icon", self.dark_icon, True)
    184         self.tray.setIcon(self.tray_icon())
    185 
    186     def tray_activated(self, reason):
    187         if reason == QSystemTrayIcon.DoubleClick:
    188             if all([w.is_hidden() for w in self.windows]):
    189                 for w in self.windows:
    190                     w.bring_to_top()
    191             else:
    192                 for w in self.windows:
    193                     w.hide()
    194 
    195     def close(self):
    196         for window in self.windows:
    197             window.close()
    198         if self.network_dialog:
    199             self.network_dialog.close()
    200         if self.lightning_dialog:
    201             self.lightning_dialog.close()
    202         if self.watchtower_dialog:
    203             self.watchtower_dialog.close()
    204         self.app.quit()
    205 
    206     def new_window(self, path, uri=None):
    207         # Use a signal as can be called from daemon thread
    208         self.app.new_window_signal.emit(path, uri)
    209 
    210     def show_lightning_dialog(self):
    211         if not self.daemon.network.has_channel_db():
    212             return
    213         if not self.lightning_dialog:
    214             self.lightning_dialog = LightningDialog(self)
    215         self.lightning_dialog.bring_to_top()
    216 
    217     def show_watchtower_dialog(self):
    218         if not self.watchtower_dialog:
    219             self.watchtower_dialog = WatchtowerDialog(self)
    220         self.watchtower_dialog.bring_to_top()
    221 
    222     def show_network_dialog(self):
    223         if self.network_dialog:
    224             self.network_dialog.on_update()
    225             self.network_dialog.show()
    226             self.network_dialog.raise_()
    227             return
    228         self.network_dialog = NetworkDialog(self.daemon.network, self.config,
    229                                 self.network_updated_signal_obj)
    230         self.network_dialog.show()
    231 
    232     def _create_window_for_wallet(self, wallet):
    233         w = ElectrumWindow(self, wallet)
    234         self.windows.append(w)
    235         self.build_tray_menu()
    236         w.warn_if_testnet()
    237         w.warn_if_watching_only()
    238         return w
    239 
    240     def count_wizards_in_progress(func):
    241         def wrapper(self: 'ElectrumGui', *args, **kwargs):
    242             with self._num_wizards_lock:
    243                 self._num_wizards_in_progress += 1
    244             try:
    245                 return func(self, *args, **kwargs)
    246             finally:
    247                 with self._num_wizards_lock:
    248                     self._num_wizards_in_progress -= 1
    249         return wrapper
    250 
    251     @count_wizards_in_progress
    252     def start_new_window(self, path, uri, *, app_is_starting=False):
    253         '''Raises the window for the wallet if it is open.  Otherwise
    254         opens the wallet and creates a new window for it'''
    255         wallet = None
    256         try:
    257             wallet = self.daemon.load_wallet(path, None)
    258         except BaseException as e:
    259             self.logger.exception('')
    260             custom_message_box(icon=QMessageBox.Warning,
    261                                parent=None,
    262                                title=_('Error'),
    263                                text=_('Cannot load wallet') + ' (1):\n' + repr(e))
    264             # if app is starting, still let wizard to appear
    265             if not app_is_starting:
    266                 return
    267         if not wallet:
    268             try:
    269                 wallet = self._start_wizard_to_select_or_create_wallet(path)
    270             except (WalletFileException, BitcoinException) as e:
    271                 self.logger.exception('')
    272                 custom_message_box(icon=QMessageBox.Warning,
    273                                    parent=None,
    274                                    title=_('Error'),
    275                                    text=_('Cannot load wallet') + ' (2):\n' + repr(e))
    276         if not wallet:
    277             return
    278         # create or raise window
    279         try:
    280             for window in self.windows:
    281                 if window.wallet.storage.path == wallet.storage.path:
    282                     break
    283             else:
    284                 window = self._create_window_for_wallet(wallet)
    285         except BaseException as e:
    286             self.logger.exception('')
    287             custom_message_box(icon=QMessageBox.Warning,
    288                                parent=None,
    289                                title=_('Error'),
    290                                text=_('Cannot create window for wallet') + ':\n' + repr(e))
    291             if app_is_starting:
    292                 wallet_dir = os.path.dirname(path)
    293                 path = os.path.join(wallet_dir, get_new_wallet_name(wallet_dir))
    294                 self.start_new_window(path, uri)
    295             return
    296         if uri:
    297             window.pay_to_URI(uri)
    298         window.bring_to_top()
    299         window.setWindowState(window.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive)
    300 
    301         window.activateWindow()
    302         return window
    303 
    304     def _start_wizard_to_select_or_create_wallet(self, path) -> Optional[Abstract_Wallet]:
    305         wizard = InstallWizard(self.config, self.app, self.plugins, gui_object=self)
    306         try:
    307             path, storage = wizard.select_storage(path, self.daemon.get_wallet)
    308             # storage is None if file does not exist
    309             if storage is None:
    310                 wizard.path = path  # needed by trustedcoin plugin
    311                 wizard.run('new')
    312                 storage, db = wizard.create_storage(path)
    313             else:
    314                 db = WalletDB(storage.read(), manual_upgrades=False)
    315                 wizard.run_upgrades(storage, db)
    316         except (UserCancelled, GoBack):
    317             return
    318         except WalletAlreadyOpenInMemory as e:
    319             return e.wallet
    320         finally:
    321             wizard.terminate()
    322         # return if wallet creation is not complete
    323         if storage is None or db.get_action():
    324             return
    325         wallet = Wallet(db, storage, config=self.config)
    326         wallet.start_network(self.daemon.network)
    327         self.daemon.add_wallet(wallet)
    328         return wallet
    329 
    330     def close_window(self, window: ElectrumWindow):
    331         if window in self.windows:
    332            self.windows.remove(window)
    333         self.build_tray_menu()
    334         # save wallet path of last open window
    335         if not self.windows:
    336             self.config.save_last_wallet(window.wallet)
    337         run_hook('on_close_window', window)
    338         self.daemon.stop_wallet(window.wallet.storage.path)
    339 
    340     def init_network(self):
    341         # Show network dialog if config does not exist
    342         if self.daemon.network:
    343             if self.config.get('auto_connect') is None:
    344                 wizard = InstallWizard(self.config, self.app, self.plugins, gui_object=self)
    345                 wizard.init_network(self.daemon.network)
    346                 wizard.terminate()
    347 
    348     def main(self):
    349         try:
    350             self.init_network()
    351         except UserCancelled:
    352             return
    353         except GoBack:
    354             return
    355         except BaseException as e:
    356             self.logger.exception('')
    357             return
    358         self.timer.start()
    359 
    360         path = self.config.get_wallet_path(use_gui_last_wallet=True)
    361         if not self.start_new_window(path, self.config.get('url'), app_is_starting=True):
    362             return
    363         signal.signal(signal.SIGINT, lambda *args: self.app.quit())
    364 
    365         def quit_after_last_window():
    366             # keep daemon running after close
    367             if self.config.get('daemon'):
    368                 return
    369             # check if a wizard is in progress
    370             with self._num_wizards_lock:
    371                 if self._num_wizards_in_progress > 0 or len(self.windows) > 0:
    372                     return
    373                 if self.config.get('persist_daemon'):
    374                     return
    375             self.app.quit()
    376         self.app.setQuitOnLastWindowClosed(False)  # so _we_ can decide whether to quit
    377         self.app.lastWindowClosed.connect(quit_after_last_window)
    378 
    379         def clean_up():
    380             # Shut down the timer cleanly
    381             self.timer.stop()
    382             # clipboard persistence. see http://www.mail-archive.com/pyqt@riverbankcomputing.com/msg17328.html
    383             event = QtCore.QEvent(QtCore.QEvent.Clipboard)
    384             self.app.sendEvent(self.app.clipboard(), event)
    385             self.tray.hide()
    386         self.app.aboutToQuit.connect(clean_up)
    387 
    388         # main loop
    389         self.app.exec_()
    390         # on some platforms the exec_ call may not return, so use clean_up()
    391 
    392     def stop(self):
    393         self.logger.info('closing GUI')
    394         self.app.quit()