tMerge branch 'master' of git://github.com/spesmilo/electrum - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit aca8cf595679ac6fc9ba3ae9f60c38ea360f610c DIR parent 15f592f0220fa339b8bb709961a7c38ad0f56fff HTML Author: ThomasV <thomasv@gitorious> Date: Sun, 5 Jul 2015 23:29:49 +0200 Merge branch 'master' of git://github.com/spesmilo/electrum Diffstat: M plugins/__init__.py | 2 +- M plugins/btchipwallet.py | 136 ++++++++++++++++++++----------- 2 files changed, 90 insertions(+), 48 deletions(-) --- DIR diff --git a/plugins/__init__.py b/plugins/__init__.py t@@ -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', DIR diff --git a/plugins/btchipwallet.py b/plugins/btchipwallet.py t@@ -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 t@@ -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 t@@ -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) t@@ -72,9 +74,19 @@ class Plugin(BasePlugin): 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')) t@@ -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: t@@ -130,6 +143,10 @@ class BTChipWallet(BIP32_HD_Wallet): if not self.accounts: return 'create_accounts' + def can_sign_xpubkey(self, x_pubkey): + xpub, sequence = BIP32_Account.parse_xpubkey(x_pubkey) + return xpub in self.master_public_keys.values() + def can_create_accounts(self): return False t@@ -150,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() t@@ -163,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() t@@ -174,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 t@@ -237,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 t@@ -260,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) t@@ -289,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 = "" t@@ -312,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 t@@ -337,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 = [] t@@ -382,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: t@@ -400,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'])): t@@ -426,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') t@@ -433,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], t@@ -445,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 t@@ -464,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: t@@ -488,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 BTChipQTHandler: -class DongleWaitingDialog(QThread): - def __init__(self): - QThread.__init__(self) - self.waiting = False + 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 start(self, message): + 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 stop(self): - self.d.hide() - self.waiting = False + def dialog_stop(self): + if self.d is not None: + self.d.hide() + self.d = None -if BTCHIP: - waitDialog = DongleWaitingDialog() +class BTChipCmdLineHandler: + + def stop(self): + pass - # Tickle the UI a bit while waiting - class DongleWaitQT(DongleWait): - def __init__(self, dongle): - self.dongle = dongle + def show_message(self, msg): + print_msg(msg) - 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