Repository: spesmilo/electrum

Issue:
"view" instead of "send" with HW1
The last rev where my HW1 properly works is : 7e0a80b697b7c79a24d0b5689a5686a4f23fbff5
After this, the send tx tab has only a view button and now way to actually send.
Signing messages still works.



Discussion:
did you enable the 'show before send' option in your preferences?

since 2.3.3, that option causes this behaviour. it is not related to HW1

Yes, that's correct. If I disable that option I can send.
But while it is enabled, I was unable to find a workflow that allowed me to send funds.
Is this a work in progress then?

after you sign, there should be a 'broadcast' button

Ah, the sign button on the tx detail dialog is missing with the HW1 wallet.

@btchip any idea?

Just noting the results of our investigation: this is caused by the tx dialog's (in more than one place) calling tx.as_dict() which blows away the annotations added to the inputs by get_spendable_coins().

Arguably non-transaction class code shouldn't be setting the inputs in the TX.  Perhaps some separate annotation is better instead?  Or as_dict() should go to some length to preserve other fields....  Probably best for ThomasV to opine on what he thinks is the best way forwards here.

here is a quick fix: 5c2235e54bff91680c2d08b7722707688b0c426d
but I agree that it would be better to avoid side effects

I've merged the latest changes (up to 5c2235e54bff91680c2d08b7722707688b0c426d) and get an exception before the transaction is displayed to be reviewed

```
Traceback (most recent call last):
  File "/media/truecrypt1/work/oss/btchip/forks/electrum/gui/qt/main_window.py", line 1155, in do_send
    self.show_transaction(tx, tx_desc)
  File "/media/truecrypt1/work/oss/btchip/forks/electrum/gui/qt/main_window.py", line 573, in show_transaction
    show_transaction(tx, self, tx_desc)
  File "/media/truecrypt1/work/oss/btchip/forks/electrum/gui/qt/transaction_dialog.py", line 37, in show_transaction
    d = TxDialog(tx, parent, desc, prompt_if_unsaved)
  File "/media/truecrypt1/work/oss/btchip/forks/electrum/gui/qt/transaction_dialog.py", line 116, in __init__
    self.update()
  File "/media/truecrypt1/work/oss/btchip/forks/electrum/gui/qt/transaction_dialog.py", line 167, in update
    tx_hash = self.tx.hash()
  File "/media/truecrypt1/work/oss/btchip/forks/electrum/lib/transaction.py", line 676, in hash
    return Hash(self.raw.decode('hex') )[::-1].encode('hex')
AttributeError: 'NoneType' object has no attribute 'decode'
```

yes I saw that too

fixed in 994717318343b2674b9a08e4534cf18bd952173b

@ulrichard can you confirm that it's fixed?

I have pending commits for that - coming shortly
On Jul 5, 2015 16:58, "ThomasV" notifications@github.com wrote:

> @ulrichard https://github.com/ulrichard can you confirm that it's fixed?
> 
> —
> Reply to this email directly or view it on GitHub
> https://github.com/spesmilo/electrum/issues/1326#issuecomment-118628785.


Resolving patch:
diff --git a/plugins/__init__.py b/plugins/__init__.py
--- a/plugins/__init__.py
+++ b/plugins/__init__.py
@@ -34,7 +34,7 @@
         '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',
diff --git a/plugins/btchipwallet.py b/plugins/btchipwallet.py
--- a/plugins/btchipwallet.py
+++ b/plugins/btchipwallet.py
@@ -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.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 @@ def __init__(self, gui, name):
         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 @@ def btchip_is_connected(self):
             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 @@ def __init__(self, storage):
         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:
@@ -130,6 +143,10 @@ def get_action(self):
         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
 
@@ -150,10 +167,10 @@ def get_client(self, noPin=False):
 
         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()
@@ -163,7 +180,6 @@ def get_client(self, noPin=False):
                         aborted = True
                         raise e
                     d = getDongle(BTCHIP_DEBUG)
-                    d.setWaitImpl(DongleWaitQT(d))
                     self.client = btchip(d)
                 try:
                     self.client.getOperationMode()
@@ -174,7 +190,6 @@ def get_client(self, noPin=False):
                         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
@@ -237,7 +252,7 @@ def get_public_key(self, bip32_path):
         # 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
@@ -260,7 +275,7 @@ def get_public_key(self, bip32_path):
         except Exception, e:
             self.give_error(e, True)
         finally:
-            waitDialog.emit(SIGNAL('dongle_done'))
+            self.plugin.handler.stop()
 
         return EncodeBase58Check(xpub)
 
@@ -289,7 +304,7 @@ def sign_message(self, address, message, password):
         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 = ""
@@ -312,8 +327,7 @@ def sign_message(self, address, message, password):
         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
 
@@ -337,8 +351,8 @@ def sign_message(self, address, message, password):
     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 = []
@@ -382,7 +396,7 @@ def sign_transaction(self, tx, password):
         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:
@@ -400,10 +414,9 @@ def sign_transaction(self, tx, password):
                 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'])):
@@ -426,6 +439,7 @@ def sign_transaction(self, tx, password):
                                 raise Exception('Invalid PIN character')
                         pin = pin2
                     else:
+                        use2FA = True
                         confirmed, p, pin = self.password_dialog()
                         if not confirmed:
                             raise Exception('Aborted by user')
@@ -433,7 +447,7 @@ def sign_transaction(self, tx, password):
                         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],
@@ -445,8 +459,7 @@ def sign_transaction(self, tx, password):
         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
@@ -464,13 +477,13 @@ def sign_transaction(self, tx, password):
     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:
@@ -488,40 +501,69 @@ def password_dialog(self, msg=None):
                     "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


Test patch: