From 724571ea1cb6a2c139d64e3bfe05ef5556f9b6a5 Mon Sep 17 00:00:00 2001 From: Chris Sulmone Date: Thu, 19 Apr 2018 21:34:08 -0500 Subject: [PATCH] Updating ledger code to latest in upstream master and moving to 1.1.1 --- .idea/electrum.iml | 1 - .idea/misc.xml | 4 - README.rst | 2 +- contrib/build-wine/electrum.nsi | 2 +- lib/version.py | 2 +- plugins/hw_wallet/cmdline.py | 3 + plugins/ledger/auth2fa.py | 189 ++++++++++++++++++-------------- plugins/ledger/ledger.py | 86 ++++++++++----- plugins/ledger/qt.py | 31 +++--- 9 files changed, 184 insertions(+), 136 deletions(-) delete mode 100644 .idea/misc.xml diff --git a/.idea/electrum.iml b/.idea/electrum.iml index 1a53fc303258..93b1287efff8 100644 --- a/.idea/electrum.iml +++ b/.idea/electrum.iml @@ -5,7 +5,6 @@ - diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 98a50070fcb8..000000000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/README.rst b/README.rst index e30c4f9e39fe..a1c60bcd9990 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ BTCP Electrum - Lightweight Bitcoin Private Client ========================================== -Current Release (P!1.1.0): https://github.com/BTCPrivate/electrum-btcp/releases/tag/P!1.1.0 +Current Release (P!1.1.1): https://github.com/BTCPrivate/electrum-btcp/releases/tag/P!1.1.1 Originally forked from **spesmilo/electrum**: https://github.com/spesmilo/electrum diff --git a/contrib/build-wine/electrum.nsi b/contrib/build-wine/electrum.nsi index 1e927349ba8b..318f9913baaf 100644 --- a/contrib/build-wine/electrum.nsi +++ b/contrib/build-wine/electrum.nsi @@ -52,7 +52,7 @@ Caption "${PRODUCT_NAME}" ;Adds the Product Version on top of the Version Tab in the Properties of the file. - VIProductVersion 1.1.0.0 + VIProductVersion 1.1.1.0 ;VIAddVersionKey - Adds a field in the Version Tab of the File Properties. This can either be a field provided by the system or a user defined field. VIAddVersionKey ProductName "${PRODUCT_NAME} Installer" diff --git a/lib/version.py b/lib/version.py index 34d60a5f9beb..f3e67ff22fe5 100644 --- a/lib/version.py +++ b/lib/version.py @@ -1,5 +1,5 @@ # version of the client package -ELECTRUM_VERSION = 'P!1.1.0' +ELECTRUM_VERSION = 'P!1.1.1' # protocol version requested PROTOCOL_VERSION = '1.1' diff --git a/plugins/hw_wallet/cmdline.py b/plugins/hw_wallet/cmdline.py index 999f82994cfd..6cd27a00108c 100644 --- a/plugins/hw_wallet/cmdline.py +++ b/plugins/hw_wallet/cmdline.py @@ -32,6 +32,9 @@ def stop(self): def show_message(self, msg, on_cancel=None): print_msg(msg) + def show_error(self, msg): + print_msg(msg) + def update_status(self, b): print_error('trezor status', b) diff --git a/plugins/ledger/auth2fa.py b/plugins/ledger/auth2fa.py index add619a82fdf..e6966855c6a7 100644 --- a/plugins/ledger/auth2fa.py +++ b/plugins/ledger/auth2fa.py @@ -1,32 +1,41 @@ +import os +import hashlib +import logging +import json +import copy from binascii import hexlify, unhexlify +import websocket + from PyQt5.Qt import QDialog, QLineEdit, QTextEdit, QVBoxLayout, QLabel import PyQt5.QtCore as QtCore from PyQt5.QtWidgets import * +from btchip.btchip import * + from electrum.i18n import _ from electrum_gui.qt.util import * from electrum.util import print_msg - -import os, hashlib, websocket, logging, json, copy +from electrum import constants, bitcoin from electrum_gui.qt.qrcodewidget import QRCodeWidget -from btchip.btchip import * DEBUG = False helpTxt = [_("Your Ledger Wallet wants to tell you a one-time PIN code.

" \ - "For best security you should unplug your device, open a text editor on another computer, " \ - "put your cursor into it, and plug your device into that computer. " \ - "It will output a summary of the transaction being signed and a one-time PIN.

" \ - "Verify the transaction summary and type the PIN code here.

" \ - "Before pressing enter, plug the device back into this computer.
" ), - _("Verify the address below.
Type the character from your security card corresponding to the BOLD character."), - _("Waiting for authentication on your mobile phone"), - _("Transaction accepted by mobile phone. Waiting for confirmation."), - _("Click Pair button to begin pairing a mobile phone."), - _("Scan this QR code with your LedgerWallet phone app to pair it with this Ledger device.
" - "To complete pairing you will need your security card to answer a challenge." ) - ] + "For best security you should unplug your device, open a text editor on another computer, " \ + "put your cursor into it, and plug your device into that computer. " \ + "It will output a summary of the transaction being signed and a one-time PIN.

" \ + "Verify the transaction summary and type the PIN code here.

" \ + "Before pressing enter, plug the device back into this computer.
"), + _( + "Verify the address below.
Type the character from your security card corresponding to the BOLD character."), + _("Waiting for authentication on your mobile phone"), + _("Transaction accepted by mobile phone. Waiting for confirmation."), + _("Click Pair button to begin pairing a mobile phone."), + _("Scan this QR code with your Ledger Wallet phone app to pair it with this Ledger device.
" + "To complete pairing you will need your security card to answer a challenge.") + ] + class LedgerAuthDialog(QDialog): def __init__(self, handler, data): @@ -37,20 +46,20 @@ def __init__(self, handler, data): self.handler = handler self.txdata = data self.idxs = self.txdata['keycardData'] if self.txdata['confirmationType'] > 1 else '' - self.setMinimumWidth(600) + self.setMinimumWidth(650) self.setWindowTitle(_("Ledger Wallet Authentication")) self.cfg = copy.deepcopy(self.handler.win.wallet.get_keystore().cfg) self.dongle = self.handler.win.wallet.get_keystore().get_client().dongle self.ws = None self.pin = '' - + self.devmode = self.getDevice2FAMode() if self.devmode == 0x11 or self.txdata['confirmationType'] == 1: self.cfg['mode'] = 0 - + vbox = QVBoxLayout() self.setLayout(vbox) - + def on_change_mode(idx): if idx < 2 and self.ws: self.ws.stop() @@ -62,14 +71,16 @@ def on_change_mode(idx): self.handler.win.wallet.get_keystore().cfg = self.cfg self.handler.win.wallet.save_keystore() self.update_dlg() + def add_pairing(): self.do_pairing() + def return_pin(): - self.pin = self.pintxt.text() if self.txdata['confirmationType'] == 1 else self.cardtxt.text() + self.pin = self.pintxt.text() if self.txdata['confirmationType'] == 1 else self.cardtxt.text() if self.cfg['mode'] == 1: - self.pin = ''.join(chr(int(str(i),16)) for i in self.pin) + self.pin = ''.join(chr(int(str(i), 16)) for i in self.pin) self.accept() - + self.modebox = QWidget() modelayout = QHBoxLayout() self.modebox.setLayout(modelayout) @@ -82,16 +93,16 @@ def return_pin(): modelayout.addStretch(1) self.modebox.setMaximumHeight(50) vbox.addWidget(self.modebox) - + self.populate_modes() self.modes.currentIndexChanged.connect(on_change_mode) self.addPair.clicked.connect(add_pairing) - + self.helpmsg = QTextEdit() self.helpmsg.setStyleSheet("QTextEdit { background-color: lightgray; }") self.helpmsg.setReadOnly(True) vbox.addWidget(self.helpmsg) - + self.pinbox = QWidget() pinlayout = QHBoxLayout() self.pinbox.setLayout(pinlayout) @@ -105,26 +116,35 @@ def return_pin(): pinlayout.addStretch(1) self.pinbox.setVisible(self.cfg['mode'] == 0) vbox.addWidget(self.pinbox) - + self.cardbox = QWidget() card = QVBoxLayout() self.cardbox.setLayout(card) self.addrtext = QTextEdit() - self.addrtext.setStyleSheet("QTextEdit { color:blue; background-color:lightgray; padding:15px 10px; border:none; font-size:20pt; }") + self.addrtext.setStyleSheet( + "QTextEdit { color:blue; background-color:lightgray; padding:15px 10px; border:none; font-size:20pt; font-family:monospace; }") self.addrtext.setReadOnly(True) - self.addrtext.setMaximumHeight(120) + self.addrtext.setMaximumHeight(130) card.addWidget(self.addrtext) - + def pin_changed(s): if len(s) < len(self.idxs): i = self.idxs[len(s)] addr = self.txdata['address'] - addr = addr[:i] + '' + addr[i:i+1] + '' + addr[i+1:] - self.addrtext.setHtml(str(addr)) + if not constants.net.TESTNET: + text = addr[:i] + '' + addr[i:i + 1] + '' + addr[i + 1:] + else: + # pin needs to be created from mainnet address + addr_mainnet = bitcoin.script_to_address(bitcoin.address_to_script(addr), + net=constants.BitcoinMainnet) + addr_mainnet = addr_mainnet[:i] + '' + addr_mainnet[i:i + 1] + '' + addr_mainnet[ + i + 1:] + text = str(addr) + '\n' + str(addr_mainnet) + self.addrtext.setHtml(str(text)) else: self.addrtext.setHtml(_("Press Enter")) - - pin_changed('') + + pin_changed('') cardpin = QHBoxLayout() cardpin.addWidget(QLabel(_("Enter PIN:"))) self.cardtxt = QLineEdit() @@ -138,7 +158,7 @@ def pin_changed(s): card.addLayout(cardpin) self.cardbox.setVisible(self.cfg['mode'] == 1) vbox.addWidget(self.cardbox) - + self.pairbox = QWidget() pairlayout = QVBoxLayout() self.pairbox.setLayout(pairlayout) @@ -151,22 +171,24 @@ def pin_changed(s): self.pairbox.setVisible(False) vbox.addWidget(self.pairbox) self.update_dlg() - + if self.cfg['mode'] > 1 and not self.ws: self.req_validation() - + def populate_modes(self): self.modes.blockSignals(True) self.modes.clear() - self.modes.addItem(_("Summary Text PIN (requires dongle replugging)") if self.txdata['confirmationType'] == 1 else _("Summary Text PIN is Disabled")) + self.modes.addItem( + _("Summary Text PIN (requires dongle replugging)") if self.txdata['confirmationType'] == 1 else _( + "Summary Text PIN is Disabled")) if self.txdata['confirmationType'] > 1: self.modes.addItem(_("Security Card Challenge")) if not self.cfg['pair']: - self.modes.addItem(_("Mobile - Not paired")) + self.modes.addItem(_("Mobile - Not paired")) else: self.modes.addItem(_("Mobile - {}").format(self.cfg['pair'][1])) self.modes.blockSignals(False) - + def update_dlg(self): self.modes.setCurrentIndex(self.cfg['mode']) self.modebox.setVisible(True) @@ -179,8 +201,8 @@ def update_dlg(self): self.pinbox.setVisible(self.cfg['mode'] == 0) self.cardbox.setVisible(self.cfg['mode'] == 1) self.pintxt.setFocus(True) if self.cfg['mode'] == 0 else self.cardtxt.setFocus(True) - self.setMaximumHeight(200) - + self.setMaximumHeight(400) + def do_pairing(self): rng = os.urandom(16) pairID = (hexlify(rng) + hexlify(hashlib.sha256(rng).digest()[0:1])).decode('utf-8') @@ -190,22 +212,22 @@ def do_pairing(self): self.pinbox.setVisible(False) self.cardbox.setVisible(False) self.pairbox.setVisible(True) - self.pairqr.setMinimumSize(300,300) + self.pairqr.setMinimumSize(300, 300) if self.ws: self.ws.stop() self.ws = LedgerWebSocket(self, pairID) self.ws.pairing_done.connect(self.pairing_done) - self.ws.start() - + self.ws.start() + def pairing_done(self, data): if data is not None: - self.cfg['pair'] = [ data['pairid'], data['name'], data['platform'] ] + self.cfg['pair'] = [data['pairid'], data['name'], data['platform']] self.cfg['mode'] = 2 self.handler.win.wallet.get_keystore().cfg = self.cfg self.handler.win.wallet.save_keystore() self.pin = 'paired' self.accept() - + def req_validation(self): if self.cfg['pair'] and 'secureScreenData' in self.txdata: if self.ws: @@ -213,23 +235,23 @@ def req_validation(self): self.ws = LedgerWebSocket(self, self.cfg['pair'][0], self.txdata) self.ws.req_updated.connect(self.req_updated) self.ws.start() - + def req_updated(self, pin): if pin == 'accepted': self.helpmsg.setText(helpTxt[3]) else: self.pin = str(pin) self.accept() - + def getDevice2FAMode(self): - apdu = [0xe0, 0x24, 0x01, 0x00, 0x00, 0x01] # get 2fa mode + apdu = [0xe0, 0x24, 0x01, 0x00, 0x00, 0x01] # get 2fa mode try: - mode = self.dongle.exchange( bytearray(apdu) ) + mode = self.dongle.exchange(bytearray(apdu)) return mode except BTChipException as e: debug_msg('Device getMode Failed') return 0x11 - + def closeEvent(self, evnt): debug_msg("CLOSE - Stop WS") if self.ws: @@ -238,28 +260,31 @@ def closeEvent(self, evnt): evnt.ignore() self.update_dlg() + class LedgerWebSocket(QThread): pairing_done = pyqtSignal(object) req_updated = pyqtSignal(str) - + def __init__(self, dlg, pairID, txdata=None): QThread.__init__(self) self.stopping = False self.pairID = pairID - self.txreq = '{"type":"request","second_factor_data":"' + hexlify(txdata['secureScreenData']).decode('utf-8') + '"}' if txdata else None + self.txreq = '{"type":"request","second_factor_data":"' + hexlify(txdata['secureScreenData']).decode( + 'utf-8') + '"}' if txdata else None self.dlg = dlg self.dongle = self.dlg.dongle self.data = None - - #websocket.enableTrace(True) + + # websocket.enableTrace(True) logging.basicConfig(level=logging.INFO) - self.ws = websocket.WebSocketApp('wss://ws.ledgerwallet.com/2fa/channels', - on_message = self.on_message, on_error = self.on_error, - on_close = self.on_close, on_open = self.on_open) - + self.ws = websocket.WebSocketApp('wss://ws.ledgerwallet.com/2fa/channels', + on_message=self.on_message, on_error=self.on_error, + on_close=self.on_close, on_open=self.on_open) + def run(self): while not self.stopping: self.ws.run_forever() + def stop(self): debug_msg("WS: Stopping") self.stopping = True @@ -269,33 +294,33 @@ def on_message(self, ws, msg): data = json.loads(msg) if data['type'] == 'identify': debug_msg('Identify') - apdu = [0xe0, 0x12, 0x01, 0x00, 0x41] # init pairing + apdu = [0xe0, 0x12, 0x01, 0x00, 0x41] # init pairing apdu.extend(unhexlify(data['public_key'])) try: - challenge = self.dongle.exchange( bytearray(apdu) ) - ws.send( '{"type":"challenge","data":"%s" }' % hexlify(challenge).decode('utf-8') ) + challenge = self.dongle.exchange(bytearray(apdu)) + ws.send('{"type":"challenge","data":"%s" }' % hexlify(challenge).decode('utf-8')) self.data = data except BTChipException as e: debug_msg('Identify Failed') - + if data['type'] == 'challenge': debug_msg('Challenge') - apdu = [0xe0, 0x12, 0x02, 0x00, 0x10] # confirm pairing + apdu = [0xe0, 0x12, 0x02, 0x00, 0x10] # confirm pairing apdu.extend(unhexlify(data['data'])) try: - self.dongle.exchange( bytearray(apdu) ) + self.dongle.exchange(bytearray(apdu)) debug_msg('Pairing Successful') - ws.send( '{"type":"pairing","is_successful":"true"}' ) + ws.send('{"type":"pairing","is_successful":"true"}') self.data['pairid'] = self.pairID self.pairing_done.emit(self.data) except BTChipException as e: debug_msg('Pairing Failed') - ws.send( '{"type":"pairing","is_successful":"false"}' ) + ws.send('{"type":"pairing","is_successful":"false"}') self.pairing_done.emit(None) - ws.send( '{"type":"disconnect"}' ) + ws.send('{"type":"disconnect"}') self.stopping = True ws.close() - + if data['type'] == 'accept': debug_msg('Accepted') self.req_updated.emit('accepted') @@ -305,44 +330,40 @@ def on_message(self, ws, msg): self.txreq = None self.stopping = True ws.close() - + if data['type'] == 'repeat': debug_msg('Repeat') if self.txreq: - ws.send( self.txreq ) + ws.send(self.txreq) debug_msg("Req Sent", self.txreq) if data['type'] == 'connect': debug_msg('Connected') if self.txreq: - ws.send( self.txreq ) + ws.send(self.txreq) debug_msg("Req Sent", self.txreq) if data['type'] == 'disconnect': debug_msg('Disconnected') ws.close() - + def on_error(self, ws, error): message = getattr(error, 'strerror', '') if not message: message = getattr(error, 'message', '') debug_msg("WS: %s" % message) - + def on_close(self, ws): debug_msg("WS: ### socket closed ###") - + def on_open(self, ws): debug_msg("WS: ### socket open ###") debug_msg("Joining with pairing ID", self.pairID) - ws.send( '{"type":"join","room":"%s"}' % self.pairID ) - ws.send( '{"type":"repeat"}' ) + ws.send('{"type":"join","room":"%s"}' % self.pairID) + ws.send('{"type":"repeat"}') if self.txreq: - ws.send( self.txreq ) + ws.send(self.txreq) debug_msg("Req Sent", self.txreq) + def debug_msg(*args): if DEBUG: - print_msg(*args) - - - - - + print_msg(*args) diff --git a/plugins/ledger/ledger.py b/plugins/ledger/ledger.py index a32173e2fb12..913009766d79 100644 --- a/plugins/ledger/ledger.py +++ b/plugins/ledger/ledger.py @@ -57,6 +57,13 @@ def label(self): def i4b(self, x): return pack('>I', x) + def has_usable_connection_with_device(self): + try: + self.dongleObject.getFirmwareVersion() + except BaseException: + return False + return True + def test_pin_unlocked(func): """Function decorator to test the Ledger for being unlocked, and if not, raise a human-readable exception. @@ -164,14 +171,11 @@ def perform_hw1_preflight(self): raise Exception('Aborted by user - please unplug the dongle and plug it again before retrying') pin = pin.encode() self.dongleObject.verifyPin(pin) - p - #self.dongleObject.setAlternateCoinVersion(0x13, 0x25, 0x13, 0xAF) apdu = [ 0xE0, 0x14, 0x01, 0x00, 0x17, 0x13, 0x25, 0x13, 0xAF, 0x01, 0x08] apdu.extend("BPrivate".encode()) apdu.append(0x04) apdu.extend("BTCP".encode()) self.dongleObject.dongle.exchange(bytearray(apdu)) - #self.dongleObject.setAlternateCoinVersion(4901, 5039) except BTChipException as e: if (e.sw == 0x6faa): raise Exception("Dongle is temporarily locked - please unplug it and replug it again") @@ -188,8 +192,9 @@ def checkDevice(self): try: self.perform_hw1_preflight() except BTChipException as e: - if (e.sw == 0x6d00): - raise BaseException("Device not in Bitcoin Private mode") + if (e.sw == 0x6d00 or e.sw == 0x6700): + print(e.sw) + raise Exception(_("Device not in Bitcoin Private mode")) from e raise e self.preflightDone = True @@ -237,6 +242,16 @@ def give_error(self, message, clear_client = False): self.client = None raise Exception(message) + def set_and_unset_signing(func): + """Function decorator to set and unset self.signing.""" + def wrapper(self, *args, **kwargs): + try: + self.signing = True + return func(self, *args, **kwargs) + finally: + self.signing = False + return wrapper + def address_id_stripped(self, address): # Strip the leading "m/" change, index = self.get_address_index(address) @@ -247,8 +262,8 @@ def address_id_stripped(self, address): def decrypt_message(self, pubkey, message, password): raise RuntimeError(_('Encryption and decryption are currently not supported for {}').format(self.device)) + @set_and_unset_signing def sign_message(self, sequence, message, password): - self.signing = True message = message.encode('utf8') message_hash = hashlib.sha256(message).hexdigest().upper() # prompt for the PIN before displaying the dialog if necessary @@ -267,16 +282,17 @@ def sign_message(self, sequence, message, password): except BTChipException as e: if e.sw == 0x6a80: self.give_error("Unfortunately, this message cannot be signed by the Ledger wallet. Only alphanumerical messages shorter than 140 characters are supported. Please remove any extra characters (tab, carriage return) and retry.") + elif e.sw == 0x6985: # cancelled by user + return b'' else: self.give_error(e, True) except UserWarning: self.handler.show_error(_('Cancelled by user')) - return '' + return b'' except Exception as e: self.give_error(e, True) finally: self.handler.finished() - self.signing = False # Parse the ASN.1 signature rLength = signature[3] r = signature[4 : 4 + rLength] @@ -289,12 +305,11 @@ def sign_message(self, sequence, message, password): # And convert it return bytes([27 + 4 + (signature[0] & 0x01)]) + r + s - + @set_and_unset_signing def sign_transaction(self, tx, password): if tx.is_complete(): return client = self.get_client() - self.signing = True inputs = [] inputsPaths = [] pubKeys = [] @@ -341,6 +356,9 @@ def sign_transaction(self, tx, password): self.give_error("No matching x_key for sign_transaction") # should never happen redeemScript = Transaction.get_preimage_script(txin) + if txin.get('prev_tx') is None: # and not Transaction.is_segwit_input(txin): + # note: offline signing does not work atm even with segwit inputs for ledger + raise Exception(_('Offline signing with {} is not supported.').format(self.device)) inputs.append([txin['prev_tx'].raw, txin['prevout_n'], redeemScript, txin['prevout_hash'], signingPos, txin.get('sequence', 0xffffffff - 1) ]) inputsPaths.append(hwAddress) pubKeys.append(pubkeys) @@ -368,7 +386,8 @@ def sign_transaction(self, tx, password): for _type, address, amount in tx.outputs(): assert _type == TYPE_ADDRESS info = tx.output_info.get(address) - if (info is not None) and (len(tx.outputs()) != 1): + if (info is not None) and len(tx.outputs()) > 1 \ + and info[0][0] == 1: # "is on 'change' branch" index, xpubs, m = info changePath = self.get_derivation()[2:] + "/%d/%d"%index changeAmount = amount @@ -408,7 +427,12 @@ def sign_transaction(self, tx, password): if segwitTransaction: self.get_client().startUntrustedTransaction(True, inputIndex, chipInputs, redeemScripts[inputIndex]) - outputData = self.get_client().finalizeInputFull(txOutput) + if changePath: + # we don't set meaningful outputAddress, amount and fees + # as we only care about the alternateEncoding==True branch + outputData = self.get_client().finalizeInput(b'', 0, 0, changePath, bfh(rawTx)) + else: + outputData = self.get_client().finalizeInputFull(txOutput) outputData['outputData'] = txOutput transactionOutput = outputData['outputData'] if outputData['confirmationNeeded']: @@ -423,7 +447,7 @@ def sign_transaction(self, tx, password): singleInput = [ chipInputs[inputIndex] ] self.get_client().startUntrustedTransaction(False, 0, singleInput, redeemScripts[inputIndex]) - inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime) + inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime, sighashType=0x41) inputSignature[0] = 0x30 # force for 1.4.9+ signatures.append(inputSignature) inputIndex = inputIndex + 1 @@ -431,7 +455,12 @@ def sign_transaction(self, tx, password): while inputIndex < len(inputs): self.get_client().startUntrustedTransaction(firstTransaction, inputIndex, chipInputs, redeemScripts[inputIndex]) - outputData = self.get_client().finalizeInputFull(txOutput) + if changePath: + # we don't set meaningful outputAddress, amount and fees + # as we only care about the alternateEncoding==True branch + outputData = self.get_client().finalizeInput(b'', 0, 0, changePath, bfh(rawTx)) + else: + outputData = self.get_client().finalizeInputFull(txOutput) outputData['outputData'] = txOutput if firstTransaction: transactionOutput = outputData['outputData'] @@ -454,6 +483,12 @@ def sign_transaction(self, tx, password): except UserWarning: self.handler.show_error(_('Cancelled by user')) return + except BTChipException as e: + if e.sw == 0x6985: # cancelled by user + return + else: + traceback.print_exc(file=sys.stderr) + self.give_error(e, True) except BaseException as e: traceback.print_exc(file=sys.stdout) self.give_error(e, True) @@ -464,10 +499,9 @@ def sign_transaction(self, tx, password): signingPos = inputs[i][4] txin['signatures'][signingPos] = bh2u(signatures[i]) tx.raw = tx.serialize() - self.signing = False + @set_and_unset_signing def show_address(self, sequence, txin_type): - self.signing = True client = self.get_client() address_path = self.get_derivation()[2:] + "/%d/%d"%sequence self.handler.show_message(_("Showing address ...")) @@ -486,7 +520,6 @@ def show_address(self, sequence, txin_type): self.handler.show_error(e) finally: self.handler.finished() - self.signing = False class LedgerPlugin(HW_PluginBase): libraries_available = BTCHIP @@ -507,17 +540,17 @@ def __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(keystore).getFirmwareVersion() - except Exception as e: - return False - return True - 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 + if device.product_key[0] == 0x2581 and device.product_key[1] == 0x3b7c: + ledger = True + if device.product_key[0] == 0x2581 and device.product_key[1] == 0x4b7c: + ledger = True + if device.product_key[0] == 0x2c97: + if device.interface_number == 0 or device.usage_page == 0xffa0: + ledger = True + else: + return None # non-compatible interface of a Nano S or Blue dev = hid.device() dev.open_path(device.path) dev.set_nonblocking(True) @@ -549,7 +582,6 @@ def get_xpub(self, device_id, derivation, xtype, wizard): 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 with devmgr.hid_lock: diff --git a/plugins/ledger/qt.py b/plugins/ledger/qt.py index cfdd7c383a05..5880acbb2af1 100644 --- a/plugins/ledger/qt.py +++ b/plugins/ledger/qt.py @@ -1,15 +1,13 @@ -import threading - -from PyQt5.Qt import QInputDialog, QLineEdit, QVBoxLayout, QLabel +# from btchip.btchipPersoWizard import StartBTChipPersoDialog from electrum.i18n import _ from electrum.plugins import hook from electrum.wallet import Standard_Wallet +from electrum_gui.qt.util import * + from .ledger import LedgerPlugin from ..hw_wallet.qt import QtHandlerBase, QtPluginBase -from electrum_gui.qt.util import * -#from btchip.btchipPersoWizard import StartBTChipPersoDialog class Plugin(LedgerPlugin, QtPluginBase): icon_unpaired = ":icons/ledger_unpaired.png" @@ -26,8 +24,10 @@ def receive_menu(self, menu, addrs, wallet): if type(keystore) == self.keystore_class and len(addrs) == 1: def show_address(): keystore.thread.add(partial(self.show_address, wallet, addrs[0])) + menu.addAction(_("Show on Ledger"), show_address) + class Ledger_Handler(QtHandlerBase): setup_signal = pyqtSignal() auth_signal = pyqtSignal(object) @@ -38,13 +38,14 @@ def __init__(self, win): self.auth_signal.connect(self.auth_dialog) def word_dialog(self, msg): - response = QInputDialog.getText(self.top_level_window(), "Ledger Wallet Authentication", msg, QLineEdit.Password) + response = QInputDialog.getText(self.top_level_window(), "Ledger Wallet Authentication", msg, + QLineEdit.Password) if not response[1]: self.word = None else: self.word = str(response[0]) self.done.set() - + def message_dialog(self, msg): self.clear_dialog() self.dialog = dialog = WindowModalDialog(self.top_level_window(), _("Ledger Status")) @@ -63,25 +64,21 @@ def auth_dialog(self, data): dialog.exec_() self.word = dialog.pin self.done.set() - + def get_auth(self, data): self.done.clear() self.auth_signal.emit(data) self.done.wait() return self.word - + def get_setup(self): self.done.clear() self.setup_signal.emit() self.done.wait() - return - + return + def setup_dialog(self): + self.show_error(_('Initialization of Ledger HW devices is currently disabled.')) + return dialog = StartBTChipPersoDialog() dialog.exec_() - - - - - -