LBRY-Vault/electrum/plugins/hw_wallet/qt.py
SomberNight 6760c3f252
hw wallets: introduce HardwareHandlerBase
previously, client.handler was sometimes
- an InstallWizard
- a QtHandlerBase where win was an ElectrumWindow
- a QtHandlerBase where win was an InstallWizard
- a CmdLineHandler

That's just too much dynamic untyped undocumented polymorphism...
Now it will never be an InstallWizard (replaced with QtHandlerBase where win is an InstallWizard),
and now in all cases client.handler is an instance of HardwareHandlerBase, yay.

related: #6063
2020-03-31 14:40:25 +02:00

272 lines
11 KiB
Python

#!/usr/bin/env python3
# -*- mode: python -*-
#
# Electrum - lightweight Bitcoin client
# Copyright (C) 2016 The Electrum developers
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import threading
from functools import partial
from typing import TYPE_CHECKING, Union, Optional, Callable, Any
from PyQt5.QtCore import QObject, pyqtSignal
from PyQt5.QtWidgets import QVBoxLayout, QLineEdit, QHBoxLayout, QLabel
from electrum.gui.qt.password_dialog import PasswordLayout, PW_PASSPHRASE
from electrum.gui.qt.util import (read_QIcon, WWLabel, OkButton, WindowModalDialog,
Buttons, CancelButton, TaskThread, char_width_in_lineedit)
from electrum.gui.qt.main_window import StatusBarButton, ElectrumWindow
from electrum.gui.qt.installwizard import InstallWizard
from electrum.i18n import _
from electrum.logging import Logger
from electrum.util import parse_URI, InvalidBitcoinURI, UserCancelled
from electrum.plugin import hook, DeviceUnpairableError
from .plugin import OutdatedHwFirmwareException, HW_PluginBase, HardwareHandlerBase
if TYPE_CHECKING:
from electrum.wallet import Abstract_Wallet
from electrum.keystore import Hardware_KeyStore
# The trickiest thing about this handler was getting windows properly
# parented on macOS.
class QtHandlerBase(HardwareHandlerBase, QObject, Logger):
'''An interface between the GUI (here, QT) and the device handling
logic for handling I/O.'''
passphrase_signal = pyqtSignal(object, object)
message_signal = pyqtSignal(object, object)
error_signal = pyqtSignal(object, object)
word_signal = pyqtSignal(object)
clear_signal = pyqtSignal()
query_signal = pyqtSignal(object, object)
yes_no_signal = pyqtSignal(object)
status_signal = pyqtSignal(object)
def __init__(self, win: Union[ElectrumWindow, InstallWizard], device: str):
QObject.__init__(self)
Logger.__init__(self)
self.clear_signal.connect(self.clear_dialog)
self.error_signal.connect(self.error_dialog)
self.message_signal.connect(self.message_dialog)
self.passphrase_signal.connect(self.passphrase_dialog)
self.word_signal.connect(self.word_dialog)
self.query_signal.connect(self.win_query_choice)
self.yes_no_signal.connect(self.win_yes_no_question)
self.status_signal.connect(self._update_status)
self.win = win
self.device = device
self.dialog = None
self.done = threading.Event()
def top_level_window(self):
return self.win.top_level_window()
def update_status(self, paired):
self.status_signal.emit(paired)
def _update_status(self, paired):
if hasattr(self, 'button'):
button = self.button
icon_name = button.icon_paired if paired else button.icon_unpaired
button.setIcon(read_QIcon(icon_name))
def query_choice(self, msg, labels):
self.done.clear()
self.query_signal.emit(msg, labels)
self.done.wait()
return self.choice
def yes_no_question(self, msg):
self.done.clear()
self.yes_no_signal.emit(msg)
self.done.wait()
return self.ok
def show_message(self, msg, on_cancel=None):
self.message_signal.emit(msg, on_cancel)
def show_error(self, msg, blocking=False):
self.done.clear()
self.error_signal.emit(msg, blocking)
if blocking:
self.done.wait()
def finished(self):
self.clear_signal.emit()
def get_word(self, msg):
self.done.clear()
self.word_signal.emit(msg)
self.done.wait()
return self.word
def get_passphrase(self, msg, confirm):
self.done.clear()
self.passphrase_signal.emit(msg, confirm)
self.done.wait()
return self.passphrase
def passphrase_dialog(self, msg, confirm):
# If confirm is true, require the user to enter the passphrase twice
parent = self.top_level_window()
d = WindowModalDialog(parent, _("Enter Passphrase"))
if confirm:
OK_button = OkButton(d)
playout = PasswordLayout(msg=msg, kind=PW_PASSPHRASE, OK_button=OK_button)
vbox = QVBoxLayout()
vbox.addLayout(playout.layout())
vbox.addLayout(Buttons(CancelButton(d), OK_button))
d.setLayout(vbox)
passphrase = playout.new_password() if d.exec_() else None
else:
pw = QLineEdit()
pw.setEchoMode(2)
pw.setMinimumWidth(200)
vbox = QVBoxLayout()
vbox.addWidget(WWLabel(msg))
vbox.addWidget(pw)
vbox.addLayout(Buttons(CancelButton(d), OkButton(d)))
d.setLayout(vbox)
passphrase = pw.text() if d.exec_() else None
self.passphrase = passphrase
self.done.set()
def word_dialog(self, msg):
dialog = WindowModalDialog(self.top_level_window(), "")
hbox = QHBoxLayout(dialog)
hbox.addWidget(QLabel(msg))
text = QLineEdit()
text.setMaximumWidth(12 * char_width_in_lineedit())
text.returnPressed.connect(dialog.accept)
hbox.addWidget(text)
hbox.addStretch(1)
dialog.exec_() # Firmware cannot handle cancellation
self.word = text.text()
self.done.set()
def message_dialog(self, msg, on_cancel):
# Called more than once during signing, to confirm output and fee
self.clear_dialog()
title = _('Please check your {} device').format(self.device)
self.dialog = dialog = WindowModalDialog(self.top_level_window(), title)
l = QLabel(msg)
vbox = QVBoxLayout(dialog)
vbox.addWidget(l)
if on_cancel:
dialog.rejected.connect(on_cancel)
vbox.addLayout(Buttons(CancelButton(dialog)))
dialog.show()
def error_dialog(self, msg, blocking):
self.win.show_error(msg, parent=self.top_level_window())
if blocking:
self.done.set()
def clear_dialog(self):
if self.dialog:
self.dialog.accept()
self.dialog = None
def win_query_choice(self, msg, labels):
self.choice = self.win.query_choice(msg, labels)
self.done.set()
def win_yes_no_question(self, msg):
self.ok = self.win.question(msg)
self.done.set()
class QtPluginBase(object):
@hook
def load_wallet(self: Union['QtPluginBase', HW_PluginBase], wallet: 'Abstract_Wallet', window: ElectrumWindow):
for keystore in wallet.get_keystores():
if not isinstance(keystore, self.keystore_class):
continue
if not self.libraries_available:
message = keystore.plugin.get_library_not_available_message()
window.show_error(message)
return
tooltip = self.device + '\n' + (keystore.label or 'unnamed')
cb = partial(self.show_settings_dialog, window, keystore)
button = StatusBarButton(read_QIcon(self.icon_unpaired), tooltip, cb)
button.icon_paired = self.icon_paired
button.icon_unpaired = self.icon_unpaired
window.statusBar().addPermanentWidget(button)
handler = self.create_handler(window)
handler.button = button
keystore.handler = handler
keystore.thread = TaskThread(window, on_error=partial(self.on_task_thread_error, window, keystore))
self.add_show_address_on_hw_device_button_for_receive_addr(wallet, keystore, window)
# Trigger a pairing
keystore.thread.add(partial(self.get_client, keystore))
def on_task_thread_error(self: Union['QtPluginBase', HW_PluginBase], window: ElectrumWindow,
keystore: 'Hardware_KeyStore', exc_info):
e = exc_info[1]
if isinstance(e, OutdatedHwFirmwareException):
if window.question(e.text_ignore_old_fw_and_continue(), title=_("Outdated device firmware")):
self.set_ignore_outdated_fw()
# will need to re-pair
devmgr = self.device_manager()
def re_pair_device():
device_id = self.choose_device(window, keystore)
devmgr.unpair_id(device_id)
self.get_client(keystore)
keystore.thread.add(re_pair_device)
return
else:
window.on_error(exc_info)
def choose_device(self: Union['QtPluginBase', HW_PluginBase], window: ElectrumWindow,
keystore: 'Hardware_KeyStore') -> Optional[str]:
'''This dialog box should be usable even if the user has
forgotten their PIN or it is in bootloader mode.'''
device_id = self.device_manager().xpub_id(keystore.xpub)
if not device_id:
try:
info = self.device_manager().select_device(self, keystore.handler, keystore)
except UserCancelled:
return
device_id = info.device.id_
return device_id
def show_settings_dialog(self, window: ElectrumWindow, keystore: 'Hardware_KeyStore') -> None:
device_id = self.choose_device(window, keystore)
def add_show_address_on_hw_device_button_for_receive_addr(self, wallet: 'Abstract_Wallet',
keystore: 'Hardware_KeyStore',
main_window: ElectrumWindow):
plugin = keystore.plugin
receive_address_e = main_window.receive_address_e
def show_address():
addr = str(receive_address_e.text())
keystore.thread.add(partial(plugin.show_address, wallet, addr, keystore))
dev_name = f"{plugin.device} ({keystore.label})"
receive_address_e.addButton("eye1.png", show_address, _("Show on {}").format(dev_name))
def create_handler(self, window: Union[ElectrumWindow, InstallWizard]) -> 'QtHandlerBase':
raise NotImplementedError()