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
This commit is contained in:
parent
187b4dc9c1
commit
21bf5a8a84
|
@ -1,6 +1,4 @@
|
||||||
####-*.patch
|
####-*.patch
|
||||||
gui/icons_rc.py
|
|
||||||
lib/icons_rc.py
|
|
||||||
*.pyc
|
*.pyc
|
||||||
*.swp
|
*.swp
|
||||||
build/
|
build/
|
||||||
|
|
|
@ -132,13 +132,6 @@ class InstallWizard(WindowModalDialog, MessageBoxMixin, WizardBase):
|
||||||
the password or None for no password."""
|
the password or None for no password."""
|
||||||
return self.pw_dialog(msg or MSG_ENTER_PASSWORD, PasswordDialog.PW_NEW)
|
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):
|
def choose_server(self, network):
|
||||||
# Show network dialog if config does not exist
|
# Show network dialog if config does not exist
|
||||||
if self.config.get('server') is None:
|
if self.config.get('server') is None:
|
||||||
|
@ -323,7 +316,7 @@ class InstallWizard(WindowModalDialog, MessageBoxMixin, WizardBase):
|
||||||
self.config.set_key('auto_connect', True, True)
|
self.config.set_key('auto_connect', True, True)
|
||||||
network.auto_connect = True
|
network.auto_connect = True
|
||||||
|
|
||||||
def choice(self, msg, choices):
|
def query_choice(self, msg, choices):
|
||||||
vbox = QVBoxLayout()
|
vbox = QVBoxLayout()
|
||||||
self.set_layout(vbox)
|
self.set_layout(vbox)
|
||||||
gb2 = QGroupBox(msg)
|
gb2 = QGroupBox(msg)
|
||||||
|
@ -335,7 +328,7 @@ class InstallWizard(WindowModalDialog, MessageBoxMixin, WizardBase):
|
||||||
group2 = QButtonGroup()
|
group2 = QButtonGroup()
|
||||||
for i,c in enumerate(choices):
|
for i,c in enumerate(choices):
|
||||||
button = QRadioButton(gb2)
|
button = QRadioButton(gb2)
|
||||||
button.setText(c[1])
|
button.setText(c)
|
||||||
vbox2.addWidget(button)
|
vbox2.addWidget(button)
|
||||||
group2.addButton(button)
|
group2.addButton(button)
|
||||||
group2.setId(button, i)
|
group2.setId(button, i)
|
||||||
|
@ -347,8 +340,7 @@ class InstallWizard(WindowModalDialog, MessageBoxMixin, WizardBase):
|
||||||
vbox.addLayout(Buttons(CancelButton(self), next_button))
|
vbox.addLayout(Buttons(CancelButton(self), next_button))
|
||||||
if not self.exec_():
|
if not self.exec_():
|
||||||
raise UserCancelled
|
raise UserCancelled
|
||||||
wallet_type = choices[group2.checkedId()][0]
|
return group2.checkedId()
|
||||||
return wallet_type
|
|
||||||
|
|
||||||
def query_multisig(self, action):
|
def query_multisig(self, action):
|
||||||
vbox = QVBoxLayout()
|
vbox = QVBoxLayout()
|
||||||
|
|
|
@ -152,6 +152,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
self.connect(self, QtCore.SIGNAL('payment_request_error'), self.payment_request_error)
|
self.connect(self, QtCore.SIGNAL('payment_request_error'), self.payment_request_error)
|
||||||
self.history_list.setFocus(True)
|
self.history_list.setFocus(True)
|
||||||
|
|
||||||
|
self.connect(self, QtCore.SIGNAL('watching_only_changed'),
|
||||||
|
self.watching_only_changed)
|
||||||
|
|
||||||
# network callbacks
|
# network callbacks
|
||||||
if self.network:
|
if self.network:
|
||||||
self.connect(self, QtCore.SIGNAL('network'), self.on_network_qt)
|
self.connect(self, QtCore.SIGNAL('network'), self.on_network_qt)
|
||||||
|
@ -280,7 +283,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
self.warn_if_watching_only()
|
self.warn_if_watching_only()
|
||||||
|
|
||||||
def watching_only_changed(self):
|
def watching_only_changed(self):
|
||||||
self.saved_wwo = self.wallet.is_watching_only()
|
|
||||||
title = 'Electrum %s - %s' % (self.wallet.electrum_version,
|
title = 'Electrum %s - %s' % (self.wallet.electrum_version,
|
||||||
self.wallet.basename())
|
self.wallet.basename())
|
||||||
if self.wallet.is_watching_only():
|
if self.wallet.is_watching_only():
|
||||||
|
@ -495,6 +497,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
|
self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
|
||||||
|
|
||||||
def timer_actions(self):
|
def timer_actions(self):
|
||||||
|
# Note this runs in the GUI thread
|
||||||
if self.need_update.is_set():
|
if self.need_update.is_set():
|
||||||
self.need_update.clear()
|
self.need_update.clear()
|
||||||
self.update_wallet()
|
self.update_wallet()
|
||||||
|
@ -504,8 +507,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
if self.require_fee_update:
|
if self.require_fee_update:
|
||||||
self.do_update_fee()
|
self.do_update_fee()
|
||||||
self.require_fee_update = False
|
self.require_fee_update = False
|
||||||
if self.saved_wwo != self.wallet.is_watching_only():
|
|
||||||
self.watching_only_changed()
|
|
||||||
run_hook('timer_actions')
|
run_hook('timer_actions')
|
||||||
|
|
||||||
def format_amount(self, x, is_diff=False, whitespaces=False):
|
def format_amount(self, x, is_diff=False, whitespaces=False):
|
||||||
|
|
|
@ -73,7 +73,7 @@ class Plugins(DaemonThread):
|
||||||
self.print_error("loaded", name)
|
self.print_error("loaded", name)
|
||||||
return plugin
|
return plugin
|
||||||
except Exception:
|
except Exception:
|
||||||
print_msg(_("Error: cannot initialize plugin"), name)
|
self.print_error("cannot initialize plugin", name)
|
||||||
traceback.print_exc(file=sys.stdout)
|
traceback.print_exc(file=sys.stdout)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -106,16 +106,17 @@ class Plugins(DaemonThread):
|
||||||
return not requires or w.wallet_type in requires
|
return not requires or w.wallet_type in requires
|
||||||
|
|
||||||
def hardware_wallets(self, action):
|
def hardware_wallets(self, action):
|
||||||
result = []
|
wallet_types, descs = [], []
|
||||||
for name, (gui_good, details) in self.hw_wallets.items():
|
for name, (gui_good, details) in self.hw_wallets.items():
|
||||||
if gui_good:
|
if gui_good:
|
||||||
try:
|
try:
|
||||||
p = self.wallet_plugin_loader(name)
|
p = self.wallet_plugin_loader(name)
|
||||||
if action == 'restore' or p.is_enabled():
|
if action == 'restore' or p.is_enabled():
|
||||||
result.append((details[1], details[2]))
|
wallet_types.append(details[1])
|
||||||
|
descs.append(details[2])
|
||||||
except:
|
except:
|
||||||
self.print_error("cannot load plugin for:", name)
|
self.print_error("cannot load plugin for:", name)
|
||||||
return result
|
return wallet_types, descs
|
||||||
|
|
||||||
def register_plugin_wallet(self, name, gui_good, details):
|
def register_plugin_wallet(self, name, gui_good, details):
|
||||||
def dynamic_constructor(storage):
|
def dynamic_constructor(storage):
|
||||||
|
|
|
@ -205,6 +205,9 @@ class Abstract_Wallet(PrintError):
|
||||||
def diagnostic_name(self):
|
def diagnostic_name(self):
|
||||||
return self.basename()
|
return self.basename()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.basename()
|
||||||
|
|
||||||
def set_use_encryption(self, use_encryption):
|
def set_use_encryption(self, use_encryption):
|
||||||
self.use_encryption = use_encryption
|
self.use_encryption = use_encryption
|
||||||
self.storage.put('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):
|
def can_create_accounts(self):
|
||||||
return not self.is_watching_only()
|
return not self.is_watching_only()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
def prefix(self):
|
def prefix(self):
|
||||||
return "/".join(self.root_derivation.split("/")[1:])
|
return "/".join(self.root_derivation.split("/")[1:])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
def account_derivation(self, account_id):
|
def account_derivation(self, account_id):
|
||||||
return self.prefix() + "/" + account_id + "'"
|
return self.prefix() + "/" + account_id + "'"
|
||||||
|
|
||||||
def address_id(self, address):
|
@classmethod
|
||||||
acc_id, (change, address_index) = self.get_address_index(address)
|
def address_derivation(self, account_id, change, address_index):
|
||||||
account_derivation = self.account_derivation(acc_id)
|
account_derivation = self.account_derivation(account_id)
|
||||||
return "%s/%d/%d" % (account_derivation, change, address_index)
|
return "%s/%d/%d" % (account_derivation, change, address_index)
|
||||||
|
|
||||||
def mnemonic_to_seed(self, mnemonic, passphrase):
|
def address_id(self, address):
|
||||||
|
acc_id, (change, address_index) = self.get_address_index(address)
|
||||||
|
return self.address_derivation(acc_id, change, address_index)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def mnemonic_to_seed(mnemonic, passphrase):
|
||||||
# See BIP39
|
# See BIP39
|
||||||
import pbkdf2, hashlib, hmac
|
import pbkdf2, hashlib, hmac
|
||||||
PBKDF2_ROUNDS = 2048
|
PBKDF2_ROUNDS = 2048
|
||||||
|
|
|
@ -76,11 +76,9 @@ class WizardBase(PrintError):
|
||||||
string like "2of3". Action is 'create' or 'restore'."""
|
string like "2of3". Action is 'create' or 'restore'."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def query_hardware(self, choices, action):
|
def query_choice(self, msg, choices):
|
||||||
"""Asks the user what kind of hardware wallet they want from the given
|
"""Asks the user which of several choices they would like.
|
||||||
choices. choices is a list of (wallet_type, translated
|
Return the index of the choice."""
|
||||||
description) tuples. Action is 'create' or 'restore'. Return
|
|
||||||
the wallet type chosen."""
|
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def show_and_verify_seed(self, seed):
|
def show_and_verify_seed(self, seed):
|
||||||
|
@ -205,8 +203,13 @@ class WizardBase(PrintError):
|
||||||
if kind == 'multisig':
|
if kind == 'multisig':
|
||||||
wallet_type = self.query_multisig(action)
|
wallet_type = self.query_multisig(action)
|
||||||
elif kind == 'hardware':
|
elif kind == 'hardware':
|
||||||
choices = self.plugins.hardware_wallets(action)
|
wallet_types, choices = self.plugins.hardware_wallets(action)
|
||||||
wallet_type = self.query_hardware(choices, 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':
|
elif kind == 'twofactor':
|
||||||
wallet_type = '2fa'
|
wallet_type = '2fa'
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -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"
|
icon_file = ":icons/keepkey.png"
|
||||||
|
|
||||||
def pin_matrix_widget_class():
|
@classmethod
|
||||||
|
def pin_matrix_widget_class(self):
|
||||||
from keepkeylib.qt.pinmatrix import PinMatrixWidget
|
from keepkeylib.qt.pinmatrix import PinMatrixWidget
|
||||||
return PinMatrixWidget
|
return PinMatrixWidget
|
||||||
|
|
|
@ -27,7 +27,7 @@ class GuiMixin(object):
|
||||||
else:
|
else:
|
||||||
cancel_callback = None
|
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()
|
return self.proto.ButtonAck()
|
||||||
|
|
||||||
def callback_PinMatrixRequest(self, msg):
|
def callback_PinMatrixRequest(self, msg):
|
||||||
|
@ -40,14 +40,14 @@ class GuiMixin(object):
|
||||||
"Note the numbers have been shuffled!"))
|
"Note the numbers have been shuffled!"))
|
||||||
else:
|
else:
|
||||||
msg = _("Please enter %s PIN")
|
msg = _("Please enter %s PIN")
|
||||||
pin = self.handler.get_pin(msg % self.device)
|
pin = self.handler().get_pin(msg % self.device)
|
||||||
if not pin:
|
if not pin:
|
||||||
return self.proto.Cancel()
|
return self.proto.Cancel()
|
||||||
return self.proto.PinMatrixAck(pin=pin)
|
return self.proto.PinMatrixAck(pin=pin)
|
||||||
|
|
||||||
def callback_PassphraseRequest(self, req):
|
def callback_PassphraseRequest(self, req):
|
||||||
msg = _("Please enter your %s passphrase")
|
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:
|
if passphrase is None:
|
||||||
return self.proto.Cancel()
|
return self.proto.Cancel()
|
||||||
return self.proto.PassphraseAck(passphrase=passphrase)
|
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):
|
class TrezorClient(protocol_mixin, GuiMixin, base_client, PrintError):
|
||||||
|
|
||||||
def __init__(self, transport, plugin):
|
def __init__(self, transport, path, plugin):
|
||||||
base_client.__init__(self, transport)
|
base_client.__init__(self, transport)
|
||||||
protocol_mixin.__init__(self, transport)
|
protocol_mixin.__init__(self, transport)
|
||||||
self.proto = proto
|
self.proto = proto
|
||||||
self.device = plugin.device
|
self.device = plugin.device
|
||||||
self.handler = None
|
self.path = path
|
||||||
|
self.wallet = None
|
||||||
self.plugin = plugin
|
self.plugin = plugin
|
||||||
self.tx_api = plugin
|
self.tx_api = plugin
|
||||||
self.bad = False
|
|
||||||
self.msg_code_override = None
|
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
|
# Copied from trezorlib/client.py as there it is not static, sigh
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -94,34 +105,8 @@ def trezor_client_class(protocol_mixin, base_client, proto):
|
||||||
path.append(abs(int(x)) | prime)
|
path.append(abs(int(x)) | prime)
|
||||||
return path
|
return path
|
||||||
|
|
||||||
def check_proper_device(self, wallet):
|
def address_from_derivation(self, derivation):
|
||||||
try:
|
return self.get_address('Bitcoin', self.expand_path(derivation))
|
||||||
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 change_label(self, label):
|
def change_label(self, label):
|
||||||
self.msg_code_override = '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):
|
def atleast_version(self, major, minor=0, patch=0):
|
||||||
return cmp(self.firmware_version(), (major, minor, patch))
|
return cmp(self.firmware_version(), (major, minor, patch))
|
||||||
|
|
||||||
def call_raw(self, msg):
|
|
||||||
try:
|
|
||||||
return base_client.call_raw(self, msg)
|
|
||||||
except:
|
|
||||||
self.print_error("Marking %s client bad" % self.device)
|
|
||||||
self.bad = True
|
|
||||||
raise
|
|
||||||
|
|
||||||
return TrezorClient
|
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 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 cls
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import re
|
import re
|
||||||
|
import time
|
||||||
|
|
||||||
from binascii import unhexlify
|
from binascii import unhexlify
|
||||||
from struct import pack
|
from struct import pack
|
||||||
from unicodedata import normalize
|
from unicodedata import normalize
|
||||||
|
@ -12,6 +14,9 @@ from electrum.transaction import (deserialize, is_extended_pubkey,
|
||||||
Transaction, x_to_xpub)
|
Transaction, x_to_xpub)
|
||||||
from electrum.wallet import BIP32_HD_Wallet, BIP44_Wallet
|
from electrum.wallet import BIP32_HD_Wallet, BIP44_Wallet
|
||||||
|
|
||||||
|
class DeviceDisconnectedError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
class TrezorCompatibleWallet(BIP44_Wallet):
|
class TrezorCompatibleWallet(BIP44_Wallet):
|
||||||
# Extend BIP44 Wallet as required by hardware implementation.
|
# Extend BIP44 Wallet as required by hardware implementation.
|
||||||
# Derived classes must set:
|
# Derived classes must set:
|
||||||
|
@ -22,11 +27,21 @@ class TrezorCompatibleWallet(BIP44_Wallet):
|
||||||
|
|
||||||
def __init__(self, storage):
|
def __init__(self, storage):
|
||||||
BIP44_Wallet.__init__(self, storage)
|
BIP44_Wallet.__init__(self, storage)
|
||||||
self.proper_device = False
|
# 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 give_error(self, message):
|
def disconnected(self):
|
||||||
self.print_error(message)
|
self.print_error("disconnected")
|
||||||
raise Exception(message)
|
self.handler.watching_only_changed()
|
||||||
|
|
||||||
|
def connected(self):
|
||||||
|
self.print_error("connected")
|
||||||
|
self.handler.watching_only_changed()
|
||||||
|
|
||||||
def get_action(self):
|
def get_action(self):
|
||||||
pass
|
pass
|
||||||
|
@ -35,29 +50,29 @@ class TrezorCompatibleWallet(BIP44_Wallet):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def is_watching_only(self):
|
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()
|
assert not self.has_seed()
|
||||||
return not self.proper_device
|
return self.plugin.lookup_client(self) is None
|
||||||
|
|
||||||
def can_change_password(self):
|
def can_change_password(self):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_client(self):
|
def client(self):
|
||||||
return self.plugin.get_client(self)
|
return self.plugin.client(self)
|
||||||
|
|
||||||
def check_proper_device(self):
|
|
||||||
return self.get_client().check_proper_device(self)
|
|
||||||
|
|
||||||
def derive_xkeys(self, root, derivation, password):
|
def derive_xkeys(self, root, derivation, password):
|
||||||
if self.master_public_keys.get(root):
|
if self.master_public_keys.get(root):
|
||||||
return BIP44_wallet.derive_xkeys(self, root, derivation, password)
|
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() + "/")
|
derivation = derivation.replace(self.root_name, self.prefix() + "/")
|
||||||
xpub = self.get_public_key(derivation)
|
xpub = self.get_public_key(derivation)
|
||||||
return xpub, None
|
return xpub, None
|
||||||
|
|
||||||
def get_public_key(self, bip32_path):
|
def get_public_key(self, bip32_path):
|
||||||
client = self.get_client()
|
client = self.client()
|
||||||
address_n = client.expand_path(bip32_path)
|
address_n = client.expand_path(bip32_path)
|
||||||
node = client.get_public_node(address_n).node
|
node = client.get_public_node(address_n).node
|
||||||
xpub = ("0488B21E".decode('hex') + chr(node.depth)
|
xpub = ("0488B21E".decode('hex') + chr(node.depth)
|
||||||
|
@ -72,25 +87,15 @@ class TrezorCompatibleWallet(BIP44_Wallet):
|
||||||
raise RuntimeError(_('Decrypt method is not implemented'))
|
raise RuntimeError(_('Decrypt method is not implemented'))
|
||||||
|
|
||||||
def sign_message(self, address, message, password):
|
def sign_message(self, address, message, password):
|
||||||
client = self.get_client()
|
client = self.client()
|
||||||
self.check_proper_device()
|
|
||||||
try:
|
|
||||||
address_path = self.address_id(address)
|
address_path = self.address_id(address)
|
||||||
address_n = client.expand_path(address_path)
|
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)
|
msg_sig = client.sign_message('Bitcoin', address_n, message)
|
||||||
except Exception as e:
|
|
||||||
self.give_error(e)
|
|
||||||
finally:
|
|
||||||
self.plugin.get_handler(self).stop()
|
|
||||||
return msg_sig.signature
|
return msg_sig.signature
|
||||||
|
|
||||||
def sign_transaction(self, tx, password):
|
def sign_transaction(self, tx, password):
|
||||||
if tx.is_complete() or self.is_watching_only():
|
if tx.is_complete() or self.is_watching_only():
|
||||||
return
|
return
|
||||||
self.check_proper_device()
|
|
||||||
# previous transactions used as inputs
|
# previous transactions used as inputs
|
||||||
prev_tx = {}
|
prev_tx = {}
|
||||||
# path of the xpubs that are involved
|
# path of the xpubs that are involved
|
||||||
|
@ -123,51 +128,172 @@ class TrezorCompatiblePlugin(BasePlugin):
|
||||||
# libraries_available, libraries_URL, minimum_firmware,
|
# libraries_available, libraries_URL, minimum_firmware,
|
||||||
# wallet_class, ckd_public, types, HidTransport
|
# 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):
|
def __init__(self, parent, config, name):
|
||||||
BasePlugin.__init__(self, parent, config, name)
|
BasePlugin.__init__(self, parent, config, name)
|
||||||
self.device = self.wallet_class.device
|
self.device = self.wallet_class.device
|
||||||
self.client = None
|
|
||||||
self.wallet_class.plugin = self
|
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):
|
@hook
|
||||||
self.print_error(message)
|
def timer_actions(self):
|
||||||
raise Exception(message)
|
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)
|
||||||
|
|
||||||
|
return client
|
||||||
|
|
||||||
def is_enabled(self):
|
def is_enabled(self):
|
||||||
return self.libraries_available
|
return self.libraries_available
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def normalize_passphrase(self, passphrase):
|
def normalize_passphrase(self, passphrase):
|
||||||
return normalize('NFKD', unicode(passphrase or ''))
|
return normalize('NFKD', unicode(passphrase or ''))
|
||||||
|
@ -192,41 +318,33 @@ class TrezorCompatiblePlugin(BasePlugin):
|
||||||
|
|
||||||
@hook
|
@hook
|
||||||
def close_wallet(self, wallet):
|
def close_wallet(self, wallet):
|
||||||
if self.client:
|
# Don't retain references to a closed wallet
|
||||||
self.print_error("clear session")
|
self.paired_wallets.discard(wallet)
|
||||||
self.client.clear_session()
|
client = self.lookup_client(wallet)
|
||||||
self.client.transport.close()
|
if client:
|
||||||
self.client = None
|
self.clear_session(client)
|
||||||
|
# Release the device
|
||||||
|
self.clients.discard(client)
|
||||||
|
client.transport.close()
|
||||||
|
|
||||||
def sign_transaction(self, wallet, tx, prev_tx, xpub_path):
|
def sign_transaction(self, wallet, tx, prev_tx, xpub_path):
|
||||||
self.prev_tx = prev_tx
|
self.prev_tx = prev_tx
|
||||||
self.xpub_path = xpub_path
|
self.xpub_path = xpub_path
|
||||||
client = self.get_client()
|
client = self.client(wallet)
|
||||||
inputs = self.tx_inputs(tx, True)
|
inputs = self.tx_inputs(tx, True)
|
||||||
outputs = self.tx_outputs(wallet, tx)
|
outputs = self.tx_outputs(wallet, tx)
|
||||||
try:
|
|
||||||
signed_tx = client.sign_tx('Bitcoin', inputs, outputs)[1]
|
signed_tx = client.sign_tx('Bitcoin', inputs, outputs)[1]
|
||||||
except Exception as e:
|
|
||||||
self.give_error(e)
|
|
||||||
finally:
|
|
||||||
self.get_handler(wallet).stop()
|
|
||||||
raw = signed_tx.encode('hex')
|
raw = signed_tx.encode('hex')
|
||||||
tx.update_signatures(raw)
|
tx.update_signatures(raw)
|
||||||
|
|
||||||
def show_address(self, wallet, address):
|
def show_address(self, wallet, address):
|
||||||
client = self.get_client()
|
client = self.client(wallet)
|
||||||
wallet.check_proper_device()
|
if not client.atleast_version(1, 3):
|
||||||
try:
|
wallet.handler.show_error(_("Your device firmware is too old"))
|
||||||
|
return
|
||||||
address_path = wallet.address_id(address)
|
address_path = wallet.address_id(address)
|
||||||
address_n = self.client_class.expand_path(address_path)
|
address_n = client.expand_path(address_path)
|
||||||
except Exception as e:
|
|
||||||
self.give_error(e)
|
|
||||||
try:
|
|
||||||
client.get_address('Bitcoin', address_n, True)
|
client.get_address('Bitcoin', address_n, True)
|
||||||
except Exception as e:
|
|
||||||
self.give_error(e)
|
|
||||||
finally:
|
|
||||||
self.get_handler(wallet).stop()
|
|
||||||
|
|
||||||
def tx_inputs(self, tx, for_sig=False):
|
def tx_inputs(self, tx, for_sig=False):
|
||||||
inputs = []
|
inputs = []
|
||||||
|
|
|
@ -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"
|
icon_file = ":icons/trezor.png"
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def pin_matrix_widget_class():
|
def pin_matrix_widget_class(self):
|
||||||
from trezorlib.qt.pinmatrix import PinMatrixWidget
|
from trezorlib.qt.pinmatrix import PinMatrixWidget
|
||||||
return PinMatrixWidget
|
return PinMatrixWidget
|
||||||
|
|
|
@ -3,7 +3,6 @@ import threading
|
||||||
|
|
||||||
from PyQt4.Qt import QGridLayout, QInputDialog, QPushButton
|
from PyQt4.Qt import QGridLayout, QInputDialog, QPushButton
|
||||||
from PyQt4.Qt import QVBoxLayout, QLabel, SIGNAL
|
from PyQt4.Qt import QVBoxLayout, QLabel, SIGNAL
|
||||||
from trezor import TrezorPlugin
|
|
||||||
from electrum_gui.qt.main_window import StatusBarButton
|
from electrum_gui.qt.main_window import StatusBarButton
|
||||||
from electrum_gui.qt.password_dialog import PasswordDialog
|
from electrum_gui.qt.password_dialog import PasswordDialog
|
||||||
from electrum_gui.qt.util import *
|
from electrum_gui.qt.util import *
|
||||||
|
@ -19,23 +18,30 @@ class QtHandler(PrintError):
|
||||||
Trezor protocol; derived classes can customize it.'''
|
Trezor protocol; derived classes can customize it.'''
|
||||||
|
|
||||||
def __init__(self, win, pin_matrix_widget_class, device):
|
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('message_dialog'), self.message_dialog)
|
||||||
win.connect(win, SIGNAL('pin_dialog'), self.pin_dialog)
|
win.connect(win, SIGNAL('pin_dialog'), self.pin_dialog)
|
||||||
win.connect(win, SIGNAL('passphrase_dialog'), self.passphrase_dialog)
|
win.connect(win, SIGNAL('passphrase_dialog'), self.passphrase_dialog)
|
||||||
|
self.window_stack = [win]
|
||||||
self.win = win
|
self.win = win
|
||||||
self.windows = [win]
|
|
||||||
self.pin_matrix_widget_class = pin_matrix_widget_class
|
self.pin_matrix_widget_class = pin_matrix_widget_class
|
||||||
self.device = device
|
self.device = device
|
||||||
self.done = threading.Event()
|
|
||||||
self.dialog = None
|
self.dialog = None
|
||||||
|
self.done = threading.Event()
|
||||||
|
|
||||||
def stop(self):
|
def watching_only_changed(self):
|
||||||
self.win.emit(SIGNAL('message_done'))
|
self.win.emit(SIGNAL('watching_only_changed'))
|
||||||
|
|
||||||
def show_message(self, msg, cancel_callback=None):
|
def show_message(self, msg, cancel_callback=None):
|
||||||
self.win.emit(SIGNAL('message_dialog'), msg, cancel_callback)
|
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):
|
def get_pin(self, msg):
|
||||||
self.done.clear()
|
self.done.clear()
|
||||||
self.win.emit(SIGNAL('pin_dialog'), msg)
|
self.win.emit(SIGNAL('pin_dialog'), msg)
|
||||||
|
@ -50,22 +56,19 @@ class QtHandler(PrintError):
|
||||||
|
|
||||||
def pin_dialog(self, msg):
|
def pin_dialog(self, msg):
|
||||||
# Needed e.g. when renaming label and haven't entered PIN
|
# Needed e.g. when renaming label and haven't entered PIN
|
||||||
self.dialog_stop()
|
dialog = WindowModalDialog(self.window_stack[-1], _("Enter PIN"))
|
||||||
d = WindowModalDialog(self.windows[-1], _("Enter PIN"))
|
|
||||||
matrix = self.pin_matrix_widget_class()
|
matrix = self.pin_matrix_widget_class()
|
||||||
vbox = QVBoxLayout()
|
vbox = QVBoxLayout()
|
||||||
vbox.addWidget(QLabel(msg))
|
vbox.addWidget(QLabel(msg))
|
||||||
vbox.addWidget(matrix)
|
vbox.addWidget(matrix)
|
||||||
vbox.addLayout(Buttons(CancelButton(d), OkButton(d)))
|
vbox.addLayout(Buttons(CancelButton(dialog), OkButton(dialog)))
|
||||||
d.setLayout(vbox)
|
dialog.setLayout(vbox)
|
||||||
if not d.exec_():
|
dialog.exec_()
|
||||||
self.response = None # FIXME: this is lost?
|
|
||||||
self.response = str(matrix.get_value())
|
self.response = str(matrix.get_value())
|
||||||
self.done.set()
|
self.done.set()
|
||||||
|
|
||||||
def passphrase_dialog(self, msg):
|
def passphrase_dialog(self, msg):
|
||||||
self.dialog_stop()
|
d = PasswordDialog(self.window_stack[-1], None, msg,
|
||||||
d = PasswordDialog(self.windows[-1], None, msg,
|
|
||||||
PasswordDialog.PW_PASSHPRASE)
|
PasswordDialog.PW_PASSHPRASE)
|
||||||
confirmed, p, passphrase = d.run()
|
confirmed, p, passphrase = d.run()
|
||||||
if confirmed:
|
if confirmed:
|
||||||
|
@ -75,9 +78,9 @@ class QtHandler(PrintError):
|
||||||
|
|
||||||
def message_dialog(self, msg, cancel_callback):
|
def message_dialog(self, msg, cancel_callback):
|
||||||
# Called more than once during signing, to confirm output and fee
|
# 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
|
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)
|
l = QLabel(msg)
|
||||||
vbox = QVBoxLayout(dialog)
|
vbox = QVBoxLayout(dialog)
|
||||||
if cancel_callback:
|
if cancel_callback:
|
||||||
|
@ -86,19 +89,25 @@ class QtHandler(PrintError):
|
||||||
vbox.addWidget(l)
|
vbox.addWidget(l)
|
||||||
dialog.show()
|
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:
|
if self.dialog:
|
||||||
self.dialog.hide()
|
self.dialog.accept()
|
||||||
self.dialog = None
|
self.dialog = None
|
||||||
|
|
||||||
def pop_window(self):
|
def exec_dialog(self, dialog):
|
||||||
self.windows.pop()
|
self.window_stack.append(dialog)
|
||||||
|
try:
|
||||||
def push_window(self, window):
|
dialog.exec_()
|
||||||
self.windows.append(window)
|
finally:
|
||||||
|
assert dialog == self.window_stack.pop()
|
||||||
|
|
||||||
|
|
||||||
class QtPlugin(TrezorPlugin):
|
def qt_plugin_class(base_plugin_class):
|
||||||
|
|
||||||
|
class QtPlugin(base_plugin_class):
|
||||||
# Derived classes must provide the following class-static variables:
|
# Derived classes must provide the following class-static variables:
|
||||||
# icon_file
|
# icon_file
|
||||||
# pin_matrix_widget_class
|
# pin_matrix_widget_class
|
||||||
|
@ -110,33 +119,28 @@ class QtPlugin(TrezorPlugin):
|
||||||
def load_wallet(self, wallet, window):
|
def load_wallet(self, wallet, window):
|
||||||
if type(wallet) != self.wallet_class:
|
if type(wallet) != self.wallet_class:
|
||||||
return
|
return
|
||||||
try:
|
window.tzb = StatusBarButton(QIcon(self.icon_file), self.device,
|
||||||
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))
|
partial(self.settings_dialog, window))
|
||||||
window.statusBar().addPermanentWidget(self.button)
|
window.statusBar().addPermanentWidget(window.tzb)
|
||||||
except Exception as e:
|
wallet.handler = self.create_handler(window)
|
||||||
window.show_error(str(e))
|
# Trigger a pairing
|
||||||
|
self.client(wallet)
|
||||||
|
|
||||||
def on_create_wallet(self, wallet, wizard):
|
def on_create_wallet(self, wallet, wizard):
|
||||||
client = self.get_client(wallet)
|
assert type(wallet) == self.wallet_class
|
||||||
client.handler = self.create_handler(wizard)
|
wallet.handler = self.create_handler(wizard)
|
||||||
|
self.select_device(wallet, wizard)
|
||||||
wallet.create_main_account(None)
|
wallet.create_main_account(None)
|
||||||
|
|
||||||
@hook
|
@hook
|
||||||
def receive_menu(self, menu, addrs, wallet):
|
def receive_menu(self, menu, addrs, wallet):
|
||||||
if type(wallet) != self.wallet_class:
|
if type(wallet) == self.wallet_class and len(addrs) == 1:
|
||||||
return
|
|
||||||
if (not wallet.is_watching_only() and
|
|
||||||
self.atleast_version(1, 3) and len(addrs) == 1):
|
|
||||||
menu.addAction(_("Show on %s") % self.device,
|
menu.addAction(_("Show on %s") % self.device,
|
||||||
lambda: self.show_address(wallet, addrs[0]))
|
lambda: self.show_address(wallet, addrs[0]))
|
||||||
|
|
||||||
def settings_dialog(self, window):
|
def settings_dialog(self, window):
|
||||||
|
handler = window.wallet.handler
|
||||||
handler = self.get_client(window.wallet).handler
|
client = self.client(window.wallet)
|
||||||
|
|
||||||
def rename():
|
def rename():
|
||||||
title = _("Set Device Label")
|
title = _("Set Device Label")
|
||||||
|
@ -145,10 +149,7 @@ class QtPlugin(TrezorPlugin):
|
||||||
if not response[1]:
|
if not response[1]:
|
||||||
return
|
return
|
||||||
new_label = str(response[0])
|
new_label = str(response[0])
|
||||||
try:
|
|
||||||
client.change_label(new_label)
|
client.change_label(new_label)
|
||||||
finally:
|
|
||||||
handler.stop()
|
|
||||||
device_label.setText(new_label)
|
device_label.setText(new_label)
|
||||||
|
|
||||||
def update_pin_info():
|
def update_pin_info():
|
||||||
|
@ -159,13 +160,9 @@ class QtPlugin(TrezorPlugin):
|
||||||
clear_pin_button.setVisible(features.pin_protection)
|
clear_pin_button.setVisible(features.pin_protection)
|
||||||
|
|
||||||
def set_pin(remove):
|
def set_pin(remove):
|
||||||
try:
|
|
||||||
client.set_pin(remove=remove)
|
client.set_pin(remove=remove)
|
||||||
finally:
|
|
||||||
handler.stop()
|
|
||||||
update_pin_info()
|
update_pin_info()
|
||||||
|
|
||||||
client = self.get_client()
|
|
||||||
features = client.features
|
features = client.features
|
||||||
noyes = [_("No"), _("Yes")]
|
noyes = [_("No"), _("Yes")]
|
||||||
bl_hash = features.bootloader_hash.encode('hex').upper()
|
bl_hash = features.bootloader_hash.encode('hex').upper()
|
||||||
|
@ -200,7 +197,7 @@ class QtPlugin(TrezorPlugin):
|
||||||
widget = item if isinstance(item, QWidget) else QLabel(item)
|
widget = item if isinstance(item, QWidget) else QLabel(item)
|
||||||
layout.addWidget(widget, row_num, col_num)
|
layout.addWidget(widget, row_num, col_num)
|
||||||
|
|
||||||
dialog = WindowModalDialog(None, _("%s Settings") % self.device)
|
dialog = WindowModalDialog(window, _("%s Settings") % self.device)
|
||||||
vbox = QVBoxLayout()
|
vbox = QVBoxLayout()
|
||||||
tabs = QTabWidget()
|
tabs = QTabWidget()
|
||||||
tabs.addTab(info_tab, _("Information"))
|
tabs.addTab(info_tab, _("Information"))
|
||||||
|
@ -210,8 +207,6 @@ class QtPlugin(TrezorPlugin):
|
||||||
vbox.addLayout(Buttons(CloseButton(dialog)))
|
vbox.addLayout(Buttons(CloseButton(dialog)))
|
||||||
|
|
||||||
dialog.setLayout(vbox)
|
dialog.setLayout(vbox)
|
||||||
handler.push_window(dialog)
|
handler.exec_dialog(dialog)
|
||||||
try:
|
|
||||||
dialog.exec_()
|
return QtPlugin
|
||||||
finally:
|
|
||||||
handler.pop_window()
|
|
||||||
|
|
Loading…
Reference in New Issue