electrum

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

qt.py (13077B)


      1 #!/usr/bin/env python3
      2 # -*- mode: python -*-
      3 #
      4 # Electrum - lightweight Bitcoin client
      5 # Copyright (C) 2016  The Electrum developers
      6 #
      7 # Permission is hereby granted, free of charge, to any person
      8 # obtaining a copy of this software and associated documentation files
      9 # (the "Software"), to deal in the Software without restriction,
     10 # including without limitation the rights to use, copy, modify, merge,
     11 # publish, distribute, sublicense, and/or sell copies of the Software,
     12 # and to permit persons to whom the Software is furnished to do so,
     13 # subject to the following conditions:
     14 #
     15 # The above copyright notice and this permission notice shall be
     16 # included in all copies or substantial portions of the Software.
     17 #
     18 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
     19 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
     20 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
     21 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
     22 # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
     23 # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
     24 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
     25 # SOFTWARE.
     26 
     27 import threading
     28 from functools import partial
     29 from typing import TYPE_CHECKING, Union, Optional, Callable, Any
     30 
     31 from PyQt5.QtCore import QObject, pyqtSignal
     32 from PyQt5.QtWidgets import QVBoxLayout, QLineEdit, QHBoxLayout, QLabel
     33 
     34 from electrum.gui.qt.password_dialog import PasswordLayout, PW_PASSPHRASE
     35 from electrum.gui.qt.util import (read_QIcon, WWLabel, OkButton, WindowModalDialog,
     36                                   Buttons, CancelButton, TaskThread, char_width_in_lineedit,
     37                                   PasswordLineEdit)
     38 from electrum.gui.qt.main_window import StatusBarButton, ElectrumWindow
     39 from electrum.gui.qt.installwizard import InstallWizard
     40 
     41 from electrum.i18n import _
     42 from electrum.logging import Logger
     43 from electrum.util import parse_URI, InvalidBitcoinURI, UserCancelled, UserFacingException
     44 from electrum.plugin import hook, DeviceUnpairableError
     45 
     46 from .plugin import OutdatedHwFirmwareException, HW_PluginBase, HardwareHandlerBase
     47 
     48 if TYPE_CHECKING:
     49     from electrum.wallet import Abstract_Wallet
     50     from electrum.keystore import Hardware_KeyStore
     51 
     52 
     53 # The trickiest thing about this handler was getting windows properly
     54 # parented on macOS.
     55 class QtHandlerBase(HardwareHandlerBase, QObject, Logger):
     56     '''An interface between the GUI (here, QT) and the device handling
     57     logic for handling I/O.'''
     58 
     59     passphrase_signal = pyqtSignal(object, object)
     60     message_signal = pyqtSignal(object, object)
     61     error_signal = pyqtSignal(object, object)
     62     word_signal = pyqtSignal(object)
     63     clear_signal = pyqtSignal()
     64     query_signal = pyqtSignal(object, object)
     65     yes_no_signal = pyqtSignal(object)
     66     status_signal = pyqtSignal(object)
     67 
     68     def __init__(self, win: Union[ElectrumWindow, InstallWizard], device: str):
     69         QObject.__init__(self)
     70         Logger.__init__(self)
     71         assert win.gui_thread == threading.current_thread(), 'must be called from GUI thread'
     72         self.clear_signal.connect(self.clear_dialog)
     73         self.error_signal.connect(self.error_dialog)
     74         self.message_signal.connect(self.message_dialog)
     75         self.passphrase_signal.connect(self.passphrase_dialog)
     76         self.word_signal.connect(self.word_dialog)
     77         self.query_signal.connect(self.win_query_choice)
     78         self.yes_no_signal.connect(self.win_yes_no_question)
     79         self.status_signal.connect(self._update_status)
     80         self.win = win
     81         self.device = device
     82         self.dialog = None
     83         self.done = threading.Event()
     84 
     85     def top_level_window(self):
     86         return self.win.top_level_window()
     87 
     88     def update_status(self, paired):
     89         self.status_signal.emit(paired)
     90 
     91     def _update_status(self, paired):
     92         if hasattr(self, 'button'):
     93             button = self.button
     94             icon_name = button.icon_paired if paired else button.icon_unpaired
     95             button.setIcon(read_QIcon(icon_name))
     96 
     97     def query_choice(self, msg, labels):
     98         self.done.clear()
     99         self.query_signal.emit(msg, labels)
    100         self.done.wait()
    101         return self.choice
    102 
    103     def yes_no_question(self, msg):
    104         self.done.clear()
    105         self.yes_no_signal.emit(msg)
    106         self.done.wait()
    107         return self.ok
    108 
    109     def show_message(self, msg, on_cancel=None):
    110         self.message_signal.emit(msg, on_cancel)
    111 
    112     def show_error(self, msg, blocking=False):
    113         self.done.clear()
    114         self.error_signal.emit(msg, blocking)
    115         if blocking:
    116             self.done.wait()
    117 
    118     def finished(self):
    119         self.clear_signal.emit()
    120 
    121     def get_word(self, msg):
    122         self.done.clear()
    123         self.word_signal.emit(msg)
    124         self.done.wait()
    125         return self.word
    126 
    127     def get_passphrase(self, msg, confirm):
    128         self.done.clear()
    129         self.passphrase_signal.emit(msg, confirm)
    130         self.done.wait()
    131         return self.passphrase
    132 
    133     def passphrase_dialog(self, msg, confirm):
    134         # If confirm is true, require the user to enter the passphrase twice
    135         parent = self.top_level_window()
    136         d = WindowModalDialog(parent, _("Enter Passphrase"))
    137         if confirm:
    138             OK_button = OkButton(d)
    139             playout = PasswordLayout(msg=msg, kind=PW_PASSPHRASE, OK_button=OK_button)
    140             vbox = QVBoxLayout()
    141             vbox.addLayout(playout.layout())
    142             vbox.addLayout(Buttons(CancelButton(d), OK_button))
    143             d.setLayout(vbox)
    144             passphrase = playout.new_password() if d.exec_() else None
    145         else:
    146             pw = PasswordLineEdit()
    147             pw.setMinimumWidth(200)
    148             vbox = QVBoxLayout()
    149             vbox.addWidget(WWLabel(msg))
    150             vbox.addWidget(pw)
    151             vbox.addLayout(Buttons(CancelButton(d), OkButton(d)))
    152             d.setLayout(vbox)
    153             passphrase = pw.text() if d.exec_() else None
    154         self.passphrase = passphrase
    155         self.done.set()
    156 
    157     def word_dialog(self, msg):
    158         dialog = WindowModalDialog(self.top_level_window(), "")
    159         hbox = QHBoxLayout(dialog)
    160         hbox.addWidget(QLabel(msg))
    161         text = QLineEdit()
    162         text.setMaximumWidth(12 * char_width_in_lineedit())
    163         text.returnPressed.connect(dialog.accept)
    164         hbox.addWidget(text)
    165         hbox.addStretch(1)
    166         dialog.exec_()  # Firmware cannot handle cancellation
    167         self.word = text.text()
    168         self.done.set()
    169 
    170     def message_dialog(self, msg, on_cancel):
    171         # Called more than once during signing, to confirm output and fee
    172         self.clear_dialog()
    173         title = _('Please check your {} device').format(self.device)
    174         self.dialog = dialog = WindowModalDialog(self.top_level_window(), title)
    175         l = QLabel(msg)
    176         vbox = QVBoxLayout(dialog)
    177         vbox.addWidget(l)
    178         if on_cancel:
    179             dialog.rejected.connect(on_cancel)
    180             vbox.addLayout(Buttons(CancelButton(dialog)))
    181         dialog.show()
    182 
    183     def error_dialog(self, msg, blocking):
    184         self.win.show_error(msg, parent=self.top_level_window())
    185         if blocking:
    186             self.done.set()
    187 
    188     def clear_dialog(self):
    189         if self.dialog:
    190             self.dialog.accept()
    191             self.dialog = None
    192 
    193     def win_query_choice(self, msg, labels):
    194         try:
    195             self.choice = self.win.query_choice(msg, labels)
    196         except UserCancelled:
    197             self.choice = None
    198         self.done.set()
    199 
    200     def win_yes_no_question(self, msg):
    201         self.ok = self.win.question(msg)
    202         self.done.set()
    203 
    204 
    205 class QtPluginBase(object):
    206 
    207     @hook
    208     def load_wallet(self: Union['QtPluginBase', HW_PluginBase], wallet: 'Abstract_Wallet', window: ElectrumWindow):
    209         relevant_keystores = [keystore for keystore in wallet.get_keystores()
    210                               if isinstance(keystore, self.keystore_class)]
    211         if not relevant_keystores:
    212             return
    213         for keystore in relevant_keystores:
    214             if not self.libraries_available:
    215                 message = keystore.plugin.get_library_not_available_message()
    216                 window.show_error(message)
    217                 return
    218             tooltip = self.device + '\n' + (keystore.label or 'unnamed')
    219             cb = partial(self._on_status_bar_button_click, window=window, keystore=keystore)
    220             button = StatusBarButton(read_QIcon(self.icon_unpaired), tooltip, cb)
    221             button.icon_paired = self.icon_paired
    222             button.icon_unpaired = self.icon_unpaired
    223             window.statusBar().addPermanentWidget(button)
    224             handler = self.create_handler(window)
    225             handler.button = button
    226             keystore.handler = handler
    227             keystore.thread = TaskThread(window, on_error=partial(self.on_task_thread_error, window, keystore))
    228             self.add_show_address_on_hw_device_button_for_receive_addr(wallet, keystore, window)
    229         # Trigger pairings
    230         def trigger_pairings():
    231             devmgr = self.device_manager()
    232             devices = devmgr.scan_devices()
    233             # first pair with all devices that can be auto-selected
    234             for keystore in relevant_keystores:
    235                 try:
    236                     self.get_client(keystore=keystore,
    237                                     force_pair=True,
    238                                     allow_user_interaction=False,
    239                                     devices=devices)
    240                 except UserCancelled:
    241                     pass
    242             # now do manual selections
    243             for keystore in relevant_keystores:
    244                 try:
    245                     self.get_client(keystore=keystore,
    246                                     force_pair=True,
    247                                     allow_user_interaction=True,
    248                                     devices=devices)
    249                 except UserCancelled:
    250                     pass
    251 
    252         some_keystore = relevant_keystores[0]
    253         some_keystore.thread.add(trigger_pairings)
    254 
    255     def _on_status_bar_button_click(self, *, window: ElectrumWindow, keystore: 'Hardware_KeyStore'):
    256         try:
    257             self.show_settings_dialog(window=window, keystore=keystore)
    258         except (UserFacingException, UserCancelled) as e:
    259             exc_info = (type(e), e, e.__traceback__)
    260             self.on_task_thread_error(window=window, keystore=keystore, exc_info=exc_info)
    261 
    262     def on_task_thread_error(self: Union['QtPluginBase', HW_PluginBase], window: ElectrumWindow,
    263                              keystore: 'Hardware_KeyStore', exc_info):
    264         e = exc_info[1]
    265         if isinstance(e, OutdatedHwFirmwareException):
    266             if window.question(e.text_ignore_old_fw_and_continue(), title=_("Outdated device firmware")):
    267                 self.set_ignore_outdated_fw()
    268                 # will need to re-pair
    269                 devmgr = self.device_manager()
    270                 def re_pair_device():
    271                     device_id = self.choose_device(window, keystore)
    272                     devmgr.unpair_id(device_id)
    273                     self.get_client(keystore)
    274                 keystore.thread.add(re_pair_device)
    275             return
    276         else:
    277             window.on_error(exc_info)
    278 
    279     def choose_device(self: Union['QtPluginBase', HW_PluginBase], window: ElectrumWindow,
    280                       keystore: 'Hardware_KeyStore') -> Optional[str]:
    281         '''This dialog box should be usable even if the user has
    282         forgotten their PIN or it is in bootloader mode.'''
    283         assert window.gui_thread != threading.current_thread(), 'must not be called from GUI thread'
    284         device_id = self.device_manager().xpub_id(keystore.xpub)
    285         if not device_id:
    286             try:
    287                 info = self.device_manager().select_device(self, keystore.handler, keystore)
    288             except UserCancelled:
    289                 return
    290             device_id = info.device.id_
    291         return device_id
    292 
    293     def show_settings_dialog(self, window: ElectrumWindow, keystore: 'Hardware_KeyStore') -> None:
    294         # default implementation (if no dialog): just try to connect to device
    295         def connect():
    296             device_id = self.choose_device(window, keystore)
    297         keystore.thread.add(connect)
    298 
    299     def add_show_address_on_hw_device_button_for_receive_addr(self, wallet: 'Abstract_Wallet',
    300                                                               keystore: 'Hardware_KeyStore',
    301                                                               main_window: ElectrumWindow):
    302         plugin = keystore.plugin
    303         receive_address_e = main_window.receive_address_e
    304 
    305         def show_address():
    306             addr = str(receive_address_e.text())
    307             keystore.thread.add(partial(plugin.show_address, wallet, addr, keystore))
    308         dev_name = f"{plugin.device} ({keystore.label})"
    309         receive_address_e.addButton("eye1.png", show_address, _("Show on {}").format(dev_name))
    310 
    311     def create_handler(self, window: Union[ElectrumWindow, InstallWizard]) -> 'QtHandlerBase':
    312         raise NotImplementedError()