#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Author: Bertrand256 # Created on: 2017-03 import threading from functools import partial from PyQt5 import QtWidgets, QtCore from typing import List, Optional, Callable, ByteString, Tuple from PyQt5.QtCore import QObject from PyQt5.QtWidgets import QDialog, QCheckBox, QRadioButton import hw_pass_dlg import hw_pin_dlg import hw_word_dlg from app_defs import HWType from app_utils import SHA256 from thread_utils import EnhRLock from wnd_utils import WndUtils class HardwareWalletPinException(Exception): def __init__(self, msg): self.msg = msg class HWNotConnectedException(Exception): def __init__(self, msg: str = None): if not msg: msg = 'Not connected to a hardware wallet' Exception.__init__(self, msg) def get_hw_type(hw_client): """ Return hardware wallet type (HWType) based on reference to a hw client. """ if hw_client: t = type(hw_client).__name__ if t.lower().find('trezor') >= 0: return HWType.trezor elif t.lower().find('keepkey') >= 0: return HWType.keepkey elif t.lower().find('btchip') >= 0: return HWType.ledger_nano_s else: raise Exception('Unknown hardware wallet type') else: raise Exception('Hardware wallet not connected') class HwSessionInfo(QObject): sig_hw_connected = QtCore.pyqtSignal() sig_hw_disconnected = QtCore.pyqtSignal() def __init__(self, get_hw_client_function: Callable[[], object], hw_connect_function: Callable[[object], None], hw_disconnect_function: Callable[[], None], app_config: object, dashd_intf: object): QObject.__init__(self) self.__locks = {} # key: hw_client, value: EnhRLock self.__app_config = app_config self.__dashd_intf = dashd_intf self.__get_hw_client_function = get_hw_client_function self.__hw_connect_function: Callable = hw_connect_function self.__hw_disconnect_function: Callable = hw_disconnect_function self.__base_bip32_path: str = '' self.__base_public_key: bytes = '' self.__hd_tree_ident: str = '' @property def hw_client(self): return self.__get_hw_client_function() @property def hw_connect(self): return self.__hw_connect_function @property def hw_disconnect(self): return self.__hw_disconnect_function def signal_hw_connected(self): self.sig_hw_connected.emit() def signal_hw_disconnected(self): self.sig_hw_disconnected.emit() @property def hw_type(self): hw_client = self.hw_client hw_type = None if hw_client: hw_type = get_hw_type(hw_client) return hw_type @property def app_config(self): return self.__app_config @property def dashd_intf(self): return self.__dashd_intf def set_dashd_intf(self, dashd_intf): self.__dashd_intf = dashd_intf def acquire_client(self): cli = self.__get_hw_client_function() lock = self.__locks.get(cli) if not lock: lock = EnhRLock() self.__locks[cli] = lock lock.acquire() def release_client(self): cli = self.__get_hw_client_function() lock = self.__locks.get(cli) if not lock: raise Exception(f'Lock for client {str(cli)} not acquired before.') lock.release() def set_base_info(self, bip32_path: str, public_key: bytes): self.__base_bip32_path = bip32_path self.__base_public_key = public_key self.__hd_tree_ident = SHA256.new(public_key).digest().hex() @property def base_bip32_path(self): return self.__base_bip32_path @property def base_public_key(self): return self.__base_public_key def get_hd_tree_ident(self, coin_name: str): if not coin_name: raise Exception('Missing coin name') if not self.__hd_tree_ident: raise HWNotConnectedException() return self.__hd_tree_ident + bytes(coin_name, 'ascii').hex() def clean_bip32_path(bip32_path): # Keepkey and Ledger don't accept BIP32 "m/" prefix bip32_path.strip() if bip32_path.lower().find('m/') >= 0: # removing m/ prefix because of keepkey library bip32_path = bip32_path[2:] return bip32_path def ask_for_pin_callback(msg, hide_numbers=True): def dlg(): ui = hw_pin_dlg.HardwareWalletPinDlg(msg, hide_numbers=hide_numbers) if ui.exec_(): return ui.pin else: return None if threading.current_thread() != threading.main_thread(): return WndUtils.call_in_main_thread(dlg) else: return dlg() def ask_for_pass_callback(): def dlg(): ui = hw_pass_dlg.HardwareWalletPassDlg() if ui.exec_(): return ui.getPassphrase() else: return None if threading.current_thread() != threading.main_thread(): return WndUtils.call_in_main_thread(dlg) else: return dlg() def ask_for_word_callback(msg: str, wordlist: List[str]) -> str: def dlg(): ui = hw_word_dlg.HardwareWalletWordDlg(msg, wordlist) if ui.exec_(): return ui.get_word() else: return None if threading.current_thread() != threading.main_thread(): return WndUtils.call_in_main_thread(dlg) else: return dlg() class SelectHWDevice(QDialog): def __init__(self, parent, label: str, device_list: List[str]): QDialog.__init__(self, parent=parent) self.device_list = device_list self.device_radiobutton_list = [] self.device_selected_index = None self.label = label self.setupUi(self) def setupUi(self, Form): Form.setObjectName("SelectHWDevice") self.lay_main = QtWidgets.QVBoxLayout(Form) self.lay_main.setContentsMargins(-1, 3, -1, 3) self.lay_main.setObjectName("lay_main") self.gb_devices = QtWidgets.QGroupBox(Form) self.gb_devices.setFlat(False) self.gb_devices.setCheckable(False) self.gb_devices.setObjectName("gb_devices") self.lay_main.addWidget(self.gb_devices) self.lay_devices = QtWidgets.QVBoxLayout(self.gb_devices) for idx, dev in enumerate(self.device_list): rb = QRadioButton(self.gb_devices) rb.setText(dev) rb.toggled.connect(partial(self.on_item_toggled, idx)) self.device_radiobutton_list.append(rb) self.lay_devices.addWidget(rb) self.btn_main = QtWidgets.QDialogButtonBox(Form) self.btn_main.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel | QtWidgets.QDialogButtonBox.Ok) self.btn_main.setObjectName("btn_main") self.lay_main.addWidget(self.btn_main) self.retranslateUi(Form) QtCore.QMetaObject.connectSlotsByName(Form) self.setFixedSize(self.sizeHint()) def retranslateUi(self, Form): _translate = QtCore.QCoreApplication.translate Form.setWindowTitle('Select hardware wallet device') self.gb_devices.setTitle(self.label) def on_btn_main_accepted(self): if self.device_selected_index is None: WndUtils.errorMsg('No item selected.') else: self.accept() def on_btn_main_rejected(self): self.reject() def on_item_toggled(self, index, checked): if checked: self.device_selected_index = index def select_hw_device(parent, label: str, devices: List[str]) -> Optional[int]: """ Invokes dialog for selecting the particular instance of hardware wallet device. :param parent: :param devices: :return: index of selected device from 'devices' list or None if user cancelled the action. """ dlg = SelectHWDevice(parent, label, devices) if dlg.exec_(): return dlg.device_selected_index return None