URI: 
       tMerge pull request #4470 from Coldcard/ckcc - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 9279f30363d0e01ec0ee501eddd6966cf9a37d7e
   DIR parent 2a5f108d4ab2145c5784934063293bc2decce67a
  HTML Author: ghost43 <somber.night@protonmail.com>
       Date:   Thu, 23 Aug 2018 16:51:52 +0200
       
       Merge pull request #4470 from Coldcard/ckcc
       
       Support for new hardware wallet: Coldcard
       Diffstat:
         M contrib/build-osx/osx.spec          |       3 +++
         M contrib/build-wine/deterministic.s… |       3 +++
         M contrib/deterministic-build/requir… |       5 +++++
         M contrib/requirements/requirements-… |       1 +
         M electrum/bitcoin.py                 |       7 +++++--
         A electrum/plugins/coldcard/README.md |      65 +++++++++++++++++++++++++++++++
         A electrum/plugins/coldcard/__init__… |       7 +++++++
         A electrum/plugins/coldcard/cmdline.… |      47 +++++++++++++++++++++++++++++++
         A electrum/plugins/coldcard/coldcard… |     684 +++++++++++++++++++++++++++++++
         A electrum/plugins/coldcard/qt.py     |     242 +++++++++++++++++++++++++++++++
         M icons.qrc                           |       2 ++
         A icons/coldcard.png                  |       0 
         A icons/coldcard_unpaired.png         |       0 
       
       13 files changed, 1064 insertions(+), 2 deletions(-)
       ---
   DIR diff --git a/contrib/build-osx/osx.spec b/contrib/build-osx/osx.spec
       t@@ -27,6 +27,7 @@ hiddenimports += collect_submodules('safetlib')
        hiddenimports += collect_submodules('btchip')
        hiddenimports += collect_submodules('keepkeylib')
        hiddenimports += collect_submodules('websocket')
       +hiddenimports += collect_submodules('ckcc')
        
        datas = [
            (electrum+'electrum/*.json', PYPKG),
       t@@ -37,6 +38,7 @@ datas += collect_data_files('trezorlib')
        datas += collect_data_files('safetlib')
        datas += collect_data_files('btchip')
        datas += collect_data_files('keepkeylib')
       +datas += collect_data_files('ckcc')
        
        # Add libusb so Trezor and Safe-T mini will work
        binaries = [(electrum + "contrib/build-osx/libusb-1.0.dylib", ".")]
       t@@ -63,6 +65,7 @@ a = Analysis([electrum+ MAIN_SCRIPT,
                      electrum+'electrum/plugins/safe_t/qt.py',
                      electrum+'electrum/plugins/keepkey/qt.py',
                      electrum+'electrum/plugins/ledger/qt.py',
       +              electrum+'electrum/plugins/coldcard/qt.py',
                      ],
                     binaries=binaries,
                     datas=datas,
   DIR diff --git a/contrib/build-wine/deterministic.spec b/contrib/build-wine/deterministic.spec
       t@@ -22,6 +22,7 @@ hiddenimports += collect_submodules('safetlib')
        hiddenimports += collect_submodules('btchip')
        hiddenimports += collect_submodules('keepkeylib')
        hiddenimports += collect_submodules('websocket')
       +hiddenimports += collect_submodules('ckcc')
        
        # Add libusb binary
        binaries = [(PYHOME+"/libusb-1.0.dll", ".")]
       t@@ -41,6 +42,7 @@ datas += collect_data_files('trezorlib')
        datas += collect_data_files('safetlib')
        datas += collect_data_files('btchip')
        datas += collect_data_files('keepkeylib')
       +datas += collect_data_files('ckcc')
        
        # We don't put these files in to actually include them in the script but to make the Analysis method scan them for imports
        a = Analysis([home+'run_electrum',
       t@@ -60,6 +62,7 @@ a = Analysis([home+'run_electrum',
                      home+'electrum/plugins/safe_t/qt.py',
                      home+'electrum/plugins/keepkey/qt.py',
                      home+'electrum/plugins/ledger/qt.py',
       +              home+'electrum/plugins/coldcard/qt.py',
                      #home+'packages/requests/utils.py'
                      ],
                     binaries=binaries,
   DIR diff --git a/contrib/deterministic-build/requirements-hw.txt b/contrib/deterministic-build/requirements-hw.txt
       t@@ -115,3 +115,8 @@ websocket-client==0.48.0 \
        wheel==0.31.1 \
            --hash=sha256:0a2e54558a0628f2145d2fc822137e322412115173e8a2ddbe1c9024338ae83c \
            --hash=sha256:80044e51ec5bbf6c894ba0bc48d26a8c20a9ba629f4ca19ea26ecfcf87685f5f
       +pyaes==1.6.1 \
       +    --hash=sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f
       +ckcc-protocol==0.7.2 \
       +    --hash=sha256:498db4ccdda018cd9f40210f5bd02ddcc98e7df583170b2eab4035c86c3cc03b \
       +    --hash=sha256:31ee5178cfba8895eb2a6b8d06dc7830b51461a0ff767a670a64707c63e6b264
   DIR diff --git a/contrib/requirements/requirements-hw.txt b/contrib/requirements/requirements-hw.txt
       t@@ -3,5 +3,6 @@ trezor[hidapi]>=0.9.0
        safet[hidapi]>=0.1.0
        keepkey
        btchip-python
       +ckcc-protocol>=0.7.2
        websocket-client
        hidapi
   DIR diff --git a/electrum/bitcoin.py b/electrum/bitcoin.py
       t@@ -626,6 +626,9 @@ def serialize_xpub(xtype, c, cK, depth=0, fingerprint=b'\x00'*4,
            return EncodeBase58Check(xpub)
        
        
       +class InvalidMasterKeyVersionBytes(BitcoinException): pass
       +
       +
        def deserialize_xkey(xkey, prv, *, net=None):
            if net is None:
                net = constants.net
       t@@ -640,8 +643,8 @@ def deserialize_xkey(xkey, prv, *, net=None):
            header = int('0x' + bh2u(xkey[0:4]), 16)
            headers = net.XPRV_HEADERS if prv else net.XPUB_HEADERS
            if header not in headers.values():
       -        raise BitcoinException('Invalid extended key format: {}'
       -                               .format(hex(header)))
       +        raise InvalidMasterKeyVersionBytes('Invalid extended key format: {}'
       +                                           .format(hex(header)))
            xtype = list(headers.keys())[list(headers.values()).index(header)]
            n = 33 if prv else 32
            K_or_k = xkey[13+n:]
   DIR diff --git a/electrum/plugins/coldcard/README.md b/electrum/plugins/coldcard/README.md
       t@@ -0,0 +1,65 @@
       +
       +# Coldcard Hardware Wallet Plugin
       +
       +## Just the glue please
       +
       +This code connects the public USB API and Electrum. Leverages all
       +the good work that's been done by the Electrum team to support
       +hardware wallets.
       +
       +## Background
       +
       +The Coldcard has a larger screen (128x64) and a number pad. For
       +this reason, all PIN code entry is done directly on the device.
       +Coldcard does not appear on the USB bus until unlocked with appropriate
       +PIN. Initial setup, and seed generation must be done offline.
       +
       +Coldcard uses an emerging standard for unsigned tranasctions:
       +
       +PSBT = Partially Signed Bitcoin Transaction = BIP174
       +
       +However, this spec is still under heavy discussion and in flux. At
       +this point, the PSBT files generated will only be compatible with
       +Coldcard.
       +
       +The Coldcard can be used 100% offline: it can generate a skeleton
       +Electrum wallet and save it to MicroSD card. Transport that file
       +to Electrum and it will fetch history, blockchain details and then
       +operate in "unpaired" mode.
       +
       +Spending transactions can be saved to MicroSD using the "Export PSBT"
       +button on the transaction preview dialog (when this plugin is
       +owner of the wallet). That PSBT can be signed on the Coldcard
       +(again using MicroSD both ways). The result is a ready-to-transmit
       +bitcoin transaction, which can be transmitted using Tools > Load
       +Transaction > From File in Electrum or really any tool.
       +
       +<https://coldcardwallet.com>
       +
       +## TODO Items
       +
       +- No effort yet to support translations or languages other than English, sorry.
       +- Coldcard PSBT format is not likely to be compatible with other devices, because the BIP174 is still in flux.
       +- Segwit support not 100% complete: can pay to them, but cannot setup wallet to receive them.
       +- Limited support for segwit wrapped in P2SH.
       +- Someday we could support multisig hardware wallets based on PSBT where each participant
       +  is using different devices/systems for signing, however, that belongs in an independant
       +  plugin that is PSBT focused and might not require a Coldcard to be present.
       +
       +### Ctags
       +
       +- I find this command useful (at top level) ... but I'm a VIM user.
       +
       +    ctags -f .tags electrum `find . -name ENV -prune -o -name \*.py`
       +
       +
       +### Working with latest ckcc-protocol
       +
       +- at top level, do this:
       +
       +    pip install -e git+ssh://git@github.com/Coldcard/ckcc-protocol.git#egg=ckcc-protocol
       +
       +- but you'll need the https version of that, not ssh like I can.
       +- also a branch name would be good in there
       +- do `pip uninstall ckcc` first
       +- see <https://stackoverflow.com/questions/4830856>
   DIR diff --git a/electrum/plugins/coldcard/__init__.py b/electrum/plugins/coldcard/__init__.py
       t@@ -0,0 +1,7 @@
       +from electrum.i18n import _
       +
       +fullname = 'Coldcard Wallet'
       +description = 'Provides support for the Coldcard hardware wallet from Coinkite'
       +requires = [('ckcc-protocol', 'github.com/Coldcard/ckcc-protocol')]
       +registers_keystore = ('hardware', 'coldcard', _("Coldcard Wallet"))
       +available_for = ['qt', 'cmdline']
   DIR diff --git a/electrum/plugins/coldcard/cmdline.py b/electrum/plugins/coldcard/cmdline.py
       t@@ -0,0 +1,47 @@
       +from electrum.plugin import hook
       +from .coldcard import ColdcardPlugin
       +from electrum.util import print_msg, print_error, raw_input, print_stderr
       +
       +class ColdcardCmdLineHandler:
       +
       +    def get_passphrase(self, msg, confirm):
       +        raise NotImplementedError
       +
       +    def get_pin(self, msg):
       +        raise NotImplementedError
       +
       +    def prompt_auth(self, msg):
       +        raise NotImplementedError
       +
       +    def yes_no_question(self, msg):
       +        print_msg(msg)
       +        return raw_input() in 'yY'
       +
       +    def stop(self):
       +        pass
       +
       +    def show_message(self, msg, on_cancel=None):
       +        print_stderr(msg)
       +
       +    def show_error(self, msg, blocking=False):
       +        print_error(msg)
       +
       +    def update_status(self, b):
       +        print_error('hw device status', b)
       +
       +    def finished(self):
       +        pass
       +
       +class Plugin(ColdcardPlugin):
       +    handler = ColdcardCmdLineHandler()
       +
       +    @hook
       +    def init_keystore(self, keystore):
       +        if not isinstance(keystore, self.keystore_class):
       +            return
       +        keystore.handler = self.handler
       +
       +    def create_handler(self, window):
       +        return self.handler
       +
       +# EOF
   DIR diff --git a/electrum/plugins/coldcard/coldcard.py b/electrum/plugins/coldcard/coldcard.py
       t@@ -0,0 +1,684 @@
       +#
       +# Coldcard Electrum plugin main code.
       +#
       +#
       +from struct import pack, unpack
       +import hashlib
       +import os, sys, time, io
       +import traceback
       +
       +from electrum import bitcoin
       +from electrum.bitcoin import serialize_xpub, deserialize_xpub, InvalidMasterKeyVersionBytes
       +from electrum import constants
       +from electrum.bitcoin import TYPE_ADDRESS, int_to_hex
       +from electrum.i18n import _
       +from electrum.plugin import BasePlugin, Device
       +from electrum.keystore import Hardware_KeyStore, xpubkey_to_pubkey, Xpub
       +from electrum.transaction import Transaction
       +from electrum.wallet import Standard_Wallet
       +from electrum.crypto import hash_160
       +from ..hw_wallet import HW_PluginBase
       +from ..hw_wallet.plugin import is_any_tx_output_on_change_branch
       +from electrum.util import print_error, bfh, bh2u, versiontuple
       +from electrum.base_wizard import ScriptTypeNotSupported
       +
       +try:
       +    import hid
       +    from ckcc.protocol import CCProtocolPacker, CCProtocolUnpacker
       +    from ckcc.protocol import CCProtoError, CCUserRefused, CCBusyError
       +    from ckcc.constants import (MAX_MSG_LEN, MAX_BLK_LEN, MSG_SIGNING_MAX_LENGTH, MAX_TXN_LEN,
       +        AF_CLASSIC, AF_P2SH, AF_P2WPKH, AF_P2WSH, AF_P2WPKH_P2SH, AF_P2WSH_P2SH)
       +    from ckcc.constants import (
       +        PSBT_GLOBAL_UNSIGNED_TX, PSBT_IN_NON_WITNESS_UTXO, PSBT_IN_WITNESS_UTXO,
       +        PSBT_IN_SIGHASH_TYPE, PSBT_IN_REDEEM_SCRIPT, PSBT_IN_WITNESS_SCRIPT,
       +        PSBT_IN_BIP32_DERIVATION, PSBT_OUT_BIP32_DERIVATION)
       +
       +    from ckcc.client import ColdcardDevice, COINKITE_VID, CKCC_PID, CKCC_SIMULATOR_PATH
       +
       +    requirements_ok = True
       +
       +
       +    class ElectrumColdcardDevice(ColdcardDevice):
       +        # avoid use of pycoin for MiTM message signature test
       +        def mitm_verify(self, sig, expect_xpub):
       +            # verify a signature (65 bytes) over the session key, using the master bip32 node
       +            # - customized to use specific EC library of Electrum.
       +            from electrum.ecc import ECPubkey
       +
       +            xtype, depth, parent_fingerprint, child_number, chain_code, K_or_k \
       +                = bitcoin.deserialize_xpub(expect_xpub)
       +
       +            pubkey = ECPubkey(K_or_k)
       +            try:
       +                pubkey.verify_message_hash(sig[1:65], self.session_key)
       +                return True
       +            except:
       +                return False
       +
       +except ImportError:
       +    requirements_ok = False
       +
       +    COINKITE_VID = 0xd13e
       +    CKCC_PID     = 0xcc10
       +
       +CKCC_SIMULATED_PID = CKCC_PID ^ 0x55aa
       +
       +def my_var_int(l):
       +    # Bitcoin serialization of integers... directly into binary!
       +    if l < 253:
       +        return pack("B", l)
       +    elif l < 0x10000:
       +        return pack("<BH", 253, l)
       +    elif l < 0x100000000:
       +        return pack("<BI", 254, l)
       +    else:
       +        return pack("<BQ", 255, l)
       +
       +def xfp_from_xpub(xpub):
       +    # sometime we need to BIP32 fingerprint value: 4 bytes of ripemd(sha256(pubkey))
       +    # UNTESTED
       +    kk = bfh(Xpub.get_pubkey_from_xpub(xpub, []))
       +    assert len(kk) == 33
       +    xfp, = unpack('<I', hash_160(kk)[0:4])
       +    return xfp
       +
       +
       +class CKCCClient:
       +    # Challenge: I haven't found anywhere that defines a base class for this 'client',
       +    # nor an API (interface) to be met. Winging it. Gets called from lib/plugins.py mostly?
       +
       +    def __init__(self, plugin, handler, dev_path, is_simulator=False):
       +        self.device = plugin.device
       +        self.handler = handler
       +
       +        # if we know what the (xfp, xpub) "should be" then track it here
       +        self._expected_device = None
       +
       +        if is_simulator:
       +            self.dev = ElectrumColdcardDevice(dev_path, encrypt=True)
       +        else:
       +            # open the real HID device
       +            import hid
       +            hd = hid.device(path=dev_path)
       +            hd.open_path(dev_path)
       +
       +            self.dev = ElectrumColdcardDevice(dev=hd, encrypt=True)
       +
       +        # NOTE: MiTM test is delayed until we have a hint as to what XPUB we
       +        # should expect. It's also kinda slow.
       +
       +    def __repr__(self):
       +        return '<CKCCClient: xfp=%08x label=%r>' % (self.dev.master_fingerprint,
       +                                                        self.label())
       +
       +    def verify_connection(self, expected_xfp, expected_xpub):
       +        ex = (expected_xfp, expected_xpub)
       +
       +        if self._expected_device == ex:
       +            # all is as expected
       +            return
       +
       +        if ( (self._expected_device is not None) 
       +                or (self.dev.master_fingerprint != expected_xfp)
       +                or (self.dev.master_xpub != expected_xpub)):
       +            # probably indicating programing error, not hacking
       +            raise RuntimeError("Expecting 0x%08x but that's not whats connected?!" %
       +                                    expected_xfp)
       +
       +        # check signature over session key
       +        # - mitm might have lied about xfp and xpub up to here
       +        # - important that we use value capture at wallet creation time, not some value
       +        #   we read over USB today
       +        self.dev.check_mitm(expected_xpub=expected_xpub)
       +
       +        self._expected_device = ex
       +
       +        print_error("[coldcard]", "Successfully verified against MiTM")
       +
       +    def is_pairable(self):
       +        # can't do anything w/ devices that aren't setup (but not normally reachable)
       +        return bool(self.dev.master_xpub)
       +
       +    def timeout(self, cutoff):
       +        # nothing to do?
       +        pass
       +
       +    def close(self):
       +        # close the HID device (so can be reused)
       +        self.dev.close()
       +        self.dev = None
       +
       +    def is_initialized(self):
       +        return bool(self.dev.master_xpub)
       +
       +    def label(self):
       +        # 'label' of this Coldcard. Warning: gets saved into wallet file, which might
       +        # not be encrypted, so better for privacy if based on xpub/fingerprint rather than
       +        # USB serial number.
       +        if self.dev.is_simulator:
       +            lab = 'Coldcard Simulator 0x%08x' % self.dev.master_fingerprint
       +        elif not self.dev.master_fingerprint:
       +            # failback; not expected
       +            lab = 'Coldcard #' + self.dev.serial
       +        else:
       +            lab = 'Coldcard 0x%08x' % self.dev.master_fingerprint
       +
       +        # Hack zone: during initial setup I need the xfp and master xpub but 
       +        # very few objects are passed between the various steps of base_wizard.
       +        # Solution: return a string with some hidden metadata
       +        # - see <https://stackoverflow.com/questions/7172772/abc-for-string>
       +        # - needs to work w/ deepcopy
       +        class LabelStr(str):
       +            def __new__(cls, s, xfp=None, xpub=None):
       +                self = super().__new__(cls, str(s))
       +                self.xfp = getattr(s, 'xfp', xfp)
       +                self.xpub = getattr(s, 'xpub', xpub)
       +                return self
       +
       +        return LabelStr(lab, self.dev.master_fingerprint, self.dev.master_xpub)
       +
       +    def has_usable_connection_with_device(self):
       +        # Do end-to-end ping test
       +        try:
       +            self.ping_check()
       +            return True
       +        except:
       +            return False
       +
       +    def get_xpub(self, bip32_path, xtype):
       +        assert xtype in ColdcardPlugin.SUPPORTED_XTYPES
       +        print_error('[coldcard]', 'Derive xtype = %r' % xtype)
       +        xpub = self.dev.send_recv(CCProtocolPacker.get_xpub(bip32_path), timeout=5000)
       +        # TODO handle timeout?
       +        # change type of xpub to the requested type
       +        try:
       +            __, depth, fingerprint, child_number, c, cK = deserialize_xpub(xpub)
       +        except InvalidMasterKeyVersionBytes:
       +            raise Exception(_('Invalid xpub magic. Make sure your {} device is set to the correct chain.')
       +                            .format(self.device)) from None
       +        if xtype != 'standard':
       +            xpub = serialize_xpub(xtype, c, cK, depth, fingerprint, child_number)
       +        return xpub
       +
       +    def ping_check(self):
       +        # check connection is working
       +        assert self.dev.session_key, 'not encrypted?'
       +        req = b'1234 Electrum Plugin 4321'      # free up to 59 bytes
       +        try:
       +            echo = self.dev.send_recv(CCProtocolPacker.ping(req))
       +            assert echo == req
       +        except:
       +            raise RuntimeError("Communication trouble with Coldcard")
       +
       +    def show_address(self, path, addr_fmt):
       +        # prompt user w/ addres, also returns it immediately.
       +        return self.dev.send_recv(CCProtocolPacker.show_address(path, addr_fmt), timeout=None)
       +
       +    def get_version(self):
       +        # gives list of strings
       +        return self.dev.send_recv(CCProtocolPacker.version(), timeout=1000).split('\n')
       +
       +    def sign_message_start(self, path, msg):
       +        # this starts the UX experience.
       +        self.dev.send_recv(CCProtocolPacker.sign_message(msg, path), timeout=None)
       +
       +    def sign_message_poll(self):
       +        # poll device... if user has approved, will get tuple: (addr, sig) else None
       +        return self.dev.send_recv(CCProtocolPacker.get_signed_msg(), timeout=None)
       +
       +    def sign_transaction_start(self, raw_psbt, finalize=True):
       +        # Multiple steps to sign:
       +        # - upload binary
       +        # - start signing UX
       +        # - wait for coldcard to complete process, or have it refused.
       +        # - download resulting txn
       +        assert 20 <= len(raw_psbt) < MAX_TXN_LEN, 'PSBT is too big'
       +        dlen, chk = self.dev.upload_file(raw_psbt)
       +
       +        resp = self.dev.send_recv(CCProtocolPacker.sign_transaction(dlen, chk, finalize=finalize),
       +                                    timeout=None)
       +
       +        if resp != None:
       +            raise ValueError(resp)
       +
       +    def sign_transaction_poll(self):
       +        # poll device... if user has approved, will get tuple: (legnth, checksum) else None
       +        return self.dev.send_recv(CCProtocolPacker.get_signed_txn(), timeout=None)
       +
       +    def download_file(self, length, checksum, file_number=1):
       +        # get a file
       +        return self.dev.download_file(length, checksum, file_number=file_number)
       +
       +        
       +
       +class Coldcard_KeyStore(Hardware_KeyStore):
       +    hw_type = 'coldcard'
       +    device = 'Coldcard'
       +
       +    def __init__(self, d):
       +        Hardware_KeyStore.__init__(self, d)
       +        # Errors and other user interaction is done through the wallet's
       +        # handler.  The handler is per-window and preserved across
       +        # device reconnects
       +        self.force_watching_only = False
       +        self.ux_busy = False
       +
       +        # Seems like only the derivation path and resulting **derived** xpub is stored in
       +        # the wallet file... however, we need to know at least the fingerprint of the master
       +        # xpub to verify against MiTM, and also so we can put the right value into the subkey paths
       +        # of PSBT files that might be generated offline. 
       +        # - save the fingerprint of the master xpub, as "xfp"
       +        # - it's a LE32 int, but hex more natural way to see it
       +        # - device reports these value during encryption setup process
       +        lab = d['label']
       +        if hasattr(lab, 'xfp'):
       +            # initial setup
       +            self.ckcc_xfp = lab.xfp
       +            self.ckcc_xpub = lab.xpub
       +        else:
       +            # wallet load: fatal if missing, we need them!
       +            self.ckcc_xfp = d['ckcc_xfp']
       +            self.ckcc_xpub = d['ckcc_xpub']
       +
       +    def dump(self):
       +        # our additions to the stored data about keystore -- only during creation?
       +        d = Hardware_KeyStore.dump(self)
       +
       +        d['ckcc_xfp'] = self.ckcc_xfp
       +        d['ckcc_xpub'] = self.ckcc_xpub
       +
       +        return d
       +
       +    def get_derivation(self):
       +        return self.derivation
       +
       +    def get_client(self):
       +        # called when user tries to do something like view address, sign somthing.
       +        # - not called during probing/setup
       +        rv = self.plugin.get_client(self)
       +        if rv:
       +            rv.verify_connection(self.ckcc_xfp, self.ckcc_xpub)
       +
       +        return rv
       +
       +    def give_error(self, message, clear_client=False):
       +        print_error(message)
       +        if not self.ux_busy:
       +            self.handler.show_error(message)
       +        else:
       +            self.ux_busy = False
       +        if clear_client:
       +            self.client = None
       +        raise Exception(message)
       +
       +    def wrap_busy(func):
       +        # decorator: function takes over the UX on the device.
       +        def wrapper(self, *args, **kwargs):
       +            try:
       +                self.ux_busy = True
       +                return func(self, *args, **kwargs)
       +            finally:
       +                self.ux_busy = False
       +        return wrapper
       +
       +    def decrypt_message(self, pubkey, message, password):
       +        raise RuntimeError(_('Encryption and decryption are currently not supported for {}').format(self.device))
       +
       +    @wrap_busy
       +    def sign_message(self, sequence, message, password):
       +        # Sign a message on device. Since we have big screen, of course we
       +        # have to show the message unabiguously there first!
       +        try:
       +            msg = message.encode('ascii', errors='strict')
       +            assert 1 <= len(msg) <= MSG_SIGNING_MAX_LENGTH
       +        except (UnicodeError, AssertionError):
       +            # there are other restrictions on message content,
       +            # but let the device enforce and report those
       +            self.handler.show_error('Only short (%d max) ASCII messages can be signed.' 
       +                                            % MSG_SIGNING_MAX_LENGTH)
       +            return b''
       +
       +        client = self.get_client()
       +        path = self.get_derivation() + ("/%d/%d" % sequence)
       +        try:
       +            cl = self.get_client()
       +            try:
       +                self.handler.show_message("Signing message (using %s)..." % path)
       +
       +                cl.sign_message_start(path, msg)
       +
       +                while 1:
       +                    # How to kill some time, without locking UI?
       +                    time.sleep(0.250)
       +
       +                    resp = cl.sign_message_poll()
       +                    if resp is not None:
       +                        break
       +
       +            finally:
       +                self.handler.finished()
       +
       +            assert len(resp) == 2
       +            addr, raw_sig = resp
       +
       +            # already encoded in Bitcoin fashion, binary.
       +            assert 40 < len(raw_sig) <= 65
       +
       +            return raw_sig
       +
       +        except (CCUserRefused, CCBusyError) as exc:
       +            self.handler.show_error(str(exc))
       +        except CCProtoError as exc:
       +            traceback.print_exc(file=sys.stderr)
       +            self.handler.show_error('{}\n\n{}'.format(
       +                _('Error showing address') + ':', str(exc)))
       +        except Exception as e:
       +            self.give_error(e, True)
       +
       +        # give empty bytes for error cases; it seems to clear the old signature box
       +        return b''
       +
       +    def build_psbt(self, tx, wallet=None, xfp=None):
       +        # Render a PSBT file, for upload to Coldcard.
       +        # 
       +        if xfp is None:
       +            # need fingerprint of MASTER xpub, not the derived key
       +            xfp = self.ckcc_xfp
       +
       +        inputs = tx.inputs()
       +
       +        if 'prev_tx' not in inputs[0]:
       +            # fetch info about inputs, if needed?
       +            # - needed during export PSBT flow, not normal online signing
       +            assert wallet, 'need wallet reference'
       +            wallet.add_hw_info(tx)
       +
       +        # wallet.add_hw_info installs this attr
       +        assert hasattr(tx, 'output_info'), 'need data about outputs'
       +
       +        # Build map of pubkey needed as derivation from master, in PSBT binary format
       +        # 1) binary version of the common subpath for all keys
       +        #       m/ => fingerprint LE32
       +        #       a/b/c => ints
       +        base_path = pack('<I', xfp)
       +        for x in self.get_derivation()[2:].split('/'):
       +            if x.endswith("'"):
       +                x = int(x[:-1]) | 0x80000000
       +            else:
       +                x = int(x)
       +            base_path += pack('<I', x)
       +
       +        # 2) all used keys in transaction
       +        subkeys = {}
       +        derivations = self.get_tx_derivations(tx)
       +        for xpubkey in derivations:
       +            pubkey = xpubkey_to_pubkey(xpubkey)
       +
       +            # assuming depth two, non-harded: change + index
       +            aa, bb = derivations[xpubkey]
       +            assert 0 <= aa < 0x80000000
       +            assert 0 <= bb < 0x80000000
       +
       +            subkeys[bfh(pubkey)] = base_path + pack('<II', aa, bb)
       +            
       +        for txin in inputs:
       +            if txin['type'] == 'coinbase':
       +                self.give_error("Coinbase not supported")     # but why not?
       +
       +            if txin['type'] in ['p2sh']:
       +                self.give_error('Not ready for multisig transactions yet')
       +
       +            #if txin['type'] in ['p2wpkh-p2sh', 'p2wsh-p2sh']:
       +            #if txin['type'] in ['p2wpkh', 'p2wsh']:
       +
       +        # Construct PSBT from start to finish.
       +        out_fd = io.BytesIO()
       +        out_fd.write(b'psbt\xff')
       +
       +        def write_kv(ktype, val, key=b''):
       +            # serialize helper: write w/ size and key byte
       +            out_fd.write(my_var_int(1 + len(key)))
       +            out_fd.write(bytes([ktype]) + key)
       +
       +            if isinstance(val, str):
       +                val = bfh(val)
       +
       +            out_fd.write(my_var_int(len(val)))
       +            out_fd.write(val)
       +
       +
       +        # global section: just the unsigned txn
       +        class CustomTXSerialization(Transaction):
       +            @classmethod
       +            def input_script(cls, txin, estimate_size=False):
       +                return ''
       +        unsigned = bfh(CustomTXSerialization(tx.serialize()).serialize_to_network(witness=False))
       +        write_kv(PSBT_GLOBAL_UNSIGNED_TX, unsigned)
       +
       +        # end globals section
       +        out_fd.write(b'\x00')
       +
       +        # inputs section
       +        for txin in inputs:
       +            utxo = txin['prev_tx'].outputs()[txin['prevout_n']]
       +            spendable = txin['prev_tx'].serialize_output(utxo)
       +            write_kv(PSBT_IN_WITNESS_UTXO, spendable)
       +
       +            pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin)
       +
       +            pubkeys = [bfh(k) for k in pubkeys]
       +
       +            for k in pubkeys:
       +                write_kv(PSBT_IN_BIP32_DERIVATION, subkeys[k], k)
       +
       +            out_fd.write(b'\x00')
       +
       +        # outputs section
       +        for o in tx.outputs():
       +            # can be empty, but must be present, and helpful to show change inputs
       +            # wallet.add_hw_info() adds some data about change outputs into tx.output_info
       +            if o.address in tx.output_info:
       +                # this address "is_mine" but might not be change (I like to sent to myself)
       +                output_info = tx.output_info.get(o.address)
       +                index, xpubs = output_info.address_index, output_info.sorted_xpubs
       +
       +                if index[0] == 1 and len(index) == 2:
       +                    # it is a change output (based on our standard derivation path)
       +                    assert len(xpubs) == 1      # not expecting multisig
       +                    xpubkey = xpubs[0]
       +
       +                    # document its bip32 derivation in output section
       +                    aa, bb = index
       +                    assert 0 <= aa < 0x80000000
       +                    assert 0 <= bb < 0x80000000
       +
       +                    deriv = base_path + pack('<II', aa, bb)
       +                    pubkey = self.get_pubkey_from_xpub(xpubkey, index)
       +
       +                    write_kv(PSBT_OUT_BIP32_DERIVATION, deriv, bfh(pubkey))
       +
       +            out_fd.write(b'\x00')
       +
       +        return out_fd.getvalue()
       +
       +
       +    @wrap_busy
       +    def sign_transaction(self, tx, password):
       +        # Build a PSBT in memory, upload it for signing.
       +        # - we can also work offline (without paired device present)
       +        if tx.is_complete():
       +            return
       +
       +        client = self.get_client()
       +
       +        assert client.dev.master_fingerprint == self.ckcc_xfp
       +
       +        raw_psbt = self.build_psbt(tx)
       +
       +        #open('debug.psbt', 'wb').write(out_fd.getvalue())
       +
       +        try:
       +            try:
       +                self.handler.show_message("Authorize Transaction...")
       +
       +                client.sign_transaction_start(raw_psbt, True)
       +
       +                while 1:
       +                    # How to kill some time, without locking UI?
       +                    time.sleep(0.250)
       +
       +                    resp = client.sign_transaction_poll()
       +                    if resp is not None:
       +                        break
       +
       +                rlen, rsha = resp
       +            
       +                # download the resulting txn.
       +                new_raw = client.download_file(rlen, rsha)
       +
       +            finally:
       +                self.handler.finished()
       +
       +        except (CCUserRefused, CCBusyError) as exc:
       +            print_error('[coldcard]', 'Did not sign:', str(exc))
       +            self.handler.show_error(str(exc))
       +            return
       +        except BaseException as e:
       +            traceback.print_exc(file=sys.stderr)
       +            self.give_error(e, True)
       +            return
       +
       +        # trust the coldcard to re-searilize final product right?
       +        tx.update(bh2u(new_raw))
       +
       +    @staticmethod
       +    def _encode_txin_type(txin_type):
       +        # Map from Electrum code names to our code numbers.
       +        return {'standard': AF_CLASSIC, 'p2pkh': AF_CLASSIC,
       +                'p2sh': AF_P2SH,
       +                'p2wpkh-p2sh': AF_P2WPKH_P2SH,
       +                'p2wpkh': AF_P2WPKH,
       +                'p2wsh-p2sh': AF_P2WSH_P2SH,
       +                'p2wsh': AF_P2WSH,
       +                }[txin_type]
       +
       +    @wrap_busy
       +    def show_address(self, sequence, txin_type):
       +        client = self.get_client()
       +        address_path = self.get_derivation()[2:] + "/%d/%d"%sequence
       +        addr_fmt = self._encode_txin_type(txin_type)
       +        try:
       +            try:
       +                self.handler.show_message(_("Showing address ..."))
       +                dev_addr = client.show_address(address_path, addr_fmt)
       +                # we could double check address here
       +            finally:
       +                self.handler.finished()
       +        except CCProtoError as exc:
       +            traceback.print_exc(file=sys.stderr)
       +            self.handler.show_error('{}\n\n{}'.format(
       +                _('Error showing address') + ':', str(exc)))
       +        except BaseException as exc:
       +            traceback.print_exc(file=sys.stderr)
       +            self.handler.show_error(exc)
       +
       +
       +
       +class ColdcardPlugin(HW_PluginBase):
       +    libraries_available = requirements_ok
       +    keystore_class = Coldcard_KeyStore
       +    client = None
       +
       +    DEVICE_IDS = [
       +        (COINKITE_VID, CKCC_PID),
       +        (COINKITE_VID, CKCC_SIMULATED_PID)
       +    ]
       +
       +    #SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh')
       +    SUPPORTED_XTYPES = ('standard', 'p2wpkh')
       +
       +    def __init__(self, parent, config, name):
       +        HW_PluginBase.__init__(self, parent, config, name)
       +
       +        if self.libraries_available:
       +            self.device_manager().register_devices(self.DEVICE_IDS)
       +
       +            self.device_manager().register_enumerate_func(self.detect_simulator)
       +
       +    def detect_simulator(self):
       +        # if there is a simulator running on this machine,
       +        # return details about it so it's offered as a pairing choice
       +        fn = CKCC_SIMULATOR_PATH
       +
       +        if os.path.exists(fn):
       +            return [Device(fn, -1, fn, (COINKITE_VID, CKCC_SIMULATED_PID), 0)]
       +
       +        return []
       +        
       +
       +    def create_client(self, device, handler):
       +        if handler:
       +            self.handler = handler
       +
       +        # We are given a HID device, or at least some details about it.
       +        # Not sure why not we aren't just given a HID library handle, but
       +        # the 'path' is unabiguous, so we'll use that.
       +        try:
       +            rv = CKCCClient(self, handler, device.path,
       +                    is_simulator=(device.product_key[1] == CKCC_SIMULATED_PID))
       +            return rv
       +        except:
       +            self.print_error('late failure connecting to device?')
       +            return None
       +
       +    def setup_device(self, device_info, wizard, purpose):
       +        devmgr = self.device_manager()
       +        device_id = device_info.device.id_
       +        client = devmgr.client_by_id(device_id)
       +        if client is None:
       +            raise Exception(_('Failed to create a client for this device.') + '\n' +
       +                            _('Make sure it is in the correct state.'))
       +        client.handler = self.create_handler(wizard)
       +
       +    def get_xpub(self, device_id, derivation, xtype, wizard):
       +        # this seems to be part of the pairing process only, not during normal ops?
       +        # base_wizard:on_hw_derivation
       +        if xtype not in self.SUPPORTED_XTYPES:
       +            raise ScriptTypeNotSupported(_('This type of script is not supported with {}.').format(self.device))
       +        devmgr = self.device_manager()
       +        client = devmgr.client_by_id(device_id)
       +        client.handler = self.create_handler(wizard)
       +        client.ping_check()
       +
       +        xpub = client.get_xpub(derivation, xtype)
       +        return xpub
       +
       +    def get_client(self, keystore, force_pair=True):
       +        # All client interaction should not be in the main GUI thread
       +        devmgr = self.device_manager()
       +        handler = keystore.handler
       +        with devmgr.hid_lock:
       +            client = devmgr.client_for_keystore(self, handler, keystore, force_pair)
       +        # returns the client for a given keystore. can use xpub
       +        #if client:
       +        #    client.used()
       +        if client is not None:
       +            client.ping_check()
       +        return client
       +
       +    def show_address(self, wallet, address, keystore=None):
       +        if keystore is None:
       +            keystore = wallet.get_keystore()
       +        if not self.show_address_helper(wallet, address, keystore):
       +            return
       +
       +        # Standard_Wallet => not multisig, must be bip32
       +        if type(wallet) is not Standard_Wallet:
       +            keystore.handler.show_error(_('This function is only available for standard wallets when using {}.').format(self.device))
       +            return
       +
       +        sequence = wallet.get_address_index(address)
       +        txin_type = wallet.get_txin_type(address)
       +        keystore.show_address(sequence, txin_type)
       +
       +# EOF
   DIR diff --git a/electrum/plugins/coldcard/qt.py b/electrum/plugins/coldcard/qt.py
       t@@ -0,0 +1,242 @@
       +import time
       +
       +from electrum.i18n import _
       +from electrum.plugin import hook
       +from electrum.wallet import Standard_Wallet
       +from electrum.gui.qt.util import *
       +
       +from .coldcard import ColdcardPlugin
       +from ..hw_wallet.qt import QtHandlerBase, QtPluginBase
       +
       +
       +class Plugin(ColdcardPlugin, QtPluginBase):
       +    icon_unpaired = ":icons/coldcard_unpaired.png"
       +    icon_paired = ":icons/coldcard.png"
       +
       +    def create_handler(self, window):
       +        return Coldcard_Handler(window)
       +
       +    @hook
       +    def receive_menu(self, menu, addrs, wallet):
       +        if type(wallet) is not Standard_Wallet:
       +            return
       +        keystore = wallet.get_keystore()
       +        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 Coldcard"), show_address)
       +
       +    @hook
       +    def transaction_dialog(self, dia):
       +        # see gui/qt/transaction_dialog.py
       +
       +        keystore = dia.wallet.get_keystore()
       +        if type(keystore) != self.keystore_class:
       +            # not a Coldcard wallet, hide feature
       +            return
       +
       +        # - add a new button, near "export"
       +        btn = QPushButton(_("Save PSBT"))
       +        btn.clicked.connect(lambda unused: self.export_psbt(dia))
       +        if dia.tx.is_complete():
       +            # but disable it for signed transactions (nothing to do if already signed)
       +            btn.setDisabled(True)
       +
       +        dia.sharing_buttons.append(btn)
       +
       +    def export_psbt(self, dia):
       +        # Called from hook in transaction dialog
       +        tx = dia.tx
       +
       +        if tx.is_complete():
       +            # if they sign while dialog is open, it can transition from unsigned to signed,
       +            # which we don't support here, so do nothing
       +            return
       +
       +        # can only expect Coldcard wallets to work with these files (right now)
       +        keystore = dia.wallet.get_keystore()
       +        assert type(keystore) == self.keystore_class
       +
       +        # convert to PSBT
       +        raw_psbt = keystore.build_psbt(tx, wallet=dia.wallet)
       +
       +        name = (dia.wallet.basename() + time.strftime('-%y%m%d-%H%M.psbt')).replace(' ', '-')
       +        fileName = dia.main_window.getSaveFileName(_("Select where to save the PSBT file"),
       +                                                        name, "*.psbt")
       +        if fileName:
       +            with open(fileName, "wb+") as f:
       +                f.write(raw_psbt)
       +            dia.show_message(_("Transaction exported successfully"))
       +            dia.saved = True
       +
       +    def show_settings_dialog(self, window, keystore):
       +        # When they click on the icon for CC we come here.
       +        device_id = self.choose_device(window, keystore)
       +        if device_id:
       +            CKCCSettingsDialog(window, self, keystore, device_id).exec_()
       +
       +
       +class Coldcard_Handler(QtHandlerBase):
       +    setup_signal = pyqtSignal()
       +    #auth_signal = pyqtSignal(object)
       +
       +    def __init__(self, win):
       +        super(Coldcard_Handler, self).__init__(win, 'Coldcard')
       +        self.setup_signal.connect(self.setup_dialog)
       +        #self.auth_signal.connect(self.auth_dialog)
       +
       +    
       +    def message_dialog(self, msg):
       +        self.clear_dialog()
       +        self.dialog = dialog = WindowModalDialog(self.top_level_window(), _("Coldcard Status"))
       +        l = QLabel(msg)
       +        vbox = QVBoxLayout(dialog)
       +        vbox.addWidget(l)
       +        dialog.show()
       +        
       +    def get_setup(self):
       +        self.done.clear()
       +        self.setup_signal.emit()
       +        self.done.wait()
       +        return 
       +        
       +    def setup_dialog(self):
       +        self.show_error(_('Please initialization your Coldcard while disconnected.'))
       +        return
       +
       +class CKCCSettingsDialog(WindowModalDialog):
       +    '''This dialog doesn't require a device be paired with a wallet.
       +    We want users to be able to wipe a device even if they've forgotten
       +    their PIN.'''
       +
       +    def __init__(self, window, plugin, keystore, device_id):
       +        title = _("{} Settings").format(plugin.device)
       +        super(CKCCSettingsDialog, self).__init__(window, title)
       +        self.setMaximumWidth(540)
       +
       +        devmgr = plugin.device_manager()
       +        config = devmgr.config
       +        handler = keystore.handler
       +        self.thread = thread = keystore.thread
       +
       +        def connect_and_doit():
       +            client = devmgr.client_by_id(device_id)
       +            if not client:
       +                raise RuntimeError("Device not connected")
       +            return client
       +
       +        body = QWidget()
       +        body_layout = QVBoxLayout(body)
       +        grid = QGridLayout()
       +        grid.setColumnStretch(2, 1)
       +
       +        # see <http://doc.qt.io/archives/qt-4.8/richtext-html-subset.html>
       +        title = QLabel('''<center>
       +<span style="font-size: x-large">Coldcard Wallet</span>
       +<br><span style="font-size: medium">from Coinkite Inc.</span>
       +<br><a href="https://coldcardwallet.com">coldcardwallet.com</a>''')
       +        title.setTextInteractionFlags(Qt.LinksAccessibleByMouse)
       +
       +        grid.addWidget(title , 0,0, 1,2, Qt.AlignHCenter)
       +        y = 3
       +
       +        rows = [
       +            ('fw_version', _("Firmware Version")),
       +            ('fw_built', _("Build Date")),
       +            ('bl_version', _("Bootloader")),
       +            ('xfp', _("Master Fingerprint")),
       +            ('serial', _("USB Serial")),
       +        ]
       +        for row_num, (member_name, label) in enumerate(rows):
       +            widget = QLabel('<tt>000000000000')
       +            widget.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard)
       +
       +            grid.addWidget(QLabel(label), y, 0, 1,1, Qt.AlignRight)
       +            grid.addWidget(widget, y, 1, 1, 1, Qt.AlignLeft)
       +            setattr(self, member_name, widget)
       +            y += 1
       +        body_layout.addLayout(grid)
       +
       +        upg_btn = QPushButton('Upgrade')
       +        #upg_btn.setDefault(False)
       +        def _start_upgrade():
       +            thread.add(connect_and_doit, on_success=self.start_upgrade)
       +        upg_btn.clicked.connect(_start_upgrade)
       +
       +        y += 3
       +        grid.addWidget(upg_btn, y, 0)
       +        grid.addWidget(CloseButton(self), y, 1)
       +
       +        dialog_vbox = QVBoxLayout(self)
       +        dialog_vbox.addWidget(body)
       +
       +        # Fetch values and show them
       +        thread.add(connect_and_doit, on_success=self.show_values)
       +
       +    def show_values(self, client):
       +        dev = client.dev
       +
       +        self.xfp.setText('<tt>0x%08x' % dev.master_fingerprint)
       +        self.serial.setText('<tt>%s' % dev.serial)
       +
       +        # ask device for versions: allow extras for future
       +        fw_date, fw_rel, bl_rel, *rfu = client.get_version()
       +
       +        self.fw_version.setText('<tt>%s' % fw_rel)
       +        self.fw_built.setText('<tt>%s' % fw_date)
       +        self.bl_version.setText('<tt>%s' % bl_rel)
       +
       +    def start_upgrade(self, client):
       +        # ask for a filename (must have already downloaded it)
       +        mw = get_parent_main_window(self)
       +        dev = client.dev
       +
       +        fileName = mw.getOpenFileName("Select upgraded firmware file", "*.dfu")
       +        if not fileName:
       +            return
       +
       +        from ckcc.utils import dfu_parse
       +        from ckcc.sigheader import FW_HEADER_SIZE, FW_HEADER_OFFSET, FW_HEADER_MAGIC
       +        from ckcc.protocol import CCProtocolPacker
       +        from hashlib import sha256
       +        import struct
       +
       +        try:
       +            with open(fileName, 'rb') as fd:
       +
       +                # unwrap firmware from the DFU
       +                offset, size, *ignored = dfu_parse(fd)
       +
       +                fd.seek(offset)
       +                firmware = fd.read(size)
       +
       +            hpos = FW_HEADER_OFFSET
       +            hdr = bytes(firmware[hpos:hpos + FW_HEADER_SIZE])        # needed later too
       +            magic = struct.unpack_from("<I", hdr)[0]
       +
       +            if magic != FW_HEADER_MAGIC:
       +                raise ValueError("Bad magic")
       +        except Exception as exc:
       +            mw.show_error("Does not appear to be a Coldcard firmware file.\n\n%s" % exc)
       +            return
       +
       +        # TODO: 
       +        # - detect if they are trying to downgrade; aint gonna work
       +        # - warn them about the reboot?
       +        # - length checks
       +        # - add progress local bar
       +        mw.show_message("Ready to Upgrade.\n\nBe patient. Unit will reboot itself when complete.")
       +
       +        def doit():
       +            dlen, _ = dev.upload_file(firmware, verify=True)
       +            assert dlen == len(firmware)
       +
       +            # append the firmware header a second time
       +            result = dev.send_recv(CCProtocolPacker.upload(size, size+FW_HEADER_SIZE, hdr))
       +
       +            # make it reboot into bootlaoder which might install it
       +            dev.send_recv(CCProtocolPacker.reboot())
       +
       +        self.thread.add(doit)
       +        self.close()
       +# EOF
   DIR diff --git a/icons.qrc b/icons.qrc
       t@@ -51,6 +51,8 @@
            <file>icons/speaker.png</file>
            <file>icons/trezor_unpaired.png</file>
            <file>icons/trezor.png</file>
       +    <file>icons/coldcard.png</file>
       +    <file>icons/coldcard_unpaired.png</file>
            <file>icons/trustedcoin-status.png</file>
            <file>icons/trustedcoin-wizard.png</file>
            <file>icons/unconfirmed.png</file>
   DIR diff --git a/icons/coldcard.png b/icons/coldcard.png
       Binary files differ.
   DIR diff --git a/icons/coldcard_unpaired.png b/icons/coldcard_unpaired.png
       Binary files differ.