URI: 
       tMerge pull request #5440 from Coldcard/multisig - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit c81f5395af88f929e423b0a54c960888128d7ab7
   DIR parent bd83ca02863920479fefd41f1e54b0315fc6a318
  HTML Author: SomberNight <somber.night@protonmail.com>
       Date:   Wed, 18 Sep 2019 18:35:05 +0200
       
       Merge pull request #5440 from Coldcard/multisig
       
       Add multisig support for Coldcard plugin
       
       Diffstat:
         M contrib/requirements/requirements-… |       2 +-
         M electrum/gui/qt/main_window.py      |      22 ++++++++++++++++------
         A electrum/plugins/coldcard/basic_ps… |     313 +++++++++++++++++++++++++++++++
         A electrum/plugins/coldcard/build_ps… |     397 +++++++++++++++++++++++++++++++
         M electrum/plugins/coldcard/coldcard… |     360 ++++++++++++++-----------------
         M electrum/plugins/coldcard/qt.py     |     204 +++++++++++++++++++++++++------
         M electrum/plugins/hw_wallet/qt.py    |       3 ++-
       
       7 files changed, 1063 insertions(+), 238 deletions(-)
       ---
   DIR diff --git a/contrib/requirements/requirements-hw.txt b/contrib/requirements/requirements-hw.txt
       t@@ -2,5 +2,5 @@ trezor[hidapi]>=0.11.0
        safet[hidapi]>=0.1.0
        keepkey>=6.0.3
        btchip-python>=0.1.26
       -ckcc-protocol>=0.7.2
       +ckcc-protocol>=0.7.7
        hidapi
   DIR diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
       t@@ -2390,29 +2390,39 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                    ks_type = str(keystore_types[0]) if keystore_types else _('No keystore')
                    grid.addWidget(QLabel(ks_type), 4, 1)
                vbox.addLayout(grid)
       +
                if self.wallet.is_deterministic():
                    mpk_text = ShowQRTextEdit()
                    mpk_text.setMaximumHeight(150)
                    mpk_text.addCopyButton(self.app)
       +
                    def show_mpk(index):
                        mpk_text.setText(mpk_list[index])
                        mpk_text.repaint()  # macOS hack for #4777
       +                
                    # only show the combobox in case multiple accounts are available
                    if len(mpk_list) > 1:
       -                def label(key):
       -                    if isinstance(self.wallet, Multisig_Wallet):
       -                        return _("cosigner") + f' {key+1} ( keystore: {keystore_types[key]} )'
       -                    return ''
       -                labels = [label(i) for i in range(len(mpk_list))]
       +                # only show the combobox if multiple master keys are defined
       +                def label(idx, ks):
       +                    if isinstance(self.wallet, Multisig_Wallet) and hasattr(ks, 'label'):
       +                        return _("cosigner") + f' {idx+1}: {ks.get_type_text()} {ks.label}'
       +                    else:
       +                        return _("keystore") + f' {idx+1}'
       +
       +                labels = [label(idx, ks) for idx, ks in enumerate(self.wallet.get_keystores())]
       +
                        on_click = lambda clayout: show_mpk(clayout.selected_index())
                        labels_clayout = ChoicesLayout(_("Master Public Keys"), labels, on_click)
                        vbox.addLayout(labels_clayout.layout())
                    else:
                        vbox.addWidget(QLabel(_("Master Public Key")))
       +
                    show_mpk(0)
                    vbox.addWidget(mpk_text)
       +
                vbox.addStretch(1)
       -        vbox.addLayout(Buttons(CloseButton(dialog)))
       +        btns = run_hook('wallet_info_buttons', self, dialog) or Buttons(CloseButton(dialog))
       +        vbox.addLayout(btns)
                dialog.setLayout(vbox)
                dialog.exec_()
        
   DIR diff --git a/electrum/plugins/coldcard/basic_psbt.py b/electrum/plugins/coldcard/basic_psbt.py
       t@@ -0,0 +1,313 @@
       +#
       +# basic_psbt.py - yet another PSBT parser/serializer but used only for test cases.
       +#
       +# - history: taken from coldcard-firmware/testing/psbt.py
       +# - trying to minimize electrum code in here, and generally, dependancies.
       +#
       +import io
       +import struct
       +from base64 import b64decode
       +from binascii import a2b_hex, b2a_hex
       +from struct import pack, unpack
       +
       +from electrum.transaction import Transaction
       +
       +# BIP-174 (aka PSBT) defined values
       +#
       +PSBT_GLOBAL_UNSIGNED_TX         = (0)
       +PSBT_GLOBAL_XPUB                 = (1)
       +
       +PSBT_IN_NON_WITNESS_UTXO         = (0)
       +PSBT_IN_WITNESS_UTXO             = (1)
       +PSBT_IN_PARTIAL_SIG             = (2)
       +PSBT_IN_SIGHASH_TYPE             = (3)
       +PSBT_IN_REDEEM_SCRIPT             = (4)
       +PSBT_IN_WITNESS_SCRIPT             = (5)
       +PSBT_IN_BIP32_DERIVATION         = (6)
       +PSBT_IN_FINAL_SCRIPTSIG         = (7)
       +PSBT_IN_FINAL_SCRIPTWITNESS = (8)
       +
       +PSBT_OUT_REDEEM_SCRIPT             = (0)
       +PSBT_OUT_WITNESS_SCRIPT         = (1)
       +PSBT_OUT_BIP32_DERIVATION         = (2)
       +
       +# Serialization/deserialization tools
       +def ser_compact_size(l):
       +    r = b""
       +    if l < 253:
       +        r = struct.pack("B", l)
       +    elif l < 0x10000:
       +        r = struct.pack("<BH", 253, l)
       +    elif l < 0x100000000:
       +        r = struct.pack("<BI", 254, l)
       +    else:
       +        r = struct.pack("<BQ", 255, l)
       +    return r
       +
       +def deser_compact_size(f):
       +    try:
       +        nit = f.read(1)[0]
       +    except IndexError:
       +        return None     # end of file
       +    
       +    if nit == 253:
       +        nit = struct.unpack("<H", f.read(2))[0]
       +    elif nit == 254:
       +        nit = struct.unpack("<I", f.read(4))[0]
       +    elif nit == 255:
       +        nit = struct.unpack("<Q", f.read(8))[0]
       +    return nit
       +
       +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)
       +
       +
       +class PSBTSection:
       +
       +    def __init__(self, fd=None, idx=None):
       +        self.defaults()
       +        self.my_index = idx
       +
       +        if not fd: return
       +
       +        while 1:
       +            ks = deser_compact_size(fd)
       +            if ks is None: break
       +            if ks == 0: break
       +
       +            key = fd.read(ks)
       +            vs = deser_compact_size(fd)
       +            val = fd.read(vs)
       +
       +            kt = key[0]
       +            self.parse_kv(kt, key[1:], val)
       +
       +    def serialize(self, fd, my_idx):
       +
       +        def wr(ktype, val, key=b''):
       +            fd.write(ser_compact_size(1 + len(key)))
       +            fd.write(bytes([ktype]) + key)
       +            fd.write(ser_compact_size(len(val)))
       +            fd.write(val)
       +
       +        self.serialize_kvs(wr)
       +
       +        fd.write(b'\0')
       +
       +class BasicPSBTInput(PSBTSection):
       +    def defaults(self):
       +        self.utxo = None
       +        self.witness_utxo = None
       +        self.part_sigs = {}
       +        self.sighash = None
       +        self.bip32_paths = {}
       +        self.redeem_script = None
       +        self.witness_script = None
       +        self.others = {}
       +
       +    def __eq__(a, b):
       +        if a.sighash != b.sighash:
       +            if a.sighash is not None and b.sighash is not None:
       +                return False
       +
       +        rv =  a.utxo == b.utxo and \
       +                a.witness_utxo == b.witness_utxo and \
       +                a.redeem_script == b.redeem_script and \
       +                a.witness_script == b.witness_script and \
       +                a.my_index == b.my_index and \
       +                a.bip32_paths == b.bip32_paths and \
       +                sorted(a.part_sigs.keys()) == sorted(b.part_sigs.keys())
       +
       +        # NOTE: equality test on signatures requires parsing DER stupidness
       +        #       and some maybe understanding of R/S values on curve that I don't have.
       +
       +        return rv
       +
       +    def parse_kv(self, kt, key, val):
       +        if kt == PSBT_IN_NON_WITNESS_UTXO:
       +            self.utxo = val
       +            assert not key
       +        elif kt == PSBT_IN_WITNESS_UTXO:
       +            self.witness_utxo = val
       +            assert not key
       +        elif kt == PSBT_IN_PARTIAL_SIG:
       +            self.part_sigs[key] = val
       +        elif kt == PSBT_IN_SIGHASH_TYPE:
       +            assert len(val) == 4
       +            self.sighash = struct.unpack("<I", val)[0]
       +            assert not key
       +        elif kt == PSBT_IN_BIP32_DERIVATION:
       +            self.bip32_paths[key] = val
       +        elif kt == PSBT_IN_REDEEM_SCRIPT:
       +            self.redeem_script = val
       +            assert not key
       +        elif kt == PSBT_IN_WITNESS_SCRIPT:
       +            self.witness_script = val
       +            assert not key
       +        elif kt in ( PSBT_IN_REDEEM_SCRIPT,
       +                     PSBT_IN_WITNESS_SCRIPT, 
       +                     PSBT_IN_FINAL_SCRIPTSIG, 
       +                     PSBT_IN_FINAL_SCRIPTWITNESS):
       +            assert not key
       +            self.others[kt] = val
       +        else:
       +            raise KeyError(kt)
       +
       +    def serialize_kvs(self, wr):
       +        if self.utxo:
       +            wr(PSBT_IN_NON_WITNESS_UTXO, self.utxo)
       +        if self.witness_utxo:
       +            wr(PSBT_IN_WITNESS_UTXO, self.witness_utxo)
       +        if self.redeem_script:
       +            wr(PSBT_IN_REDEEM_SCRIPT, self.redeem_script)
       +        if self.witness_script:
       +            wr(PSBT_IN_WITNESS_SCRIPT, self.witness_script)
       +        for pk, val in sorted(self.part_sigs.items()):
       +            wr(PSBT_IN_PARTIAL_SIG, val, pk)
       +        if self.sighash is not None:
       +            wr(PSBT_IN_SIGHASH_TYPE, struct.pack('<I', self.sighash))
       +        for k in self.bip32_paths:
       +            wr(PSBT_IN_BIP32_DERIVATION, self.bip32_paths[k], k)
       +        for k in self.others:
       +            wr(k, self.others[k])
       +
       +class BasicPSBTOutput(PSBTSection):
       +    def defaults(self):
       +        self.redeem_script = None
       +        self.witness_script = None
       +        self.bip32_paths = {}
       +
       +    def __eq__(a, b):
       +        return  a.redeem_script == b.redeem_script and \
       +                a.witness_script == b.witness_script and \
       +                a.my_index == b.my_index and \
       +                a.bip32_paths == b.bip32_paths
       +
       +    def parse_kv(self, kt, key, val):
       +        if kt == PSBT_OUT_REDEEM_SCRIPT:
       +            self.redeem_script = val
       +            assert not key
       +        elif kt == PSBT_OUT_WITNESS_SCRIPT:
       +            self.witness_script = val
       +            assert not key
       +        elif kt == PSBT_OUT_BIP32_DERIVATION:
       +            self.bip32_paths[key] = val
       +        else:
       +            raise ValueError(kt)
       +
       +    def serialize_kvs(self, wr):
       +        if self.redeem_script:
       +            wr(PSBT_OUT_REDEEM_SCRIPT, self.redeem_script)
       +        if self.witness_script:
       +            wr(PSBT_OUT_WITNESS_SCRIPT, self.witness_script)
       +        for k in self.bip32_paths:
       +            wr(PSBT_OUT_BIP32_DERIVATION, self.bip32_paths[k], k)
       +
       +
       +class BasicPSBT:
       +    "Just? parse and store"
       +
       +    def __init__(self):
       +
       +        self.txn = None
       +        self.filename = None
       +        self.parsed_txn = None
       +        self.xpubs = []
       +
       +        self.inputs = []
       +        self.outputs = []
       +
       +    def __eq__(a, b):
       +        return a.txn == b.txn and \
       +            len(a.inputs) == len(b.inputs) and \
       +            len(a.outputs) == len(b.outputs) and \
       +            all(a.inputs[i] == b.inputs[i] for i in range(len(a.inputs))) and \
       +            all(a.outputs[i] == b.outputs[i] for i in range(len(a.outputs))) and \
       +            sorted(a.xpubs) == sorted(b.xpubs)
       +
       +    def parse(self, raw, filename=None):
       +        # auto-detect and decode Base64 and Hex.
       +        if raw[0:10].lower() == b'70736274ff':
       +            raw = a2b_hex(raw.strip())
       +        if raw[0:6] == b'cHNidP':
       +            raw = b64decode(raw)
       +        assert raw[0:5] == b'psbt\xff', "bad magic"
       +
       +        self.filename = filename
       +
       +        with io.BytesIO(raw[5:]) as fd:
       +            
       +            # globals
       +            while 1:
       +                ks = deser_compact_size(fd)
       +                if ks is None: break
       +
       +                if ks == 0: break
       +
       +                key = fd.read(ks)
       +                vs = deser_compact_size(fd)
       +                val = fd.read(vs)
       +
       +                kt = key[0]
       +                if kt == PSBT_GLOBAL_UNSIGNED_TX:
       +                    self.txn = val
       +
       +                    self.parsed_txn = Transaction(val.hex())
       +                    num_ins = len(self.parsed_txn.inputs())
       +                    num_outs = len(self.parsed_txn.outputs())
       +
       +                elif kt == PSBT_GLOBAL_XPUB:
       +                    # key=(xpub) => val=(path)
       +                    self.xpubs.append( (key, val) )
       +                else:
       +                    raise ValueError('unknown global key type: 0x%02x' % kt)
       +
       +            assert self.txn, 'missing reqd section'
       +
       +            self.inputs = [BasicPSBTInput(fd, idx) for idx in range(num_ins)]
       +            self.outputs = [BasicPSBTOutput(fd, idx) for idx in range(num_outs)]
       +
       +            sep = fd.read(1)
       +            assert sep == b''
       +
       +        return self
       +
       +    def serialize(self, fd):
       +
       +        def wr(ktype, val, key=b''):
       +            fd.write(ser_compact_size(1 + len(key)))
       +            fd.write(bytes([ktype]) + key)
       +            fd.write(ser_compact_size(len(val)))
       +            fd.write(val)
       +
       +        fd.write(b'psbt\xff')
       +
       +        wr(PSBT_GLOBAL_UNSIGNED_TX, self.txn)
       +
       +        for k,v in self.xpubs:
       +            wr(PSBT_GLOBAL_XPUB, v, key=k)
       +
       +        # sep
       +        fd.write(b'\0')
       +
       +        for idx, inp in enumerate(self.inputs):
       +            inp.serialize(fd, idx)
       +
       +        for idx, outp in enumerate(self.outputs):
       +            outp.serialize(fd, idx)
       +
       +    def as_bytes(self):
       +        with io.BytesIO() as fd:
       +            self.serialize(fd)
       +            return fd.getvalue()
       +
       +# EOF
       +
   DIR diff --git a/electrum/plugins/coldcard/build_psbt.py b/electrum/plugins/coldcard/build_psbt.py
       t@@ -0,0 +1,397 @@
       +#
       +# build_psbt.py - create a PSBT from (unsigned) transaction and keystore data.
       +#
       +import io
       +import struct
       +from binascii import a2b_hex, b2a_hex
       +from struct import pack, unpack
       +
       +from electrum.transaction import (Transaction, multisig_script, parse_redeemScript_multisig,
       +                                  NotRecognizedRedeemScript)
       +
       +from electrum.logging import get_logger
       +from electrum.wallet import Standard_Wallet, Multisig_Wallet, Abstract_Wallet
       +from electrum.keystore import xpubkey_to_pubkey, Xpub
       +from electrum.util import bfh, bh2u
       +from electrum.crypto import hash_160, sha256
       +from electrum.bitcoin import DecodeBase58Check
       +from electrum.i18n import _
       +
       +from .basic_psbt import (
       +        PSBT_GLOBAL_UNSIGNED_TX, PSBT_GLOBAL_XPUB, PSBT_IN_NON_WITNESS_UTXO, PSBT_IN_WITNESS_UTXO,
       +        PSBT_IN_SIGHASH_TYPE, PSBT_IN_REDEEM_SCRIPT, PSBT_IN_WITNESS_SCRIPT, PSBT_IN_PARTIAL_SIG,
       +        PSBT_IN_BIP32_DERIVATION, PSBT_OUT_BIP32_DERIVATION,
       +        PSBT_OUT_REDEEM_SCRIPT, PSBT_OUT_WITNESS_SCRIPT)
       +from .basic_psbt import BasicPSBT
       +
       +
       +_logger = get_logger(__name__)
       +
       +def xfp2str(xfp):
       +    # Standardized way to show an xpub's fingerprint... it's a 4-byte string
       +    # and not really an integer. Used to show as '0x%08x' but that's wrong endian.
       +    return b2a_hex(pack('<I', xfp)).decode('ascii').upper()
       +
       +def xfp_from_xpub(xpub):
       +    # sometime we need to BIP32 fingerprint value: 4 bytes of ripemd(sha256(pubkey))
       +    kk = bfh(Xpub.get_pubkey_from_xpub(xpub, []))
       +    assert len(kk) == 33
       +    xfp, = unpack('<I', hash_160(kk)[0:4])
       +    return xfp
       +
       +def packed_xfp_path(xfp, text_path, int_path=[]):
       +    # Convert text subkey derivation path into binary format needed for PSBT
       +    # - binary LE32 values, first one is the fingerprint
       +    rv = pack('<I', xfp)
       +
       +    for x in text_path.split('/'):
       +        if x == 'm': continue
       +        if x.endswith("'"):
       +            x = int(x[:-1]) | 0x80000000
       +        else:
       +            x = int(x)
       +        rv += pack('<I', x)
       +
       +    for x in int_path:
       +        rv += pack('<I', x)
       +
       +    return rv
       +
       +def unpacked_xfp_path(xfp, text_path):
       +    # Convert text subkey derivation path into format needed for PSBT
       +    # - binary LE32 values, first one is the fingerprint
       +    # - but as ints, not bytes yet
       +    rv = [xfp]
       +
       +    for x in text_path.split('/'):
       +        if x == 'm': continue
       +        if x.endswith("'"):
       +            x = int(x[:-1]) | 0x80000000
       +        else:
       +            x = int(x)
       +        rv.append(x)
       +
       +    return rv
       +
       +def xfp_for_keystore(ks):
       +    # Need the fingerprint of the MASTER key for a keystore we're playing with.
       +    xfp = getattr(ks, 'ckcc_xfp', None)
       +
       +    if xfp is None:
       +        xfp = xfp_from_xpub(ks.get_master_public_key())
       +        setattr(ks, 'ckcc_xfp', xfp)
       +
       +    return xfp
       +
       +def packed_xfp_path_for_keystore(ks, int_path=[]):
       +    # Return XFP + common prefix path for keystore, as binary ready for PSBT
       +    derv = getattr(ks, 'derivation', 'm')
       +    return packed_xfp_path(xfp_for_keystore(ks), derv[2:] or 'm', int_path=int_path)
       +
       +# Serialization/deserialization tools
       +def ser_compact_size(l):
       +    r = b""
       +    if l < 253:
       +        r = struct.pack("B", l)
       +    elif l < 0x10000:
       +        r = struct.pack("<BH", 253, l)
       +    elif l < 0x100000000:
       +        r = struct.pack("<BI", 254, l)
       +    else:
       +        r = struct.pack("<BQ", 255, l)
       +    return r
       +
       +def deser_compact_size(f):
       +    try:
       +        nit = f.read(1)[0]
       +    except IndexError:
       +        return None     # end of file
       +    
       +    if nit == 253:
       +        nit = struct.unpack("<H", f.read(2))[0]
       +    elif nit == 254:
       +        nit = struct.unpack("<I", f.read(4))[0]
       +    elif nit == 255:
       +        nit = struct.unpack("<Q", f.read(8))[0]
       +    return nit
       +
       +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 build_psbt(tx: Transaction, wallet: Abstract_Wallet):
       +    # Render a PSBT file, for possible upload to Coldcard.
       +    # 
       +    # TODO this should be part of Wallet object, or maybe Transaction?
       +
       +    if getattr(tx, 'raw_psbt', False):
       +        _logger.info('PSBT cache hit')
       +        return tx.raw_psbt
       +
       +    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
       +        wallet.add_hw_info(tx)
       +
       +    # wallet.add_hw_info installs this attr
       +    assert tx.output_info is not None, 'need data about outputs'
       +
       +    # Build a map of all pubkeys needed as derivation from master XFP, in PSBT binary format
       +    # 1) binary version of the common subpath for all keys
       +    #       m/ => fingerprint LE32
       +    #       a/b/c => ints
       +    #
       +    # 2) all used keys in transaction:
       +    #    - for all inputs and outputs (when its change back)
       +    #    - for all keystores, if multisig
       +    #
       +    subkeys = {}
       +    for ks in wallet.get_keystores():
       +
       +        # XFP + fixed prefix for this keystore
       +        ks_prefix = packed_xfp_path_for_keystore(ks)
       +
       +        # all pubkeys needed for input signing
       +        for xpubkey, derivation in ks.get_tx_derivations(tx).items():
       +            pubkey = xpubkey_to_pubkey(xpubkey)
       +
       +            # assuming depth two, non-harded: change + index
       +            aa, bb = derivation
       +            assert 0 <= aa < 0x80000000 and 0 <= bb < 0x80000000
       +
       +            subkeys[bfh(pubkey)] = ks_prefix + pack('<II', aa, bb)
       +
       +        # all keys related to change outputs
       +        for o in tx.outputs():
       +            if o.address in tx.output_info:
       +                # this address "is_mine" but might not be change (if I send funds to myself)
       +                output_info = tx.output_info.get(o.address)
       +                if not output_info.is_change:
       +                    continue
       +                chg_path = output_info.address_index
       +                assert chg_path[0] == 1 and len(chg_path) == 2, f"unexpected change path: {chg_path}"
       +                pubkey = ks.derive_pubkey(True, chg_path[1])
       +                subkeys[bfh(pubkey)] = ks_prefix + pack('<II', *chg_path)
       +
       +    for txin in inputs:
       +        assert txin['type'] != 'coinbase', _("Coinbase not supported")
       +
       +        if txin['type'] in ['p2sh', 'p2wsh-p2sh', 'p2wsh']:
       +            assert type(wallet) is Multisig_Wallet
       +
       +    # 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)
       +
       +    if type(wallet) is Multisig_Wallet:
       +
       +        # always put the xpubs into the PSBT, useful at least for checking
       +        for xp, ks in zip(wallet.get_master_public_keys(), wallet.get_keystores()):
       +            ks_prefix = packed_xfp_path_for_keystore(ks)
       +
       +            write_kv(PSBT_GLOBAL_XPUB, ks_prefix, DecodeBase58Check(xp))
       +
       +    # end globals section
       +    out_fd.write(b'\x00')
       +
       +    # inputs section
       +    for txin in inputs:
       +        if Transaction.is_segwit_input(txin):
       +            utxo = txin['prev_tx'].outputs()[txin['prevout_n']]
       +            spendable = txin['prev_tx'].serialize_output(utxo)
       +            write_kv(PSBT_IN_WITNESS_UTXO, spendable)
       +        else:
       +            write_kv(PSBT_IN_NON_WITNESS_UTXO, str(txin['prev_tx']))
       +
       +        pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin)
       +
       +        pubkeys = [bfh(k) for k in pubkeys]
       +
       +        if type(wallet) is Multisig_Wallet:
       +            # always need a redeem script for multisig
       +            scr = Transaction.get_preimage_script(txin)
       +
       +            if Transaction.is_segwit_input(txin):
       +                # needed for both p2wsh-p2sh and p2wsh
       +                write_kv(PSBT_IN_WITNESS_SCRIPT, bfh(scr))
       +            else:
       +                write_kv(PSBT_IN_REDEEM_SCRIPT, bfh(scr))
       +
       +        sigs = txin.get('signatures')
       +
       +        for pk_pos, (pubkey, x_pubkey) in enumerate(zip(pubkeys, x_pubkeys)):
       +            if pubkey in subkeys:
       +                # faster? case ... calculated above
       +                write_kv(PSBT_IN_BIP32_DERIVATION, subkeys[pubkey], pubkey)
       +            else:
       +                # when an input is partly signed, tx.get_tx_derivations()
       +                # doesn't include that keystore's value and yet we need it
       +                # because we need to show a correct keypath... 
       +                assert x_pubkey[0:2] == 'ff', x_pubkey
       +
       +                for ks in wallet.get_keystores():
       +                    d = ks.get_pubkey_derivation(x_pubkey)
       +                    if d is not None:
       +                        ks_path = packed_xfp_path_for_keystore(ks, d)
       +                        write_kv(PSBT_IN_BIP32_DERIVATION, ks_path, pubkey)
       +                        break
       +                else:
       +                    raise AssertionError("no keystore for: %s" % x_pubkey)
       +
       +            if txin['type'] == 'p2wpkh-p2sh':
       +                assert len(pubkeys) == 1, 'can be only one redeem script per input'
       +                pa = hash_160(pubkey)
       +                assert len(pa) == 20
       +                write_kv(PSBT_IN_REDEEM_SCRIPT, b'\x00\x14'+pa)
       +
       +            # optional? insert (partial) signatures that we already have
       +            if sigs and sigs[pk_pos]:
       +                write_kv(PSBT_IN_PARTIAL_SIG, bfh(sigs[pk_pos]), pubkey)
       +
       +        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 (if I send funds to myself)
       +            output_info = tx.output_info.get(o.address)
       +            if output_info.is_change:
       +                pubkeys = [bfh(i) for i in wallet.get_public_keys(o.address)]
       +
       +                # Add redeem/witness script?
       +                if type(wallet) is Multisig_Wallet:
       +                    # always need a redeem script for multisig cases
       +                    scr = bfh(multisig_script([bh2u(i) for i in sorted(pubkeys)], wallet.m))
       +
       +                    if output_info.script_type == 'p2wsh-p2sh':
       +                        write_kv(PSBT_OUT_WITNESS_SCRIPT, scr)
       +                        write_kv(PSBT_OUT_REDEEM_SCRIPT, b'\x00\x20' + sha256(scr))
       +                    elif output_info.script_type == 'p2wsh':
       +                        write_kv(PSBT_OUT_WITNESS_SCRIPT, scr)
       +                    elif output_info.script_type == 'p2sh':
       +                        write_kv(PSBT_OUT_REDEEM_SCRIPT, scr)
       +                    else:
       +                        raise ValueError(output_info.script_type)
       +
       +                elif output_info.script_type == 'p2wpkh-p2sh':
       +                    # need a redeem script when P2SH is used to wrap p2wpkh
       +                    assert len(pubkeys) == 1
       +                    pa = hash_160(pubkeys[0])
       +                    write_kv(PSBT_OUT_REDEEM_SCRIPT, b'\x00\x14' + pa)
       +
       +                # Document change output's bip32 derivation(s)
       +                for pubkey in pubkeys:
       +                    sk = subkeys[pubkey]
       +                    write_kv(PSBT_OUT_BIP32_DERIVATION, sk, pubkey)
       +
       +        out_fd.write(b'\x00')
       +
       +    # capture for later use
       +    tx.raw_psbt = out_fd.getvalue()
       +
       +    return tx.raw_psbt
       +
       +
       +def recover_tx_from_psbt(first: BasicPSBT, wallet: Abstract_Wallet) -> Transaction:
       +    # Take a PSBT object and re-construct the Electrum transaction object.
       +    # - does not include signatures, see merge_sigs_from_psbt
       +    # - any PSBT in the group could be used for this purpose; all must share tx details
       +    
       +    tx = Transaction(first.txn.hex())
       +    tx.deserialize(force_full_parse=True)
       +
       +    # .. add back some data that's been preserved in the PSBT, but isn't part of
       +    # of the unsigned bitcoin txn
       +    tx.is_partial_originally = True
       +
       +    for idx, inp in enumerate(tx.inputs()):
       +        scr = first.inputs[idx].redeem_script or first.inputs[idx].witness_script
       +
       +        # XXX should use transaction.py parse_scriptSig() here!
       +        if scr:
       +            try:
       +                M, N, __, pubkeys, __ = parse_redeemScript_multisig(scr)
       +            except NotRecognizedRedeemScript:
       +                # limitation: we can only handle M-of-N multisig here
       +                raise ValueError("Cannot handle non M-of-N multisig input")
       +
       +            inp['pubkeys'] = pubkeys
       +            inp['x_pubkeys'] = pubkeys
       +            inp['num_sig'] = M
       +            inp['type'] = 'p2wsh' if first.inputs[idx].witness_script else 'p2sh'
       +
       +            # bugfix: transaction.py:parse_input() puts empty dict here, but need a list
       +            inp['signatures'] = [None] * N
       +
       +        if 'prev_tx' not in inp:
       +            # fetch info about inputs' previous txn
       +            wallet.add_hw_info(tx)
       +
       +        if 'value' not in inp:
       +            # we'll need to know the value of the outpts used as part
       +            # of the witness data, much later...
       +            inp['value'] = inp['prev_tx'].outputs()[inp['prevout_n']].value
       +
       +    return tx
       +
       +def merge_sigs_from_psbt(tx: Transaction, psbt: BasicPSBT):
       +    # Take new signatures from PSBT, and merge into in-memory transaction object.
       +    # - "we trust everyone here" ... no validation/checks
       +
       +    count = 0
       +    for inp_idx, inp in enumerate(psbt.inputs):
       +        if not inp.part_sigs:
       +            continue
       +
       +        scr = inp.redeem_script or inp.witness_script
       +
       +        # need to map from pubkey to signing position in redeem script
       +        M, N, _, pubkeys, _ = parse_redeemScript_multisig(scr)
       +        #assert (M, N) == (wallet.m, wallet.n)
       +
       +        for sig_pk in inp.part_sigs:
       +            pk_pos = pubkeys.index(sig_pk.hex())
       +            tx.add_signature_to_txin(inp_idx, pk_pos, inp.part_sigs[sig_pk].hex())
       +            count += 1
       +
       +        #print("#%d: sigs = %r" % (inp_idx, tx.inputs()[inp_idx]['signatures']))
       +    
       +    # reset serialization of TX
       +    tx.raw = tx.serialize()
       +    tx.raw_psbt = None
       +
       +    return count
       +
       +# EOF
       +
   DIR diff --git a/electrum/plugins/coldcard/coldcard.py b/electrum/plugins/coldcard/coldcard.py
       t@@ -8,18 +8,20 @@ import traceback
        
        from electrum.bip32 import BIP32Node, InvalidMasterKeyVersionBytes
        from electrum.i18n import _
       -from electrum.plugin import 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 electrum.plugin import Device, hook
       +from electrum.keystore import Hardware_KeyStore
       +from electrum.transaction import Transaction, multisig_script
       +from electrum.wallet import Standard_Wallet, Multisig_Wallet
        from electrum.util import bfh, bh2u, versiontuple, UserFacingException
        from electrum.base_wizard import ScriptTypeNotSupported
        from electrum.logging import get_logger
        
        from ..hw_wallet import HW_PluginBase
       -from ..hw_wallet.plugin import LibraryFoundButUnusable
       +from ..hw_wallet.plugin import LibraryFoundButUnusable, only_hook_if_libraries_available
        
       +from .basic_psbt import BasicPSBT
       +from .build_psbt import (build_psbt, xfp2str, unpacked_xfp_path,
       +                            merge_sigs_from_psbt, xfp_for_keystore)
        
        _logger = get_logger(__name__)
        
       t@@ -30,10 +32,6 @@ try:
            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, PSBT_OUT_REDEEM_SCRIPT)
        
            from ckcc.client import ColdcardDevice, COINKITE_VID, CKCC_PID, CKCC_SIMULATOR_PATH
        
       t@@ -60,26 +58,6 @@ except ImportError:
        
        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?
       t@@ -105,24 +83,27 @@ class CKCCClient:
                # should expect. It's also kinda slow.
        
            def __repr__(self):
       -        return '<CKCCClient: xfp=%08x label=%r>' % (self.dev.master_fingerprint,
       +        return '<CKCCClient: xfp=%s label=%r>' % (xfp2str(self.dev.master_fingerprint),
                                                                self.label())
        
       -    def verify_connection(self, expected_xfp, expected_xpub):
       +    def verify_connection(self, expected_xfp, expected_xpub=None):
                ex = (expected_xfp, expected_xpub)
        
                if self._expected_device == ex:
                    # all is as expected
                    return
        
       +        if expected_xpub is None:
       +            expected_xpub = self.dev.master_xpub
       +
                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
                    _logger.info(f"xpubs. reported by device: {self.dev.master_xpub}. "
                                 f"stored in file: {expected_xpub}")
       -            raise RuntimeError("Expecting 0x%08x but that's not what's connected?!" %
       -                               expected_xfp)
       +            raise RuntimeError("Expecting %s but that's not what's connected?!" %
       +                               xfp2str(expected_xfp))
        
                # check signature over session key
                # - mitm might have lied about xfp and xpub up to here
       t@@ -132,10 +113,13 @@ class CKCCClient:
        
                self._expected_device = ex
        
       +        if not getattr(self, 'ckcc_xpub', None):
       +            self.ckcc_xpub = expected_xpub
       +
                _logger.info("Successfully verified against MiTM")
        
            def is_pairable(self):
       -        # can't do anything w/ devices that aren't setup (but not normally reachable)
       +        # can't do anything w/ devices that aren't setup (this code not normally reachable)
                return bool(self.dev.master_xpub)
        
            def timeout(self, cutoff):
       t@@ -155,12 +139,12 @@ class CKCCClient:
                # 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
       +            lab = 'Coldcard Simulator ' + xfp2str(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
       +            lab = 'Coldcard ' + xfp2str(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.
       t@@ -210,9 +194,13 @@ class CKCCClient:
                    raise RuntimeError("Communication trouble with Coldcard")
        
            def show_address(self, path, addr_fmt):
       -        # prompt user w/ addres, also returns it immediately.
       +        # prompt user w/ address, also returns it immediately.
                return self.dev.send_recv(CCProtocolPacker.show_address(path, addr_fmt), timeout=None)
        
       +    def show_p2sh_address(self, *args, **kws):
       +        # prompt user w/ p2sh address, also returns it immediately.
       +        return self.dev.send_recv(CCProtocolPacker.show_p2sh_address(*args, **kws), timeout=None)
       +
            def get_version(self):
                # gives list of strings
                return self.dev.send_recv(CCProtocolPacker.version(), timeout=1000).split('\n')
       t@@ -262,22 +250,27 @@ class Coldcard_KeyStore(Hardware_KeyStore):
                self.force_watching_only = False
                self.ux_busy = False
        
       +        # for multisig I need to know what wallet this keystore is part of
       +        # will be set by link_wallet
       +        self.my_wallet = None
       +
                # 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
       +        # - it's a LE32 int, but hex BE32 is more natural way to view it
                # - device reports these value during encryption setup process
       +        # - full xpub value now optional
                lab = d['label']
                if hasattr(lab, 'xfp'):
                    # initial setup
                    self.ckcc_xfp = lab.xfp
       -            self.ckcc_xpub = lab.xpub
       +            self.ckcc_xpub = getattr(lab, 'xpub', None)
                else:
                    # wallet load: fatal if missing, we need them!
                    self.ckcc_xfp = d['ckcc_xfp']
       -            self.ckcc_xpub = d['ckcc_xpub']
       +            self.ckcc_xpub = d.get('ckcc_xpub', None)
        
            def dump(self):
                # our additions to the stored data about keystore -- only during creation?
       t@@ -294,6 +287,7 @@ class Coldcard_KeyStore(Hardware_KeyStore):
            def get_client(self):
                # called when user tries to do something like view address, sign somthing.
                # - not called during probing/setup
       +        # - will fail if indicated device can't produce the xpub (at derivation) expected
                rv = self.plugin.get_client(self)
                if rv:
                    rv.verify_connection(self.ckcc_xfp, self.ckcc_xpub)
       t@@ -377,162 +371,29 @@ class Coldcard_KeyStore(Hardware_KeyStore):
                # give empty bytes for error cases; it seems to clear the old signature box
                return b''
        
       -    def build_psbt(self, tx: Transaction, 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 tx.output_info is not None, '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")
       -
       -            if txin['type'] in ['p2sh', 'p2wsh-p2sh', 'p2wsh']:
       -                self.give_error('No support yet for inputs of type: ' + txin['type'])
       -
       -        # 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:
       -            if Transaction.is_segwit_input(txin):
       -                utxo = txin['prev_tx'].outputs()[txin['prevout_n']]
       -                spendable = txin['prev_tx'].serialize_output(utxo)
       -                write_kv(PSBT_IN_WITNESS_UTXO, spendable)
       -            else:
       -                write_kv(PSBT_IN_NON_WITNESS_UTXO, str(txin['prev_tx']))
       -
       -            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)
       -
       -                if txin['type'] == 'p2wpkh-p2sh':
       -                    assert len(pubkeys) == 1, 'can be only one redeem script per input'
       -                    pa = hash_160(k)
       -                    assert len(pa) == 20
       -                    write_kv(PSBT_IN_REDEEM_SCRIPT, b'\x00\x14'+pa)
       -
       -            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 = bfh(self.get_pubkey_from_xpub(xpubkey, index))
       -
       -                    write_kv(PSBT_OUT_BIP32_DERIVATION, deriv, pubkey)
       -
       -                    if output_info.script_type == 'p2wpkh-p2sh':
       -                        pa = hash_160(pubkey)
       -                        assert len(pa) == 20
       -                        write_kv(PSBT_OUT_REDEEM_SCRIPT, b'\x00\x14' + pa)
       -
       -            out_fd.write(b'\x00')
       -
       -        return out_fd.getvalue()
       -
       -
            @wrap_busy
       -    def sign_transaction(self, tx, password):
       +    def sign_transaction(self, tx: Transaction, password):
                # Build a PSBT in memory, upload it for signing.
                # - we can also work offline (without paired device present)
                if tx.is_complete():
                    return
        
       +        assert self.my_wallet, "Not clear which wallet associated with this Coldcard"
       +
                client = self.get_client()
        
                assert client.dev.master_fingerprint == self.ckcc_xfp
        
       -        raw_psbt = self.build_psbt(tx)
       +        # makes PSBT required
       +        raw_psbt = build_psbt(tx, self.my_wallet)
        
       -        #open('debug.psbt', 'wb').write(out_fd.getvalue())
       +        cc_finalize = not (type(self.my_wallet) is Multisig_Wallet)
        
                try:
                    try:
                        self.handler.show_message("Authorize Transaction...")
        
       -                client.sign_transaction_start(raw_psbt, True)
       +                client.sign_transaction_start(raw_psbt, cc_finalize)
        
                        while 1:
                            # How to kill some time, without locking UI?
       t@@ -545,7 +406,7 @@ class Coldcard_KeyStore(Hardware_KeyStore):
                        rlen, rsha = resp
                    
                        # download the resulting txn.
       -                new_raw = client.download_file(rlen, rsha)
       +                raw_resp = client.download_file(rlen, rsha)
        
                    finally:
                        self.handler.finished()
       t@@ -559,8 +420,18 @@ class Coldcard_KeyStore(Hardware_KeyStore):
                    self.give_error(e, True)
                    return
        
       -        # trust the coldcard to re-searilize final product right?
       -        tx.update(bh2u(new_raw))
       +        if cc_finalize:
       +            # We trust the coldcard to re-serialize final transaction ready to go
       +            tx.update(bh2u(raw_resp))
       +        else:
       +            # apply partial signatures back into txn
       +            psbt = BasicPSBT()
       +            psbt.parse(raw_resp, client.label())
       +
       +            merge_sigs_from_psbt(tx, psbt)
       +
       +            # caller's logic looks at tx now and if it's sufficiently signed,
       +            # will send it if that's the user's intent.
        
            @staticmethod
            def _encode_txin_type(txin_type):
       t@@ -593,11 +464,33 @@ class Coldcard_KeyStore(Hardware_KeyStore):
                    self.logger.exception('')
                    self.handler.show_error(exc)
        
       +    @wrap_busy
       +    def show_p2sh_address(self, M, script, xfp_paths, txin_type):
       +        client = self.get_client()
       +        addr_fmt = self._encode_txin_type(txin_type)
       +        try:
       +            try:
       +                self.handler.show_message(_("Showing address ..."))
       +                dev_addr = client.show_p2sh_address(M, xfp_paths, script, addr_fmt=addr_fmt)
       +                # we could double check address here
       +            finally:
       +                self.handler.finished()
       +        except CCProtoError as exc:
       +            self.logger.exception('Error showing address')
       +            self.handler.show_error('{}.\n{}\n\n{}'.format(
       +                _('Error showing address'),
       +                _('Make sure you have imported the correct wallet description '
       +                  'file on the device for this multisig wallet.'),
       +                str(exc)))
       +        except BaseException as exc:
       +            self.logger.exception('')
       +            self.handler.show_error(exc)
       +
        
        
        class ColdcardPlugin(HW_PluginBase):
            keystore_class = Coldcard_KeyStore
       -    minimum_library = (0, 7, 2)
       +    minimum_library = (0, 7, 7)
            client = None
        
            DEVICE_IDS = [
       t@@ -605,8 +498,7 @@ class ColdcardPlugin(HW_PluginBase):
                (COINKITE_VID, CKCC_SIMULATED_PID)
            ]
        
       -    #SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh')
       -    SUPPORTED_XTYPES = ('standard', 'p2wpkh', 'p2wpkh-p2sh')
       +    SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh')
        
            def __init__(self, parent, config, name):
                HW_PluginBase.__init__(self, parent, config, name)
       t@@ -682,31 +574,109 @@ class ColdcardPlugin(HW_PluginBase):
                return xpub
        
            def get_client(self, keystore, force_pair=True):
       -        # All client interaction should not be in the main GUI thread
       +        # Acquire a connection to the hardware device (via USB)
                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
        
       +    @staticmethod
       +    def export_ms_wallet(wallet, fp, name):
       +        # Build the text file Coldcard needs to understand the multisig wallet
       +        # it is participating in. All involved Coldcards can share same file.
       +
       +        print('# Exported from Electrum', file=fp)
       +        print(f'Name: {name:.20s}', file=fp)
       +        print(f'Policy: {wallet.m} of {wallet.n}', file=fp)
       +        print(f'Format: {wallet.txin_type.upper()}' , file=fp)
       +
       +        xpubs = []
       +        derivs = set()
       +        for xp, ks in zip(wallet.get_master_public_keys(), wallet.get_keystores()):
       +            xfp = xfp_for_keystore(ks)
       +            dd = getattr(ks, 'derivation', 'm')
       +
       +            xpubs.append( (xfp2str(xfp), xp, dd) )
       +            derivs.add(dd)
       +
       +        # Derivation doesn't matter too much to the Coldcard, since it
       +        # uses key path data from PSBT or USB request as needed. However,
       +        # if there is a clear value, provide it.
       +        if len(derivs) == 1:
       +            print("Derivation: " + derivs.pop(), file=fp)
       +
       +        print('', file=fp)
       +
       +        assert len(xpubs) == wallet.n
       +        for xfp, xp, dd in xpubs:
       +            if derivs:
       +                # show as a comment if unclear
       +                print(f'# derivation: {dd}', file=fp)
       +
       +            print(f'{xfp}: {xp}\n', file=fp)
       +
            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
        
       +        txin_type = wallet.get_txin_type(address)
       +
                # Standard_Wallet => not multisig, must be bip32
       -        if type(wallet) is not Standard_Wallet:
       +        if type(wallet) is Standard_Wallet:
       +            sequence = wallet.get_address_index(address)
       +            keystore.show_address(sequence, txin_type)
       +        elif type(wallet) is Multisig_Wallet:
       +            # More involved for P2SH/P2WSH addresses: need M, and all public keys, and their
       +            # derivation paths. Must construct script, and track fingerprints+paths for
       +            # all those keys
       +
       +            pubkeys = wallet.get_public_keys(address)
       +
       +            xfps = []
       +            for xp, ks in zip(wallet.get_master_public_keys(), wallet.get_keystores()):
       +                path = "%s/%d/%d" % (getattr(ks, 'derivation', 'm'),
       +                                        *wallet.get_address_index(address))
       +
       +                # need master XFP for each co-signers
       +                ks_xfp = xfp_for_keystore(ks)
       +                xfps.append(unpacked_xfp_path(ks_xfp, path))
       +
       +            # put into BIP45 (sorted) order
       +            pkx = list(sorted(zip(pubkeys, xfps)))
       +
       +            script = bfh(multisig_script([pk for pk,xfp in pkx], wallet.m))
       +
       +            keystore.show_p2sh_address(wallet.m, script, [xfp for pk,xfp in pkx], txin_type)
       +
       +        else:
                    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)
       +    @classmethod
       +    def link_wallet(cls, wallet):
       +        # PROBLEM: wallet.sign_transaction() does not pass in the wallet to the individual
       +        # keystores, and we need to know about our co-signers at that time.
       +        # FIXME the keystore needs a reference to the wallet object because
       +        #       it constructs a PSBT from an electrum tx object inside keystore.sign_transaction.
       +        #       instead keystore.sign_transaction's API should be changed such that its input
       +        #       *is* a PSBT and not an electrum tx object
       +        for ks in wallet.get_keystores():
       +            if type(ks) == Coldcard_KeyStore:
       +                if not ks.my_wallet:
       +                    ks.my_wallet = wallet
       +
       +    @hook
       +    def load_wallet(self, wallet, window):
       +        # make sure hook in superclass also runs:
       +        if hasattr(super(), 'load_wallet'):
       +            super().load_wallet(wallet, window)
       +        self.link_wallet(wallet)
        
        # EOF
   DIR diff --git a/electrum/plugins/coldcard/qt.py b/electrum/plugins/coldcard/qt.py
       t@@ -1,18 +1,27 @@
       -import time
       +import time, os
        from functools import partial
        
        from PyQt5.QtCore import Qt, pyqtSignal
        from PyQt5.QtWidgets import QPushButton, QLabel, QVBoxLayout, QWidget, QGridLayout
       +from PyQt5.QtWidgets import QFileDialog
        
        from electrum.i18n import _
        from electrum.plugin import hook
       -from electrum.wallet import Standard_Wallet
       -from electrum.gui.qt.util import WindowModalDialog, CloseButton, get_parent_main_window
       +from electrum.wallet import Standard_Wallet, Multisig_Wallet
       +from electrum.gui.qt.util import WindowModalDialog, CloseButton, get_parent_main_window, Buttons
       +from electrum.transaction import Transaction
        
       -from .coldcard import ColdcardPlugin
       +from .coldcard import ColdcardPlugin, xfp2str
        from ..hw_wallet.qt import QtHandlerBase, QtPluginBase
        from ..hw_wallet.plugin import only_hook_if_libraries_available
        
       +from binascii import a2b_hex
       +from base64 import b64encode, b64decode
       +
       +from .basic_psbt import BasicPSBT
       +from .build_psbt import build_psbt, merge_sigs_from_psbt, recover_tx_from_psbt
       +
       +CC_DEBUG = False
        
        class Plugin(ColdcardPlugin, QtPluginBase):
            icon_unpaired = "coldcard_unpaired.png"
       t@@ -24,22 +33,53 @@ class Plugin(ColdcardPlugin, QtPluginBase):
            @only_hook_if_libraries_available
            @hook
            def receive_menu(self, menu, addrs, wallet):
       -        if type(wallet) is not Standard_Wallet:
       +        # Context menu on each address in the Addresses Tab, right click...
       +        if len(addrs) != 1:
       +            return
       +        for keystore in wallet.get_keystores():
       +            if type(keystore) == self.keystore_class:
       +                def show_address(keystore=keystore):
       +                    keystore.thread.add(partial(self.show_address, wallet, addrs[0], keystore=keystore))
       +                device_name = "{} ({})".format(self.device, keystore.label)
       +                menu.addAction(_("Show on {}").format(device_name), show_address)
       +
       +    @only_hook_if_libraries_available
       +    @hook
       +    def wallet_info_buttons(self, main_window, dialog):
       +        # user is about to see the "Wallet Information" dialog
       +        # - add a button if multisig wallet, and a Coldcard is a cosigner.
       +        wallet = main_window.wallet
       +
       +        if type(wallet) is not Multisig_Wallet:
       +            return
       +
       +        if not any(type(ks) == self.keystore_class for ks in wallet.get_keystores()):
       +            # doesn't involve a Coldcard wallet, hide feature
                    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)
       +
       +        btn = QPushButton(_("Export for Coldcard"))
       +        btn.clicked.connect(lambda unused: self.export_multisig_setup(main_window, wallet))
       +
       +        return Buttons(btn, CloseButton(dialog))
       +
       +    def export_multisig_setup(self, main_window, wallet):
       +
       +        basename = wallet.basename().rsplit('.', 1)[0]        # trim .json
       +        name = f'{basename}-cc-export.txt'.replace(' ', '-')
       +        fileName = main_window.getSaveFileName(_("Select where to save the setup file"),
       +                                                        name, "*.txt")
       +        if fileName:
       +            with open(fileName, "wt") as f:
       +                ColdcardPlugin.export_ms_wallet(wallet, f, basename)
       +            main_window.show_message(_("Wallet setup file exported successfully"))
        
            @only_hook_if_libraries_available
            @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
       +        # if not a Coldcard wallet, hide feature
       +        if not any(type(ks) == self.keystore_class for ks in dia.wallet.get_keystores()):
                    return
        
                # - add a new button, near "export"
       t@@ -60,28 +100,108 @@ class Plugin(ColdcardPlugin, QtPluginBase):
                    # 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)
       +        build_psbt(tx, dia.wallet)
        
       -        name = (dia.wallet.basename() + time.strftime('-%y%m%d-%H%M.psbt')).replace(' ', '-')
       +        name = (dia.wallet.basename() + time.strftime('-%y%m%d-%H%M.psbt'))\
       +                    .replace(' ', '-').replace('.json', '')
                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)
       +                f.write(tx.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_()
       +        # - doesn't matter if device not connected, continue
       +        CKCCSettingsDialog(window, self, keystore).exec_()
        
       +    @hook
       +    def init_menubar_tools(self, main_window, tools_menu):
       +        # add some PSBT-related tools to the "Load Transaction" menu.
       +        rt = main_window.raw_transaction_menu
       +        wallet = main_window.wallet
       +        rt.addAction(_("From &PSBT File or Files"), lambda: self.psbt_combiner(main_window, wallet))
       +
       +    def psbt_combiner(self, window, wallet):
       +        title = _("Select the PSBT file to load or PSBT files to combine")
       +        directory = ''
       +        fnames, __ = QFileDialog.getOpenFileNames(window, title, directory, "PSBT Files (*.psbt)")
       +
       +        psbts = []
       +        for fn in fnames:
       +            try:
       +                with open(fn, "rb") as f:
       +                    raw = f.read()
       +
       +                    psbt = BasicPSBT()
       +                    psbt.parse(raw, fn)
       +
       +                    psbts.append(psbt)
       +            except (AssertionError, ValueError, IOError, os.error) as reason:
       +                window.show_critical(_("Electrum was unable to open your PSBT file") + "\n" + str(reason), title=_("Unable to read file"))
       +                return
       +
       +        warn = []
       +        if not psbts: return        # user picked nothing
       +
       +        # Consistency checks and warnings.
       +        try:
       +            first = psbts[0]
       +            for p in psbts:
       +                fn = os.path.split(p.filename)[1]
       +
       +                assert (p.txn == first.txn), \
       +                    "All must relate to the same unsigned transaction."
       +
       +                for idx, inp in enumerate(p.inputs):
       +                    if not inp.part_sigs:
       +                        warn.append(fn + ':\n  ' + _("No partial signatures found for input #%d") % idx)
       +
       +                    assert first.inputs[idx].redeem_script == inp.redeem_script, "Mismatched redeem scripts"
       +                    assert first.inputs[idx].witness_script == inp.witness_script, "Mismatched witness"
       +                    
       +        except AssertionError as exc:
       +            # Fatal errors stop here.
       +            window.show_critical(str(exc),
       +                    title=_("Unable to combine PSBT files, check: ")+p.filename)
       +            return
       +
       +        if warn:
       +            # Lots of potential warnings...
       +            window.show_warning('\n\n'.join(warn), title=_("PSBT warnings"))
       +
       +        # Construct an Electrum transaction object from data in first PSBT file.
       +        try:
       +            tx = recover_tx_from_psbt(first, wallet)
       +        except BaseException as exc:
       +            if CC_DEBUG:
       +                from PyQt5.QtCore import pyqtRemoveInputHook; pyqtRemoveInputHook()
       +                import pdb; pdb.post_mortem()
       +            window.show_critical(str(exc), title=_("Unable to understand PSBT file"))
       +            return
       +
       +        # Combine the signatures from all the PSBTS (may do nothing if unsigned PSBTs)
       +        for p in psbts:
       +            try:
       +                merge_sigs_from_psbt(tx, p)
       +            except BaseException as exc:
       +                if CC_DEBUG:
       +                    from PyQt5.QtCore import pyqtRemoveInputHook; pyqtRemoveInputHook()
       +                    import pdb; pdb.post_mortem()
       +                window.show_critical("Unable to merge signatures: " + str(exc), 
       +                    title=_("Unable to combine PSBT file: ") + p.filename)
       +                return
       +
       +        # Display result, might not be complete yet, but hopefully it's ready to transmit!
       +        if len(psbts) == 1:
       +            desc = _("From PSBT file: ") + fn
       +        else:
       +            desc = _("Combined from %d PSBT files") % len(psbts)
       +
       +        window.show_transaction(tx, desc)
        
        class Coldcard_Handler(QtHandlerBase):
            setup_signal = pyqtSignal()
       t@@ -112,21 +232,25 @@ class Coldcard_Handler(QtHandlerBase):
                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):
       +    def __init__(self, window, plugin, keystore):
                title = _("{} Settings").format(plugin.device)
                super(CKCCSettingsDialog, self).__init__(window, title)
                self.setMaximumWidth(540)
        
       +        # Note: Coldcard may **not** be connected at present time. Keep working!
       +
                devmgr = plugin.device_manager()
       -        config = devmgr.config
       -        handler = keystore.handler
       +        #config = devmgr.config
       +        #handler = keystore.handler
                self.thread = thread = keystore.thread
       +        self.keystore = keystore
        
                def connect_and_doit():
       +            # Attempt connection to device, or raise.
       +            device_id = plugin.choose_device(window, keystore)
       +            if not device_id:
       +                raise RuntimeError("Device not connected")
                    client = devmgr.client_by_id(device_id)
                    if not client:
                        raise RuntimeError("Device not connected")
       t@@ -148,13 +272,14 @@ class CKCCSettingsDialog(WindowModalDialog):
                y = 3
        
                rows = [
       +            ('xfp', _("Master Fingerprint")),
       +            ('serial', _("USB Serial")),
                    ('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):
       +            # XXX we know xfp already, even if not connected
                    widget = QLabel('<tt>000000000000')
                    widget.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard)
        
       t@@ -164,7 +289,7 @@ class CKCCSettingsDialog(WindowModalDialog):
                    y += 1
                body_layout.addLayout(grid)
        
       -        upg_btn = QPushButton('Upgrade')
       +        upg_btn = QPushButton(_('Upgrade'))
                #upg_btn.setDefault(False)
                def _start_upgrade():
                    thread.add(connect_and_doit, on_success=self.start_upgrade)
       t@@ -177,13 +302,22 @@ class CKCCSettingsDialog(WindowModalDialog):
                dialog_vbox = QVBoxLayout(self)
                dialog_vbox.addWidget(body)
        
       -        # Fetch values and show them
       -        thread.add(connect_and_doit, on_success=self.show_values)
       +        # Fetch firmware/versions values and show them.
       +        thread.add(connect_and_doit, on_success=self.show_values, on_error=self.show_placeholders)
       +
       +    def show_placeholders(self, unclear_arg):
       +        # device missing, so hide lots of detail.
       +        self.xfp.setText('<tt>%s' % xfp2str(self.keystore.ckcc_xfp))
       +        self.serial.setText('(not connected)')
       +        self.fw_version.setText('')
       +        self.fw_built.setText('')
       +        self.bl_version.setText('')
        
            def show_values(self, client):
       +
                dev = client.dev
        
       -        self.xfp.setText('<tt>0x%08x' % dev.master_fingerprint)
       +        self.xfp.setText('<tt>%s' % xfp2str(dev.master_fingerprint))
                self.serial.setText('<tt>%s' % dev.serial)
        
                # ask device for versions: allow extras for future
   DIR diff --git a/electrum/plugins/hw_wallet/qt.py b/electrum/plugins/hw_wallet/qt.py
       t@@ -265,4 +265,5 @@ class QtPluginBase(object):
                    else:
                        addr = uri.get('address')
                    keystore.thread.add(partial(plugin.show_address, wallet, addr, keystore))
       -        receive_address_e.addButton("eye1.png", show_address, _("Show on {}").format(plugin.device))
       +        dev_name = f"{plugin.device} ({keystore.label})"
       +        receive_address_e.addButton("eye1.png", show_address, _("Show on {}").format(dev_name))