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_wallet_type': ['btchip'],
'registers_wallet_type': ('hardware', 'btchip', _("BTChip wallet")),
'available_for': ['qt'],
'available_for': ['qt', 'cmdline'],
},
{
'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
from binascii import unhexlify
from binascii import hexlify
@ -16,8 +16,9 @@ from electrum.plugins import BasePlugin, hook
from electrum.transaction import deserialize
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 threading
try:
from btchip.btchipComm import getDongle, DongleWait
@ -38,6 +39,7 @@ class Plugin(BasePlugin):
BasePlugin.__init__(self, gui, name)
self._is_available = self._init()
self.wallet = None
self.handler = None
def constructor(self, s):
return BTChipWallet(s)
@ -71,10 +73,20 @@ class Plugin(BasePlugin):
return False
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
def load_wallet(self, wallet, window):
self.wallet = wallet
self.wallet.plugin = self
self.window = window
if self.handler is None:
self.handler = BTChipQTHandler(self.window.app)
if self.btchip_is_connected():
if not self.wallet.check_proper_device():
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
def give_error(self, message, clear_client = False):
print_error(message)
if not self.signing:
QMessageBox.warning(QDialog(), _('Warning'), _(message), _('OK'))
else:
@ -154,10 +167,10 @@ class BTChipWallet(BIP32_HD_Wallet):
aborted = False
if not self.client or self.client.bad:
try:
try:
d = getDongle(BTCHIP_DEBUG)
d.setWaitImpl(DongleWaitQT(d))
self.client = btchip(d)
self.client.handler = self.plugin.handler
firmware = self.client.getFirmwareVersion()['version'].split(".")
if not checkFirmware(firmware):
d.close()
@ -167,7 +180,6 @@ class BTChipWallet(BIP32_HD_Wallet):
aborted = True
raise e
d = getDongle(BTCHIP_DEBUG)
d.setWaitImpl(DongleWaitQT(d))
self.client = btchip(d)
try:
self.client.getOperationMode()
@ -178,7 +190,6 @@ class BTChipWallet(BIP32_HD_Wallet):
dialog.exec_()
# Then fetch the reference again as it was invalidated
d = getDongle(BTCHIP_DEBUG)
d.setWaitImpl(DongleWaitQT(d))
self.client = btchip(d)
else:
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
# This only happens once so it's bearable
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:
splitPath = bip32_path.split('/')
fingerprint = 0
@ -264,7 +275,7 @@ class BTChipWallet(BIP32_HD_Wallet):
except Exception, e:
self.give_error(e, True)
finally:
waitDialog.emit(SIGNAL('dongle_done'))
self.plugin.handler.stop()
return EncodeBase58Check(xpub)
@ -293,7 +304,7 @@ class BTChipWallet(BIP32_HD_Wallet):
if not self.check_proper_device():
self.give_error('Wrong device or password')
address_path = self.address_id(address)
waitDialog.start("Signing Message ...")
self.plugin.handler.show_message("Signing message ...")
try:
info = self.get_client().signMessagePrepare(address_path, message)
pin = ""
@ -316,8 +327,7 @@ class BTChipWallet(BIP32_HD_Wallet):
except Exception, e:
self.give_error(e, True)
finally:
if waitDialog.waiting:
waitDialog.emit(SIGNAL('dongle_done'))
self.plugin.handler.stop()
self.client.bad = use2FA
self.signing = False
@ -341,8 +351,8 @@ class BTChipWallet(BIP32_HD_Wallet):
def sign_transaction(self, tx, password):
if tx.is_complete():
return
if tx.error:
raise BaseException(tx.error)
#if tx.error:
# raise BaseException(tx.error)
self.signing = True
inputs = []
inputsPaths = []
@ -386,7 +396,7 @@ class BTChipWallet(BIP32_HD_Wallet):
if not self.check_proper_device():
self.give_error('Wrong device or password')
waitDialog.start("Signing Transaction ...")
self.plugin.handler.show_message("Signing Transaction ...")
try:
# Get trusted inputs from the original transactions
for utxo in inputs:
@ -404,10 +414,9 @@ class BTChipWallet(BIP32_HD_Wallet):
format_satoshis_plain(self.get_tx_fee(tx)), changePath, bytearray(rawTx.decode('hex')))
if firstTransaction:
transactionOutput = outputData['outputData']
if outputData['confirmationNeeded']:
use2FA = True
if outputData['confirmationNeeded']:
# 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:
pin2 = ""
for keycardIndex in range(len(outputData['keycardData'])):
@ -430,6 +439,7 @@ class BTChipWallet(BIP32_HD_Wallet):
raise Exception('Invalid PIN character')
pin = pin2
else:
use2FA = True
confirmed, p, pin = self.password_dialog()
if not confirmed:
raise Exception('Aborted by user')
@ -437,7 +447,7 @@ class BTChipWallet(BIP32_HD_Wallet):
self.client.bad = True
self.device_checked = False
self.get_client(True)
waitDialog.start("Signing ...")
self.plugin.handler.show_message("Signing ...")
else:
# Sign input with the provided PIN
inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex],
@ -449,8 +459,7 @@ class BTChipWallet(BIP32_HD_Wallet):
except Exception, e:
self.give_error(e, True)
finally:
if waitDialog.waiting:
waitDialog.emit(SIGNAL('dongle_done'))
self.plugin.handler.stop()
# Reformat transaction
inputIndex = 0
@ -468,13 +477,13 @@ class BTChipWallet(BIP32_HD_Wallet):
def check_proper_device(self):
pubKey = DecodeBase58Check(self.master_public_keys["x/0'"])[45:]
if not self.device_checked:
waitDialog.start("Checking device")
self.plugin.handler.show_message("Checking device")
try:
nodeData = self.get_client().getWalletPublicKey("44'/0'/0'")
except Exception, e:
self.give_error(e, True)
finally:
waitDialog.emit(SIGNAL('dongle_done'))
self.plugin.handler.stop()
pubKeyDevice = compress_public_key(nodeData['publicKey'])
self.device_checked = True
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" \
"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)")
d = QDialog()
d.setModal(1)
d.setLayout( make_password_dialog(d, None, msg, False) )
return run_password_dialog(d, None, None)
response = self.plugin.handler.prompt_auth(msg)
if response is None:
return False, None, None
return True, response, response
class DongleWaitingDialog(QThread):
def __init__(self):
QThread.__init__(self)
self.waiting = False
class BTChipQTHandler:
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.setModal(1)
self.d.setWindowTitle('Please Wait')
self.d.setWindowTitle('BTChip')
self.d.setWindowFlags(self.d.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)
l = QLabel(message)
l = QLabel(self.message)
vbox = QVBoxLayout(self.d)
vbox.addWidget(l)
self.d.show()
if not self.waiting:
self.waiting = True
self.d.connect(waitDialog, SIGNAL('dongle_done'), self.stop)
def dialog_stop(self):
if self.d is not None:
self.d.hide()
self.d = None
class BTChipCmdLineHandler:
def stop(self):
self.d.hide()
self.waiting = False
pass
if BTCHIP:
waitDialog = DongleWaitingDialog()
def show_message(self, msg):
print_msg(msg)
# Tickle the UI a bit while waiting
class DongleWaitQT(DongleWait):
def __init__(self, dongle):
self.dongle = dongle
def waitFirstResponse(self, timeout):
return self.dongle.waitFirstResponse(timeout)
def prompt_auth(self, msg):
import getpass
print_msg(msg)
response = getpass.getpass('')
if len(response) == 0:
return None
return response