tMerge pull request #1909 from btchip/ledger-nanos - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit 5be93bdb738dfb12e8fcb02572592b55814d8f88 DIR parent 357ea62303633e6514378c4ebe231fab911a614b HTML Author: ThomasV <thomasv@electrum.org> Date: Sun, 28 Aug 2016 17:56:57 +0200 Merge pull request #1909 from btchip/ledger-nanos Ledger pack : Nano S support, rewrite/cleanup, P2SH Diffstat: M icons.qrc | 2 ++ A icons/ledger.png | 0 A icons/ledger_unpaired.png | 0 M lib/plugins.py | 5 ++++- M plugins/ledger/ledger.py | 487 ++++++++++++++++++------------- M plugins/ledger/qt.py | 35 +++++++++++++++++++++---------- 6 files changed, 322 insertions(+), 207 deletions(-) --- DIR diff --git a/icons.qrc b/icons.qrc t@@ -17,6 +17,8 @@ <file>icons/keepkey.png</file> <file>icons/keepkey_unpaired.png</file> <file>icons/key.png</file> + <file>icons/ledger.png</file> + <file>icons/ledger_unpaired.png</file> <file>icons/lock.png</file> <file>icons/microphone.png</file> <file>icons/network.png</file> DIR diff --git a/icons/ledger.png b/icons/ledger.png Binary files differ. DIR diff --git a/icons/ledger_unpaired.png b/icons/ledger_unpaired.png Binary files differ. DIR diff --git a/lib/plugins.py b/lib/plugins.py t@@ -500,8 +500,11 @@ class DeviceMgr(ThreadJob, PrintError): if product_key in self.recognised_hardware: # Older versions of hid don't provide interface_number interface_number = d.get('interface_number', 0) + serial = d['serial_number'] + if len(serial) == 0: + serial = d['path'] devices.append(Device(d['path'], interface_number, - d['serial_number'], product_key)) + serial, product_key)) # Now find out what was disconnected pairs = [(dev.path, dev.id_) for dev in devices] DIR diff --git a/plugins/ledger/ledger.py b/plugins/ledger/ledger.py t@@ -1,10 +1,10 @@ from binascii import hexlify -from struct import unpack +from struct import pack, unpack import hashlib import time import electrum -from electrum.bitcoin import EncodeBase58Check, DecodeBase58Check, TYPE_ADDRESS +from electrum.bitcoin import EncodeBase58Check, DecodeBase58Check, TYPE_ADDRESS, int_to_hex, var_int from electrum.i18n import _ from electrum.plugins import BasePlugin, hook from electrum.keystore import Hardware_KeyStore t@@ -13,9 +13,10 @@ from electrum.util import format_satoshis_plain, print_error try: - from btchip.btchipComm import getDongle, DongleWait + import hid + from btchip.btchipComm import HIDDongleHIDAPI, DongleWait from btchip.btchip import btchip - from btchip.btchipUtils import compress_public_key,format_transaction, get_regular_input_script + from btchip.btchipUtils import compress_public_key,format_transaction, get_regular_input_script, get_p2sh_input_script from btchip.bitcoinTransaction import bitcoinTransaction from btchip.btchipPersoWizard import StartBTChipPersoDialog from btchip.btchipFirmwareWizard import checkFirmware, updateFirmware t@@ -25,8 +26,136 @@ try: except ImportError: BTCHIP = False +class Ledger_Client(): + def __init__(self, hidDevice): + self.dongleObject = btchip(hidDevice) + self.preflightDone = False + + def is_pairable(self): + return True + + def close(self): + self.dongleObject.dongle.close() + + def timeout(self, cutoff): + pass + + def is_initialized(self): + return True + + def label(self): + return "" + + def i4b(self, x): + return pack('>I', x) + + def get_xpub(self, bip32_path): + self.checkDevice() + # bip32_path is of the form 44'/0'/1' + # S-L-O-W - we don't handle the fingerprint directly, so compute + # it manually from the previous node + # This only happens once so it's bearable + #self.get_client() # prompt for the PIN before displaying the dialog if necessary + #self.handler.show_message("Computing master public key") + try: + splitPath = bip32_path.split('/') + if splitPath[0] == 'm': + splitPath = splitPath[1:] + bip32_path = bip32_path[2:] + fingerprint = 0 + if len(splitPath) > 1: + prevPath = "/".join(splitPath[0:len(splitPath) - 1]) + nodeData = self.dongleObject.getWalletPublicKey(prevPath) + publicKey = compress_public_key(nodeData['publicKey']) + h = hashlib.new('ripemd160') + h.update(hashlib.sha256(publicKey).digest()) + fingerprint = unpack(">I", h.digest()[0:4])[0] + nodeData = self.dongleObject.getWalletPublicKey(bip32_path) + publicKey = compress_public_key(nodeData['publicKey']) + depth = len(splitPath) + lastChild = splitPath[len(splitPath) - 1].split('\'') + if len(lastChild) == 1: + childnum = int(lastChild[0]) + else: + childnum = 0x80000000 | int(lastChild[0]) + xpub = "0488B21E".decode('hex') + chr(depth) + self.i4b(fingerprint) + self.i4b(childnum) + str(nodeData['chainCode']) + str(publicKey) + except Exception, e: + #self.give_error(e, True) + return None + finally: + #self.handler.clear_dialog() + pass + + return EncodeBase58Check(xpub) + + def has_detached_pin_support(self, client): + try: + client.getVerifyPinRemainingAttempts() + return True + except BTChipException, e: + if e.sw == 0x6d00: + return False + raise e + + def is_pin_validated(self, client): + try: + # Invalid SET OPERATION MODE to verify the PIN status + client.dongle.exchange(bytearray([0xe0, 0x26, 0x00, 0x00, 0x01, 0xAB])) + except BTChipException, e: + if (e.sw == 0x6982): + return False + if (e.sw == 0x6A80): + return True + raise e + + def perform_hw1_preflight(self): + try: + firmware = self.dongleObject.getFirmwareVersion()['version'].split(".") + if not checkFirmware(firmware): + self.dongleObject.close() + raise Exception("HW1 firmware version too old. Please update at https://www.ledgerwallet.com") + try: + self.dongleObject.getOperationMode() + except BTChipException, e: + if (e.sw == 0x6985): + self.dongleObject.close() + dialog = StartBTChipPersoDialog() + dialog.exec_() + # Acquire the new client on the next run + else: + raise e + if self.has_detached_pin_support(self.dongleObject) and not self.is_pin_validated(self.dongleObject) and (self.handler <> None): + remaining_attempts = self.dongleObject.getVerifyPinRemainingAttempts() + if remaining_attempts <> 1: + msg = "Enter your Ledger PIN - remaining attempts : " + str(remaining_attempts) + else: + msg = "Enter your Ledger PIN - WARNING : LAST ATTEMPT. If the PIN is not correct, the dongle will be wiped." + confirmed, p, pin = self.password_dialog(msg) + if not confirmed: + raise Exception('Aborted by user - please unplug the dongle and plug it again before retrying') + pin = pin.encode() + self.dongleObject.verifyPin(pin) + except BTChipException, e: + if (e.sw == 0x6faa): + raise Exception("Dongle is temporarily locked - please unplug it and replug it again") + if ((e.sw & 0xFFF0) == 0x63c0): + raise Exception("Invalid PIN - please unplug the dongle and plug it again before retrying") + raise e + + def checkDevice(self): + if not self.preflightDone: + self.perform_hw1_preflight() + self.preflightDone = True + + def password_dialog(self, msg=None): + response = self.handler.get_word(msg) + if response is None: + return False, None, None + return True, response, response + class Ledger_KeyStore(Hardware_KeyStore): + hw_type = 'ledger' device = 'Ledger' def __init__(self, d): t@@ -35,16 +164,14 @@ class Ledger_KeyStore(Hardware_KeyStore): # handler. The handler is per-window and preserved across # device reconnects self.force_watching_only = False - self.device_checked = False self.signing = False - def get_client(self): - return self.plugin.get_client() - - def init_xpub(self): - client = self.get_client() - self.xpub = self.get_public_key(self.get_derivation()) + def get_derivation(self): + return self.derivation + def get_client(self): + return self.plugin.get_client(self) + def give_error(self, message, clear_client = False): print_error(message) if not self.signing: t@@ -53,38 +180,34 @@ class Ledger_KeyStore(Hardware_KeyStore): self.signing = False if clear_client: self.client = None - self.device_checked = False raise Exception(message) - def address_id(self, address): + def address_id_stripped(self, address): # Strip the leading "m/" - return BIP32_HW_Wallet.address_id(self, address)[2:] + change, index = self.get_address_index(address) + derivation = self.derivation + address_path = "%s/%d/%d"%(derivation, change, index) + return address_path[2:] def decrypt_message(self, pubkey, message, password): - self.give_error("Not supported") + raise RuntimeError(_('Encryption and decryption are currently not supported for %s') % self.device) - def sign_message(self, address, message, password): - use2FA = False + def sign_message(self, sequence, message, password): self.signing = True # prompt for the PIN before displaying the dialog if necessary client = self.get_client() - if not self.check_proper_device(): - self.give_error('Wrong device or password') - address_path = self.address_id(address) + address_path = self.get_derivation()[2:] + "/%d/%d"%sequence self.handler.show_message("Signing message ...") try: info = self.get_client().signMessagePrepare(address_path, message) pin = "" if info['confirmationNeeded']: # TODO : handle different confirmation types. For the time being only supports keyboard 2FA - use2FA = True confirmed, p, pin = self.password_dialog() if not confirmed: raise Exception('Aborted by user') pin = pin.encode() - client.bad = True - self.device_checked = False - self.plugin.get_client(self, True, True) + #self.plugin.get_client(self, True, True) signature = self.get_client().signMessageSign(pin) except BTChipException, e: if e.sw == 0x6a80: t@@ -95,7 +218,6 @@ class Ledger_KeyStore(Hardware_KeyStore): self.give_error(e, True) finally: self.handler.clear_dialog() - client.bad = use2FA self.signing = False # Parse the ASN.1 signature t@@ -122,7 +244,7 @@ class Ledger_KeyStore(Hardware_KeyStore): inputs = [] inputsPaths = [] pubKeys = [] - trustedInputs = [] + chipInputs = [] redeemScripts = [] signatures = [] preparedTrustedInputs = [] t@@ -130,53 +252,91 @@ class Ledger_KeyStore(Hardware_KeyStore): changeAmount = None output = None outputAmount = None - use2FA = False + p2shTransaction = False pin = "" + self.get_client() # prompt for the PIN before displaying the dialog if necessary rawTx = tx.serialize() # Fetch inputs of the transaction to sign for txinput in tx.inputs(): if ('is_coinbase' in txinput and txinput['is_coinbase']): self.give_error("Coinbase not supported") # should never happen - inputs.append([ self.transactions[txinput['prevout_hash']].raw, - txinput['prevout_n'] ]) - address = txinput['address'] - inputsPaths.append(self.address_id(address)) - pubKeys.append(self.get_public_keys(address)) + redeemScript = None + signingPos = -1 + hwAddress = "%s/%d/%d" % (self.get_derivation()[2:], txinput['derivation'][0], txinput['derivation'][1]) + if len(txinput['pubkeys']) > 1: + p2shTransaction = True + if 'redeemScript' in txinput: + redeemScript = txinput['redeemScript'] + if p2shTransaction: + chipPublicKey = compress_public_key(self.get_client().getWalletPublicKey(hwAddress)['publicKey']) + for currentIndex, key in enumerate(txinput['pubkeys']): + if chipPublicKey == key.decode('hex'): + signingPos = currentIndex + break + if signingPos == -1: + self.give_error("No matching key for multisignature input") # should never happen + + inputs.append([ txinput['prev_tx'].raw, + txinput['prevout_n'], redeemScript, txinput['prevout_hash'], signingPos ]) + inputsPaths.append(hwAddress) + pubKeys.append(txinput['pubkeys']) + + # Sanity check + if p2shTransaction: + for txinput in tx.inputs(): + if len(txinput['pubkeys']) < 2: + self.give_error("P2SH / regular input mixed in same transaction not supported") # should never happen + txOutput = var_int(len(tx.outputs())) + for output in tx.outputs(): + output_type, addr, amount = output + txOutput += int_to_hex(amount, 8) + script = tx.pay_script(output_type, addr) + txOutput += var_int(len(script)/2) + txOutput += script + txOutput = txOutput.decode('hex') # Recognize outputs - only one output and one change is authorized - if len(tx.outputs()) > 2: # should never happen - self.give_error("Transaction with more than 2 outputs not supported") - for type, address, amount in tx.outputs(): - assert type == TYPE_ADDRESS - if self.is_change(address): - changePath = self.address_id(address) - changeAmount = amount - else: - if output <> None: # should never happen - self.give_error("Multiple outputs with no change not supported") - output = address - outputAmount = amount - - self.get_client() # prompt for the PIN before displaying the dialog if necessary - if not self.check_proper_device(): - self.give_error('Wrong device or password') - + if not p2shTransaction: + if len(tx.outputs()) > 2: # should never happen + self.give_error("Transaction with more than 2 outputs not supported") + for i, (_type, address, amount) in enumerate(tx.outputs()): + assert _type == TYPE_ADDRESS + change, index = tx.output_info[i] + if change: + changePath = "%s/%d/%d" % (self.get_derivation()[2:], change, index) + changeAmount = amount + else: + if output <> None: # should never happen + self.give_error("Multiple outputs with no change not supported") + output = address + outputAmount = amount + self.handler.show_message("Signing Transaction ...") try: # Get trusted inputs from the original transactions - for utxo in inputs: - txtmp = bitcoinTransaction(bytearray(utxo[0].decode('hex'))) - trustedInputs.append(self.get_client().getTrustedInput(txtmp, utxo[1])) - # TODO : Support P2SH later - redeemScripts.append(txtmp.outputs[utxo[1]].script) + for utxo in inputs: + if not p2shTransaction: + txtmp = bitcoinTransaction(bytearray(utxo[0].decode('hex'))) + chipInputs.append(self.get_client().getTrustedInput(txtmp, utxo[1])) + redeemScripts.append(txtmp.outputs[utxo[1]].script) + else: + tmp = utxo[3].decode('hex')[::-1].encode('hex') + tmp += int_to_hex(utxo[1], 4) + chipInputs.append({ 'value' : tmp.decode('hex') }) + redeemScripts.append(bytearray(utxo[2].decode('hex'))) + # Sign all inputs firstTransaction = True inputIndex = 0 while inputIndex < len(inputs): self.get_client().startUntrustedTransaction(firstTransaction, inputIndex, - trustedInputs, redeemScripts[inputIndex]) - outputData = self.get_client().finalizeInput(output, format_satoshis_plain(outputAmount), - format_satoshis_plain(self.get_tx_fee(tx)), changePath, bytearray(rawTx.decode('hex'))) + chipInputs, redeemScripts[inputIndex]) + if not p2shTransaction: + outputData = self.get_client().finalizeInput(output, format_satoshis_plain(outputAmount), + format_satoshis_plain(tx.get_fee()), changePath, bytearray(rawTx.decode('hex'))) + else: + outputData = self.get_client().finalizeInputFull(txOutput) + outputData['outputData'] = txOutput if firstTransaction: transactionOutput = outputData['outputData'] if outputData['confirmationNeeded']: t@@ -204,14 +364,11 @@ class Ledger_KeyStore(Hardware_KeyStore): raise Exception('Invalid PIN character') pin = pin2 else: - use2FA = True confirmed, p, pin = self.password_dialog() if not confirmed: raise Exception('Aborted by user') pin = pin.encode() - client.bad = True - self.device_checked = False - self.plugin.get_client(self, True, True) + #self.plugin.get_client(self, True, True) self.handler.show_message("Signing ...") else: # Sign input with the provided PIN t@@ -229,35 +386,19 @@ class Ledger_KeyStore(Hardware_KeyStore): # Reformat transaction inputIndex = 0 while inputIndex < len(inputs): - # TODO : Support P2SH later - inputScript = get_regular_input_script(signatures[inputIndex], pubKeys[inputIndex][0].decode('hex')) - preparedTrustedInputs.append([ trustedInputs[inputIndex]['value'], inputScript ]) + if p2shTransaction: + signaturesPack = [signatures[inputIndex]] * len(pubKeys[inputIndex]) + inputScript = get_p2sh_input_script(redeemScripts[inputIndex], signaturesPack) + preparedTrustedInputs.append([ ("\x00" * 4) + chipInputs[inputIndex]['value'], inputScript ]) + else: + inputScript = get_regular_input_script(signatures[inputIndex], pubKeys[inputIndex][0].decode('hex')) + preparedTrustedInputs.append([ chipInputs[inputIndex]['value'], inputScript ]) inputIndex = inputIndex + 1 updatedTransaction = format_transaction(transactionOutput, preparedTrustedInputs) updatedTransaction = hexlify(updatedTransaction) - tx.update(updatedTransaction) - client.bad = use2FA + tx.update_signatures(updatedTransaction) self.signing = False - def check_proper_device(self): - pubKey = DecodeBase58Check(self.xpub)[45:] - if not self.device_checked: - self.handler.show_message("Checking device") - try: - nodeData = self.get_client().getWalletPublicKey("44'/0'/0'") - except Exception, e: - self.give_error(e, True) - finally: - self.handler.clear_dialog() - pubKeyDevice = compress_public_key(nodeData['publicKey']) - self.device_checked = True - if pubKey != pubKeyDevice: - self.proper_device = False - else: - self.proper_device = True - - return self.proper_device - def password_dialog(self, msg=None): if not msg: msg = _("Do not enter your device PIN here !\r\n\r\n" \ t@@ -279,121 +420,77 @@ class Ledger_KeyStore(Hardware_KeyStore): class LedgerPlugin(HW_PluginBase): libraries_available = BTCHIP keystore_class = Ledger_KeyStore - hw_type='ledger' client = None + DEVICE_IDS = [ + (0x2581, 0x1807), # HW.1 legacy btchip + (0x2581, 0x2b7c), # HW.1 transitional production + (0x2581, 0x3b7c), # HW.1 ledger production + (0x2581, 0x4b7c), # HW.1 ledger test + (0x2c97, 0x0000), # Blue + (0x2c97, 0x0001) # Nano-S + ] + + def __init__(self, parent, config, name): + HW_PluginBase.__init__(self, parent, config, name) + if self.libraries_available: + self.device_manager().register_devices(self.DEVICE_IDS) def btchip_is_connected(self, keystore): try: - self.get_client().getFirmwareVersion() + self.get_client(keystore).getFirmwareVersion() except Exception as e: - self.print_error("get_client", str(e)) return False return True - def get_client(self, force_pair=True, noPin=False): - aborted = False - client = self.client - if not client or client.bad: - try: - d = getDongle(BTCHIP_DEBUG) - client = btchip(d) - firmware = client.getFirmwareVersion()['version'].split(".") - if not checkFirmware(firmware): - d.close() - try: - updateFirmware() - except Exception, e: - aborted = True - raise e - d = getDongle(BTCHIP_DEBUG) - client = btchip(d) - try: - client.getOperationMode() - except BTChipException, e: - if (e.sw == 0x6985): - d.close() - dialog = StartBTChipPersoDialog() - dialog.exec_() - # Then fetch the reference again as it was invalidated - d = getDongle(BTCHIP_DEBUG) - client = btchip(d) - else: - raise e - if not noPin: - # Immediately prompts for the PIN - remaining_attempts = client.getVerifyPinRemainingAttempts() - if remaining_attempts <> 1: - msg = "Enter your Ledger PIN - remaining attempts : " + str(remaining_attempts) - else: - msg = "Enter your Ledger PIN - WARNING : LAST ATTEMPT. If the PIN is not correct, the dongle will be wiped." - confirmed, p, pin = wallet.password_dialog(msg) - if not confirmed: - aborted = True - raise Exception('Aborted by user - please unplug the dongle and plug it again before retrying') - pin = pin.encode() - client.verifyPin(pin) - - except BTChipException, e: - try: - client.dongle.close() - except: - pass - client = None - if (e.sw == 0x6faa): - raise Exception("Dongle is temporarily locked - please unplug it and replug it again") - if ((e.sw & 0xFFF0) == 0x63c0): - raise Exception("Invalid PIN - please unplug the dongle and plug it again before retrying") - raise e - except Exception, e: - try: - client.dongle.close() - except: - pass - client = None - if not aborted: - raise Exception("Could not connect to your Ledger wallet. Please verify access permissions, PIN, or unplug the dongle and plug it again") - else: - raise e - client.bad = False - self.device_checked = False - self.proper_device = False - self.client = client - - return self.client - - def get_public_key(self, bip32_path): - # bip32_path is of the form 44'/0'/1' - # S-L-O-W - we don't handle the fingerprint directly, so compute - # it manually from the previous node - # This only happens once so it's bearable - self.get_client() # prompt for the PIN before displaying the dialog if necessary - self.handler.show_message("Computing master public key") - try: - splitPath = bip32_path.split('/') - if splitPath[0] == 'm': - splitPath = splitPath[1:] - bip32_path = bip32_path[2:] - fingerprint = 0 - if len(splitPath) > 1: - prevPath = "/".join(splitPath[0:len(splitPath) - 1]) - nodeData = self.get_client().getWalletPublicKey(prevPath) - publicKey = compress_public_key(nodeData['publicKey']) - h = hashlib.new('ripemd160') - h.update(hashlib.sha256(publicKey).digest()) - fingerprint = unpack(">I", h.digest()[0:4])[0] - nodeData = self.get_client().getWalletPublicKey(bip32_path) - publicKey = compress_public_key(nodeData['publicKey']) - depth = len(splitPath) - lastChild = splitPath[len(splitPath) - 1].split('\'') - if len(lastChild) == 1: - childnum = int(lastChild[0]) - else: - childnum = 0x80000000 | int(lastChild[0]) - xpub = "0488B21E".decode('hex') + chr(depth) + self.i4b(fingerprint) + self.i4b(childnum) + str(nodeData['chainCode']) + str(publicKey) - except Exception, e: - self.give_error(e, True) - finally: - self.handler.clear_dialog() - - return EncodeBase58Check(xpub) - + def get_btchip_device(self, device): + ledger = False + if (device.product_key[0] == 0x2581 and device.product_key[1] == 0x3b7c) or (device.product_key[0] == 0x2581 and device.product_key[1] == 0x4b7c) or (device.product_key[0] == 0x2c97): + ledger = True + dev = hid.device() + dev.open_path(device.path) + dev.set_nonblocking(True) + return HIDDongleHIDAPI(dev, ledger, BTCHIP_DEBUG) + + def verify_btchip_pin(self): + pass + + def create_client(self, device, handler): + self.handler = handler + + client = self.get_btchip_device(device) + if client <> None: + client = Ledger_Client(client) + return client + + def setup_device(self, device_info, wizard): + devmgr = self.device_manager() + device_id = device_info.device.id_ + client = devmgr.client_by_id(device_id) + #client.handler = wizard + client.handler = self.create_handler(wizard) + client.get_xpub('m') + + def get_xpub(self, device_id, derivation, wizard): + devmgr = self.device_manager() + client = devmgr.client_by_id(device_id) + #client.handler = wizard + client.handler = self.create_handler(wizard) + client.checkDevice() + xpub = client.get_xpub(derivation) + return xpub + + def get_client(self, keystore, force_pair=True): + # All client interaction should not be in the main GUI thread + #assert self.main_thread != threading.current_thread() + devmgr = self.device_manager() + handler = keystore.handler + handler = keystore.handler + with devmgr.hid_lock: + client = devmgr.client_for_keystore(self, handler, keystore, force_pair) + # returns the client for a given keystore. can use xpub + #if client: + # client.used() + if client <> None: + client.checkDevice() + client = client.dongleObject + return client DIR diff --git a/plugins/ledger/qt.py b/plugins/ledger/qt.py t@@ -3,27 +3,35 @@ import threading from PyQt4.Qt import (QDialog, QInputDialog, QLineEdit, QVBoxLayout, QLabel, SIGNAL) import PyQt4.QtCore as QtCore +from electrum_gui.qt.main_window import StatusBarButton from electrum.i18n import _ from electrum.plugins import hook from .ledger import LedgerPlugin, Ledger_KeyStore from ..hw_wallet.qt import QtHandlerBase +from electrum_gui.qt.util import * class Plugin(LedgerPlugin): + icon_unpaired = ":icons/ledger_unpaired.png" + icon_paired = ":icons/ledger.png" @hook def load_wallet(self, wallet, window): - keystore = wallet.get_keystore() - if type(keystore) != self.keystore_class: - return - keystore.handler = BTChipQTHandler(window) - if self.btchip_is_connected(keystore): - if not keystore.check_proper_device(): - window.show_error(_("This wallet does not match your Ledger device")) - wallet.force_watching_only = True - else: - window.show_error(_("Ledger device not detected.\nContinuing in watching-only mode.")) - wallet.force_watching_only = True + for keystore in wallet.get_keystores(): + if type(keystore) != self.keystore_class: + continue + tooltip = self.device + cb = partial(self.show_settings_dialog, window, keystore) + button = StatusBarButton(QIcon(self.icon_unpaired), tooltip, cb) + button.icon_paired = self.icon_paired + button.icon_unpaired = self.icon_unpaired + window.statusBar().addPermanentWidget(button) + handler = BTChipQTHandler(window) + handler.button = button + keystore.handler = handler + keystore.thread = TaskThread(window, window.on_error) + # Trigger a pairing + keystore.thread.add(partial(self.get_client, keystore)) def create_keystore(self, hw_type, derivation, wizard): from electrum.keystore import hardware_keystore t@@ -40,6 +48,11 @@ class Plugin(LedgerPlugin): k = hardware_keystore(hw_type, d) return k + def create_handler(self, wizard): + return BTChipQTHandler(wizard) + + def show_settings_dialog(self, window, keystore): + pass class BTChipQTHandler(QtHandlerBase):