URI: 
       tqt.py - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
       tqt.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()