electrum

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

commit 21bf5a8a84316b2f71540c95e3a66205e2c3d12a
parent 187b4dc9c1d1b03a13ab5bcea9516a970aed5ff9
Author: Neil Booth <kyuupichan@gmail.com>
Date:   Sat,  2 Jan 2016 09:43:56 +0900

Better support for USB devices

Benefits of this rewrite include:

- support of disconnecting / reconnecting a device without having
  to close the wallet, even in a different USB socket
- support of multiple keepkey / trezor devices, both during wallet
  creation and general use
- wallet is watching-only dynamically according to whether the
  associated device is currently plugged in or not

Diffstat:
M.gitignore | 2--
Mgui/qt/installwizard.py | 14+++-----------
Mgui/qt/main_window.py | 7++++---
Mlib/plugins.py | 9+++++----
Mlib/wallet.py | 16+++++++++++++---
Mlib/wizard.py | 17++++++++++-------
Mplugins/keepkey/qt.py | 8+++++---
Mplugins/trezor/client.py | 85+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mplugins/trezor/plugin.py | 294+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Mplugins/trezor/qt.py | 9+++++----
Mplugins/trezor/qt_generic.py | 105++++++++++++++++++++++++++++++++++++++-----------------------------------------
11 files changed, 343 insertions(+), 223 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -1,6 +1,4 @@ ####-*.patch -gui/icons_rc.py -lib/icons_rc.py *.pyc *.swp build/ diff --git a/gui/qt/installwizard.py b/gui/qt/installwizard.py @@ -132,13 +132,6 @@ class InstallWizard(WindowModalDialog, MessageBoxMixin, WizardBase): the password or None for no password.""" return self.pw_dialog(msg or MSG_ENTER_PASSWORD, PasswordDialog.PW_NEW) - def query_hardware(self, choices, action): - if action == 'create': - msg = _('Select the hardware wallet to create') - else: - msg = _('Select the hardware wallet to restore') - return self.choice(msg, choices) - def choose_server(self, network): # Show network dialog if config does not exist if self.config.get('server') is None: @@ -323,7 +316,7 @@ class InstallWizard(WindowModalDialog, MessageBoxMixin, WizardBase): self.config.set_key('auto_connect', True, True) network.auto_connect = True - def choice(self, msg, choices): + def query_choice(self, msg, choices): vbox = QVBoxLayout() self.set_layout(vbox) gb2 = QGroupBox(msg) @@ -335,7 +328,7 @@ class InstallWizard(WindowModalDialog, MessageBoxMixin, WizardBase): group2 = QButtonGroup() for i,c in enumerate(choices): button = QRadioButton(gb2) - button.setText(c[1]) + button.setText(c) vbox2.addWidget(button) group2.addButton(button) group2.setId(button, i) @@ -347,8 +340,7 @@ class InstallWizard(WindowModalDialog, MessageBoxMixin, WizardBase): vbox.addLayout(Buttons(CancelButton(self), next_button)) if not self.exec_(): raise UserCancelled - wallet_type = choices[group2.checkedId()][0] - return wallet_type + return group2.checkedId() def query_multisig(self, action): vbox = QVBoxLayout() diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py @@ -152,6 +152,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.connect(self, QtCore.SIGNAL('payment_request_error'), self.payment_request_error) self.history_list.setFocus(True) + self.connect(self, QtCore.SIGNAL('watching_only_changed'), + self.watching_only_changed) + # network callbacks if self.network: self.connect(self, QtCore.SIGNAL('network'), self.on_network_qt) @@ -280,7 +283,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.warn_if_watching_only() def watching_only_changed(self): - self.saved_wwo = self.wallet.is_watching_only() title = 'Electrum %s - %s' % (self.wallet.electrum_version, self.wallet.basename()) if self.wallet.is_watching_only(): @@ -495,6 +497,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions) def timer_actions(self): + # Note this runs in the GUI thread if self.need_update.is_set(): self.need_update.clear() self.update_wallet() @@ -504,8 +507,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): if self.require_fee_update: self.do_update_fee() self.require_fee_update = False - if self.saved_wwo != self.wallet.is_watching_only(): - self.watching_only_changed() run_hook('timer_actions') def format_amount(self, x, is_diff=False, whitespaces=False): diff --git a/lib/plugins.py b/lib/plugins.py @@ -73,7 +73,7 @@ class Plugins(DaemonThread): self.print_error("loaded", name) return plugin except Exception: - print_msg(_("Error: cannot initialize plugin"), name) + self.print_error("cannot initialize plugin", name) traceback.print_exc(file=sys.stdout) return None @@ -106,16 +106,17 @@ class Plugins(DaemonThread): return not requires or w.wallet_type in requires def hardware_wallets(self, action): - result = [] + wallet_types, descs = [], [] for name, (gui_good, details) in self.hw_wallets.items(): if gui_good: try: p = self.wallet_plugin_loader(name) if action == 'restore' or p.is_enabled(): - result.append((details[1], details[2])) + wallet_types.append(details[1]) + descs.append(details[2]) except: self.print_error("cannot load plugin for:", name) - return result + return wallet_types, descs def register_plugin_wallet(self, name, gui_good, details): def dynamic_constructor(storage): diff --git a/lib/wallet.py b/lib/wallet.py @@ -205,6 +205,9 @@ class Abstract_Wallet(PrintError): def diagnostic_name(self): return self.basename() + def __str__(self): + return self.basename() + def set_use_encryption(self, use_encryption): self.use_encryption = use_encryption self.storage.put('use_encryption', use_encryption) @@ -1718,18 +1721,25 @@ class BIP44_Wallet(BIP32_HD_Wallet): def can_create_accounts(self): return not self.is_watching_only() + @classmethod def prefix(self): return "/".join(self.root_derivation.split("/")[1:]) + @classmethod def account_derivation(self, account_id): return self.prefix() + "/" + account_id + "'" + @classmethod + def address_derivation(self, account_id, change, address_index): + account_derivation = self.account_derivation(account_id) + return "%s/%d/%d" % (account_derivation, change, address_index) + def address_id(self, address): acc_id, (change, address_index) = self.get_address_index(address) - account_derivation = self.account_derivation(acc_id) - return "%s/%d/%d" % (account_derivation, change, address_index) + return self.address_derivation(acc_id, change, address_index) - def mnemonic_to_seed(self, mnemonic, passphrase): + @staticmethod + def mnemonic_to_seed(mnemonic, passphrase): # See BIP39 import pbkdf2, hashlib, hmac PBKDF2_ROUNDS = 2048 diff --git a/lib/wizard.py b/lib/wizard.py @@ -76,11 +76,9 @@ class WizardBase(PrintError): string like "2of3". Action is 'create' or 'restore'.""" raise NotImplementedError - def query_hardware(self, choices, action): - """Asks the user what kind of hardware wallet they want from the given - choices. choices is a list of (wallet_type, translated - description) tuples. Action is 'create' or 'restore'. Return - the wallet type chosen.""" + def query_choice(self, msg, choices): + """Asks the user which of several choices they would like. + Return the index of the choice.""" raise NotImplementedError def show_and_verify_seed(self, seed): @@ -205,8 +203,13 @@ class WizardBase(PrintError): if kind == 'multisig': wallet_type = self.query_multisig(action) elif kind == 'hardware': - choices = self.plugins.hardware_wallets(action) - wallet_type = self.query_hardware(choices, action) + wallet_types, choices = self.plugins.hardware_wallets(action) + if action == 'create': + msg = _('Select the hardware wallet to create') + else: + msg = _('Select the hardware wallet to restore') + choice = self.query_choice(msg, choices) + wallet_type = wallet_types[choice] elif kind == 'twofactor': wallet_type = '2fa' else: diff --git a/plugins/keepkey/qt.py b/plugins/keepkey/qt.py @@ -1,9 +1,11 @@ -from plugins.trezor.qt_generic import QtPlugin +from plugins.trezor.qt_generic import qt_plugin_class +from keepkey import KeepKeyPlugin -class Plugin(QtPlugin): +class Plugin(qt_plugin_class(KeepKeyPlugin)): icon_file = ":icons/keepkey.png" - def pin_matrix_widget_class(): + @classmethod + def pin_matrix_widget_class(self): from keepkeylib.qt.pinmatrix import PinMatrixWidget return PinMatrixWidget diff --git a/plugins/trezor/client.py b/plugins/trezor/client.py @@ -27,7 +27,7 @@ class GuiMixin(object): else: cancel_callback = None - self.handler.show_message(message % self.device, cancel_callback) + self.handler().show_message(message % self.device, cancel_callback) return self.proto.ButtonAck() def callback_PinMatrixRequest(self, msg): @@ -40,14 +40,14 @@ class GuiMixin(object): "Note the numbers have been shuffled!")) else: msg = _("Please enter %s PIN") - pin = self.handler.get_pin(msg % self.device) + pin = self.handler().get_pin(msg % self.device) if not pin: return self.proto.Cancel() return self.proto.PinMatrixAck(pin=pin) def callback_PassphraseRequest(self, req): msg = _("Please enter your %s passphrase") - passphrase = self.handler.get_passphrase(msg % self.device) + passphrase = self.handler().get_passphrase(msg % self.device) if passphrase is None: return self.proto.Cancel() return self.proto.PassphraseAck(passphrase=passphrase) @@ -65,18 +65,29 @@ def trezor_client_class(protocol_mixin, base_client, proto): class TrezorClient(protocol_mixin, GuiMixin, base_client, PrintError): - def __init__(self, transport, plugin): + def __init__(self, transport, path, plugin): base_client.__init__(self, transport) protocol_mixin.__init__(self, transport) self.proto = proto self.device = plugin.device - self.handler = None + self.path = path + self.wallet = None self.plugin = plugin self.tx_api = plugin - self.bad = False self.msg_code_override = None - self.proper_device = False - self.checked_device = False + + def __str__(self): + return "%s/%s/%s" % (self.label(), self.device_id(), self.path[0]) + + def label(self): + return self.features.label + + def device_id(self): + return self.features.device_id + + def handler(self): + assert self.wallet and self.wallet.handler + return self.wallet.handler # Copied from trezorlib/client.py as there it is not static, sigh @staticmethod @@ -94,34 +105,8 @@ def trezor_client_class(protocol_mixin, base_client, proto): path.append(abs(int(x)) | prime) return path - def check_proper_device(self, wallet): - try: - self.ping('t') - except BaseException as e: - self.plugin.give_error( - __("%s device not detected. Continuing in watching-only " - "mode.") % self.device + "\n\n" + str(e)) - if not self.is_proper_device(wallet): - self.plugin.give_error(_('Wrong device or password')) - - def is_proper_device(self, wallet): - if not self.checked_device: - addresses = wallet.addresses(False) - if not addresses: # Wallet being created? - return True - - address = addresses[0] - address_id = wallet.address_id(address) - path = self.expand_path(address_id) - self.checked_device = True - try: - device_address = self.get_address('Bitcoin', path) - self.proper_device = (device_address == address) - except: - self.proper_device = False - wallet.proper_device = self.proper_device - - return self.proper_device + def address_from_derivation(self, derivation): + return self.get_address('Bitcoin', self.expand_path(derivation)) def change_label(self, label): self.msg_code_override = 'label' @@ -144,12 +129,26 @@ def trezor_client_class(protocol_mixin, base_client, proto): def atleast_version(self, major, minor=0, patch=0): return cmp(self.firmware_version(), (major, minor, patch)) - def call_raw(self, msg): + + def wrapper(func): + '''Wrap base class methods to show exceptions and clear + any dialog box it opened.''' + + def wrapped(self, *args, **kwargs): + handler = self.handler() try: - return base_client.call_raw(self, msg) - except: - self.print_error("Marking %s client bad" % self.device) - self.bad = True - raise + return func(self, *args, **kwargs) + except BaseException as e: + handler.show_error(str(e)) + raise e + finally: + handler.finished() + + return wrapped + + cls = TrezorClient + for method in ['apply_settings', 'change_pin', 'get_address', + 'get_public_node', 'sign_message', 'sign_tx']: + setattr(cls, method, wrapper(getattr(cls, method))) - return TrezorClient + return cls diff --git a/plugins/trezor/plugin.py b/plugins/trezor/plugin.py @@ -1,4 +1,6 @@ import re +import time + from binascii import unhexlify from struct import pack from unicodedata import normalize @@ -12,6 +14,9 @@ from electrum.transaction import (deserialize, is_extended_pubkey, Transaction, x_to_xpub) from electrum.wallet import BIP32_HD_Wallet, BIP44_Wallet +class DeviceDisconnectedError(Exception): + pass + class TrezorCompatibleWallet(BIP44_Wallet): # Extend BIP44 Wallet as required by hardware implementation. # Derived classes must set: @@ -22,11 +27,21 @@ class TrezorCompatibleWallet(BIP44_Wallet): def __init__(self, storage): BIP44_Wallet.__init__(self, storage) - self.proper_device = False - - def give_error(self, message): - self.print_error(message) - raise Exception(message) + # This is set when paired with a device, and used to re-pair + # a device that is disconnected and re-connected + self.device_id = None + # Errors and other user interaction is done through the wallet's + # handler. The handler is per-window and preserved across + # device reconnects + self.handler = None + + def disconnected(self): + self.print_error("disconnected") + self.handler.watching_only_changed() + + def connected(self): + self.print_error("connected") + self.handler.watching_only_changed() def get_action(self): pass @@ -35,29 +50,29 @@ class TrezorCompatibleWallet(BIP44_Wallet): return False def is_watching_only(self): + '''The wallet is watching-only if its trezor device is not + connected. This result is dynamic and changes over time.''' assert not self.has_seed() - return not self.proper_device + return self.plugin.lookup_client(self) is None def can_change_password(self): return False - def get_client(self): - return self.plugin.get_client(self) - - def check_proper_device(self): - return self.get_client().check_proper_device(self) + def client(self): + return self.plugin.client(self) def derive_xkeys(self, root, derivation, password): if self.master_public_keys.get(root): return BIP44_wallet.derive_xkeys(self, root, derivation, password) - # Happens when creating a wallet + # When creating a wallet we need to ask the device for the + # master public key derivation = derivation.replace(self.root_name, self.prefix() + "/") xpub = self.get_public_key(derivation) return xpub, None def get_public_key(self, bip32_path): - client = self.get_client() + client = self.client() address_n = client.expand_path(bip32_path) node = client.get_public_node(address_n).node xpub = ("0488B21E".decode('hex') + chr(node.depth) @@ -72,25 +87,15 @@ class TrezorCompatibleWallet(BIP44_Wallet): raise RuntimeError(_('Decrypt method is not implemented')) def sign_message(self, address, message, password): - client = self.get_client() - self.check_proper_device() - try: - address_path = self.address_id(address) - address_n = client.expand_path(address_path) - except Exception as e: - self.give_error(e) - try: - msg_sig = client.sign_message('Bitcoin', address_n, message) - except Exception as e: - self.give_error(e) - finally: - self.plugin.get_handler(self).stop() + client = self.client() + address_path = self.address_id(address) + address_n = client.expand_path(address_path) + msg_sig = client.sign_message('Bitcoin', address_n, message) return msg_sig.signature def sign_transaction(self, tx, password): if tx.is_complete() or self.is_watching_only(): return - self.check_proper_device() # previous transactions used as inputs prev_tx = {} # path of the xpubs that are involved @@ -123,50 +128,171 @@ class TrezorCompatiblePlugin(BasePlugin): # libraries_available, libraries_URL, minimum_firmware, # wallet_class, ckd_public, types, HidTransport + # This plugin automatically keeps track of attached devices, and + # connects to anything attached creating a new Client instance. + # When disconnected, the client is informed via a callback. + # As a device can be disconnected and/or reconnected in a different + # USB port (giving it a new path), the wallet must be dynamic in + # asking for its client. + # If a wallet is successfully paired with a given device, the plugin + # stores its serial number in the wallet so it can be automatically + # re-paired if the same device is connected elsewhere. + # Approaching things this way permits several devices to be connected + # simultaneously and handled smoothly. + def __init__(self, parent, config, name): BasePlugin.__init__(self, parent, config, name) self.device = self.wallet_class.device - self.client = None self.wallet_class.plugin = self + # A set of client instances to USB paths + self.clients = set() + # The device wallets we have seen to inform on reconnection + self.paired_wallets = set() + # Do an initial scan + self.last_scan = 0 + self.timer_actions() - def give_error(self, message): - self.print_error(message) - raise Exception(message) - - def is_enabled(self): - return self.libraries_available + @hook + def timer_actions(self): + if self.libraries_available: + # Scan connected devices every second + now = time.time() + if now > self.last_scan + 1: + self.last_scan = now + self.scan_devices() + + def scan_devices(self): + paths = self.HidTransport.enumerate() + connected = set([c for c in self.clients if c.path in paths]) + disconnected = self.clients - connected + + # Inform clients and wallets they were disconnected + for client in disconnected: + self.print_error("device disconnected:", client) + if client.wallet: + client.wallet.disconnected() + + for path in paths: + # Look for new paths + if any(c.path == path for c in connected): + continue + + try: + transport = self.HidTransport(path) + except BaseException as e: + # We were probably just disconnected; never mind + self.print_error("cannot connect at", path, str(e)) + continue + + self.print_error("connected to device at", path[0]) + + try: + client = self.client_class(transport, path, self) + except BaseException as e: + self.print_error("cannot create client for", path, str(e)) + else: + connected.add(client) + self.print_error("new device:", client) + + # Inform reconnected wallets + for wallet in self.paired_wallets: + if wallet.device_id == client.features.device_id: + client.wallet = wallet + wallet.connected() + + self.clients = connected + + def clear_session(self, client): + # Clearing the session forces pin re-entry + self.print_error("clear session:", client) + client.clear_session() + + def select_device(self, wallet, wizard): + '''Called when creating a new wallet. Select the device + to use.''' + clients = list(self.clients) + if not len(clients): + return + if len(clients) > 1: + labels = [client.label() for client in clients] + msg = _("Please select which %s device to use:") % self.device + client = clients[wizard.query_choice(msg, labels)] + else: + client = clients[0] + self.pair_wallet(wallet, client) + + def pair_wallet(self, wallet, client): + self.print_error("pairing wallet %s to device %s" % (wallet, client)) + self.paired_wallets.add(wallet) + wallet.device_id = client.features.device_id + client.wallet = wallet + wallet.connected() + + def try_to_pair_wallet(self, wallet): + '''Call this when loading an existing wallet to find if the + associated device is connected.''' + account = '0' + if not account in wallet.accounts: + self.print_error("try pair_wallet: wallet has no accounts") + return None + + first_address = wallet.accounts[account].first_address()[0] + derivation = wallet.address_derivation(account, 0, 0) + for client in self.clients: + if client.wallet: + continue + + if not client.atleast_version(*self.minimum_firmware): + wallet.handler.show_error( + _('Outdated %s firmware for device labelled %s. Please ' + 'download the updated firmware from %s') % + (self.device, client.label(), self.firmware_URL)) + continue + + # This gives us a handler + client.wallet = wallet + device_address = None + try: + device_address = client.address_from_derivation(derivation) + finally: + client.wallet = None + + if first_address == device_address: + self.pair_wallet(wallet, client) + return client + + return None + + def lookup_client(self, wallet): + for client in self.clients: + if client.features.device_id == wallet.device_id: + return client + return None + + def client(self, wallet): + '''Returns a wrapped client which handles cleanup in case of + thrown exceptions, etc.''' + assert isinstance(wallet, self.wallet_class) + assert wallet.handler != None + + if wallet.device_id is None: + client = self.try_to_pair_wallet(wallet) + else: + client = self.lookup_client(wallet) + + if not client: + msg = (_('Could not connect to your %s. Verify the ' + 'cable is connected and that no other app is ' + 'using it.\nContinuing in watching-only mode ' + 'until the device is re-connected.') % self.device) + if not self.clients: + wallet.handler.show_error(msg) + raise DeviceDisconnectedError(msg) - def create_client(self): - if not self.libraries_available: - self.give_error(_('please install the %s libraries from %s') - % (self.device, self.libraries_URL)) - - devices = self.HidTransport.enumerate() - if not devices: - self.give_error(_('Could not connect to your %s. Verify the ' - 'cable is connected and that no other app is ' - 'using it.\nContinuing in watching-only mode.' - % self.device)) - - transport = self.HidTransport(devices[0]) - client = self.client_class(transport, self) - if not client.atleast_version(*self.minimum_firmware): - self.give_error(_('Outdated %s firmware. Please update the ' - 'firmware from %s') - % (self.device, self.firmware_URL)) return client - def get_handler(self, wallet): - return self.get_client(wallet).handler - - def get_client(self, wallet=None): - if not self.client or self.client.bad: - self.client = self.create_client() - - return self.client - - def atleast_version(self, major, minor=0, patch=0): - return self.get_client().atleast_version(major, minor, patch) + def is_enabled(self): + return self.libraries_available @staticmethod def normalize_passphrase(self, passphrase): @@ -192,41 +318,33 @@ class TrezorCompatiblePlugin(BasePlugin): @hook def close_wallet(self, wallet): - if self.client: - self.print_error("clear session") - self.client.clear_session() - self.client.transport.close() - self.client = None + # Don't retain references to a closed wallet + self.paired_wallets.discard(wallet) + client = self.lookup_client(wallet) + if client: + self.clear_session(client) + # Release the device + self.clients.discard(client) + client.transport.close() def sign_transaction(self, wallet, tx, prev_tx, xpub_path): self.prev_tx = prev_tx self.xpub_path = xpub_path - client = self.get_client() + client = self.client(wallet) inputs = self.tx_inputs(tx, True) outputs = self.tx_outputs(wallet, tx) - try: - signed_tx = client.sign_tx('Bitcoin', inputs, outputs)[1] - except Exception as e: - self.give_error(e) - finally: - self.get_handler(wallet).stop() + signed_tx = client.sign_tx('Bitcoin', inputs, outputs)[1] raw = signed_tx.encode('hex') tx.update_signatures(raw) def show_address(self, wallet, address): - client = self.get_client() - wallet.check_proper_device() - try: - address_path = wallet.address_id(address) - address_n = self.client_class.expand_path(address_path) - except Exception as e: - self.give_error(e) - try: - client.get_address('Bitcoin', address_n, True) - except Exception as e: - self.give_error(e) - finally: - self.get_handler(wallet).stop() + client = self.client(wallet) + if not client.atleast_version(1, 3): + wallet.handler.show_error(_("Your device firmware is too old")) + return + address_path = wallet.address_id(address) + address_n = client.expand_path(address_path) + client.get_address('Bitcoin', address_n, True) def tx_inputs(self, tx, for_sig=False): inputs = [] diff --git a/plugins/trezor/qt.py b/plugins/trezor/qt.py @@ -1,10 +1,11 @@ -from plugins.trezor.qt_generic import QtPlugin +from plugins.trezor.qt_generic import qt_plugin_class +from trezor import TrezorPlugin -class Plugin(QtPlugin): +class Plugin(qt_plugin_class(TrezorPlugin)): icon_file = ":icons/trezor.png" - @staticmethod - def pin_matrix_widget_class(): + @classmethod + def pin_matrix_widget_class(self): from trezorlib.qt.pinmatrix import PinMatrixWidget return PinMatrixWidget diff --git a/plugins/trezor/qt_generic.py b/plugins/trezor/qt_generic.py @@ -3,7 +3,6 @@ import threading from PyQt4.Qt import QGridLayout, QInputDialog, QPushButton from PyQt4.Qt import QVBoxLayout, QLabel, SIGNAL -from trezor import TrezorPlugin from electrum_gui.qt.main_window import StatusBarButton from electrum_gui.qt.password_dialog import PasswordDialog from electrum_gui.qt.util import * @@ -19,23 +18,30 @@ class QtHandler(PrintError): Trezor protocol; derived classes can customize it.''' def __init__(self, win, pin_matrix_widget_class, device): - win.connect(win, SIGNAL('message_done'), self.dialog_stop) + win.connect(win, SIGNAL('clear_dialog'), self.clear_dialog) + win.connect(win, SIGNAL('error_dialog'), self.error_dialog) win.connect(win, SIGNAL('message_dialog'), self.message_dialog) win.connect(win, SIGNAL('pin_dialog'), self.pin_dialog) win.connect(win, SIGNAL('passphrase_dialog'), self.passphrase_dialog) + self.window_stack = [win] self.win = win - self.windows = [win] self.pin_matrix_widget_class = pin_matrix_widget_class self.device = device - self.done = threading.Event() self.dialog = None + self.done = threading.Event() - def stop(self): - self.win.emit(SIGNAL('message_done')) + def watching_only_changed(self): + self.win.emit(SIGNAL('watching_only_changed')) def show_message(self, msg, cancel_callback=None): self.win.emit(SIGNAL('message_dialog'), msg, cancel_callback) + def show_error(self, msg): + self.win.emit(SIGNAL('error_dialog'), msg) + + def finished(self): + self.win.emit(SIGNAL('clear_dialog')) + def get_pin(self, msg): self.done.clear() self.win.emit(SIGNAL('pin_dialog'), msg) @@ -50,22 +56,19 @@ class QtHandler(PrintError): def pin_dialog(self, msg): # Needed e.g. when renaming label and haven't entered PIN - self.dialog_stop() - d = WindowModalDialog(self.windows[-1], _("Enter PIN")) + dialog = WindowModalDialog(self.window_stack[-1], _("Enter PIN")) matrix = self.pin_matrix_widget_class() vbox = QVBoxLayout() vbox.addWidget(QLabel(msg)) vbox.addWidget(matrix) - vbox.addLayout(Buttons(CancelButton(d), OkButton(d))) - d.setLayout(vbox) - if not d.exec_(): - self.response = None # FIXME: this is lost? + vbox.addLayout(Buttons(CancelButton(dialog), OkButton(dialog))) + dialog.setLayout(vbox) + dialog.exec_() self.response = str(matrix.get_value()) self.done.set() def passphrase_dialog(self, msg): - self.dialog_stop() - d = PasswordDialog(self.windows[-1], None, msg, + d = PasswordDialog(self.window_stack[-1], None, msg, PasswordDialog.PW_PASSHPRASE) confirmed, p, passphrase = d.run() if confirmed: @@ -75,9 +78,9 @@ class QtHandler(PrintError): def message_dialog(self, msg, cancel_callback): # Called more than once during signing, to confirm output and fee - self.dialog_stop() + self.clear_dialog() title = _('Please check your %s device') % self.device - dialog = self.dialog = WindowModalDialog(self.windows[-1], title) + self.dialog = dialog = WindowModalDialog(self.window_stack[-1], title) l = QLabel(msg) vbox = QVBoxLayout(dialog) if cancel_callback: @@ -86,19 +89,25 @@ class QtHandler(PrintError): vbox.addWidget(l) dialog.show() - def dialog_stop(self): + def error_dialog(self, msg): + self.win.show_error(msg, parent=self.window_stack[-1]) + + def clear_dialog(self): if self.dialog: - self.dialog.hide() + self.dialog.accept() self.dialog = None - def pop_window(self): - self.windows.pop() + def exec_dialog(self, dialog): + self.window_stack.append(dialog) + try: + dialog.exec_() + finally: + assert dialog == self.window_stack.pop() - def push_window(self, window): - self.windows.append(window) +def qt_plugin_class(base_plugin_class): -class QtPlugin(TrezorPlugin): + class QtPlugin(base_plugin_class): # Derived classes must provide the following class-static variables: # icon_file # pin_matrix_widget_class @@ -110,33 +119,28 @@ class QtPlugin(TrezorPlugin): def load_wallet(self, wallet, window): if type(wallet) != self.wallet_class: return - try: - client = self.get_client(wallet) - client.handler = self.create_handler(window) - client.check_proper_device(wallet) - self.button = StatusBarButton(QIcon(self.icon_file), self.device, - partial(self.settings_dialog, window)) - window.statusBar().addPermanentWidget(self.button) - except Exception as e: - window.show_error(str(e)) + window.tzb = StatusBarButton(QIcon(self.icon_file), self.device, + partial(self.settings_dialog, window)) + window.statusBar().addPermanentWidget(window.tzb) + wallet.handler = self.create_handler(window) + # Trigger a pairing + self.client(wallet) def on_create_wallet(self, wallet, wizard): - client = self.get_client(wallet) - client.handler = self.create_handler(wizard) + assert type(wallet) == self.wallet_class + wallet.handler = self.create_handler(wizard) + self.select_device(wallet, wizard) wallet.create_main_account(None) @hook def receive_menu(self, menu, addrs, wallet): - if type(wallet) != self.wallet_class: - return - if (not wallet.is_watching_only() and - self.atleast_version(1, 3) and len(addrs) == 1): + if type(wallet) == self.wallet_class and len(addrs) == 1: menu.addAction(_("Show on %s") % self.device, lambda: self.show_address(wallet, addrs[0])) def settings_dialog(self, window): - - handler = self.get_client(window.wallet).handler + handler = window.wallet.handler + client = self.client(window.wallet) def rename(): title = _("Set Device Label") @@ -145,10 +149,7 @@ class QtPlugin(TrezorPlugin): if not response[1]: return new_label = str(response[0]) - try: - client.change_label(new_label) - finally: - handler.stop() + client.change_label(new_label) device_label.setText(new_label) def update_pin_info(): @@ -159,13 +160,9 @@ class QtPlugin(TrezorPlugin): clear_pin_button.setVisible(features.pin_protection) def set_pin(remove): - try: - client.set_pin(remove=remove) - finally: - handler.stop() + client.set_pin(remove=remove) update_pin_info() - client = self.get_client() features = client.features noyes = [_("No"), _("Yes")] bl_hash = features.bootloader_hash.encode('hex').upper() @@ -200,7 +197,7 @@ class QtPlugin(TrezorPlugin): widget = item if isinstance(item, QWidget) else QLabel(item) layout.addWidget(widget, row_num, col_num) - dialog = WindowModalDialog(None, _("%s Settings") % self.device) + dialog = WindowModalDialog(window, _("%s Settings") % self.device) vbox = QVBoxLayout() tabs = QTabWidget() tabs.addTab(info_tab, _("Information")) @@ -210,8 +207,6 @@ class QtPlugin(TrezorPlugin): vbox.addLayout(Buttons(CloseButton(dialog))) dialog.setLayout(vbox) - handler.push_window(dialog) - try: - dialog.exec_() - finally: - handler.pop_window() + handler.exec_dialog(dialog) + + return QtPlugin