Review UI, support command line mode

This commit is contained in:
BTChip 2015-07-05 22:14:53 +02:00
parent c167ef6d35
commit ea097fd7f5
2 changed files with 86 additions and 48 deletions

View File

@ -34,7 +34,7 @@ descriptions = [
'requires': [('btchip', 'github.com/btchip/btchip-python')], 'requires': [('btchip', 'github.com/btchip/btchip-python')],
'requires_wallet_type': ['btchip'], 'requires_wallet_type': ['btchip'],
'registers_wallet_type': ('hardware', 'btchip', _("BTChip wallet")), 'registers_wallet_type': ('hardware', 'btchip', _("BTChip wallet")),
'available_for': ['qt'], 'available_for': ['qt', 'cmdline'],
}, },
{ {
'name': 'cosigner_pool', 'name': 'cosigner_pool',

View File

@ -1,4 +1,4 @@
from PyQt4.Qt import QApplication, QMessageBox, QDialog, QVBoxLayout, QLabel, QThread, SIGNAL from PyQt4.Qt import QApplication, QMessageBox, QDialog, QInputDialog, QLineEdit, QVBoxLayout, QLabel, QThread, SIGNAL
import PyQt4.QtCore as QtCore import PyQt4.QtCore as QtCore
from binascii import unhexlify from binascii import unhexlify
from binascii import hexlify from binascii import hexlify
@ -16,8 +16,9 @@ from electrum.plugins import BasePlugin, hook
from electrum.transaction import deserialize from electrum.transaction import deserialize
from electrum.wallet import BIP32_HD_Wallet, BIP32_Wallet from electrum.wallet import BIP32_HD_Wallet, BIP32_Wallet
from electrum.util import format_satoshis_plain from electrum.util import format_satoshis_plain, print_error, print_msg
import hashlib import hashlib
import threading
try: try:
from btchip.btchipComm import getDongle, DongleWait from btchip.btchipComm import getDongle, DongleWait
@ -38,6 +39,7 @@ class Plugin(BasePlugin):
BasePlugin.__init__(self, gui, name) BasePlugin.__init__(self, gui, name)
self._is_available = self._init() self._is_available = self._init()
self.wallet = None self.wallet = None
self.handler = None
def constructor(self, s): def constructor(self, s):
return BTChipWallet(s) return BTChipWallet(s)
@ -71,10 +73,20 @@ class Plugin(BasePlugin):
return False return False
return True return True
@hook
def cmdline_load_wallet(self, wallet):
self.wallet = wallet
self.wallet.plugin = self
if self.handler is None:
self.handler = BTChipCmdLineHandler()
@hook @hook
def load_wallet(self, wallet, window): def load_wallet(self, wallet, window):
self.wallet = wallet self.wallet = wallet
self.wallet.plugin = self
self.window = window self.window = window
if self.handler is None:
self.handler = BTChipQTHandler(self.window.app)
if self.btchip_is_connected(): if self.btchip_is_connected():
if not self.wallet.check_proper_device(): if not self.wallet.check_proper_device():
QMessageBox.information(self.window, _('Error'), _("This wallet does not match your BTChip device"), _('OK')) QMessageBox.information(self.window, _('Error'), _("This wallet does not match your BTChip device"), _('OK'))
@ -117,6 +129,7 @@ class BTChipWallet(BIP32_HD_Wallet):
self.force_watching_only = False self.force_watching_only = False
def give_error(self, message, clear_client = False): def give_error(self, message, clear_client = False):
print_error(message)
if not self.signing: if not self.signing:
QMessageBox.warning(QDialog(), _('Warning'), _(message), _('OK')) QMessageBox.warning(QDialog(), _('Warning'), _(message), _('OK'))
else: else:
@ -156,8 +169,8 @@ class BTChipWallet(BIP32_HD_Wallet):
if not self.client or self.client.bad: if not self.client or self.client.bad:
try: try:
d = getDongle(BTCHIP_DEBUG) d = getDongle(BTCHIP_DEBUG)
d.setWaitImpl(DongleWaitQT(d))
self.client = btchip(d) self.client = btchip(d)
self.client.handler = self.plugin.handler
firmware = self.client.getFirmwareVersion()['version'].split(".") firmware = self.client.getFirmwareVersion()['version'].split(".")
if not checkFirmware(firmware): if not checkFirmware(firmware):
d.close() d.close()
@ -167,7 +180,6 @@ class BTChipWallet(BIP32_HD_Wallet):
aborted = True aborted = True
raise e raise e
d = getDongle(BTCHIP_DEBUG) d = getDongle(BTCHIP_DEBUG)
d.setWaitImpl(DongleWaitQT(d))
self.client = btchip(d) self.client = btchip(d)
try: try:
self.client.getOperationMode() self.client.getOperationMode()
@ -178,7 +190,6 @@ class BTChipWallet(BIP32_HD_Wallet):
dialog.exec_() dialog.exec_()
# Then fetch the reference again as it was invalidated # Then fetch the reference again as it was invalidated
d = getDongle(BTCHIP_DEBUG) d = getDongle(BTCHIP_DEBUG)
d.setWaitImpl(DongleWaitQT(d))
self.client = btchip(d) self.client = btchip(d)
else: else:
raise e raise e
@ -241,7 +252,7 @@ class BTChipWallet(BIP32_HD_Wallet):
# S-L-O-W - we don't handle the fingerprint directly, so compute it manually from the previous node # 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 # This only happens once so it's bearable
self.get_client() # prompt for the PIN before displaying the dialog if necessary self.get_client() # prompt for the PIN before displaying the dialog if necessary
waitDialog.start("Computing master public key") self.plugin.handler.show_message("Computing master public key")
try: try:
splitPath = bip32_path.split('/') splitPath = bip32_path.split('/')
fingerprint = 0 fingerprint = 0
@ -264,7 +275,7 @@ class BTChipWallet(BIP32_HD_Wallet):
except Exception, e: except Exception, e:
self.give_error(e, True) self.give_error(e, True)
finally: finally:
waitDialog.emit(SIGNAL('dongle_done')) self.plugin.handler.stop()
return EncodeBase58Check(xpub) return EncodeBase58Check(xpub)
@ -293,7 +304,7 @@ class BTChipWallet(BIP32_HD_Wallet):
if not self.check_proper_device(): if not self.check_proper_device():
self.give_error('Wrong device or password') self.give_error('Wrong device or password')
address_path = self.address_id(address) address_path = self.address_id(address)
waitDialog.start("Signing Message ...") self.plugin.handler.show_message("Signing message ...")
try: try:
info = self.get_client().signMessagePrepare(address_path, message) info = self.get_client().signMessagePrepare(address_path, message)
pin = "" pin = ""
@ -316,8 +327,7 @@ class BTChipWallet(BIP32_HD_Wallet):
except Exception, e: except Exception, e:
self.give_error(e, True) self.give_error(e, True)
finally: finally:
if waitDialog.waiting: self.plugin.handler.stop()
waitDialog.emit(SIGNAL('dongle_done'))
self.client.bad = use2FA self.client.bad = use2FA
self.signing = False self.signing = False
@ -341,8 +351,8 @@ class BTChipWallet(BIP32_HD_Wallet):
def sign_transaction(self, tx, password): def sign_transaction(self, tx, password):
if tx.is_complete(): if tx.is_complete():
return return
if tx.error: #if tx.error:
raise BaseException(tx.error) # raise BaseException(tx.error)
self.signing = True self.signing = True
inputs = [] inputs = []
inputsPaths = [] inputsPaths = []
@ -386,7 +396,7 @@ class BTChipWallet(BIP32_HD_Wallet):
if not self.check_proper_device(): if not self.check_proper_device():
self.give_error('Wrong device or password') self.give_error('Wrong device or password')
waitDialog.start("Signing Transaction ...") self.plugin.handler.show_message("Signing Transaction ...")
try: try:
# Get trusted inputs from the original transactions # Get trusted inputs from the original transactions
for utxo in inputs: for utxo in inputs:
@ -405,9 +415,8 @@ class BTChipWallet(BIP32_HD_Wallet):
if firstTransaction: if firstTransaction:
transactionOutput = outputData['outputData'] transactionOutput = outputData['outputData']
if outputData['confirmationNeeded']: if outputData['confirmationNeeded']:
use2FA = True
# TODO : handle different confirmation types. For the time being only supports keyboard 2FA # TODO : handle different confirmation types. For the time being only supports keyboard 2FA
waitDialog.emit(SIGNAL('dongle_done')) self.plugin.handler.stop()
if 'keycardData' in outputData: if 'keycardData' in outputData:
pin2 = "" pin2 = ""
for keycardIndex in range(len(outputData['keycardData'])): for keycardIndex in range(len(outputData['keycardData'])):
@ -430,6 +439,7 @@ class BTChipWallet(BIP32_HD_Wallet):
raise Exception('Invalid PIN character') raise Exception('Invalid PIN character')
pin = pin2 pin = pin2
else: else:
use2FA = True
confirmed, p, pin = self.password_dialog() confirmed, p, pin = self.password_dialog()
if not confirmed: if not confirmed:
raise Exception('Aborted by user') raise Exception('Aborted by user')
@ -437,7 +447,7 @@ class BTChipWallet(BIP32_HD_Wallet):
self.client.bad = True self.client.bad = True
self.device_checked = False self.device_checked = False
self.get_client(True) self.get_client(True)
waitDialog.start("Signing ...") self.plugin.handler.show_message("Signing ...")
else: else:
# Sign input with the provided PIN # Sign input with the provided PIN
inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex],
@ -449,8 +459,7 @@ class BTChipWallet(BIP32_HD_Wallet):
except Exception, e: except Exception, e:
self.give_error(e, True) self.give_error(e, True)
finally: finally:
if waitDialog.waiting: self.plugin.handler.stop()
waitDialog.emit(SIGNAL('dongle_done'))
# Reformat transaction # Reformat transaction
inputIndex = 0 inputIndex = 0
@ -468,13 +477,13 @@ class BTChipWallet(BIP32_HD_Wallet):
def check_proper_device(self): def check_proper_device(self):
pubKey = DecodeBase58Check(self.master_public_keys["x/0'"])[45:] pubKey = DecodeBase58Check(self.master_public_keys["x/0'"])[45:]
if not self.device_checked: if not self.device_checked:
waitDialog.start("Checking device") self.plugin.handler.show_message("Checking device")
try: try:
nodeData = self.get_client().getWalletPublicKey("44'/0'/0'") nodeData = self.get_client().getWalletPublicKey("44'/0'/0'")
except Exception, e: except Exception, e:
self.give_error(e, True) self.give_error(e, True)
finally: finally:
waitDialog.emit(SIGNAL('dongle_done')) self.plugin.handler.stop()
pubKeyDevice = compress_public_key(nodeData['publicKey']) pubKeyDevice = compress_public_key(nodeData['publicKey'])
self.device_checked = True self.device_checked = True
if pubKey != pubKeyDevice: if pubKey != pubKeyDevice:
@ -492,40 +501,69 @@ class BTChipWallet(BIP32_HD_Wallet):
"It should show itself to your computer as a keyboard and output the second factor along with a summary of the transaction it is signing into the text-editor.\r\n\r\n" \ "It should show itself to your computer as a keyboard and output the second factor along with a summary of the transaction it is signing into the text-editor.\r\n\r\n" \
"Check that summary and then enter the second factor code here.\r\n" \ "Check that summary and then enter the second factor code here.\r\n" \
"Before clicking OK, re-plug the device once more (unplug it and plug it again if you read the second factor code on the same computer)") "Before clicking OK, re-plug the device once more (unplug it and plug it again if you read the second factor code on the same computer)")
d = QDialog() response = self.plugin.handler.prompt_auth(msg)
d.setModal(1) if response is None:
d.setLayout( make_password_dialog(d, None, msg, False) ) return False, None, None
return run_password_dialog(d, None, None) return True, response, response
class DongleWaitingDialog(QThread): class BTChipQTHandler:
def __init__(self):
QThread.__init__(self)
self.waiting = False
def start(self, message): def __init__(self, win):
self.win = win
self.win.connect(win, SIGNAL('btchip_done'), self.dialog_stop)
self.win.connect(win, SIGNAL('message_dialog'), self.message_dialog)
self.win.connect(win, SIGNAL('auth_dialog'), self.auth_dialog)
self.done = threading.Event()
def stop(self):
self.win.emit(SIGNAL('btchip_done'))
def show_message(self, msg):
self.message = msg
self.win.emit(SIGNAL('message_dialog'))
def prompt_auth(self, msg):
self.done.clear()
self.message = msg
self.win.emit(SIGNAL('auth_dialog'))
self.done.wait()
return self.response
def auth_dialog(self):
response = QInputDialog.getText(None, "BTChip Authentication", self.message, QLineEdit.Password)
if not response[1]:
self.response = None
else:
self.response = str(response[0])
self.done.set()
def message_dialog(self):
self.d = QDialog() self.d = QDialog()
self.d.setModal(1) self.d.setModal(1)
self.d.setWindowTitle('Please Wait') self.d.setWindowTitle('BTChip')
self.d.setWindowFlags(self.d.windowFlags() | QtCore.Qt.WindowStaysOnTopHint) self.d.setWindowFlags(self.d.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)
l = QLabel(message) l = QLabel(self.message)
vbox = QVBoxLayout(self.d) vbox = QVBoxLayout(self.d)
vbox.addWidget(l) vbox.addWidget(l)
self.d.show() self.d.show()
if not self.waiting:
self.waiting = True def dialog_stop(self):
self.d.connect(waitDialog, SIGNAL('dongle_done'), self.stop) if self.d is not None:
self.d.hide()
self.d = None
class BTChipCmdLineHandler:
def stop(self): def stop(self):
self.d.hide() pass
self.waiting = False
if BTCHIP: def show_message(self, msg):
waitDialog = DongleWaitingDialog() print_msg(msg)
# Tickle the UI a bit while waiting def prompt_auth(self, msg):
class DongleWaitQT(DongleWait): import getpass
def __init__(self, dongle): print_msg(msg)
self.dongle = dongle response = getpass.getpass('')
if len(response) == 0:
def waitFirstResponse(self, timeout): return None
return self.dongle.waitFirstResponse(timeout) return response