URI: 
       tMerge pull request #5721 from SomberNight/201910_psbt - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 707b74d22b28d942c445754311736f158e505990
   DIR parent 6d12ebabbb686bcd028b659c6a6ce2cb4782bc14
  HTML Author: ThomasV <thomasv@electrum.org>
       Date:   Thu,  7 Nov 2019 17:10:20 +0100
       
       Merge pull request #5721 from SomberNight/201910_psbt
       
       integrate PSBT support natively. WIP
       Diffstat:
         M electrum/address_synchronizer.py    |      94 +++++++++++++++----------------
         M electrum/base_wizard.py             |       9 ++++++---
         M electrum/bip32.py                   |      71 ++++++++++++++++++++++++++++---
         M electrum/bitcoin.py                 |      24 ++++++++++++++++--------
         M electrum/coinchooser.py             |      81 +++++++++++++++++--------------
         M electrum/commands.py                |      97 ++++++++++++++++++-------------
         M electrum/ecc.py                     |      10 ++++++++++
         M electrum/gui/kivy/main_window.py    |      16 ++++++----------
         M electrum/gui/kivy/uix/dialogs/__in… |      11 ++++++++---
         M electrum/gui/kivy/uix/dialogs/tx_d… |      37 +++++++++++++++++++++++--------
         M electrum/gui/kivy/uix/screens.py    |      12 +++++-------
         M electrum/gui/qt/address_dialog.py   |      12 ++++++++----
         M electrum/gui/qt/main_window.py      |     104 ++++++++++++++++----------------
         M electrum/gui/qt/paytoedit.py        |      40 +++++++++++++++----------------
         M electrum/gui/qt/transaction_dialog… |     227 ++++++++++++++++++++++++-------
         M electrum/gui/qt/util.py             |       9 ++++++---
         M electrum/gui/qt/utxo_list.py        |      48 ++++++++++++++++---------------
         M electrum/gui/stdio.py               |       9 +++++----
         M electrum/gui/text.py                |       9 +++++----
         M electrum/json_db.py                 |      72 ++++++++++++++++++++++---------
         M electrum/keystore.py                |     372 ++++++++++++++++++-------------
         M electrum/lnchannel.py               |      48 ++++++++++++++-----------------
         M electrum/lnpeer.py                  |      28 +++++++++++++++-------------
         M electrum/lnsweep.py                 |     113 +++++++++++++------------------
         M electrum/lnutil.py                  |     105 +++++++++++++++----------------
         M electrum/lnwatcher.py               |       6 ++++--
         M electrum/lnworker.py                |       2 +-
         M electrum/network.py                 |       5 +++--
         M electrum/paymentrequest.py          |      23 ++++++++++++-----------
         M electrum/plugin.py                  |       8 ++++++--
         M electrum/plugins/audio_modem/qt.py  |       8 ++++++--
         D electrum/plugins/coldcard/basic_ps… |     313 -------------------------------
         D electrum/plugins/coldcard/build_ps… |     397 -------------------------------
         M electrum/plugins/coldcard/coldcard… |     169 ++++++++++++-------------------
         M electrum/plugins/coldcard/qt.py     |     145 ++++---------------------------
         M electrum/plugins/cosigner_pool/qt.… |      48 ++++++++++++++++----------------
         M electrum/plugins/digitalbitbox/dig… |     103 +++++++++++++------------------
         M electrum/plugins/digitalbitbox/qt.… |      12 ++++++------
         M electrum/plugins/greenaddress_inst… |       9 +++++----
         M electrum/plugins/hw_wallet/plugin.… |      47 ++++++++++++++++++++++---------
         M electrum/plugins/keepkey/keepkey.py |     220 ++++++++++++++-----------------
         M electrum/plugins/ledger/ledger.py   |     141 +++++++++++++------------------
         M electrum/plugins/safe_t/safe_t.py   |     220 ++++++++++++++-----------------
         M electrum/plugins/trezor/trezor.py   |     155 ++++++++++++++-----------------
         M electrum/plugins/trustedcoin/cmdli… |       2 +-
         A electrum/plugins/trustedcoin/legac… |     106 ++++++++++++++++++++++++++++++
         M electrum/plugins/trustedcoin/trust… |      37 ++++++++++++++++++-------------
         M electrum/scripts/bip70.py           |       3 ++-
         M electrum/segwit_addr.py             |       2 ++
         M electrum/synchronizer.py            |       6 +++---
         M electrum/tests/regtest/regtest.sh   |       6 +++---
         M electrum/tests/test_bitcoin.py      |      10 +++++++++-
         M electrum/tests/test_commands.py     |      21 +++++++++++++++++++++
         M electrum/tests/test_lnchannel.py    |       2 +-
         M electrum/tests/test_lnutil.py       |      11 ++++-------
         A electrum/tests/test_psbt.py         |     269 +++++++++++++++++++++++++++++++
         M electrum/tests/test_transaction.py  |     289 +++++++++++++++----------------
         M electrum/tests/test_wallet.py       |       2 +-
         M electrum/tests/test_wallet_vertica… |     463 +++++++++++++++++++++++--------
         M electrum/transaction.py             |    2130 ++++++++++++++++++++-----------
         M electrum/util.py                    |       6 ++++--
         M electrum/wallet.py                  |     537 ++++++++++++++++++-------------
       
       62 files changed, 4171 insertions(+), 3420 deletions(-)
       ---
   DIR diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py
       t@@ -29,9 +29,9 @@ from collections import defaultdict
        from typing import TYPE_CHECKING, Dict, Optional, Set, Tuple, NamedTuple, Sequence
        
        from . import bitcoin
       -from .bitcoin import COINBASE_MATURITY, TYPE_ADDRESS, TYPE_PUBKEY
       +from .bitcoin import COINBASE_MATURITY
        from .util import profiler, bfh, TxMinedInfo
       -from .transaction import Transaction, TxOutput
       +from .transaction import Transaction, TxOutput, TxInput, PartialTxInput, TxOutpoint, PartialTransaction
        from .synchronizer import Synchronizer
        from .verifier import SPV
        from .blockchain import hash_header
       t@@ -125,12 +125,12 @@ class AddressSynchronizer(Logger):
                """Return number of transactions where address is involved."""
                return len(self._history_local.get(addr, ()))
        
       -    def get_txin_address(self, txi) -> Optional[str]:
       -        addr = txi.get('address')
       -        if addr and addr != "(pubkey)":
       -            return addr
       -        prevout_hash = txi.get('prevout_hash')
       -        prevout_n = txi.get('prevout_n')
       +    def get_txin_address(self, txin: TxInput) -> Optional[str]:
       +        if isinstance(txin, PartialTxInput):
       +            if txin.address:
       +                return txin.address
       +        prevout_hash = txin.prevout.txid.hex()
       +        prevout_n = txin.prevout.out_idx
                for addr in self.db.get_txo_addresses(prevout_hash):
                    l = self.db.get_txo_addr(prevout_hash, addr)
                    for n, v, is_cb in l:
       t@@ -138,14 +138,8 @@ class AddressSynchronizer(Logger):
                            return addr
                return None
        
       -    def get_txout_address(self, txo: TxOutput):
       -        if txo.type == TYPE_ADDRESS:
       -            addr = txo.address
       -        elif txo.type == TYPE_PUBKEY:
       -            addr = bitcoin.public_key_to_p2pkh(bfh(txo.address))
       -        else:
       -            addr = None
       -        return addr
       +    def get_txout_address(self, txo: TxOutput) -> Optional[str]:
       +        return txo.address
        
            def load_unverified_transactions(self):
                # review transactions that are in the history
       t@@ -183,7 +177,7 @@ class AddressSynchronizer(Logger):
                if self.synchronizer:
                    self.synchronizer.add(address)
        
       -    def get_conflicting_transactions(self, tx_hash, tx, include_self=False):
       +    def get_conflicting_transactions(self, tx_hash, tx: Transaction, include_self=False):
                """Returns a set of transaction hashes from the wallet history that are
                directly conflicting with tx, i.e. they have common outpoints being
                spent with tx.
       t@@ -194,10 +188,10 @@ class AddressSynchronizer(Logger):
                conflicting_txns = set()
                with self.transaction_lock:
                    for txin in tx.inputs():
       -                if txin['type'] == 'coinbase':
       +                if txin.is_coinbase():
                            continue
       -                prevout_hash = txin['prevout_hash']
       -                prevout_n = txin['prevout_n']
       +                prevout_hash = txin.prevout.txid.hex()
       +                prevout_n = txin.prevout.out_idx
                        spending_tx_hash = self.db.get_spent_outpoint(prevout_hash, prevout_n)
                        if spending_tx_hash is None:
                            continue
       t@@ -213,7 +207,7 @@ class AddressSynchronizer(Logger):
                            conflicting_txns -= {tx_hash}
                    return conflicting_txns
        
       -    def add_transaction(self, tx_hash, tx, allow_unrelated=False) -> bool:
       +    def add_transaction(self, tx_hash, tx: Transaction, allow_unrelated=False) -> bool:
                """Returns whether the tx was successfully added to the wallet history."""
                assert tx_hash, tx_hash
                assert tx, tx
       t@@ -226,7 +220,7 @@ class AddressSynchronizer(Logger):
                    # BUT we track is_mine inputs in a txn, and during subsequent calls
                    # of add_transaction tx, we might learn of more-and-more inputs of
                    # being is_mine, as we roll the gap_limit forward
       -            is_coinbase = tx.inputs()[0]['type'] == 'coinbase'
       +            is_coinbase = tx.inputs()[0].is_coinbase()
                    tx_height = self.get_tx_height(tx_hash).height
                    if not allow_unrelated:
                        # note that during sync, if the transactions are not properly sorted,
       t@@ -277,11 +271,11 @@ class AddressSynchronizer(Logger):
                                        self._get_addr_balance_cache.pop(addr, None)  # invalidate cache
                                    return
                    for txi in tx.inputs():
       -                if txi['type'] == 'coinbase':
       +                if txi.is_coinbase():
                            continue
       -                prevout_hash = txi['prevout_hash']
       -                prevout_n = txi['prevout_n']
       -                ser = prevout_hash + ':%d' % prevout_n
       +                prevout_hash = txi.prevout.txid.hex()
       +                prevout_n = txi.prevout.out_idx
       +                ser = txi.prevout.to_str()
                        self.db.set_spent_outpoint(prevout_hash, prevout_n, tx_hash)
                        add_value_from_prev_output()
                    # add outputs
       t@@ -310,10 +304,10 @@ class AddressSynchronizer(Logger):
                    if tx is not None:
                        # if we have the tx, this branch is faster
                        for txin in tx.inputs():
       -                    if txin['type'] == 'coinbase':
       +                    if txin.is_coinbase():
                                continue
       -                    prevout_hash = txin['prevout_hash']
       -                    prevout_n = txin['prevout_n']
       +                    prevout_hash = txin.prevout.txid.hex()
       +                    prevout_n = txin.prevout.out_idx
                            self.db.remove_spent_outpoint(prevout_hash, prevout_n)
                    else:
                        # expensive but always works
       t@@ -572,7 +566,7 @@ class AddressSynchronizer(Logger):
                    return cached_local_height
                return self.network.get_local_height() if self.network else self.db.get('stored_height', 0)
        
       -    def add_future_tx(self, tx, num_blocks):
       +    def add_future_tx(self, tx: Transaction, num_blocks):
                with self.lock:
                    self.add_transaction(tx.txid(), tx)
                    self.future_tx[tx.txid()] = num_blocks
       t@@ -649,14 +643,16 @@ class AddressSynchronizer(Logger):
                    if self.is_mine(addr):
                        is_mine = True
                        is_relevant = True
       -                d = self.db.get_txo_addr(txin['prevout_hash'], addr)
       +                d = self.db.get_txo_addr(txin.prevout.txid.hex(), addr)
                        for n, v, cb in d:
       -                    if n == txin['prevout_n']:
       +                    if n == txin.prevout.out_idx:
                                value = v
                                break
                        else:
                            value = None
                        if value is None:
       +                    value = txin.value_sats()
       +                if value is None:
                            is_pruned = True
                        else:
                            v_in += value
       t@@ -736,23 +732,19 @@ class AddressSynchronizer(Logger):
                            sent[txi] = height
                return received, sent
        
       -    def get_addr_utxo(self, address):
       +    def get_addr_utxo(self, address: str) -> Dict[TxOutpoint, PartialTxInput]:
                coins, spent = self.get_addr_io(address)
                for txi in spent:
                    coins.pop(txi)
                out = {}
       -        for txo, v in coins.items():
       +        for prevout_str, v in coins.items():
                    tx_height, value, is_cb = v
       -            prevout_hash, prevout_n = txo.split(':')
       -            x = {
       -                'address':address,
       -                'value':value,
       -                'prevout_n':int(prevout_n),
       -                'prevout_hash':prevout_hash,
       -                'height':tx_height,
       -                'coinbase':is_cb
       -            }
       -            out[txo] = x
       +            prevout = TxOutpoint.from_str(prevout_str)
       +            utxo = PartialTxInput(prevout=prevout)
       +            utxo._trusted_address = address
       +            utxo._trusted_value_sats = value
       +            utxo.block_height = tx_height
       +            out[prevout] = utxo
                return out
        
            # return the total amount ever received by an address
       t@@ -799,7 +791,8 @@ class AddressSynchronizer(Logger):
        
            @with_local_height_cached
            def get_utxos(self, domain=None, *, excluded_addresses=None,
       -                  mature_only: bool = False, confirmed_only: bool = False, nonlocal_only: bool = False):
       +                  mature_only: bool = False, confirmed_only: bool = False,
       +                  nonlocal_only: bool = False) -> Sequence[PartialTxInput]:
                coins = []
                if domain is None:
                    domain = self.get_addresses()
       t@@ -809,14 +802,15 @@ class AddressSynchronizer(Logger):
                mempool_height = self.get_local_height() + 1  # height of next block
                for addr in domain:
                    utxos = self.get_addr_utxo(addr)
       -            for x in utxos.values():
       -                if confirmed_only and x['height'] <= 0:
       +            for utxo in utxos.values():
       +                if confirmed_only and utxo.block_height <= 0:
                            continue
       -                if nonlocal_only and x['height'] == TX_HEIGHT_LOCAL:
       +                if nonlocal_only and utxo.block_height == TX_HEIGHT_LOCAL:
                            continue
       -                if mature_only and x['coinbase'] and x['height'] + COINBASE_MATURITY > mempool_height:
       +                if (mature_only and utxo.prevout.is_coinbase()
       +                        and utxo.block_height + COINBASE_MATURITY > mempool_height):
                            continue
       -                coins.append(x)
       +                coins.append(utxo)
                        continue
                return coins
        
   DIR diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py
       t@@ -33,7 +33,7 @@ from typing import List, TYPE_CHECKING, Tuple, NamedTuple, Any, Dict, Optional
        from . import bitcoin
        from . import keystore
        from . import mnemonic
       -from .bip32 import is_bip32_derivation, xpub_type, normalize_bip32_derivation
       +from .bip32 import is_bip32_derivation, xpub_type, normalize_bip32_derivation, BIP32Node
        from .keystore import bip44_derivation, purpose48_derivation
        from .wallet import (Imported_Wallet, Standard_Wallet, Multisig_Wallet,
                             wallet_types, Wallet, Abstract_Wallet)
       t@@ -230,7 +230,7 @@ class BaseWizard(Logger):
                        assert bitcoin.is_private_key(pk)
                        txin_type, pubkey = k.import_privkey(pk, None)
                        addr = bitcoin.pubkey_to_address(txin_type, pubkey)
       -                self.data['addresses'][addr] = {'type':txin_type, 'pubkey':pubkey, 'redeem_script':None}
       +                self.data['addresses'][addr] = {'type':txin_type, 'pubkey':pubkey}
                    self.keystores.append(k)
                else:
                    return self.terminate()
       t@@ -394,7 +394,7 @@ class BaseWizard(Logger):
                    # For segwit, a custom path is used, as there is no standard at all.
                    default_choice_idx = 2
                    choices = [
       -                ('standard',   'legacy multisig (p2sh)',            "m/45'/0"),
       +                ('standard',   'legacy multisig (p2sh)',            normalize_bip32_derivation("m/45'/0")),
                        ('p2wsh-p2sh', 'p2sh-segwit multisig (p2wsh-p2sh)', purpose48_derivation(0, xtype='p2wsh-p2sh')),
                        ('p2wsh',      'native segwit multisig (p2wsh)',    purpose48_derivation(0, xtype='p2wsh')),
                    ]
       t@@ -420,16 +420,19 @@ class BaseWizard(Logger):
                from .keystore import hardware_keystore
                try:
                    xpub = self.plugin.get_xpub(device_info.device.id_, derivation, xtype, self)
       +            root_xpub = self.plugin.get_xpub(device_info.device.id_, 'm', 'standard', self)
                except ScriptTypeNotSupported:
                    raise  # this is handled in derivation_dialog
                except BaseException as e:
                    self.logger.exception('')
                    self.show_error(e)
                    return
       +        xfp = BIP32Node.from_xkey(root_xpub).calc_fingerprint_of_this_node().hex().lower()
                d = {
                    'type': 'hardware',
                    'hw_type': name,
                    'derivation': derivation,
       +            'root_fingerprint': xfp,
                    'xpub': xpub,
                    'label': device_info.label,
                }
   DIR diff --git a/electrum/bip32.py b/electrum/bip32.py
       t@@ -3,7 +3,7 @@
        # file LICENCE or http://www.opensource.org/licenses/mit-license.php
        
        import hashlib
       -from typing import List, Tuple, NamedTuple, Union, Iterable
       +from typing import List, Tuple, NamedTuple, Union, Iterable, Sequence, Optional
        
        from .util import bfh, bh2u, BitcoinException
        from . import constants
       t@@ -116,7 +116,7 @@ class BIP32Node(NamedTuple):
            eckey: Union[ecc.ECPubkey, ecc.ECPrivkey]
            chaincode: bytes
            depth: int = 0
       -    fingerprint: bytes = b'\x00'*4
       +    fingerprint: bytes = b'\x00'*4  # as in serialized format, this is the *parent's* fingerprint
            child_number: bytes = b'\x00'*4
        
            @classmethod
       t@@ -161,7 +161,18 @@ class BIP32Node(NamedTuple):
                                 eckey=ecc.ECPrivkey(master_k),
                                 chaincode=master_c)
        
       +    @classmethod
       +    def from_bytes(cls, b: bytes) -> 'BIP32Node':
       +        if len(b) != 78:
       +            raise Exception(f"unexpected xkey raw bytes len {len(b)} != 78")
       +        xkey = EncodeBase58Check(b)
       +        return cls.from_xkey(xkey)
       +
            def to_xprv(self, *, net=None) -> str:
       +        payload = self.to_xprv_bytes(net=net)
       +        return EncodeBase58Check(payload)
       +
       +    def to_xprv_bytes(self, *, net=None) -> bytes:
                if not self.is_private():
                    raise Exception("cannot serialize as xprv; private key missing")
                payload = (xprv_header(self.xtype, net=net) +
       t@@ -172,9 +183,13 @@ class BIP32Node(NamedTuple):
                           bytes([0]) +
                           self.eckey.get_secret_bytes())
                assert len(payload) == 78, f"unexpected xprv payload len {len(payload)}"
       -        return EncodeBase58Check(payload)
       +        return payload
        
            def to_xpub(self, *, net=None) -> str:
       +        payload = self.to_xpub_bytes(net=net)
       +        return EncodeBase58Check(payload)
       +
       +    def to_xpub_bytes(self, *, net=None) -> bytes:
                payload = (xpub_header(self.xtype, net=net) +
                           bytes([self.depth]) +
                           self.fingerprint +
       t@@ -182,7 +197,7 @@ class BIP32Node(NamedTuple):
                           self.chaincode +
                           self.eckey.get_public_key_bytes(compressed=True))
                assert len(payload) == 78, f"unexpected xpub payload len {len(payload)}"
       -        return EncodeBase58Check(payload)
       +        return payload
        
            def to_xkey(self, *, net=None) -> str:
                if self.is_private():
       t@@ -190,6 +205,12 @@ class BIP32Node(NamedTuple):
                else:
                    return self.to_xpub(net=net)
        
       +    def to_bytes(self, *, net=None) -> bytes:
       +        if self.is_private():
       +            return self.to_xprv_bytes(net=net)
       +        else:
       +            return self.to_xpub_bytes(net=net)
       +
            def convert_to_public(self) -> 'BIP32Node':
                if not self.is_private():
                    return self
       t@@ -248,6 +269,12 @@ class BIP32Node(NamedTuple):
                                 fingerprint=fingerprint,
                                 child_number=child_number)
        
       +    def calc_fingerprint_of_this_node(self) -> bytes:
       +        """Returns the fingerprint of this node.
       +        Note that self.fingerprint is of the *parent*.
       +        """
       +        return hash_160(self.eckey.get_public_key_bytes(compressed=True))[0:4]
       +
        
        def xpub_type(x):
            return BIP32Node.from_xkey(x).xtype
       t@@ -308,7 +335,7 @@ def convert_bip32_path_to_list_of_uint32(n: str) -> List[int]:
            return path
        
        
       -def convert_bip32_intpath_to_strpath(path: List[int]) -> str:
       +def convert_bip32_intpath_to_strpath(path: Sequence[int]) -> str:
            s = "m/"
            for child_index in path:
                if not isinstance(child_index, int):
       t@@ -336,8 +363,40 @@ def is_bip32_derivation(s: str) -> bool:
                return True
        
        
       -def normalize_bip32_derivation(s: str) -> str:
       +def normalize_bip32_derivation(s: Optional[str]) -> Optional[str]:
       +    if s is None:
       +        return None
            if not is_bip32_derivation(s):
                raise ValueError(f"invalid bip32 derivation: {s}")
            ints = convert_bip32_path_to_list_of_uint32(s)
            return convert_bip32_intpath_to_strpath(ints)
       +
       +
       +def is_all_public_derivation(path: Union[str, Iterable[int]]) -> bool:
       +    """Returns whether all levels in path use non-hardened derivation."""
       +    if isinstance(path, str):
       +        path = convert_bip32_path_to_list_of_uint32(path)
       +    for child_index in path:
       +        if child_index < 0:
       +            raise ValueError('the bip32 index needs to be non-negative')
       +        if child_index & BIP32_PRIME:
       +            return False
       +    return True
       +
       +
       +def root_fp_and_der_prefix_from_xkey(xkey: str) -> Tuple[Optional[str], Optional[str]]:
       +    """Returns the root bip32 fingerprint and the derivation path from the
       +    root to the given xkey, if they can be determined. Otherwise (None, None).
       +    """
       +    node = BIP32Node.from_xkey(xkey)
       +    derivation_prefix = None
       +    root_fingerprint = None
       +    assert node.depth >= 0, node.depth
       +    if node.depth == 0:
       +        derivation_prefix = 'm'
       +        root_fingerprint = node.calc_fingerprint_of_this_node().hex().lower()
       +    elif node.depth == 1:
       +        child_number_int = int.from_bytes(node.child_number, 'big')
       +        derivation_prefix = convert_bip32_intpath_to_strpath([child_number_int])
       +        root_fingerprint = node.fingerprint.hex()
       +    return root_fingerprint, derivation_prefix
   DIR diff --git a/electrum/bitcoin.py b/electrum/bitcoin.py
       t@@ -45,6 +45,7 @@ COIN = 100000000
        TOTAL_COIN_SUPPLY_LIMIT_IN_BTC = 21000000
        
        # supported types of transaction outputs
       +# TODO kill these with fire
        TYPE_ADDRESS = 0
        TYPE_PUBKEY  = 1
        TYPE_SCRIPT  = 2
       t@@ -237,6 +238,9 @@ def script_num_to_hex(i: int) -> str:
        
        def var_int(i: int) -> str:
            # https://en.bitcoin.it/wiki/Protocol_specification#Variable_length_integer
       +    # https://github.com/bitcoin/bitcoin/blob/efe1ee0d8d7f82150789f1f6840f139289628a2b/src/serialize.h#L247
       +    # "CompactSize"
       +    assert i >= 0, i
            if i<0xfd:
                return int_to_hex(i)
            elif i<=0xffff:
       t@@ -372,24 +376,28 @@ def pubkey_to_address(txin_type: str, pubkey: str, *, net=None) -> str:
            else:
                raise NotImplementedError(txin_type)
        
       -def redeem_script_to_address(txin_type: str, redeem_script: str, *, net=None) -> str:
       +
       +# TODO this method is confusingly named
       +def redeem_script_to_address(txin_type: str, scriptcode: str, *, net=None) -> str:
            if net is None: net = constants.net
            if txin_type == 'p2sh':
       -        return hash160_to_p2sh(hash_160(bfh(redeem_script)), net=net)
       +        # given scriptcode is a redeem_script
       +        return hash160_to_p2sh(hash_160(bfh(scriptcode)), net=net)
            elif txin_type == 'p2wsh':
       -        return script_to_p2wsh(redeem_script, net=net)
       +        # given scriptcode is a witness_script
       +        return script_to_p2wsh(scriptcode, net=net)
            elif txin_type == 'p2wsh-p2sh':
       -        scriptSig = p2wsh_nested_script(redeem_script)
       -        return hash160_to_p2sh(hash_160(bfh(scriptSig)), net=net)
       +        # given scriptcode is a witness_script
       +        redeem_script = p2wsh_nested_script(scriptcode)
       +        return hash160_to_p2sh(hash_160(bfh(redeem_script)), net=net)
            else:
                raise NotImplementedError(txin_type)
        
        
        def script_to_address(script: str, *, net=None) -> str:
            from .transaction import get_address_from_output_script
       -    t, addr = get_address_from_output_script(bfh(script), net=net)
       -    assert t == TYPE_ADDRESS
       -    return addr
       +    return get_address_from_output_script(bfh(script), net=net)
       +
        
        def address_to_script(addr: str, *, net=None) -> str:
            if net is None: net = constants.net
   DIR diff --git a/electrum/coinchooser.py b/electrum/coinchooser.py
       t@@ -24,11 +24,11 @@
        # SOFTWARE.
        from collections import defaultdict
        from math import floor, log10
       -from typing import NamedTuple, List, Callable
       +from typing import NamedTuple, List, Callable, Sequence, Union, Dict, Tuple
        from decimal import Decimal
        
       -from .bitcoin import sha256, COIN, TYPE_ADDRESS, is_address
       -from .transaction import Transaction, TxOutput
       +from .bitcoin import sha256, COIN, is_address
       +from .transaction import Transaction, TxOutput, PartialTransaction, PartialTxInput, PartialTxOutput
        from .util import NotEnoughFunds
        from .logging import Logger
        
       t@@ -73,21 +73,21 @@ class PRNG:
        
        class Bucket(NamedTuple):
            desc: str
       -    weight: int         # as in BIP-141
       -    value: int          # in satoshis
       -    effective_value: int   # estimate of value left after subtracting fees. in satoshis
       -    coins: List[dict]   # UTXOs
       -    min_height: int     # min block height where a coin was confirmed
       -    witness: bool       # whether any coin uses segwit
       +    weight: int                   # as in BIP-141
       +    value: int                    # in satoshis
       +    effective_value: int          # estimate of value left after subtracting fees. in satoshis
       +    coins: List[PartialTxInput]   # UTXOs
       +    min_height: int               # min block height where a coin was confirmed
       +    witness: bool                 # whether any coin uses segwit
        
        
        class ScoredCandidate(NamedTuple):
            penalty: float
       -    tx: Transaction
       +    tx: PartialTransaction
            buckets: List[Bucket]
        
        
       -def strip_unneeded(bkts, sufficient_funds):
       +def strip_unneeded(bkts: List[Bucket], sufficient_funds) -> List[Bucket]:
            '''Remove buckets that are unnecessary in achieving the spend amount'''
            if sufficient_funds([], bucket_value_sum=0):
                # none of the buckets are needed
       t@@ -108,26 +108,27 @@ class CoinChooserBase(Logger):
            def __init__(self):
                Logger.__init__(self)
        
       -    def keys(self, coins):
       +    def keys(self, coins: Sequence[PartialTxInput]) -> Sequence[str]:
                raise NotImplementedError
        
       -    def bucketize_coins(self, coins, *, fee_estimator_vb):
       +    def bucketize_coins(self, coins: Sequence[PartialTxInput], *, fee_estimator_vb):
                keys = self.keys(coins)
       -        buckets = defaultdict(list)
       +        buckets = defaultdict(list)  # type: Dict[str, List[PartialTxInput]]
                for key, coin in zip(keys, coins):
                    buckets[key].append(coin)
                # fee_estimator returns fee to be paid, for given vbytes.
                # guess whether it is just returning a constant as follows.
                constant_fee = fee_estimator_vb(2000) == fee_estimator_vb(200)
        
       -        def make_Bucket(desc, coins):
       +        def make_Bucket(desc: str, coins: List[PartialTxInput]):
                    witness = any(Transaction.is_segwit_input(coin, guess_for_address=True) for coin in coins)
                    # note that we're guessing whether the tx uses segwit based
                    # on this single bucket
                    weight = sum(Transaction.estimated_input_weight(coin, witness)
                                 for coin in coins)
       -            value = sum(coin['value'] for coin in coins)
       -            min_height = min(coin['height'] for coin in coins)
       +            value = sum(coin.value_sats() for coin in coins)
       +            min_height = min(coin.block_height for coin in coins)
       +            assert min_height is not None
                    # the fee estimator is typically either a constant or a linear function,
                    # so the "function:" effective_value(bucket) will be homomorphic for addition
                    # i.e. effective_value(b1) + effective_value(b2) = effective_value(b1 + b2)
       t@@ -148,10 +149,12 @@ class CoinChooserBase(Logger):
        
                return list(map(make_Bucket, buckets.keys(), buckets.values()))
        
       -    def penalty_func(self, base_tx, *, tx_from_buckets) -> Callable[[List[Bucket]], ScoredCandidate]:
       +    def penalty_func(self, base_tx, *,
       +                     tx_from_buckets: Callable[[List[Bucket]], Tuple[PartialTransaction, List[PartialTxOutput]]]) \
       +            -> Callable[[List[Bucket]], ScoredCandidate]:
                raise NotImplementedError
        
       -    def _change_amounts(self, tx, count, fee_estimator_numchange) -> List[int]:
       +    def _change_amounts(self, tx: PartialTransaction, count: int, fee_estimator_numchange) -> List[int]:
                # Break change up if bigger than max_change
                output_amounts = [o.value for o in tx.outputs()]
                # Don't split change of less than 0.02 BTC
       t@@ -205,7 +208,8 @@ class CoinChooserBase(Logger):
        
                return amounts
        
       -    def _change_outputs(self, tx, change_addrs, fee_estimator_numchange, dust_threshold):
       +    def _change_outputs(self, tx: PartialTransaction, change_addrs, fee_estimator_numchange,
       +                        dust_threshold) -> List[PartialTxOutput]:
                amounts = self._change_amounts(tx, len(change_addrs), fee_estimator_numchange)
                assert min(amounts) >= 0
                assert len(change_addrs) >= len(amounts)
       t@@ -213,21 +217,23 @@ class CoinChooserBase(Logger):
                # If change is above dust threshold after accounting for the
                # size of the change output, add it to the transaction.
                amounts = [amount for amount in amounts if amount >= dust_threshold]
       -        change = [TxOutput(TYPE_ADDRESS, addr, amount)
       +        change = [PartialTxOutput.from_address_and_value(addr, amount)
                          for addr, amount in zip(change_addrs, amounts)]
                return change
        
       -    def _construct_tx_from_selected_buckets(self, *, buckets, base_tx, change_addrs,
       -                                            fee_estimator_w, dust_threshold, base_weight):
       +    def _construct_tx_from_selected_buckets(self, *, buckets: Sequence[Bucket],
       +                                            base_tx: PartialTransaction, change_addrs,
       +                                            fee_estimator_w, dust_threshold,
       +                                            base_weight) -> Tuple[PartialTransaction, List[PartialTxOutput]]:
                # make a copy of base_tx so it won't get mutated
       -        tx = Transaction.from_io(base_tx.inputs()[:], base_tx.outputs()[:])
       +        tx = PartialTransaction.from_io(base_tx.inputs()[:], base_tx.outputs()[:])
        
                tx.add_inputs([coin for b in buckets for coin in b.coins])
                tx_weight = self._get_tx_weight(buckets, base_weight=base_weight)
        
                # change is sent back to sending address unless specified
                if not change_addrs:
       -            change_addrs = [tx.inputs()[0]['address']]
       +            change_addrs = [tx.inputs()[0].address]
                    # note: this is not necessarily the final "first input address"
                    # because the inputs had not been sorted at this point
                    assert is_address(change_addrs[0])
       t@@ -240,7 +246,7 @@ class CoinChooserBase(Logger):
        
                return tx, change
        
       -    def _get_tx_weight(self, buckets, *, base_weight) -> int:
       +    def _get_tx_weight(self, buckets: Sequence[Bucket], *, base_weight: int) -> int:
                """Given a collection of buckets, return the total weight of the
                resulting transaction.
                base_weight is the weight of the tx that includes the fixed (non-change)
       t@@ -260,8 +266,9 @@ class CoinChooserBase(Logger):
        
                return total_weight
        
       -    def make_tx(self, coins, inputs, outputs, change_addrs, fee_estimator_vb,
       -                dust_threshold):
       +    def make_tx(self, *, coins: Sequence[PartialTxInput], inputs: List[PartialTxInput],
       +                outputs: List[PartialTxOutput], change_addrs: Sequence[str],
       +                fee_estimator_vb: Callable, dust_threshold: int) -> PartialTransaction:
                """Select unspent coins to spend to pay outputs.  If the change is
                greater than dust_threshold (after adding the change output to
                the transaction) it is kept, otherwise none is sent and it is
       t@@ -276,11 +283,11 @@ class CoinChooserBase(Logger):
                assert outputs, 'tx outputs cannot be empty'
        
                # Deterministic randomness from coins
       -        utxos = [c['prevout_hash'] + str(c['prevout_n']) for c in coins]
       -        self.p = PRNG(''.join(sorted(utxos)))
       +        utxos = [c.prevout.serialize_to_network() for c in coins]
       +        self.p = PRNG(b''.join(sorted(utxos)))
        
                # Copy the outputs so when adding change we don't modify "outputs"
       -        base_tx = Transaction.from_io(inputs[:], outputs[:])
       +        base_tx = PartialTransaction.from_io(inputs[:], outputs[:])
                input_value = base_tx.input_value()
        
                # Weight of the transaction with no inputs and no change
       t@@ -331,14 +338,15 @@ class CoinChooserBase(Logger):
        
                return tx
        
       -    def choose_buckets(self, buckets, sufficient_funds,
       +    def choose_buckets(self, buckets: List[Bucket],
       +                       sufficient_funds: Callable,
                               penalty_func: Callable[[List[Bucket]], ScoredCandidate]) -> ScoredCandidate:
                raise NotImplemented('To be subclassed')
        
        
        class CoinChooserRandom(CoinChooserBase):
        
       -    def bucket_candidates_any(self, buckets, sufficient_funds):
       +    def bucket_candidates_any(self, buckets: List[Bucket], sufficient_funds) -> List[List[Bucket]]:
                '''Returns a list of bucket sets.'''
                if not buckets:
                    raise NotEnoughFunds()
       t@@ -373,7 +381,8 @@ class CoinChooserRandom(CoinChooserBase):
                candidates = [[buckets[n] for n in c] for c in candidates]
                return [strip_unneeded(c, sufficient_funds) for c in candidates]
        
       -    def bucket_candidates_prefer_confirmed(self, buckets, sufficient_funds):
       +    def bucket_candidates_prefer_confirmed(self, buckets: List[Bucket],
       +                                           sufficient_funds) -> List[List[Bucket]]:
                """Returns a list of bucket sets preferring confirmed coins.
        
                Any bucket can be:
       t@@ -433,13 +442,13 @@ class CoinChooserPrivacy(CoinChooserRandom):
            """
        
            def keys(self, coins):
       -        return [coin['address'] for coin in coins]
       +        return [coin.scriptpubkey.hex() for coin in coins]
        
            def penalty_func(self, base_tx, *, tx_from_buckets):
                min_change = min(o.value for o in base_tx.outputs()) * 0.75
                max_change = max(o.value for o in base_tx.outputs()) * 1.33
        
       -        def penalty(buckets) -> ScoredCandidate:
       +        def penalty(buckets: List[Bucket]) -> ScoredCandidate:
                    # Penalize using many buckets (~inputs)
                    badness = len(buckets) - 1
                    tx, change_outputs = tx_from_buckets(buckets)
   DIR diff --git a/electrum/commands.py b/electrum/commands.py
       t@@ -35,16 +35,17 @@ import asyncio
        import inspect
        from functools import wraps, partial
        from decimal import Decimal
       -from typing import Optional, TYPE_CHECKING, Dict
       +from typing import Optional, TYPE_CHECKING, Dict, List
        
        from .import util, ecc
        from .util import bfh, bh2u, format_satoshis, json_decode, json_encode, is_hash256_str, is_hex_str, to_bytes, timestamp_to_datetime
        from .util import standardize_path
        from . import bitcoin
       -from .bitcoin import is_address,  hash_160, COIN, TYPE_ADDRESS
       +from .bitcoin import is_address,  hash_160, COIN
        from .bip32 import BIP32Node
        from .i18n import _
       -from .transaction import Transaction, multisig_script, TxOutput
       +from .transaction import (Transaction, multisig_script, TxOutput, PartialTransaction, PartialTxOutput,
       +                          tx_from_any, PartialTxInput, TxOutpoint)
        from .paymentrequest import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED
        from .synchronizer import Notifier
        from .wallet import Abstract_Wallet, create_new_wallet, restore_wallet_from_text
       t@@ -299,11 +300,13 @@ class Commands:
            async def listunspent(self, wallet: Abstract_Wallet = None):
                """List unspent outputs. Returns the list of unspent transaction
                outputs in your wallet."""
       -        l = copy.deepcopy(wallet.get_utxos())
       -        for i in l:
       -            v = i["value"]
       -            i["value"] = str(Decimal(v)/COIN) if v is not None else None
       -        return l
       +        coins = []
       +        for txin in wallet.get_utxos():
       +            d = txin.to_json()
       +            v = d.pop("value_sats")
       +            d["value"] = str(Decimal(v)/COIN) if v is not None else None
       +            coins.append(d)
       +        return coins
        
            @command('n')
            async def getaddressunspent(self, address):
       t@@ -320,46 +323,50 @@ class Commands:
                Outputs must be a list of {'address':address, 'value':satoshi_amount}.
                """
                keypairs = {}
       -        inputs = jsontx.get('inputs')
       -        outputs = jsontx.get('outputs')
       +        inputs = []  # type: List[PartialTxInput]
                locktime = jsontx.get('lockTime', 0)
       -        for txin in inputs:
       -            if txin.get('output'):
       -                prevout_hash, prevout_n = txin['output'].split(':')
       -                txin['prevout_n'] = int(prevout_n)
       -                txin['prevout_hash'] = prevout_hash
       -            sec = txin.get('privkey')
       +        for txin_dict in jsontx.get('inputs'):
       +            if txin_dict.get('prevout_hash') is not None and txin_dict.get('prevout_n') is not None:
       +                prevout = TxOutpoint(txid=bfh(txin_dict['prevout_hash']), out_idx=int(txin_dict['prevout_n']))
       +            elif txin_dict.get('output'):
       +                prevout = TxOutpoint.from_str(txin_dict['output'])
       +            else:
       +                raise Exception("missing prevout for txin")
       +            txin = PartialTxInput(prevout=prevout)
       +            txin._trusted_value_sats = int(txin_dict['value'])
       +            sec = txin_dict.get('privkey')
                    if sec:
                        txin_type, privkey, compressed = bitcoin.deserialize_privkey(sec)
                        pubkey = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed)
                        keypairs[pubkey] = privkey, compressed
       -                txin['type'] = txin_type
       -                txin['x_pubkeys'] = [pubkey]
       -                txin['signatures'] = [None]
       -                txin['num_sig'] = 1
       -
       -        outputs = [TxOutput(TYPE_ADDRESS, x['address'], int(x['value'])) for x in outputs]
       -        tx = Transaction.from_io(inputs, outputs, locktime=locktime)
       +                txin.script_type = txin_type
       +                txin.pubkeys = [bfh(pubkey)]
       +                txin.num_sig = 1
       +            inputs.append(txin)
       +
       +        outputs = [PartialTxOutput.from_address_and_value(txout['address'], int(txout['value']))
       +                   for txout in jsontx.get('outputs')]
       +        tx = PartialTransaction.from_io(inputs, outputs, locktime=locktime)
                tx.sign(keypairs)
       -        return tx.as_dict()
       +        return tx.serialize()
        
            @command('wp')
            async def signtransaction(self, tx, privkey=None, password=None, wallet: Abstract_Wallet = None):
                """Sign a transaction. The wallet keys will be used unless a private key is provided."""
       -        tx = Transaction(tx)
       +        tx = PartialTransaction(tx)
                if privkey:
                    txin_type, privkey2, compressed = bitcoin.deserialize_privkey(privkey)
                    pubkey = ecc.ECPrivkey(privkey2).get_public_key_bytes(compressed=compressed).hex()
                    tx.sign({pubkey:(privkey2, compressed)})
                else:
                    wallet.sign_transaction(tx, password)
       -        return tx.as_dict()
       +        return tx.serialize()
        
            @command('')
            async def deserialize(self, tx):
                """Deserialize a serialized transaction"""
       -        tx = Transaction(tx)
       -        return tx.deserialize(force_full_parse=True)
       +        tx = tx_from_any(tx)
       +        return tx.to_json()
        
            @command('n')
            async def broadcast(self, tx):
       t@@ -392,9 +399,9 @@ class Commands:
                if isinstance(address, str):
                    address = address.strip()
                if is_address(address):
       -            return wallet.export_private_key(address, password)[0]
       +            return wallet.export_private_key(address, password)
                domain = address
       -        return [wallet.export_private_key(address, password)[0] for address in domain]
       +        return [wallet.export_private_key(address, password) for address in domain]
        
            @command('w')
            async def ismine(self, address, wallet: Abstract_Wallet = None):
       t@@ -513,8 +520,13 @@ class Commands:
                privkeys = privkey.split()
                self.nocheck = nocheck
                #dest = self._resolver(destination)
       -        tx = sweep(privkeys, self.network, self.config, destination, tx_fee, imax)
       -        return tx.as_dict() if tx else None
       +        tx = sweep(privkeys,
       +                   network=self.network,
       +                   config=self.config,
       +                   to_address=destination,
       +                   fee=tx_fee,
       +                   imax=imax)
       +        return tx.serialize() if tx else None
        
            @command('wp')
            async def signmessage(self, address, message, password=None, wallet: Abstract_Wallet = None):
       t@@ -541,17 +553,20 @@ class Commands:
                for address, amount in outputs:
                    address = self._resolver(address, wallet)
                    amount = satoshis(amount)
       -            final_outputs.append(TxOutput(TYPE_ADDRESS, address, amount))
       +            final_outputs.append(PartialTxOutput.from_address_and_value(address, amount))
        
                coins = wallet.get_spendable_coins(domain_addr)
                if domain_coins is not None:
       -            coins = [coin for coin in coins if (coin['prevout_hash'] + ':' + str(coin['prevout_n']) in domain_coins)]
       +            coins = [coin for coin in coins if (coin.prevout.to_str() in domain_coins)]
                if feerate is not None:
                    fee_per_kb = 1000 * Decimal(feerate)
                    fee_estimator = partial(SimpleConfig.estimate_fee_for_feerate, fee_per_kb)
                else:
                    fee_estimator = fee
       -        tx = wallet.make_unsigned_transaction(coins, final_outputs, fee_estimator, change_addr)
       +        tx = wallet.make_unsigned_transaction(coins=coins,
       +                                              outputs=final_outputs,
       +                                              fee=fee_estimator,
       +                                              change_addr=change_addr)
                if locktime is not None:
                    tx.locktime = locktime
                if rbf is None:
       t@@ -581,7 +596,7 @@ class Commands:
                                rbf=rbf,
                                password=password,
                                locktime=locktime)
       -        return tx.as_dict()
       +        return tx.serialize()
        
            @command('wp')
            async def paytomany(self, outputs, fee=None, feerate=None, from_addr=None, from_coins=None, change_addr=None,
       t@@ -602,7 +617,7 @@ class Commands:
                                rbf=rbf,
                                password=password,
                                locktime=locktime)
       -        return tx.as_dict()
       +        return tx.serialize()
        
            @command('w')
            async def onchain_history(self, year=None, show_addresses=False, show_fiat=False, wallet: Abstract_Wallet = None):
       t@@ -703,7 +718,7 @@ class Commands:
                        raise Exception("Unknown transaction")
                if tx.txid() != txid:
                    raise Exception("Mismatching txid")
       -        return tx.as_dict()
       +        return tx.serialize()
        
            @command('')
            async def encrypt(self, pubkey, message) -> str:
       t@@ -960,7 +975,7 @@ class Commands:
                chan_id, _ = channel_id_from_funding_tx(txid, int(index))
                chan = wallet.lnworker.channels[chan_id]
                tx = chan.force_close_tx()
       -        return tx.as_dict()
       +        return tx.serialize()
        
        def eval_bool(x: str) -> bool:
            if x == 'false': return False
       t@@ -1037,7 +1052,7 @@ command_options = {
        
        
        # don't use floats because of rounding errors
       -from .transaction import tx_from_str
       +from .transaction import convert_tx_str_to_hex
        json_loads = lambda x: json.loads(x, parse_float=lambda x: str(Decimal(x)))
        arg_types = {
            'num': int,
       t@@ -1046,7 +1061,7 @@ arg_types = {
            'year': int,
            'from_height': int,
            'to_height': int,
       -    'tx': tx_from_str,
       +    'tx': convert_tx_str_to_hex,
            'pubkeys': json_loads,
            'jsontx': json_loads,
            'inputs': json_loads,
   DIR diff --git a/electrum/ecc.py b/electrum/ecc.py
       t@@ -25,6 +25,7 @@
        
        import base64
        import hashlib
       +import functools
        from typing import Union, Tuple, Optional
        
        import ecdsa
       t@@ -181,6 +182,7 @@ class _PubkeyForPointAtInfinity:
            point = ecdsa.ellipticcurve.INFINITY
        
        
       +@functools.total_ordering
        class ECPubkey(object):
        
            def __init__(self, b: Optional[bytes]):
       t@@ -257,6 +259,14 @@ class ECPubkey(object):
            def __ne__(self, other):
                return not (self == other)
        
       +    def __hash__(self):
       +        return hash(self._pubkey.point.x())
       +
       +    def __lt__(self, other):
       +        if not isinstance(other, ECPubkey):
       +            raise TypeError('comparison not defined for ECPubkey and {}'.format(type(other)))
       +        return self._pubkey.point.x() < other._pubkey.point.x()
       +
            def verify_message_for_address(self, sig65: bytes, message: bytes, algo=lambda x: sha256d(msg_magic(x))) -> None:
                assert_bytes(message)
                h = algo(message)
   DIR diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py
       t@@ -9,7 +9,6 @@ import threading
        import asyncio
        from typing import TYPE_CHECKING, Optional
        
       -from electrum.bitcoin import TYPE_ADDRESS
        from electrum.storage import WalletStorage, StorageReadWriteError
        from electrum.wallet import Wallet, InternalAddressCorruption
        from electrum.util import profiler, InvalidPassword, send_exception_to_crash_reporter
       t@@ -398,12 +397,9 @@ class ElectrumWindow(App):
                    self.set_ln_invoice(data)
                    return
                # try to decode transaction
       -        from electrum.transaction import Transaction
       -        from electrum.util import bh2u
       +        from electrum.transaction import tx_from_any
                try:
       -            text = bh2u(base_decode(data, None, base=43))
       -            tx = Transaction(text)
       -            tx.deserialize()
       +            tx = tx_from_any(data)
                except:
                    tx = None
                if tx:
       t@@ -855,7 +851,7 @@ class ElectrumWindow(App):
                    self._trigger_update_status()
        
            def get_max_amount(self):
       -        from electrum.transaction import TxOutput
       +        from electrum.transaction import PartialTxOutput
                if run_hook('abort_send', self):
                    return ''
                inputs = self.wallet.get_spendable_coins(None)
       t@@ -866,9 +862,9 @@ class ElectrumWindow(App):
                    addr = str(self.send_screen.screen.address)
                if not addr:
                    addr = self.wallet.dummy_address()
       -        outputs = [TxOutput(TYPE_ADDRESS, addr, '!')]
       +        outputs = [PartialTxOutput.from_address_and_value(addr, '!')]
                try:
       -            tx = self.wallet.make_unsigned_transaction(inputs, outputs)
       +            tx = self.wallet.make_unsigned_transaction(coins=inputs, outputs=outputs)
                except NoDynamicFeeEstimates as e:
                    Clock.schedule_once(lambda dt, bound_e=e: self.show_error(str(bound_e)))
                    return ''
       t@@ -1199,7 +1195,7 @@ class ElectrumWindow(App):
                    if not self.wallet.can_export():
                        return
                    try:
       -                key = str(self.wallet.export_private_key(addr, password)[0])
       +                key = str(self.wallet.export_private_key(addr, password))
                        pk_label.data = key
                    except InvalidPassword:
                        self.show_error("Invalid PIN")
   DIR diff --git a/electrum/gui/kivy/uix/dialogs/__init__.py b/electrum/gui/kivy/uix/dialogs/__init__.py
       t@@ -1,3 +1,5 @@
       +from typing import TYPE_CHECKING, Sequence
       +
        from kivy.app import App
        from kivy.clock import Clock
        from kivy.factory import Factory
       t@@ -8,6 +10,9 @@ from kivy.uix.boxlayout import BoxLayout
        
        from electrum.gui.kivy.i18n import _
        
       +if TYPE_CHECKING:
       +    from ...main_window import ElectrumWindow
       +    from electrum.transaction import TxOutput
        
        
        class AnimatedPopup(Factory.Popup):
       t@@ -202,13 +207,13 @@ class OutputList(RecycleView):
        
            def __init__(self, **kwargs):
                super(OutputList, self).__init__(**kwargs)
       -        self.app = App.get_running_app()
       +        self.app = App.get_running_app()  # type: ElectrumWindow
        
       -    def update(self, outputs):
       +    def update(self, outputs: Sequence['TxOutput']):
                res = []
                for o in outputs:
                    value = self.app.format_amount_and_units(o.value)
       -            res.append({'address': o.address, 'value': value})
       +            res.append({'address': o.get_ui_address_str(), 'value': value})
                self.data = res
        
        
   DIR diff --git a/electrum/gui/kivy/uix/dialogs/tx_dialog.py b/electrum/gui/kivy/uix/dialogs/tx_dialog.py
       t@@ -1,5 +1,6 @@
       +import copy
        from datetime import datetime
       -from typing import NamedTuple, Callable
       +from typing import NamedTuple, Callable, TYPE_CHECKING
        
        from kivy.app import App
        from kivy.factory import Factory
       t@@ -16,6 +17,10 @@ from electrum.gui.kivy.i18n import _
        from electrum.util import InvalidPassword
        from electrum.address_synchronizer import TX_HEIGHT_LOCAL
        from electrum.wallet import CannotBumpFee
       +from electrum.transaction import Transaction, PartialTransaction
       +
       +if TYPE_CHECKING:
       +    from ...main_window import ElectrumWindow
        
        
        Builder.load_string('''
       t@@ -121,11 +126,16 @@ class TxDialog(Factory.Popup):
        
            def __init__(self, app, tx):
                Factory.Popup.__init__(self)
       -        self.app = app
       +        self.app = app  # type: ElectrumWindow
                self.wallet = self.app.wallet
       -        self.tx = tx
       +        self.tx = tx  # type: Transaction
                self._action_button_fn = lambda btn: None
        
       +        # if the wallet can populate the inputs with more info, do it now.
       +        # as a result, e.g. we might learn an imported address tx is segwit,
       +        # or that a beyond-gap-limit address is is_mine
       +        tx.add_info_from_wallet(self.wallet)
       +
            def on_open(self):
                self.update()
        
       t@@ -150,6 +160,7 @@ class TxDialog(Factory.Popup):
                    self.date_label = ''
                    self.date_str = ''
        
       +        self.can_sign = self.wallet.can_sign(self.tx)
                if amount is None:
                    self.amount_str = _("Transaction unrelated to your wallet")
                elif amount > 0:
       t@@ -158,15 +169,18 @@ class TxDialog(Factory.Popup):
                else:
                    self.is_mine = True
                    self.amount_str = format_amount(-amount)
       -        if fee is not None:
       +        risk_of_burning_coins = (isinstance(self.tx, PartialTransaction)
       +                                 and self.can_sign
       +                                 and fee is not None
       +                                 and self.tx.is_there_risk_of_burning_coins_as_fees())
       +        if fee is not None and not risk_of_burning_coins:
                    self.fee_str = format_amount(fee)
                    fee_per_kb = fee / self.tx.estimated_size() * 1000
                    self.feerate_str = self.app.format_fee_rate(fee_per_kb)
                else:
                    self.fee_str = _('unknown')
                    self.feerate_str = _('unknown')
       -        self.can_sign = self.wallet.can_sign(self.tx)
       -        self.ids.output_list.update(self.tx.get_outputs_for_UI())
       +        self.ids.output_list.update(self.tx.outputs())
                self.is_local_tx = tx_mined_status.height == TX_HEIGHT_LOCAL
                self.update_action_button()
        
       t@@ -252,10 +266,15 @@ class TxDialog(Factory.Popup):
        
            def show_qr(self):
                from electrum.bitcoin import base_encode, bfh
       -        raw_tx = str(self.tx)
       -        text = bfh(raw_tx)
       +        original_raw_tx = str(self.tx)
       +        tx = copy.deepcopy(self.tx)  # make copy as we mutate tx
       +        if isinstance(tx, PartialTransaction):
       +            # this makes QR codes a lot smaller (or just possible in the first place!)
       +            tx.convert_all_utxos_to_witness_utxos()
       +
       +        text = tx.serialize_as_bytes()
                text = base_encode(text, base=43)
       -        self.app.qr_dialog(_("Raw Transaction"), text, text_for_clipboard=raw_tx)
       +        self.app.qr_dialog(_("Raw Transaction"), text, text_for_clipboard=original_raw_tx)
        
            def remove_local_tx(self):
                txid = self.tx.txid()
   DIR diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py
       t@@ -22,11 +22,10 @@ from kivy.lang import Builder
        from kivy.factory import Factory
        from kivy.utils import platform
        
       -from electrum.bitcoin import TYPE_ADDRESS
        from electrum.util import profiler, parse_URI, format_time, InvalidPassword, NotEnoughFunds, Fiat
        from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN
        from electrum import bitcoin, constants
       -from electrum.transaction import TxOutput, Transaction, tx_from_str
       +from electrum.transaction import Transaction, tx_from_any, PartialTransaction, PartialTxOutput
        from electrum.util import send_exception_to_crash_reporter, parse_URI, InvalidBitcoinURI
        from electrum.util import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED, PR_INFLIGHT, TxMinedInfo, get_request_status, pr_expiration_values
        from electrum.plugin import run_hook
       t@@ -276,8 +275,7 @@ class SendScreen(CScreen):
                    return
                # try to decode as transaction
                try:
       -            raw_tx = tx_from_str(data)
       -            tx = Transaction(raw_tx)
       +            tx = tx_from_any(data)
                    tx.deserialize()
                except:
                    tx = None
       t@@ -313,7 +311,7 @@ class SendScreen(CScreen):
                    if not bitcoin.is_address(address):
                        self.app.show_error(_('Invalid Bitcoin Address') + ':\n' + address)
                        return
       -            outputs = [TxOutput(TYPE_ADDRESS, address, amount)]
       +            outputs = [PartialTxOutput.from_address_and_value(address, amount)]
                    return self.app.wallet.create_invoice(outputs, message, self.payment_request, self.parsed_URI)
        
            def do_save(self):
       t@@ -353,11 +351,11 @@ class SendScreen(CScreen):
        
            def _do_pay_onchain(self, invoice, rbf):
                # make unsigned transaction
       -        outputs = invoice['outputs']  # type: List[TxOutput]
       +        outputs = invoice['outputs']  # type: List[PartialTxOutput]
                amount = sum(map(lambda x: x.value, outputs))
                coins = self.app.wallet.get_spendable_coins(None)
                try:
       -            tx = self.app.wallet.make_unsigned_transaction(coins, outputs, None)
       +            tx = self.app.wallet.make_unsigned_transaction(coins=coins, outputs=outputs)
                except NotEnoughFunds:
                    self.app.show_error(_("Not enough funds"))
                    return
   DIR diff --git a/electrum/gui/qt/address_dialog.py b/electrum/gui/qt/address_dialog.py
       t@@ -84,16 +84,20 @@ class AddressDialog(WindowModalDialog):
                        pubkey_e.setReadOnly(True)
                        vbox.addWidget(pubkey_e)
        
       -        try:
       -            redeem_script = self.wallet.pubkeys_to_redeem_script(pubkeys)
       -        except BaseException as e:
       -            redeem_script = None
       +        redeem_script = self.wallet.get_redeem_script(address)
                if redeem_script:
                    vbox.addWidget(QLabel(_("Redeem Script") + ':'))
                    redeem_e = ShowQRTextEdit(text=redeem_script)
                    redeem_e.addCopyButton(self.app)
                    vbox.addWidget(redeem_e)
        
       +        witness_script = self.wallet.get_witness_script(address)
       +        if witness_script:
       +            vbox.addWidget(QLabel(_("Witness Script") + ':'))
       +            witness_e = ShowQRTextEdit(text=witness_script)
       +            witness_e.addCopyButton(self.app)
       +            vbox.addWidget(witness_e)
       +
                vbox.addWidget(QLabel(_("History")))
                addr_hist_model = AddressHistoryModel(self.parent, self.address)
                self.hw = HistoryList(self.parent, addr_hist_model)
   DIR diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
       t@@ -36,7 +36,7 @@ import base64
        from functools import partial
        import queue
        import asyncio
       -from typing import Optional, TYPE_CHECKING
       +from typing import Optional, TYPE_CHECKING, Sequence, List, Union
        
        from PyQt5.QtGui import QPixmap, QKeySequence, QIcon, QCursor, QFont
        from PyQt5.QtCore import Qt, QRect, QStringListModel, QSize, pyqtSignal
       t@@ -50,7 +50,7 @@ from PyQt5.QtWidgets import (QMessageBox, QComboBox, QSystemTrayIcon, QTabWidget
        import electrum
        from electrum import (keystore, simple_config, ecc, constants, util, bitcoin, commands,
                              coinchooser, paymentrequest)
       -from electrum.bitcoin import COIN, is_address, TYPE_ADDRESS
       +from electrum.bitcoin import COIN, is_address
        from electrum.plugin import run_hook
        from electrum.i18n import _
        from electrum.util import (format_time, format_satoshis, format_fee_satoshis,
       t@@ -64,7 +64,8 @@ from electrum.util import (format_time, format_satoshis, format_fee_satoshis,
                                   InvalidBitcoinURI, InvoiceError)
        from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN
        from electrum.lnutil import PaymentFailure, SENT, RECEIVED
       -from electrum.transaction import Transaction, TxOutput
       +from electrum.transaction import (Transaction, PartialTxInput,
       +                                  PartialTransaction, PartialTxOutput)
        from electrum.address_synchronizer import AddTransactionException
        from electrum.wallet import (Multisig_Wallet, CannotBumpFee, Abstract_Wallet,
                                     sweep_preparations, InternalAddressCorruption)
       t@@ -922,7 +923,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
        
            def show_transaction(self, tx, *, invoice=None, tx_desc=None):
                '''tx_desc is set only for txs created in the Send tab'''
       -        show_transaction(tx, self, invoice=invoice, desc=tx_desc)
       +        show_transaction(tx, parent=self, invoice=invoice, desc=tx_desc)
        
            def create_receive_tab(self):
                # A 4-column grid layout.  All the stretch is in the last column.
       t@@ -1434,11 +1435,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
            def update_fee(self):
                self.require_fee_update = True
        
       -    def get_payto_or_dummy(self):
       -        r = self.payto_e.get_recipient()
       +    def get_payto_or_dummy(self) -> bytes:
       +        r = self.payto_e.get_destination_scriptpubkey()
                if r:
                    return r
       -        return (TYPE_ADDRESS, self.wallet.dummy_address())
       +        return bfh(bitcoin.address_to_script(self.wallet.dummy_address()))
        
            def do_update_fee(self):
                '''Recalculate the fee.  If the fee was manually input, retain it, but
       t@@ -1461,13 +1462,15 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                coins = self.get_coins()
        
                if not outputs:
       -            _type, addr = self.get_payto_or_dummy()
       -            outputs = [TxOutput(_type, addr, amount)]
       +            scriptpubkey = self.get_payto_or_dummy()
       +            outputs = [PartialTxOutput(scriptpubkey=scriptpubkey, value=amount)]
                is_sweep = bool(self.tx_external_keypairs)
                make_tx = lambda fee_est: \
                    self.wallet.make_unsigned_transaction(
       -                coins, outputs,
       -                fixed_fee=fee_est, is_sweep=is_sweep)
       +                coins=coins,
       +                outputs=outputs,
       +                fee=fee_est,
       +                is_sweep=is_sweep)
                try:
                    tx = make_tx(fee_estimator)
                    self.not_enough_funds = False
       t@@ -1546,7 +1549,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                menu.addAction(_("Remove"), lambda: self.from_list_delete(item))
                menu.exec_(self.from_list.viewport().mapToGlobal(position))
        
       -    def set_pay_from(self, coins):
       +    def set_pay_from(self, coins: Sequence[PartialTxInput]):
                self.pay_from = list(coins)
                self.redraw_from_list()
        
       t@@ -1555,12 +1558,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                self.from_label.setHidden(len(self.pay_from) == 0)
                self.from_list.setHidden(len(self.pay_from) == 0)
        
       -        def format(x):
       -            h = x.get('prevout_hash')
       -            return h[0:10] + '...' + h[-10:] + ":%d"%x.get('prevout_n') + '\t' + "%s"%x.get('address') + '\t'
       +        def format(txin: PartialTxInput):
       +            h = txin.prevout.txid.hex()
       +            out_idx = txin.prevout.out_idx
       +            addr = txin.address
       +            return h[0:10] + '...' + h[-10:] + ":%d"%out_idx + '\t' + addr + '\t'
        
                for coin in self.pay_from:
       -            item = QTreeWidgetItem([format(coin), self.format_amount(coin['value'])])
       +            item = QTreeWidgetItem([format(coin), self.format_amount(coin.value_sats())])
                    item.setFont(0, QFont(MONOSPACE_FONT))
                    self.from_list.addTopLevelItem(item)
        
       t@@ -1620,14 +1625,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                    fee_estimator = None
                return fee_estimator
        
       -    def read_outputs(self):
       +    def read_outputs(self) -> List[PartialTxOutput]:
                if self.payment_request:
                    outputs = self.payment_request.get_outputs()
                else:
                    outputs = self.payto_e.get_outputs(self.max_button.isChecked())
                return outputs
        
       -    def check_send_tab_onchain_outputs_and_show_errors(self, outputs) -> bool:
       +    def check_send_tab_onchain_outputs_and_show_errors(self, outputs: List[PartialTxOutput]) -> bool:
                """Returns whether there are errors with outputs.
                Also shows error dialog to user if so.
                """
       t@@ -1636,12 +1641,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                    return True
        
                for o in outputs:
       -            if o.address is None:
       +            if o.scriptpubkey is None:
                        self.show_error(_('Bitcoin Address is None'))
                        return True
       -            if o.type == TYPE_ADDRESS and not bitcoin.is_address(o.address):
       -                self.show_error(_('Invalid Bitcoin Address'))
       -                return True
                    if o.value is None:
                        self.show_error(_('Invalid Amount'))
                        return True
       t@@ -1749,20 +1751,23 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                    return
                elif invoice['type'] == PR_TYPE_ONCHAIN:
                    message = invoice['message']
       -            outputs = invoice['outputs']
       +            outputs = invoice['outputs']  # type: List[PartialTxOutput]
                else:
                    raise Exception('unknown invoice type')
        
                if run_hook('abort_send', self):
                    return
        
       -        outputs = [TxOutput(*x) for x in outputs]
       +        for txout in outputs:
       +            assert isinstance(txout, PartialTxOutput)
                fee_estimator = self.get_send_fee_estimator()
                coins = self.get_coins()
                try:
                    is_sweep = bool(self.tx_external_keypairs)
                    tx = self.wallet.make_unsigned_transaction(
       -                coins, outputs, fixed_fee=fee_estimator,
       +                coins=coins,
       +                outputs=outputs,
       +                fee=fee_estimator,
                        is_sweep=is_sweep)
                except (NotEnoughFunds, NoDynamicFeeEstimates) as e:
                    self.show_message(str(e))
       t@@ -1837,7 +1842,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
            def sign_tx(self, tx, callback, password):
                self.sign_tx_with_password(tx, callback, password)
        
       -    def sign_tx_with_password(self, tx, callback, password):
       +    def sign_tx_with_password(self, tx: PartialTransaction, callback, password):
                '''Sign the transaction in a separate thread.  When done, calls
                the callback with a success code of True or False.
                '''
       t@@ -1849,13 +1854,13 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                on_success = run_hook('tc_sign_wrapper', self.wallet, tx, on_success, on_failure) or on_success
                if self.tx_external_keypairs:
                    # can sign directly
       -            task = partial(Transaction.sign, tx, self.tx_external_keypairs)
       +            task = partial(tx.sign, self.tx_external_keypairs)
                else:
                    task = partial(self.wallet.sign_transaction, tx, password)
                msg = _('Signing transaction...')
                WaitingDialog(self, msg, task, on_success, on_failure)
        
       -    def broadcast_transaction(self, tx, *, invoice=None, tx_desc=None):
       +    def broadcast_transaction(self, tx: Transaction, *, invoice=None, tx_desc=None):
        
                def broadcast_thread():
                    # non-GUI thread
       t@@ -1879,7 +1884,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                    if pr:
                        self.payment_request = None
                        refund_address = self.wallet.get_receiving_address()
       -                coro = pr.send_payment_and_receive_paymentack(str(tx), refund_address)
       +                coro = pr.send_payment_and_receive_paymentack(tx.serialize(), refund_address)
                        fut = asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop)
                        ack_status, ack_msg = fut.result(timeout=20)
                        self.logger.info(f"Payment ACK: {ack_status}. Ack message: {ack_msg}")
       t@@ -2077,7 +2082,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                self.utxo_list.update()
                self.update_fee()
        
       -    def set_frozen_state_of_coins(self, utxos, freeze: bool):
       +    def set_frozen_state_of_coins(self, utxos: Sequence[PartialTxInput], freeze: bool):
                self.wallet.set_frozen_state_of_coins(utxos, freeze)
                self.utxo_list.update()
                self.update_fee()
       t@@ -2124,7 +2129,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                else:
                    return self.wallet.get_spendable_coins(None)
        
       -    def spend_coins(self, coins):
       +    def spend_coins(self, coins: Sequence[PartialTxInput]):
                self.set_pay_from(coins)
                self.set_onchain(len(coins) > 0)
                self.show_send_tab()
       t@@ -2527,7 +2532,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                if not address:
                    return
                try:
       -            pk, redeem_script = self.wallet.export_private_key(address, password)
       +            pk = self.wallet.export_private_key(address, password)
                except Exception as e:
                    self.logger.exception('')
                    self.show_message(repr(e))
       t@@ -2542,11 +2547,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                keys_e = ShowQRTextEdit(text=pk)
                keys_e.addCopyButton(self.app)
                vbox.addWidget(keys_e)
       -        if redeem_script:
       -            vbox.addWidget(QLabel(_("Redeem Script") + ':'))
       -            rds_e = ShowQRTextEdit(text=redeem_script)
       -            rds_e.addCopyButton(self.app)
       -            vbox.addWidget(rds_e)
       +        # if redeem_script:
       +        #     vbox.addWidget(QLabel(_("Redeem Script") + ':'))
       +        #     rds_e = ShowQRTextEdit(text=redeem_script)
       +        #     rds_e.addCopyButton(self.app)
       +        #     vbox.addWidget(rds_e)
                vbox.addLayout(Buttons(CloseButton(d)))
                d.setLayout(vbox)
                d.exec_()
       t@@ -2718,11 +2723,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                d = PasswordDialog(parent, msg)
                return d.run()
        
       -    def tx_from_text(self, txt) -> Optional[Transaction]:
       -        from electrum.transaction import tx_from_str
       +    def tx_from_text(self, data: Union[str, bytes]) -> Union[None, 'PartialTransaction', 'Transaction']:
       +        from electrum.transaction import tx_from_any
                try:
       -            tx = tx_from_str(txt)
       -            return Transaction(tx)
       +            return tx_from_any(data)
                except BaseException as e:
                    self.show_critical(_("Electrum was unable to parse your transaction") + ":\n" + repr(e))
                    return
       t@@ -2741,25 +2745,21 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                    self.pay_to_URI(data)
                    return
                # else if the user scanned an offline signed tx
       -        try:
       -            data = bh2u(bitcoin.base_decode(data, length=None, base=43))
       -        except BaseException as e:
       -            self.show_error((_('Could not decode QR code')+':\n{}').format(repr(e)))
       -            return
                tx = self.tx_from_text(data)
                if not tx:
                    return
                self.show_transaction(tx)
        
            def read_tx_from_file(self) -> Optional[Transaction]:
       -        fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
       +        fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn;;*.psbt")
                if not fileName:
                    return
                try:
                    with open(fileName, "r") as f:
       -                file_content = f.read()
       +                file_content = f.read()  # type: Union[str, bytes]
                except (ValueError, IOError, os.error) as reason:
       -            self.show_critical(_("Electrum was unable to open your transaction file") + "\n" + str(reason), title=_("Unable to read file or no transaction found"))
       +            self.show_critical(_("Electrum was unable to open your transaction file") + "\n" + str(reason),
       +                               title=_("Unable to read file or no transaction found"))
                    return
                return self.tx_from_text(file_content)
        
       t@@ -2831,7 +2831,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                        time.sleep(0.1)
                        if done or cancelled:
                            break
       -                privkey = self.wallet.export_private_key(addr, password)[0]
       +                privkey = self.wallet.export_private_key(addr, password)
                        private_keys[addr] = privkey
                        self.computing_privkeys_signal.emit()
                    if not cancelled:
       t@@ -3130,7 +3130,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                vbox.addLayout(Buttons(CloseButton(d)))
                d.exec_()
        
       -    def cpfp(self, parent_tx: Transaction, new_tx: Transaction) -> None:
       +    def cpfp(self, parent_tx: Transaction, new_tx: PartialTransaction) -> None:
                total_size = parent_tx.estimated_size() + new_tx.estimated_size()
                parent_txid = parent_tx.txid()
                assert parent_txid
       t@@ -3257,7 +3257,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                    new_tx.set_rbf(False)
                self.show_transaction(new_tx, tx_desc=tx_label)
        
       -    def save_transaction_into_wallet(self, tx):
       +    def save_transaction_into_wallet(self, tx: Transaction):
                win = self.top_level_window()
                try:
                    if not self.wallet.add_transaction(tx.txid(), tx):
   DIR diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py
       t@@ -25,13 +25,13 @@
        
        import re
        from decimal import Decimal
       -from typing import NamedTuple, Sequence
       +from typing import NamedTuple, Sequence, Optional, List
        
        from PyQt5.QtGui import QFontMetrics
        
        from electrum import bitcoin
        from electrum.util import bfh
       -from electrum.transaction import TxOutput, push_script
       +from electrum.transaction import push_script, PartialTxOutput
        from electrum.bitcoin import opcodes
        from electrum.logging import Logger
        from electrum.lnaddr import LnDecodeException
       t@@ -65,12 +65,12 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
                self.heightMax = 150
                self.c = None
                self.textChanged.connect(self.check_text)
       -        self.outputs = []
       +        self.outputs = []  # type: List[PartialTxOutput]
                self.errors = []  # type: Sequence[PayToLineError]
                self.is_pr = False
                self.is_alias = False
                self.update_size()
       -        self.payto_address = None
       +        self.payto_scriptpubkey = None  # type: Optional[bytes]
                self.lightning_invoice = None
                self.previous_payto = ''
        
       t@@ -86,19 +86,19 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
            def setExpired(self):
                self.setStyleSheet(util.ColorScheme.RED.as_stylesheet(True))
        
       -    def parse_address_and_amount(self, line):
       +    def parse_address_and_amount(self, line) -> PartialTxOutput:
                x, y = line.split(',')
       -        out_type, out = self.parse_output(x)
       +        scriptpubkey = self.parse_output(x)
                amount = self.parse_amount(y)
       -        return TxOutput(out_type, out, amount)
       +        return PartialTxOutput(scriptpubkey=scriptpubkey, value=amount)
        
       -    def parse_output(self, x):
       +    def parse_output(self, x) -> bytes:
                try:
                    address = self.parse_address(x)
       -            return bitcoin.TYPE_ADDRESS, address
       +            return bfh(bitcoin.address_to_script(address))
                except:
                    script = self.parse_script(x)
       -            return bitcoin.TYPE_SCRIPT, script
       +            return bfh(script)
        
            def parse_script(self, x):
                script = ''
       t@@ -131,9 +131,9 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
                    return
                # filter out empty lines
                lines = [i for i in self.lines() if i]
       -        outputs = []
       +        outputs = []  # type: List[PartialTxOutput]
                total = 0
       -        self.payto_address = None
       +        self.payto_scriptpubkey = None
                self.lightning_invoice = None
                if len(lines) == 1:
                    data = lines[0]
       t@@ -152,10 +152,10 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
                            self.lightning_invoice = lower
                        return
                    try:
       -                self.payto_address = self.parse_output(data)
       +                self.payto_scriptpubkey = self.parse_output(data)
                    except:
                        pass
       -            if self.payto_address:
       +            if self.payto_scriptpubkey:
                        self.win.set_onchain(True)
                        self.win.lock_amount(False)
                        return
       t@@ -177,7 +177,7 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
        
                self.win.max_button.setChecked(is_max)
                self.outputs = outputs
       -        self.payto_address = None
       +        self.payto_scriptpubkey = None
        
                if self.win.max_button.isChecked():
                    self.win.do_update_fee()
       t@@ -188,18 +188,16 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
            def get_errors(self) -> Sequence[PayToLineError]:
                return self.errors
        
       -    def get_recipient(self):
       -        return self.payto_address
       +    def get_destination_scriptpubkey(self) -> Optional[bytes]:
       +        return self.payto_scriptpubkey
        
            def get_outputs(self, is_max):
       -        if self.payto_address:
       +        if self.payto_scriptpubkey:
                    if is_max:
                        amount = '!'
                    else:
                        amount = self.amount_edit.get_amount()
       -
       -            _type, addr = self.payto_address
       -            self.outputs = [TxOutput(_type, addr, amount)]
       +            self.outputs = [PartialTxOutput(scriptpubkey=self.payto_scriptpubkey, value=amount)]
        
                return self.outputs[:]
        
   DIR diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py
       t@@ -26,12 +26,12 @@
        import sys
        import copy
        import datetime
       -import json
        import traceback
       -from typing import TYPE_CHECKING
       +import time
       +from typing import TYPE_CHECKING, Callable
        
        from PyQt5.QtCore import QSize, Qt
       -from PyQt5.QtGui import QTextCharFormat, QBrush, QFont
       +from PyQt5.QtGui import QTextCharFormat, QBrush, QFont, QPixmap
        from PyQt5.QtWidgets import (QDialog, QLabel, QPushButton, QHBoxLayout, QVBoxLayout,
                                     QTextEdit, QFrame, QAction, QToolButton, QMenu)
        import qrcode
       t@@ -42,11 +42,12 @@ from electrum.i18n import _
        from electrum.plugin import run_hook
        from electrum import simple_config
        from electrum.util import bfh
       -from electrum.transaction import SerializationError, Transaction
       +from electrum.transaction import SerializationError, Transaction, PartialTransaction, PartialTxInput
        from electrum.logging import get_logger
        
       -from .util import (MessageBoxMixin, read_QIcon, Buttons, CopyButton,
       -                   MONOSPACE_FONT, ColorScheme, ButtonsLineEdit)
       +from .util import (MessageBoxMixin, read_QIcon, Buttons, CopyButton, icon_path,
       +                   MONOSPACE_FONT, ColorScheme, ButtonsLineEdit, text_dialog,
       +                   char_width_in_lineedit)
        
        if TYPE_CHECKING:
            from .main_window import ElectrumWindow
       t@@ -60,9 +61,9 @@ _logger = get_logger(__name__)
        dialogs = []  # Otherwise python randomly garbage collects the dialogs...
        
        
       -def show_transaction(tx, parent, *, invoice=None, desc=None, prompt_if_unsaved=False):
       +def show_transaction(tx: Transaction, *, parent: 'ElectrumWindow', invoice=None, desc=None, prompt_if_unsaved=False):
            try:
       -        d = TxDialog(tx, parent, invoice, desc, prompt_if_unsaved)
       +        d = TxDialog(tx, parent=parent, invoice=invoice, desc=desc, prompt_if_unsaved=prompt_if_unsaved)
            except SerializationError as e:
                _logger.exception('unable to deserialize the transaction')
                parent.show_critical(_("Electrum was unable to deserialize the transaction:") + "\n" + str(e))
       t@@ -73,7 +74,7 @@ def show_transaction(tx, parent, *, invoice=None, desc=None, prompt_if_unsaved=F
        
        class TxDialog(QDialog, MessageBoxMixin):
        
       -    def __init__(self, tx: Transaction, parent: 'ElectrumWindow', invoice, desc, prompt_if_unsaved):
       +    def __init__(self, tx: Transaction, *, parent: 'ElectrumWindow', invoice, desc, prompt_if_unsaved):
                '''Transactions in the wallet will show their description.
                Pass desc to give a description for txs not yet in the wallet.
                '''
       t@@ -96,8 +97,8 @@ class TxDialog(QDialog, MessageBoxMixin):
        
                # if the wallet can populate the inputs with more info, do it now.
                # as a result, e.g. we might learn an imported address tx is segwit,
       -        # in which case it's ok to display txid
       -        tx.add_inputs_info(self.wallet)
       +        # or that a beyond-gap-limit address is is_mine
       +        tx.add_info_from_wallet(self.wallet)
        
                self.setMinimumWidth(950)
                self.setWindowTitle(_("Transaction"))
       t@@ -115,7 +116,15 @@ class TxDialog(QDialog, MessageBoxMixin):
        
                self.add_tx_stats(vbox)
                vbox.addSpacing(10)
       -        self.add_io(vbox)
       +
       +        self.inputs_header = QLabel()
       +        vbox.addWidget(self.inputs_header)
       +        self.inputs_textedit = QTextEditWithDefaultSize()
       +        vbox.addWidget(self.inputs_textedit)
       +        self.outputs_header = QLabel()
       +        vbox.addWidget(self.outputs_header)
       +        self.outputs_textedit = QTextEditWithDefaultSize()
       +        vbox.addWidget(self.outputs_textedit)
        
                self.sign_button = b = QPushButton(_("Sign"))
                b.clicked.connect(self.sign)
       t@@ -136,23 +145,35 @@ class TxDialog(QDialog, MessageBoxMixin):
                b.clicked.connect(self.close)
                b.setDefault(True)
        
       -        export_actions_menu = QMenu()
       -        action = QAction(_("Copy to clipboard"), self)
       -        action.triggered.connect(lambda: parent.app.clipboard().setText((lambda: str(self.tx))()))
       -        export_actions_menu.addAction(action)
       -        action = QAction(read_QIcon(qr_icon), _("Show as QR code"), self)
       -        action.triggered.connect(self.show_qr)
       -        export_actions_menu.addAction(action)
       -        action = QAction(_("Export to file"), self)
       -        action.triggered.connect(self.export)
       -        export_actions_menu.addAction(action)
       +        self.export_actions_menu = export_actions_menu = QMenu()
       +        self.add_export_actions_to_menu(export_actions_menu)
       +        export_actions_menu.addSeparator()
       +        if isinstance(tx, PartialTransaction):
       +            export_for_coinjoin_submenu = export_actions_menu.addMenu(_("For CoinJoin; strip privates"))
       +            self.add_export_actions_to_menu(export_for_coinjoin_submenu, gettx=self._gettx_for_coinjoin)
       +
                self.export_actions_button = QToolButton()
                self.export_actions_button.setText(_("Export"))
                self.export_actions_button.setMenu(export_actions_menu)
                self.export_actions_button.setPopupMode(QToolButton.InstantPopup)
        
       +        partial_tx_actions_menu = QMenu()
       +        ptx_merge_sigs_action = QAction(_("Merge signatures from"), self)
       +        ptx_merge_sigs_action.triggered.connect(self.merge_sigs)
       +        partial_tx_actions_menu.addAction(ptx_merge_sigs_action)
       +        ptx_join_txs_action = QAction(_("Join inputs/outputs"), self)
       +        ptx_join_txs_action.triggered.connect(self.join_tx_with_another)
       +        partial_tx_actions_menu.addAction(ptx_join_txs_action)
       +        self.partial_tx_actions_button = QToolButton()
       +        self.partial_tx_actions_button.setText(_("Combine"))
       +        self.partial_tx_actions_button.setMenu(partial_tx_actions_menu)
       +        self.partial_tx_actions_button.setPopupMode(QToolButton.InstantPopup)
       +
                # Action buttons
       -        self.buttons = [self.sign_button, self.broadcast_button, self.cancel_button]
       +        self.buttons = []
       +        if isinstance(tx, PartialTransaction):
       +            self.buttons.append(self.partial_tx_actions_button)
       +        self.buttons += [self.sign_button, self.broadcast_button, self.cancel_button]
                # Transaction sharing buttons
                self.sharing_buttons = [self.export_actions_button, self.save_button]
        
       t@@ -189,8 +210,43 @@ class TxDialog(QDialog, MessageBoxMixin):
                # Override escape-key to close normally (and invoke closeEvent)
                self.close()
        
       -    def show_qr(self):
       -        text = bfh(str(self.tx))
       +    def add_export_actions_to_menu(self, menu: QMenu, *, gettx: Callable[[], Transaction] = None) -> None:
       +        if gettx is None:
       +            gettx = lambda: None
       +
       +        action = QAction(_("Copy to clipboard"), self)
       +        action.triggered.connect(lambda: self.copy_to_clipboard(tx=gettx()))
       +        menu.addAction(action)
       +
       +        qr_icon = "qrcode_white.png" if ColorScheme.dark_scheme else "qrcode.png"
       +        action = QAction(read_QIcon(qr_icon), _("Show as QR code"), self)
       +        action.triggered.connect(lambda: self.show_qr(tx=gettx()))
       +        menu.addAction(action)
       +
       +        action = QAction(_("Export to file"), self)
       +        action.triggered.connect(lambda: self.export_to_file(tx=gettx()))
       +        menu.addAction(action)
       +
       +    def _gettx_for_coinjoin(self) -> PartialTransaction:
       +        if not isinstance(self.tx, PartialTransaction):
       +            raise Exception("Can only export partial transactions for coinjoins.")
       +        tx = copy.deepcopy(self.tx)
       +        tx.prepare_for_export_for_coinjoin()
       +        return tx
       +
       +    def copy_to_clipboard(self, *, tx: Transaction = None):
       +        if tx is None:
       +            tx = self.tx
       +        self.main_window.app.clipboard().setText(str(tx))
       +
       +    def show_qr(self, *, tx: Transaction = None):
       +        if tx is None:
       +            tx = self.tx
       +        tx = copy.deepcopy(tx)  # make copy as we mutate tx
       +        if isinstance(tx, PartialTransaction):
       +            # this makes QR codes a lot smaller (or just possible in the first place!)
       +            tx.convert_all_utxos_to_witness_utxos()
       +        text = tx.serialize_as_bytes()
                text = base_encode(text, base=43)
                try:
                    self.main_window.show_qrcode(text, 'Transaction', parent=self)
       t@@ -222,17 +278,68 @@ class TxDialog(QDialog, MessageBoxMixin):
                    self.saved = True
                self.main_window.pop_top_level_window(self)
        
       -
       -    def export(self):
       -        name = 'signed_%s.txn' % (self.tx.txid()[0:8]) if self.tx.is_complete() else 'unsigned.txn'
       -        fileName = self.main_window.getSaveFileName(_("Select where to save your signed transaction"), name, "*.txn")
       -        if fileName:
       +    def export_to_file(self, *, tx: Transaction = None):
       +        if tx is None:
       +            tx = self.tx
       +        if isinstance(tx, PartialTransaction):
       +            tx.finalize_psbt()
       +        if tx.is_complete():
       +            name = 'signed_%s.txn' % (tx.txid()[0:8])
       +        else:
       +            name = self.wallet.basename() + time.strftime('-%Y%m%d-%H%M.psbt')
       +        fileName = self.main_window.getSaveFileName(_("Select where to save your signed transaction"), name, "*.txn;;*.psbt")
       +        if not fileName:
       +            return
       +        if tx.is_complete():  # network tx hex
                    with open(fileName, "w+") as f:
       -                f.write(json.dumps(self.tx.as_dict(), indent=4) + '\n')
       -            self.show_message(_("Transaction exported successfully"))
       -            self.saved = True
       +                network_tx_hex = tx.serialize_to_network()
       +                f.write(network_tx_hex + '\n')
       +        else:  # if partial: PSBT bytes
       +            assert isinstance(tx, PartialTransaction)
       +            with open(fileName, "wb+") as f:
       +                f.write(tx.serialize_as_bytes())
       +
       +        self.show_message(_("Transaction exported successfully"))
       +        self.saved = True
       +
       +    def merge_sigs(self):
       +        if not isinstance(self.tx, PartialTransaction):
       +            return
       +        text = text_dialog(self, _('Input raw transaction'),
       +                           _("Transaction to merge signatures from") + ":",
       +                           _("Load transaction"))
       +        if not text:
       +            return
       +        tx = self.main_window.tx_from_text(text)
       +        if not tx:
       +            return
       +        try:
       +            self.tx.combine_with_other_psbt(tx)
       +        except Exception as e:
       +            self.show_error(_("Error combining partial transactions") + ":\n" + repr(e))
       +            return
       +        self.update()
       +
       +    def join_tx_with_another(self):
       +        if not isinstance(self.tx, PartialTransaction):
       +            return
       +        text = text_dialog(self, _('Input raw transaction'),
       +                           _("Transaction to join with") + " (" + _("add inputs and outputs") + "):",
       +                           _("Load transaction"))
       +        if not text:
       +            return
       +        tx = self.main_window.tx_from_text(text)
       +        if not tx:
       +            return
       +        try:
       +            self.tx.join_with_other_psbt(tx)
       +        except Exception as e:
       +            self.show_error(_("Error joining partial transactions") + ":\n" + repr(e))
       +            return
       +        self.update()
        
            def update(self):
       +        self.update_io()
                desc = self.desc
                base_unit = self.main_window.base_unit()
                format_amount = self.main_window.format_amount
       t@@ -287,13 +394,17 @@ class TxDialog(QDialog, MessageBoxMixin):
                    feerate_warning = simple_config.FEERATE_WARNING_HIGH_FEE
                    if fee_rate > feerate_warning:
                        fee_str += ' - ' + _('Warning') + ': ' + _("high fee") + '!'
       +        if isinstance(self.tx, PartialTransaction):
       +            risk_of_burning_coins = (can_sign and fee is not None
       +                                     and self.tx.is_there_risk_of_burning_coins_as_fees())
       +            self.fee_warning_icon.setVisible(risk_of_burning_coins)
                self.amount_label.setText(amount_str)
                self.fee_label.setText(fee_str)
                self.size_label.setText(size_str)
                run_hook('transaction_dialog_update', self)
        
       -    def add_io(self, vbox):
       -        vbox.addWidget(QLabel(_("Inputs") + ' (%d)'%len(self.tx.inputs())))
       +    def update_io(self):
       +        self.inputs_header.setText(_("Inputs") + ' (%d)'%len(self.tx.inputs()))
                ext = QTextCharFormat()
                rec = QTextCharFormat()
                rec.setBackground(QBrush(ColorScheme.GREEN.as_color(background=True)))
       t@@ -315,39 +426,39 @@ class TxDialog(QDialog, MessageBoxMixin):
                def format_amount(amt):
                    return self.main_window.format_amount(amt, whitespaces=True)
        
       -        i_text = QTextEditWithDefaultSize()
       +        i_text = self.inputs_textedit
       +        i_text.clear()
                i_text.setFont(QFont(MONOSPACE_FONT))
                i_text.setReadOnly(True)
                cursor = i_text.textCursor()
       -        for x in self.tx.inputs():
       -            if x['type'] == 'coinbase':
       +        for txin in self.tx.inputs():
       +            if txin.is_coinbase():
                        cursor.insertText('coinbase')
                    else:
       -                prevout_hash = x.get('prevout_hash')
       -                prevout_n = x.get('prevout_n')
       +                prevout_hash = txin.prevout.txid.hex()
       +                prevout_n = txin.prevout.out_idx
                        cursor.insertText(prevout_hash + ":%-4d " % prevout_n, ext)
       -                addr = self.wallet.get_txin_address(x)
       +                addr = self.wallet.get_txin_address(txin)
                        if addr is None:
                            addr = ''
                        cursor.insertText(addr, text_format(addr))
       -                if x.get('value'):
       -                    cursor.insertText(format_amount(x['value']), ext)
       +                if isinstance(txin, PartialTxInput) and txin.value_sats() is not None:
       +                    cursor.insertText(format_amount(txin.value_sats()), ext)
                    cursor.insertBlock()
        
       -        vbox.addWidget(i_text)
       -        vbox.addWidget(QLabel(_("Outputs") + ' (%d)'%len(self.tx.outputs())))
       -        o_text = QTextEditWithDefaultSize()
       +        self.outputs_header.setText(_("Outputs") + ' (%d)'%len(self.tx.outputs()))
       +        o_text = self.outputs_textedit
       +        o_text.clear()
                o_text.setFont(QFont(MONOSPACE_FONT))
                o_text.setReadOnly(True)
                cursor = o_text.textCursor()
       -        for o in self.tx.get_outputs_for_UI():
       -            addr, v = o.address, o.value
       +        for o in self.tx.outputs():
       +            addr, v = o.get_ui_address_str(), o.value
                    cursor.insertText(addr, text_format(addr))
                    if v is not None:
                        cursor.insertText('\t', ext)
                        cursor.insertText(format_amount(v), ext)
                    cursor.insertBlock()
       -        vbox.addWidget(o_text)
        
            def add_tx_stats(self, vbox):
                hbox_stats = QHBoxLayout()
       t@@ -362,8 +473,24 @@ class TxDialog(QDialog, MessageBoxMixin):
                vbox_left.addWidget(self.date_label)
                self.amount_label = TxDetailLabel()
                vbox_left.addWidget(self.amount_label)
       +
       +        fee_hbox = QHBoxLayout()
                self.fee_label = TxDetailLabel()
       -        vbox_left.addWidget(self.fee_label)
       +        fee_hbox.addWidget(self.fee_label)
       +        self.fee_warning_icon = QLabel()
       +        pixmap = QPixmap(icon_path("warning"))
       +        pixmap_size = round(2 * char_width_in_lineedit())
       +        pixmap = pixmap.scaled(pixmap_size, pixmap_size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
       +        self.fee_warning_icon.setPixmap(pixmap)
       +        self.fee_warning_icon.setToolTip(_("Warning") + ": "
       +                                         + _("The fee could not be verified. Signing non-segwit inputs is risky:\n"
       +                                             "if this transaction was maliciously modified before you sign,\n"
       +                                             "you might end up paying a higher mining fee than displayed."))
       +        self.fee_warning_icon.setVisible(False)
       +        fee_hbox.addWidget(self.fee_warning_icon)
       +        fee_hbox.addStretch(1)
       +        vbox_left.addLayout(fee_hbox)
       +
                vbox_left.addStretch(1)
                hbox_stats.addLayout(vbox_left, 50)
        
   DIR diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py
       t@@ -840,13 +840,16 @@ def export_meta_gui(electrum_window, title, exporter):
        def get_parent_main_window(widget):
            """Returns a reference to the ElectrumWindow this widget belongs to."""
            from .main_window import ElectrumWindow
       +    from .transaction_dialog import TxDialog
            for _ in range(100):
                if widget is None:
                    return None
       -        if not isinstance(widget, ElectrumWindow):
       -            widget = widget.parentWidget()
       -        else:
       +        if isinstance(widget, ElectrumWindow):
                    return widget
       +        elif isinstance(widget, TxDialog):
       +            return widget.main_window
       +        else:
       +            widget = widget.parentWidget()
            return None
        
        
   DIR diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py
       t@@ -23,7 +23,7 @@
        # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        # SOFTWARE.
        
       -from typing import Optional, List
       +from typing import Optional, List, Dict
        from enum import IntEnum
        
        from PyQt5.QtCore import Qt
       t@@ -31,9 +31,11 @@ from PyQt5.QtGui import QStandardItemModel, QStandardItem, QFont
        from PyQt5.QtWidgets import QAbstractItemView, QMenu
        
        from electrum.i18n import _
       +from electrum.transaction import PartialTxInput
        
        from .util import MyTreeView, ColorScheme, MONOSPACE_FONT
        
       +
        class UTXOList(MyTreeView):
        
            class Columns(IntEnum):
       t@@ -64,21 +66,21 @@ class UTXOList(MyTreeView):
            def update(self):
                self.wallet = self.parent.wallet
                utxos = self.wallet.get_utxos()
       -        self.utxo_dict = {}
       +        self.utxo_dict = {}  # type: Dict[str, PartialTxInput]
                self.model().clear()
                self.update_headers(self.__class__.headers)
       -        for idx, x in enumerate(utxos):
       -            self.insert_utxo(idx, x)
       +        for idx, utxo in enumerate(utxos):
       +            self.insert_utxo(idx, utxo)
                self.filter()
        
       -    def insert_utxo(self, idx, x):
       -        address = x['address']
       -        height = x.get('height')
       -        name = x.get('prevout_hash') + ":%d"%x.get('prevout_n')
       -        name_short = x.get('prevout_hash')[:16] + '...' + ":%d"%x.get('prevout_n')
       -        self.utxo_dict[name] = x
       -        label = self.wallet.get_label(x.get('prevout_hash'))
       -        amount = self.parent.format_amount(x['value'], whitespaces=True)
       +    def insert_utxo(self, idx, utxo: PartialTxInput):
       +        address = utxo.address
       +        height = utxo.block_height
       +        name = utxo.prevout.to_str()
       +        name_short = utxo.prevout.txid.hex()[:16] + '...' + ":%d" % utxo.prevout.out_idx
       +        self.utxo_dict[name] = utxo
       +        label = self.wallet.get_label(utxo.prevout.txid.hex())
       +        amount = self.parent.format_amount(utxo.value_sats(), whitespaces=True)
                labels = [name_short, address, label, amount, '%d'%height]
                utxo_item = [QStandardItem(x) for x in labels]
                self.set_editability(utxo_item)
       t@@ -89,7 +91,7 @@ class UTXOList(MyTreeView):
                if self.wallet.is_frozen_address(address):
                    utxo_item[self.Columns.ADDRESS].setBackground(ColorScheme.BLUE.as_color(True))
                    utxo_item[self.Columns.ADDRESS].setToolTip(_('Address is frozen'))
       -        if self.wallet.is_frozen_coin(x):
       +        if self.wallet.is_frozen_coin(utxo):
                    utxo_item[self.Columns.OUTPOINT].setBackground(ColorScheme.BLUE.as_color(True))
                    utxo_item[self.Columns.OUTPOINT].setToolTip(f"{name}\n{_('Coin is frozen')}")
                else:
       t@@ -114,26 +116,26 @@ class UTXOList(MyTreeView):
                menu.addAction(_("Spend"), lambda: self.parent.spend_coins(coins))
                assert len(coins) >= 1, len(coins)
                if len(coins) == 1:
       -            utxo_dict = coins[0]
       -            addr = utxo_dict['address']
       -            txid = utxo_dict['prevout_hash']
       +            utxo = coins[0]
       +            addr = utxo.address
       +            txid = utxo.prevout.txid.hex()
                    # "Details"
                    tx = self.wallet.db.get_transaction(txid)
                    if tx:
                        label = self.wallet.get_label(txid) or None # Prefer None if empty (None hides the Description: field in the window)
       -                menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx, label))
       +                menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx, tx_desc=label))
                    # "Copy ..."
                    idx = self.indexAt(position)
                    if not idx.isValid():
                        return
                    self.add_copy_menu(menu, idx)
                    # "Freeze coin"
       -            if not self.wallet.is_frozen_coin(utxo_dict):
       -                menu.addAction(_("Freeze Coin"), lambda: self.parent.set_frozen_state_of_coins([utxo_dict], True))
       +            if not self.wallet.is_frozen_coin(utxo):
       +                menu.addAction(_("Freeze Coin"), lambda: self.parent.set_frozen_state_of_coins([utxo], True))
                    else:
                        menu.addSeparator()
                        menu.addAction(_("Coin is frozen"), lambda: None).setEnabled(False)
       -                menu.addAction(_("Unfreeze Coin"), lambda: self.parent.set_frozen_state_of_coins([utxo_dict], False))
       +                menu.addAction(_("Unfreeze Coin"), lambda: self.parent.set_frozen_state_of_coins([utxo], False))
                        menu.addSeparator()
                    # "Freeze address"
                    if not self.wallet.is_frozen_address(addr):
       t@@ -146,9 +148,9 @@ class UTXOList(MyTreeView):
                else:
                    # multiple items selected
                    menu.addSeparator()
       -            addrs = [utxo_dict['address'] for utxo_dict in coins]
       -            is_coin_frozen = [self.wallet.is_frozen_coin(utxo_dict) for utxo_dict in coins]
       -            is_addr_frozen = [self.wallet.is_frozen_address(utxo_dict['address']) for utxo_dict in coins]
       +            addrs = [utxo.address for utxo in coins]
       +            is_coin_frozen = [self.wallet.is_frozen_coin(utxo) for utxo in coins]
       +            is_addr_frozen = [self.wallet.is_frozen_address(utxo.address) for utxo in coins]
                    if not all(is_coin_frozen):
                        menu.addAction(_("Freeze Coins"), lambda: self.parent.set_frozen_state_of_coins(coins, True))
                    if any(is_coin_frozen):
   DIR diff --git a/electrum/gui/stdio.py b/electrum/gui/stdio.py
       t@@ -5,8 +5,8 @@ import logging
        
        from electrum import WalletStorage, Wallet
        from electrum.util import format_satoshis
       -from electrum.bitcoin import is_address, COIN, TYPE_ADDRESS
       -from electrum.transaction import TxOutput
       +from electrum.bitcoin import is_address, COIN
       +from electrum.transaction import PartialTxOutput
        from electrum.network import TxBroadcastError, BestEffortRequestFailed
        from electrum.logging import console_stderr_handler
        
       t@@ -197,8 +197,9 @@ class ElectrumGui:
                    if c == "n": return
        
                try:
       -            tx = self.wallet.mktx([TxOutput(TYPE_ADDRESS, self.str_recipient, amount)],
       -                                  password, self.config, fee)
       +            tx = self.wallet.mktx(outputs=[PartialTxOutput.from_address_and_value(self.str_recipient, amount)],
       +                                  password=password,
       +                                  fee=fee)
                except Exception as e:
                    print(repr(e))
                    return
   DIR diff --git a/electrum/gui/text.py b/electrum/gui/text.py
       t@@ -9,8 +9,8 @@ import logging
        
        import electrum
        from electrum.util import format_satoshis
       -from electrum.bitcoin import is_address, COIN, TYPE_ADDRESS
       -from electrum.transaction import TxOutput
       +from electrum.bitcoin import is_address, COIN
       +from electrum.transaction import PartialTxOutput
        from electrum.wallet import Wallet
        from electrum.storage import WalletStorage
        from electrum.network import NetworkParameters, TxBroadcastError, BestEffortRequestFailed
       t@@ -360,8 +360,9 @@ class ElectrumGui:
                else:
                    password = None
                try:
       -            tx = self.wallet.mktx([TxOutput(TYPE_ADDRESS, self.str_recipient, amount)],
       -                                  password, self.config, fee)
       +            tx = self.wallet.mktx(outputs=[PartialTxOutput.from_address_and_value(self.str_recipient, amount)],
       +                                  password=password,
       +                                  fee=fee)
                except Exception as e:
                    self.show_message(repr(e))
                    return
   DIR diff --git a/electrum/json_db.py b/electrum/json_db.py
       t@@ -28,7 +28,7 @@ import json
        import copy
        import threading
        from collections import defaultdict
       -from typing import Dict, Optional, List, Tuple, Set, Iterable, NamedTuple
       +from typing import Dict, Optional, List, Tuple, Set, Iterable, NamedTuple, Sequence
        
        from . import util, bitcoin
        from .util import profiler, WalletFileException, multisig_type, TxMinedInfo
       t@@ -40,15 +40,11 @@ from .logging import Logger
        
        OLD_SEED_VERSION = 4        # electrum versions < 2.0
        NEW_SEED_VERSION = 11       # electrum versions >= 2.0
       -FINAL_SEED_VERSION = 19     # electrum >= 2.7 will set this to prevent
       +FINAL_SEED_VERSION = 20     # electrum >= 2.7 will set this to prevent
                                    # old versions from overwriting new format
        
        
       -class JsonDBJsonEncoder(util.MyEncoder):
       -    def default(self, obj):
       -        if isinstance(obj, Transaction):
       -            return str(obj)
       -        return super().default(obj)
       +JsonDBJsonEncoder = util.MyEncoder
        
        
        class TxFeesValue(NamedTuple):
       t@@ -217,6 +213,7 @@ class JsonDB(Logger):
                self._convert_version_17()
                self._convert_version_18()
                self._convert_version_19()
       +        self._convert_version_20()
                self.put('seed_version', FINAL_SEED_VERSION)  # just to be sure
        
                self._after_upgrade_tasks()
       t@@ -425,10 +422,10 @@ class JsonDB(Logger):
                for txid, raw_tx in transactions.items():
                    tx = Transaction(raw_tx)
                    for txin in tx.inputs():
       -                if txin['type'] == 'coinbase':
       +                if txin.is_coinbase():
                            continue
       -                prevout_hash = txin['prevout_hash']
       -                prevout_n = txin['prevout_n']
       +                prevout_hash = txin.prevout.txid.hex()
       +                prevout_n = txin.prevout.out_idx
                        spent_outpoints[prevout_hash][str(prevout_n)] = txid
                self.put('spent_outpoints', spent_outpoints)
        
       t@@ -448,10 +445,45 @@ class JsonDB(Logger):
                self.put('tx_fees', None)
                self.put('seed_version', 19)
        
       -    # def _convert_version_20(self):
       -    #     TODO for "next" upgrade:
       -    #       - move "pw_hash_version" from keystore to storage
       -    #     pass
       +    def _convert_version_20(self):
       +        # store 'derivation' (prefix) and 'root_fingerprint' in all xpub-based keystores.
       +        # store explicit None values if we cannot retroactively determine them
       +        if not self._is_upgrade_method_needed(19, 19):
       +            return
       +
       +        from .bip32 import BIP32Node, convert_bip32_intpath_to_strpath
       +        # note: This upgrade method reimplements bip32.root_fp_and_der_prefix_from_xkey.
       +        #       This is done deliberately, to avoid introducing that method as a dependency to this upgrade.
       +        for ks_name in ('keystore', *['x{}/'.format(i) for i in range(1, 16)]):
       +            ks = self.get(ks_name, None)
       +            if ks is None: continue
       +            xpub = ks.get('xpub', None)
       +            if xpub is None: continue
       +            bip32node = BIP32Node.from_xkey(xpub)
       +            # derivation prefix
       +            derivation_prefix = ks.get('derivation', None)
       +            if derivation_prefix is None:
       +                assert bip32node.depth >= 0, bip32node.depth
       +                if bip32node.depth == 0:
       +                    derivation_prefix = 'm'
       +                elif bip32node.depth == 1:
       +                    child_number_int = int.from_bytes(bip32node.child_number, 'big')
       +                    derivation_prefix = convert_bip32_intpath_to_strpath([child_number_int])
       +                ks['derivation'] = derivation_prefix
       +            # root fingerprint
       +            root_fingerprint = ks.get('ckcc_xfp', None)
       +            if root_fingerprint is not None:
       +                root_fingerprint = root_fingerprint.to_bytes(4, byteorder="little", signed=False).hex().lower()
       +            if root_fingerprint is None:
       +                if bip32node.depth == 0:
       +                    root_fingerprint = bip32node.calc_fingerprint_of_this_node().hex().lower()
       +                elif bip32node.depth == 1:
       +                    root_fingerprint = bip32node.fingerprint.hex()
       +            ks['root_fingerprint'] = root_fingerprint
       +            ks.pop('ckcc_xfp', None)
       +            self.put(ks_name, ks)
       +
       +        self.put('seed_version', 20)
        
            def _convert_imported(self):
                if not self._is_upgrade_method_needed(0, 13):
       t@@ -758,16 +790,16 @@ class JsonDB(Logger):
        
            @modifier
            def add_change_address(self, addr):
       -        self._addr_to_addr_index[addr] = (True, len(self.change_addresses))
       +        self._addr_to_addr_index[addr] = (1, len(self.change_addresses))
                self.change_addresses.append(addr)
        
            @modifier
            def add_receiving_address(self, addr):
       -        self._addr_to_addr_index[addr] = (False, len(self.receiving_addresses))
       +        self._addr_to_addr_index[addr] = (0, len(self.receiving_addresses))
                self.receiving_addresses.append(addr)
        
            @locked
       -    def get_address_index(self, address):
       +    def get_address_index(self, address) -> Optional[Sequence[int]]:
                return self._addr_to_addr_index.get(address)
        
            @modifier
       t@@ -801,11 +833,11 @@ class JsonDB(Logger):
                            self.data['addresses'][name] = []
                    self.change_addresses = self.data['addresses']['change']
                    self.receiving_addresses = self.data['addresses']['receiving']
       -            self._addr_to_addr_index = {}  # key: address, value: (is_change, index)
       +            self._addr_to_addr_index = {}  # type: Dict[str, Sequence[int]]  # key: address, value: (is_change, index)
                    for i, addr in enumerate(self.receiving_addresses):
       -                self._addr_to_addr_index[addr] = (False, i)
       +                self._addr_to_addr_index[addr] = (0, i)
                    for i, addr in enumerate(self.change_addresses):
       -                self._addr_to_addr_index[addr] = (True, i)
       +                self._addr_to_addr_index[addr] = (1, i)
        
            @profiler
            def _load_transactions(self):
   DIR diff --git a/electrum/keystore.py b/electrum/keystore.py
       t@@ -26,16 +26,17 @@
        
        from unicodedata import normalize
        import hashlib
       -from typing import Tuple, TYPE_CHECKING, Union, Sequence
       +import re
       +from typing import Tuple, TYPE_CHECKING, Union, Sequence, Optional, Dict, List, NamedTuple
        
        from . import bitcoin, ecc, constants, bip32
       -from .bitcoin import (deserialize_privkey, serialize_privkey,
       -                      public_key_to_p2pkh)
       +from .bitcoin import deserialize_privkey, serialize_privkey
        from .bip32 import (convert_bip32_path_to_list_of_uint32, BIP32_PRIME,
       -                    is_xpub, is_xprv, BIP32Node)
       +                    is_xpub, is_xprv, BIP32Node, normalize_bip32_derivation,
       +                    convert_bip32_intpath_to_strpath)
        from .ecc import string_to_number, number_to_string
        from .crypto import (pw_decode, pw_encode, sha256, sha256d, PW_HASH_VERSION_LATEST,
       -                     SUPPORTED_PW_HASH_VERSIONS, UnsupportedPasswordHashVersion)
       +                     SUPPORTED_PW_HASH_VERSIONS, UnsupportedPasswordHashVersion, hash_160)
        from .util import (InvalidPassword, WalletFileException,
                           BitcoinException, bh2u, bfh, inv_dict)
        from .mnemonic import Mnemonic, load_wordlist, seed_type, is_seed
       t@@ -43,13 +44,14 @@ from .plugin import run_hook
        from .logging import Logger
        
        if TYPE_CHECKING:
       -    from .transaction import Transaction
       +    from .transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput
        
        
        class KeyStore(Logger):
        
            def __init__(self):
                Logger.__init__(self)
       +        self.is_requesting_to_be_rewritten_to_wallet_file = False  # type: bool
        
            def has_seed(self):
                return False
       t@@ -67,25 +69,19 @@ class KeyStore(Logger):
                """Returns whether the keystore can be encrypted with a password."""
                raise NotImplementedError()
        
       -    def get_tx_derivations(self, tx):
       +    def get_tx_derivations(self, tx: 'PartialTransaction') -> Dict[str, Union[Sequence[int], str]]:
                keypairs = {}
                for txin in tx.inputs():
       -            num_sig = txin.get('num_sig')
       -            if num_sig is None:
       +            if txin.is_complete():
                        continue
       -            x_signatures = txin['signatures']
       -            signatures = [sig for sig in x_signatures if sig]
       -            if len(signatures) == num_sig:
       -                # input is complete
       -                continue
       -            for k, x_pubkey in enumerate(txin['x_pubkeys']):
       -                if x_signatures[k] is not None:
       +            for pubkey in txin.pubkeys:
       +                if pubkey in txin.part_sigs:
                            # this pubkey already signed
                            continue
       -                derivation = self.get_pubkey_derivation(x_pubkey)
       +                derivation = self.get_pubkey_derivation(pubkey, txin)
                        if not derivation:
                            continue
       -                keypairs[x_pubkey] = derivation
       +                keypairs[pubkey.hex()] = derivation
                return keypairs
        
            def can_sign(self, tx):
       t@@ -108,9 +104,64 @@ class KeyStore(Logger):
            def decrypt_message(self, sequence, message, password) -> bytes:
                raise NotImplementedError()  # implemented by subclasses
        
       -    def sign_transaction(self, tx: 'Transaction', password) -> None:
       +    def sign_transaction(self, tx: 'PartialTransaction', password) -> None:
                raise NotImplementedError()  # implemented by subclasses
        
       +    def get_pubkey_derivation(self, pubkey: bytes,
       +                              txinout: Union['PartialTxInput', 'PartialTxOutput'],
       +                              *, only_der_suffix=True) \
       +            -> Union[Sequence[int], str, None]:
       +        """Returns either a derivation int-list if the pubkey can be HD derived from this keystore,
       +        the pubkey itself (hex) if the pubkey belongs to the keystore but not HD derived,
       +        or None if the pubkey is unrelated.
       +        """
       +        def test_der_suffix_against_pubkey(der_suffix: Sequence[int], pubkey: bytes) -> bool:
       +            if len(der_suffix) != 2:
       +                return False
       +            if pubkey.hex() != self.derive_pubkey(*der_suffix):
       +                return False
       +            return True
       +
       +        if hasattr(self, 'get_root_fingerprint'):
       +            if pubkey not in txinout.bip32_paths:
       +                return None
       +            fp_found, path_found = txinout.bip32_paths[pubkey]
       +            der_suffix = None
       +            full_path = None
       +            # try fp against our root
       +            my_root_fingerprint_hex = self.get_root_fingerprint()
       +            my_der_prefix_str = self.get_derivation_prefix()
       +            ks_der_prefix = convert_bip32_path_to_list_of_uint32(my_der_prefix_str) if my_der_prefix_str else None
       +            if (my_root_fingerprint_hex is not None and ks_der_prefix is not None and
       +                    fp_found.hex() == my_root_fingerprint_hex):
       +                if path_found[:len(ks_der_prefix)] == ks_der_prefix:
       +                    der_suffix = path_found[len(ks_der_prefix):]
       +                    if not test_der_suffix_against_pubkey(der_suffix, pubkey):
       +                        der_suffix = None
       +            # try fp against our intermediate fingerprint
       +            if (der_suffix is None and hasattr(self, 'xpub') and
       +                    fp_found == BIP32Node.from_xkey(self.xpub).calc_fingerprint_of_this_node()):
       +                der_suffix = path_found
       +                if not test_der_suffix_against_pubkey(der_suffix, pubkey):
       +                    der_suffix = None
       +            if der_suffix is None:
       +                return None
       +            if ks_der_prefix is not None:
       +                full_path = ks_der_prefix + list(der_suffix)
       +            return der_suffix if only_der_suffix else full_path
       +        return None
       +
       +    def find_my_pubkey_in_txinout(
       +            self, txinout: Union['PartialTxInput', 'PartialTxOutput'],
       +            *, only_der_suffix: bool = False
       +    ) -> Tuple[Optional[bytes], Optional[List[int]]]:
       +        # note: we assume that this cosigner only has one pubkey in this txin/txout
       +        for pubkey in txinout.bip32_paths:
       +            path = self.get_pubkey_derivation(pubkey, txinout, only_der_suffix=only_der_suffix)
       +            if path and not isinstance(path, (str, bytes)):
       +                return pubkey, list(path)
       +        return None, None
       +
        
        class Software_KeyStore(KeyStore):
        
       t@@ -210,14 +261,10 @@ class Imported_KeyStore(Software_KeyStore):
                    raise InvalidPassword()
                return privkey, compressed
        
       -    def get_pubkey_derivation(self, x_pubkey):
       -        if x_pubkey[0:2] in ['02', '03', '04']:
       -            if x_pubkey in self.keypairs.keys():
       -                return x_pubkey
       -        elif x_pubkey[0:2] == 'fd':
       -            addr = bitcoin.script_to_address(x_pubkey[2:])
       -            if addr in self.addresses:
       -                return self.addresses[addr].get('pubkey')
       +    def get_pubkey_derivation(self, pubkey, txin, *, only_der_suffix=True):
       +        if pubkey.hex() in self.keypairs:
       +            return pubkey.hex()
       +        return None
        
            def update_password(self, old_password, new_password):
                self.check_password(old_password)
       t@@ -230,7 +277,6 @@ class Imported_KeyStore(Software_KeyStore):
                self.pw_hash_version = PW_HASH_VERSION_LATEST
        
        
       -
        class Deterministic_KeyStore(Software_KeyStore):
        
            def __init__(self, d):
       t@@ -277,15 +323,85 @@ class Deterministic_KeyStore(Software_KeyStore):
        
        class Xpub:
        
       -    def __init__(self):
       +    def __init__(self, *, derivation_prefix: str = None, root_fingerprint: str = None):
                self.xpub = None
                self.xpub_receive = None
                self.xpub_change = None
        
       +        # "key origin" info (subclass should persist these):
       +        self._derivation_prefix = derivation_prefix  # type: Optional[str]
       +        self._root_fingerprint = root_fingerprint  # type: Optional[str]
       +
            def get_master_public_key(self):
                return self.xpub
        
       -    def derive_pubkey(self, for_change, n):
       +    def get_derivation_prefix(self) -> Optional[str]:
       +        """Returns to bip32 path from some root node to self.xpub
       +        Note that the return value might be None; if it is unknown.
       +        """
       +        return self._derivation_prefix
       +
       +    def get_root_fingerprint(self) -> Optional[str]:
       +        """Returns the bip32 fingerprint of the top level node.
       +        This top level node is the node at the beginning of the derivation prefix,
       +        i.e. applying the derivation prefix to it will result self.xpub
       +        Note that the return value might be None; if it is unknown.
       +        """
       +        return self._root_fingerprint
       +
       +    def get_fp_and_derivation_to_be_used_in_partial_tx(self, der_suffix: Sequence[int], *,
       +                                                       only_der_suffix: bool = True) -> Tuple[bytes, Sequence[int]]:
       +        """Returns fingerprint and derivation path corresponding to a derivation suffix.
       +        The fingerprint is either the root fp or the intermediate fp, depending on what is available
       +        and 'only_der_suffix', and the derivation path is adjusted accordingly.
       +        """
       +        fingerprint_hex = self.get_root_fingerprint()
       +        der_prefix_str = self.get_derivation_prefix()
       +        if not only_der_suffix and fingerprint_hex is not None and der_prefix_str is not None:
       +            # use root fp, and true full path
       +            fingerprint_bytes = bfh(fingerprint_hex)
       +            der_prefix_ints = convert_bip32_path_to_list_of_uint32(der_prefix_str)
       +        else:
       +            # use intermediate fp, and claim der suffix is the full path
       +            fingerprint_bytes = BIP32Node.from_xkey(self.xpub).calc_fingerprint_of_this_node()
       +            der_prefix_ints = convert_bip32_path_to_list_of_uint32('m')
       +        der_full = der_prefix_ints + list(der_suffix)
       +        return fingerprint_bytes, der_full
       +
       +    def get_xpub_to_be_used_in_partial_tx(self, *, only_der_suffix: bool) -> str:
       +        assert self.xpub
       +        fp_bytes, der_full = self.get_fp_and_derivation_to_be_used_in_partial_tx(der_suffix=[],
       +                                                                                 only_der_suffix=only_der_suffix)
       +        bip32node = BIP32Node.from_xkey(self.xpub)
       +        depth = len(der_full)
       +        child_number_int = der_full[-1] if len(der_full) >= 1 else 0
       +        child_number_bytes = child_number_int.to_bytes(length=4, byteorder="big")
       +        fingerprint = bytes(4) if depth == 0 else bip32node.fingerprint
       +        bip32node = bip32node._replace(depth=depth,
       +                                       fingerprint=fingerprint,
       +                                       child_number=child_number_bytes)
       +        return bip32node.to_xpub()
       +
       +    def add_key_origin_from_root_node(self, *, derivation_prefix: str, root_node: BIP32Node):
       +        assert self.xpub
       +        # try to derive ourselves from what we were given
       +        child_node1 = root_node.subkey_at_private_derivation(derivation_prefix)
       +        child_pubkey_bytes1 = child_node1.eckey.get_public_key_bytes(compressed=True)
       +        child_node2 = BIP32Node.from_xkey(self.xpub)
       +        child_pubkey_bytes2 = child_node2.eckey.get_public_key_bytes(compressed=True)
       +        if child_pubkey_bytes1 != child_pubkey_bytes2:
       +            raise Exception("(xpub, derivation_prefix, root_node) inconsistency")
       +        self.add_key_origin(derivation_prefix=derivation_prefix,
       +                            root_fingerprint=root_node.calc_fingerprint_of_this_node().hex().lower())
       +
       +    def add_key_origin(self, *, derivation_prefix: Optional[str], root_fingerprint: Optional[str]):
       +        assert self.xpub
       +        self._root_fingerprint = root_fingerprint
       +        self._derivation_prefix = normalize_bip32_derivation(derivation_prefix)
       +
       +    def derive_pubkey(self, for_change, n) -> str:
       +        for_change = int(for_change)
       +        assert for_change in (0, 1)
                xpub = self.xpub_change if for_change else self.xpub_receive
                if xpub is None:
                    rootnode = BIP32Node.from_xkey(self.xpub)
       t@@ -301,54 +417,13 @@ class Xpub:
                node = BIP32Node.from_xkey(xpub).subkey_at_public_derivation(sequence)
                return node.eckey.get_public_key_hex(compressed=True)
        
       -    def get_xpubkey(self, c, i):
       -        def encode_path_int(path_int) -> str:
       -            if path_int < 0xffff:
       -                hex = bitcoin.int_to_hex(path_int, 2)
       -            else:
       -                hex = 'ffff' + bitcoin.int_to_hex(path_int, 4)
       -            return hex
       -        s = ''.join(map(encode_path_int, (c, i)))
       -        return 'ff' + bh2u(bitcoin.DecodeBase58Check(self.xpub)) + s
       -
       -    @classmethod
       -    def parse_xpubkey(self, pubkey):
       -        # type + xpub + derivation
       -        assert pubkey[0:2] == 'ff'
       -        pk = bfh(pubkey)
       -        # xpub:
       -        pk = pk[1:]
       -        xkey = bitcoin.EncodeBase58Check(pk[0:78])
       -        # derivation:
       -        dd = pk[78:]
       -        s = []
       -        while dd:
       -            # 2 bytes for derivation path index
       -            n = int.from_bytes(dd[0:2], byteorder="little")
       -            dd = dd[2:]
       -            # in case of overflow, drop these 2 bytes; and use next 4 bytes instead
       -            if n == 0xffff:
       -                n = int.from_bytes(dd[0:4], byteorder="little")
       -                dd = dd[4:]
       -            s.append(n)
       -        assert len(s) == 2
       -        return xkey, s
       -
       -    def get_pubkey_derivation(self, x_pubkey):
       -        if x_pubkey[0:2] != 'ff':
       -            return
       -        xpub, derivation = self.parse_xpubkey(x_pubkey)
       -        if self.xpub != xpub:
       -            return
       -        return derivation
       -
        
        class BIP32_KeyStore(Deterministic_KeyStore, Xpub):
        
            type = 'bip32'
        
            def __init__(self, d):
       -        Xpub.__init__(self)
       +        Xpub.__init__(self, derivation_prefix=d.get('derivation'), root_fingerprint=d.get('root_fingerprint'))
                Deterministic_KeyStore.__init__(self, d)
                self.xpub = d.get('xpub')
                self.xprv = d.get('xprv')
       t@@ -360,6 +435,8 @@ class BIP32_KeyStore(Deterministic_KeyStore, Xpub):
                d = Deterministic_KeyStore.dump(self)
                d['xpub'] = self.xpub
                d['xprv'] = self.xprv
       +        d['derivation'] = self.get_derivation_prefix()
       +        d['root_fingerprint'] = self.get_root_fingerprint()
                return d
        
            def get_master_private_key(self, password):
       t@@ -388,14 +465,22 @@ class BIP32_KeyStore(Deterministic_KeyStore, Xpub):
            def is_watching_only(self):
                return self.xprv is None
        
       +    def add_xpub(self, xpub):
       +        assert is_xpub(xpub)
       +        self.xpub = xpub
       +        root_fingerprint, derivation_prefix = bip32.root_fp_and_der_prefix_from_xkey(xpub)
       +        self.add_key_origin(derivation_prefix=derivation_prefix, root_fingerprint=root_fingerprint)
       +
            def add_xprv(self, xprv):
       +        assert is_xprv(xprv)
                self.xprv = xprv
       -        self.xpub = bip32.xpub_from_xprv(xprv)
       +        self.add_xpub(bip32.xpub_from_xprv(xprv))
        
            def add_xprv_from_seed(self, bip32_seed, xtype, derivation):
                rootnode = BIP32Node.from_rootseed(bip32_seed, xtype=xtype)
                node = rootnode.subkey_at_private_derivation(derivation)
                self.add_xprv(node.to_xprv())
       +        self.add_key_origin_from_root_node(derivation_prefix=derivation, root_node=rootnode)
        
            def get_private_key(self, sequence, password):
                xprv = self.get_master_private_key(password)
       t@@ -415,6 +500,7 @@ class Old_KeyStore(Deterministic_KeyStore):
            def __init__(self, d):
                Deterministic_KeyStore.__init__(self, d)
                self.mpk = d.get('mpk')
       +        self._root_fingerprint = None
        
            def get_hex_seed(self, password):
                return pw_decode(self.seed, password, version=self.pw_hash_version).encode('utf8')
       t@@ -477,7 +563,7 @@ class Old_KeyStore(Deterministic_KeyStore):
                public_key = master_public_key + z*ecc.generator()
                return public_key.get_public_key_hex(compressed=False)
        
       -    def derive_pubkey(self, for_change, n):
       +    def derive_pubkey(self, for_change, n) -> str:
                return self.get_pubkey_from_mpk(self.mpk, for_change, n)
        
            def get_private_key_from_stretched_exponent(self, for_change, n, secexp):
       t@@ -508,31 +594,25 @@ class Old_KeyStore(Deterministic_KeyStore):
            def get_master_public_key(self):
                return self.mpk
        
       -    def get_xpubkey(self, for_change, n):
       -        s = ''.join(map(lambda x: bitcoin.int_to_hex(x,2), (for_change, n)))
       -        return 'fe' + self.mpk + s
       -
       -    @classmethod
       -    def parse_xpubkey(self, x_pubkey):
       -        assert x_pubkey[0:2] == 'fe'
       -        pk = x_pubkey[2:]
       -        mpk = pk[0:128]
       -        dd = pk[128:]
       -        s = []
       -        while dd:
       -            n = int(bitcoin.rev_hex(dd[0:4]), 16)
       -            dd = dd[4:]
       -            s.append(n)
       -        assert len(s) == 2
       -        return mpk, s
       -
       -    def get_pubkey_derivation(self, x_pubkey):
       -        if x_pubkey[0:2] != 'fe':
       -            return
       -        mpk, derivation = self.parse_xpubkey(x_pubkey)
       -        if self.mpk != mpk:
       -            return
       -        return derivation
       +    def get_derivation_prefix(self) -> str:
       +        return 'm'
       +
       +    def get_root_fingerprint(self) -> str:
       +        if self._root_fingerprint is None:
       +            master_public_key = ecc.ECPubkey(bfh('04'+self.mpk))
       +            xfp = hash_160(master_public_key.get_public_key_bytes(compressed=True))[0:4]
       +            self._root_fingerprint = xfp.hex().lower()
       +        return self._root_fingerprint
       +
       +    # TODO Old_KeyStore and Xpub could share a common baseclass?
       +    def get_fp_and_derivation_to_be_used_in_partial_tx(self, der_suffix: Sequence[int], *,
       +                                                       only_der_suffix: bool = True) -> Tuple[bytes, Sequence[int]]:
       +        fingerprint_hex = self.get_root_fingerprint()
       +        der_prefix_str = self.get_derivation_prefix()
       +        fingerprint_bytes = bfh(fingerprint_hex)
       +        der_prefix_ints = convert_bip32_path_to_list_of_uint32(der_prefix_str)
       +        der_full = der_prefix_ints + list(der_suffix)
       +        return fingerprint_bytes, der_full
        
            def update_password(self, old_password, new_password):
                self.check_password(old_password)
       t@@ -554,14 +634,13 @@ class Hardware_KeyStore(KeyStore, Xpub):
            type = 'hardware'
        
            def __init__(self, d):
       -        Xpub.__init__(self)
       +        Xpub.__init__(self, derivation_prefix=d.get('derivation'), root_fingerprint=d.get('root_fingerprint'))
                KeyStore.__init__(self)
                # Errors and other user interaction is done through the wallet's
                # handler.  The handler is per-window and preserved across
                # device reconnects
                self.xpub = d.get('xpub')
                self.label = d.get('label')
       -        self.derivation = d.get('derivation')
                self.handler = None
                run_hook('init_keystore', self)
        
       t@@ -582,7 +661,8 @@ class Hardware_KeyStore(KeyStore, Xpub):
                    'type': self.type,
                    'hw_type': self.hw_type,
                    'xpub': self.xpub,
       -            'derivation':self.derivation,
       +            'derivation': self.get_derivation_prefix(),
       +            'root_fingerprint': self.get_root_fingerprint(),
                    'label':self.label,
                }
        
       t@@ -624,6 +704,16 @@ class Hardware_KeyStore(KeyStore, Xpub):
            def ready_to_sign(self):
                return super().ready_to_sign() and self.has_usable_connection_with_device()
        
       +    def opportunistically_fill_in_missing_info_from_device(self, client):
       +        assert client is not None
       +        if self._root_fingerprint is None:
       +            # digitalbitbox (at least) does not reveal xpubs corresponding to unhardened paths
       +            # so ask for a direct child, and read out fingerprint from that:
       +            child_of_root_xpub = client.get_xpub("m/0'", xtype='standard')
       +            root_fingerprint = BIP32Node.from_xkey(child_of_root_xpub).fingerprint.hex().lower()
       +            self._root_fingerprint = root_fingerprint
       +            self.is_requesting_to_be_rewritten_to_wallet_file = True
       +
        
        def bip39_normalize_passphrase(passphrase):
            return normalize('NFKD', passphrase or '')
       t@@ -684,16 +774,17 @@ PURPOSE48_SCRIPT_TYPES_INV = inv_dict(PURPOSE48_SCRIPT_TYPES)
        
        def xtype_from_derivation(derivation: str) -> str:
            """Returns the script type to be used for this derivation."""
       -    if derivation.startswith("m/84'"):
       -        return 'p2wpkh'
       -    elif derivation.startswith("m/49'"):
       -        return 'p2wpkh-p2sh'
       -    elif derivation.startswith("m/44'"):
       -        return 'standard'
       -    elif derivation.startswith("m/45'"):
       -        return 'standard'
       -
            bip32_indices = convert_bip32_path_to_list_of_uint32(derivation)
       +    if len(bip32_indices) >= 1:
       +        if bip32_indices[0] == 84 + BIP32_PRIME:
       +            return 'p2wpkh'
       +        elif bip32_indices[0] == 49 + BIP32_PRIME:
       +            return 'p2wpkh-p2sh'
       +        elif bip32_indices[0] == 44 + BIP32_PRIME:
       +            return 'standard'
       +        elif bip32_indices[0] == 45 + BIP32_PRIME:
       +            return 'standard'
       +
            if len(bip32_indices) >= 4:
                if bip32_indices[0] == 48 + BIP32_PRIME:
                    # m / purpose' / coin_type' / account' / script_type' / change / address_index
       t@@ -704,40 +795,6 @@ def xtype_from_derivation(derivation: str) -> str:
            return 'standard'
        
        
       -# extended pubkeys
       -
       -def is_xpubkey(x_pubkey):
       -    return x_pubkey[0:2] == 'ff'
       -
       -
       -def parse_xpubkey(x_pubkey):
       -    assert x_pubkey[0:2] == 'ff'
       -    return BIP32_KeyStore.parse_xpubkey(x_pubkey)
       -
       -
       -def xpubkey_to_address(x_pubkey):
       -    if x_pubkey[0:2] == 'fd':
       -        address = bitcoin.script_to_address(x_pubkey[2:])
       -        return x_pubkey, address
       -    if x_pubkey[0:2] in ['02', '03', '04']:
       -        pubkey = x_pubkey
       -    elif x_pubkey[0:2] == 'ff':
       -        xpub, s = BIP32_KeyStore.parse_xpubkey(x_pubkey)
       -        pubkey = BIP32_KeyStore.get_pubkey_from_xpub(xpub, s)
       -    elif x_pubkey[0:2] == 'fe':
       -        mpk, s = Old_KeyStore.parse_xpubkey(x_pubkey)
       -        pubkey = Old_KeyStore.get_pubkey_from_mpk(mpk, s[0], s[1])
       -    else:
       -        raise BitcoinException("Cannot parse pubkey. prefix: {}"
       -                               .format(x_pubkey[0:2]))
       -    if pubkey:
       -        address = public_key_to_p2pkh(bfh(pubkey))
       -    return pubkey, address
       -
       -def xpubkey_to_pubkey(x_pubkey):
       -    pubkey, address = xpubkey_to_address(x_pubkey)
       -    return pubkey
       -
        hw_keystores = {}
        
        def register_keystore(hw_type, constructor):
       t@@ -770,7 +827,7 @@ def load_keystore(storage, name) -> KeyStore:
        
        def is_old_mpk(mpk: str) -> bool:
            try:
       -        int(mpk, 16)
       +        int(mpk, 16)  # test if hex string
            except:
                return False
            if len(mpk) != 128:
       t@@ -804,16 +861,18 @@ def is_private_key_list(text, *, allow_spaces_inside_key=True, raise_on_error=Fa
                                         raise_on_error=raise_on_error))
        
        
       -is_mpk = lambda x: is_old_mpk(x) or is_xpub(x)
       -is_private = lambda x: is_seed(x) or is_xprv(x) or is_private_key_list(x)
       -is_master_key = lambda x: is_old_mpk(x) or is_xprv(x) or is_xpub(x)
       -is_private_key = lambda x: is_xprv(x) or is_private_key_list(x)
       -is_bip32_key = lambda x: is_xprv(x) or is_xpub(x)
       +def is_master_key(x):
       +    return is_old_mpk(x) or is_bip32_key(x)
       +
       +
       +def is_bip32_key(x):
       +    return is_xprv(x) or is_xpub(x)
        
        
        def bip44_derivation(account_id, bip43_purpose=44):
            coin = constants.net.BIP44_COIN_TYPE
       -    return "m/%d'/%d'/%d'" % (bip43_purpose, coin, int(account_id))
       +    der = "m/%d'/%d'/%d'" % (bip43_purpose, coin, int(account_id))
       +    return normalize_bip32_derivation(der)
        
        
        def purpose48_derivation(account_id: int, xtype: str) -> str:
       t@@ -824,7 +883,8 @@ def purpose48_derivation(account_id: int, xtype: str) -> str:
            script_type_int = PURPOSE48_SCRIPT_TYPES.get(xtype)
            if script_type_int is None:
                raise Exception('unknown xtype: {}'.format(xtype))
       -    return "m/%d'/%d'/%d'/%d'" % (bip43_purpose, coin, account_id, script_type_int)
       +    der = "m/%d'/%d'/%d'/%d'" % (bip43_purpose, coin, account_id, script_type_int)
       +    return normalize_bip32_derivation(der)
        
        
        def from_seed(seed, passphrase, is_p2sh=False):
       t@@ -861,14 +921,12 @@ def from_old_mpk(mpk):
        
        def from_xpub(xpub):
            k = BIP32_KeyStore({})
       -    k.xpub = xpub
       +    k.add_xpub(xpub)
            return k
        
        def from_xprv(xprv):
       -    xpub = bip32.xpub_from_xprv(xprv)
            k = BIP32_KeyStore({})
       -    k.xprv = xprv
       -    k.xpub = xpub
       +    k.add_xprv(xprv)
            return k
        
        def from_master_key(text):
   DIR diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py
       t@@ -32,10 +32,9 @@ import time
        
        from . import ecc
        from .util import bfh, bh2u
       -from .bitcoin import TYPE_SCRIPT, TYPE_ADDRESS
        from .bitcoin import redeem_script_to_address
        from .crypto import sha256, sha256d
       -from .transaction import Transaction
       +from .transaction import Transaction, PartialTransaction
        from .logging import Logger
        
        from .lnonion import decode_onion_error
       t@@ -528,19 +527,19 @@ class Channel(Logger):
                ctx = self.make_commitment(subject, point, ctn)
                return secret, ctx
        
       -    def get_commitment(self, subject, ctn):
       +    def get_commitment(self, subject, ctn) -> PartialTransaction:
                secret, ctx = self.get_secret_and_commitment(subject, ctn)
                return ctx
        
       -    def get_next_commitment(self, subject: HTLCOwner) -> Transaction:
       +    def get_next_commitment(self, subject: HTLCOwner) -> PartialTransaction:
                ctn = self.get_next_ctn(subject)
                return self.get_commitment(subject, ctn)
        
       -    def get_latest_commitment(self, subject: HTLCOwner) -> Transaction:
       +    def get_latest_commitment(self, subject: HTLCOwner) -> PartialTransaction:
                ctn = self.get_latest_ctn(subject)
                return self.get_commitment(subject, ctn)
        
       -    def get_oldest_unrevoked_commitment(self, subject: HTLCOwner) -> Transaction:
       +    def get_oldest_unrevoked_commitment(self, subject: HTLCOwner) -> PartialTransaction:
                ctn = self.get_oldest_unrevoked_ctn(subject)
                return self.get_commitment(subject, ctn)
        
       t@@ -603,7 +602,7 @@ class Channel(Logger):
                self.hm.recv_fail(htlc_id)
        
            def pending_local_fee(self):
       -        return self.constraints.capacity - sum(x[2] for x in self.get_next_commitment(LOCAL).outputs())
       +        return self.constraints.capacity - sum(x.value for x in self.get_next_commitment(LOCAL).outputs())
        
            def update_fee(self, feerate: int, from_us: bool):
                # feerate uses sat/kw
       t@@ -658,7 +657,7 @@ class Channel(Logger):
            def __str__(self):
                return str(self.serialize())
        
       -    def make_commitment(self, subject, this_point, ctn) -> Transaction:
       +    def make_commitment(self, subject, this_point, ctn) -> PartialTransaction:
                assert type(subject) is HTLCOwner
                feerate = self.get_feerate(subject, ctn)
                other = REMOTE if LOCAL == subject else LOCAL
       t@@ -717,21 +716,20 @@ class Channel(Logger):
                    onchain_fees,
                    htlcs=htlcs)
        
       -    def get_local_index(self):
       -        return int(self.config[LOCAL].multisig_key.pubkey > self.config[REMOTE].multisig_key.pubkey)
       -
            def make_closing_tx(self, local_script: bytes, remote_script: bytes,
       -                        fee_sat: int) -> Tuple[bytes, Transaction]:
       +                        fee_sat: int) -> Tuple[bytes, PartialTransaction]:
                """ cooperative close """
       -        _, outputs = make_commitment_outputs({
       +        _, outputs = make_commitment_outputs(
       +                fees_per_participant={
                            LOCAL:  fee_sat * 1000 if     self.constraints.is_initiator else 0,
                            REMOTE: fee_sat * 1000 if not self.constraints.is_initiator else 0,
                        },
       -                self.balance(LOCAL),
       -                self.balance(REMOTE),
       -                (TYPE_SCRIPT, bh2u(local_script)),
       -                (TYPE_SCRIPT, bh2u(remote_script)),
       -                [], self.config[LOCAL].dust_limit_sat)
       +                local_amount_msat=self.balance(LOCAL),
       +                remote_amount_msat=self.balance(REMOTE),
       +                local_script=bh2u(local_script),
       +                remote_script=bh2u(remote_script),
       +                htlcs=[],
       +                dust_limit_sat=self.config[LOCAL].dust_limit_sat)
        
                closing_tx = make_closing_tx(self.config[LOCAL].multisig_key.pubkey,
                                             self.config[REMOTE].multisig_key.pubkey,
       t@@ -744,25 +742,23 @@ class Channel(Logger):
                sig = ecc.sig_string_from_der_sig(der_sig[:-1])
                return sig, closing_tx
        
       -    def signature_fits(self, tx):
       +    def signature_fits(self, tx: PartialTransaction):
                remote_sig = self.config[LOCAL].current_commitment_signature
                preimage_hex = tx.serialize_preimage(0)
       -        pre_hash = sha256d(bfh(preimage_hex))
       +        msg_hash = sha256d(bfh(preimage_hex))
                assert remote_sig
       -        res = ecc.verify_signature(self.config[REMOTE].multisig_key.pubkey, remote_sig, pre_hash)
       +        res = ecc.verify_signature(self.config[REMOTE].multisig_key.pubkey, remote_sig, msg_hash)
                return res
        
            def force_close_tx(self):
                tx = self.get_latest_commitment(LOCAL)
                assert self.signature_fits(tx)
       -        tx = Transaction(str(tx))
       -        tx.deserialize(True)
                tx.sign({bh2u(self.config[LOCAL].multisig_key.pubkey): (self.config[LOCAL].multisig_key.privkey, True)})
                remote_sig = self.config[LOCAL].current_commitment_signature
                remote_sig = ecc.der_sig_from_sig_string(remote_sig) + b"\x01"
       -        sigs = tx._inputs[0]["signatures"]
       -        none_idx = sigs.index(None)
       -        tx.add_signature_to_txin(0, none_idx, bh2u(remote_sig))
       +        tx.add_signature_to_txin(txin_idx=0,
       +                                 signing_pubkey=self.config[REMOTE].multisig_key.pubkey.hex(),
       +                                 sig=remote_sig.hex())
                assert tx.is_complete()
                return tx
        
   DIR diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py
       t@@ -11,7 +11,7 @@ import asyncio
        import os
        import time
        from functools import partial
       -from typing import List, Tuple, Dict, TYPE_CHECKING, Optional, Callable
       +from typing import List, Tuple, Dict, TYPE_CHECKING, Optional, Callable, Union
        import traceback
        import sys
        from datetime import datetime
       t@@ -24,7 +24,7 @@ from . import ecc
        from .ecc import sig_string_from_r_and_s, get_r_and_s_from_sig_string, der_sig_from_sig_string
        from . import constants
        from .util import bh2u, bfh, log_exceptions, list_enabled_bits, ignore_exceptions, chunks, SilentTaskGroup
       -from .transaction import Transaction, TxOutput
       +from .transaction import Transaction, TxOutput, PartialTxOutput
        from .logging import Logger
        from .lnonion import (new_onion_packet, decode_onion_error, OnionFailureCode, calc_hops_data_for_payment,
                              process_onion_packet, OnionPacket, construct_onion_error, OnionRoutingFailureMessage,
       t@@ -48,7 +48,7 @@ from .interface import GracefulDisconnect, NetworkException
        from .lnrouter import fee_for_edge_msat
        
        if TYPE_CHECKING:
       -    from .lnworker import LNWorker
       +    from .lnworker import LNWorker, LNGossip, LNWallet
            from .lnrouter import RouteEdge
        
        
       t@@ -62,7 +62,7 @@ def channel_id_from_funding_tx(funding_txid: str, funding_index: int) -> Tuple[b
        
        class Peer(Logger):
        
       -    def __init__(self, lnworker: 'LNWorker', pubkey:bytes, transport: LNTransportBase):
       +    def __init__(self, lnworker: Union['LNGossip', 'LNWallet'], pubkey:bytes, transport: LNTransportBase):
                self.initialized = asyncio.Event()
                self.querying = asyncio.Event()
                self.transport = transport
       t@@ -483,8 +483,8 @@ class Peer(Logger):
                                                 push_msat: int, temp_channel_id: bytes) -> Channel:
                wallet = self.lnworker.wallet
                # dry run creating funding tx to see if we even have enough funds
       -        funding_tx_test = wallet.mktx([TxOutput(bitcoin.TYPE_ADDRESS, wallet.dummy_address(), funding_sat)],
       -                                      password, nonlocal_only=True)
       +        funding_tx_test = wallet.mktx(outputs=[PartialTxOutput.from_address_and_value(wallet.dummy_address(), funding_sat)],
       +                                      password=password, nonlocal_only=True)
                await asyncio.wait_for(self.initialized.wait(), LN_P2P_NETWORK_TIMEOUT)
                feerate = self.lnworker.current_feerate_per_kw()
                local_config = self.make_local_config(funding_sat, push_msat, LOCAL)
       t@@ -563,8 +563,8 @@ class Peer(Logger):
                # create funding tx
                redeem_script = funding_output_script(local_config, remote_config)
                funding_address = bitcoin.redeem_script_to_address('p2wsh', redeem_script)
       -        funding_output = TxOutput(bitcoin.TYPE_ADDRESS, funding_address, funding_sat)
       -        funding_tx = wallet.mktx([funding_output], password, nonlocal_only=True)
       +        funding_output = PartialTxOutput.from_address_and_value(funding_address, funding_sat)
       +        funding_tx = wallet.mktx(outputs=[funding_output], password=password, nonlocal_only=True)
                funding_txid = funding_tx.txid()
                funding_index = funding_tx.outputs().index(funding_output)
                # remote commitment transaction
       t@@ -691,7 +691,7 @@ class Peer(Logger):
                outp = funding_tx.outputs()[funding_idx]
                redeem_script = funding_output_script(chan.config[REMOTE], chan.config[LOCAL])
                funding_address = bitcoin.redeem_script_to_address('p2wsh', redeem_script)
       -        if outp != TxOutput(bitcoin.TYPE_ADDRESS, funding_address, funding_sat):
       +        if not (outp.address == funding_address and outp.value == funding_sat):
                    chan.set_state('DISCONNECTED')
                    raise Exception('funding outpoint mismatch')
        
       t@@ -1485,11 +1485,13 @@ class Peer(Logger):
                        break
                    # TODO: negotiate better
                    our_fee = their_fee
       -        # index of our_sig
       -        i = chan.get_local_index()
                # add signatures
       -        closing_tx.add_signature_to_txin(0, i, bh2u(der_sig_from_sig_string(our_sig) + b'\x01'))
       -        closing_tx.add_signature_to_txin(0, 1-i, bh2u(der_sig_from_sig_string(their_sig) + b'\x01'))
       +        closing_tx.add_signature_to_txin(txin_idx=0,
       +                                         signing_pubkey=chan.config[LOCAL].multisig_key.pubkey,
       +                                         sig=bh2u(der_sig_from_sig_string(our_sig) + b'\x01'))
       +        closing_tx.add_signature_to_txin(txin_idx=0,
       +                                         signing_pubkey=chan.config[REMOTE].multisig_key.pubkey,
       +                                         sig=bh2u(der_sig_from_sig_string(their_sig) + b'\x01'))
                # broadcast
                await self.network.broadcast_transaction(closing_tx)
                return closing_tx.txid()
   DIR diff --git a/electrum/lnsweep.py b/electrum/lnsweep.py
       t@@ -6,7 +6,7 @@ from typing import Optional, Dict, List, Tuple, TYPE_CHECKING, NamedTuple, Calla
        from enum import Enum, auto
        
        from .util import bfh, bh2u
       -from .bitcoin import TYPE_ADDRESS, redeem_script_to_address, dust_threshold
       +from .bitcoin import redeem_script_to_address, dust_threshold
        from . import ecc
        from .lnutil import (make_commitment_output_to_remote_address, make_commitment_output_to_local_witness_script,
                             derive_privkey, derive_pubkey, derive_blinded_pubkey, derive_blinded_privkey,
       t@@ -15,7 +15,8 @@ from .lnutil import (make_commitment_output_to_remote_address, make_commitment_o
                             get_ordered_channel_configs, privkey_to_pubkey, get_per_commitment_secret_from_seed,
                             RevocationStore, extract_ctn_from_tx_and_chan, UnableToDeriveSecret, SENT, RECEIVED,
                             map_htlcs_to_ctx_output_idxs, Direction)
       -from .transaction import Transaction, TxOutput, construct_witness
       +from .transaction import (Transaction, TxOutput, construct_witness, PartialTransaction, PartialTxInput,
       +                          PartialTxOutput, TxOutpoint)
        from .simple_config import SimpleConfig
        from .logging import get_logger
        
       t@@ -254,7 +255,7 @@ def create_sweeptxs_for_our_ctx(*, chan: 'Channel', ctx: Transaction,
                    is_revocation=False,
                    config=chan.lnworker.config)
                # side effect
       -        txs[htlc_tx.prevout(0)] = SweepInfo(name='first-stage-htlc',
       +        txs[htlc_tx.inputs()[0].prevout.to_str()] = SweepInfo(name='first-stage-htlc',
                                                    csv_delay=0,
                                                    cltv_expiry=htlc_tx.locktime,
                                                    gen_tx=lambda: htlc_tx)
       t@@ -336,7 +337,7 @@ def create_sweeptxs_for_their_ctx(*, chan: 'Channel', ctx: Transaction,
                gen_tx = create_sweeptx_for_their_revoked_ctx(chan, ctx, per_commitment_secret, chan.sweep_address)
                if gen_tx:
                    tx = gen_tx()
       -            txs[tx.prevout(0)] = SweepInfo(name='to_local_for_revoked_ctx',
       +            txs[tx.inputs()[0].prevout.to_str()] = SweepInfo(name='to_local_for_revoked_ctx',
                                                   csv_delay=0,
                                                   cltv_expiry=0,
                                                   gen_tx=gen_tx)
       t@@ -433,66 +434,58 @@ def create_htlctx_that_spends_from_our_ctx(chan: 'Channel', our_pcp: bytes,
            local_htlc_sig = bfh(htlc_tx.sign_txin(0, local_htlc_privkey))
            txin = htlc_tx.inputs()[0]
            witness_program = bfh(Transaction.get_preimage_script(txin))
       -    txin['witness'] = bh2u(make_htlc_tx_witness(remote_htlc_sig, local_htlc_sig, preimage, witness_program))
       +    txin.witness = make_htlc_tx_witness(remote_htlc_sig, local_htlc_sig, preimage, witness_program)
            return witness_script, htlc_tx
        
        
        def create_sweeptx_their_ctx_htlc(ctx: Transaction, witness_script: bytes, sweep_address: str,
                                          preimage: Optional[bytes], output_idx: int,
                                          privkey: bytes, is_revocation: bool,
       -                                  cltv_expiry: int, config: SimpleConfig) -> Optional[Transaction]:
       +                                  cltv_expiry: int, config: SimpleConfig) -> Optional[PartialTransaction]:
            assert type(cltv_expiry) is int
            preimage = preimage or b''  # preimage is required iff (not is_revocation and htlc is offered)
            val = ctx.outputs()[output_idx].value
       -    sweep_inputs = [{
       -        'scriptSig': '',
       -        'type': 'p2wsh',
       -        'signatures': [],
       -        'num_sig': 0,
       -        'prevout_n': output_idx,
       -        'prevout_hash': ctx.txid(),
       -        'value': val,
       -        'coinbase': False,
       -        'preimage_script': bh2u(witness_script),
       -    }]
       +    prevout = TxOutpoint(txid=bfh(ctx.txid()), out_idx=output_idx)
       +    txin = PartialTxInput(prevout=prevout)
       +    txin._trusted_value_sats = val
       +    txin.witness_script = witness_script
       +    txin.script_sig = b''
       +    sweep_inputs = [txin]
            tx_size_bytes = 200  # TODO (depends on offered/received and is_revocation)
            fee = config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True)
            outvalue = val - fee
            if outvalue <= dust_threshold(): return None
       -    sweep_outputs = [TxOutput(TYPE_ADDRESS, sweep_address, outvalue)]
       -    tx = Transaction.from_io(sweep_inputs, sweep_outputs, version=2, locktime=cltv_expiry)
       +    sweep_outputs = [PartialTxOutput.from_address_and_value(sweep_address, outvalue)]
       +    tx = PartialTransaction.from_io(sweep_inputs, sweep_outputs, version=2, locktime=cltv_expiry)
            sig = bfh(tx.sign_txin(0, privkey))
            if not is_revocation:
                witness = construct_witness([sig, preimage, witness_script])
            else:
                revocation_pubkey = privkey_to_pubkey(privkey)
                witness = construct_witness([sig, revocation_pubkey, witness_script])
       -    tx.inputs()[0]['witness'] = witness
       +    tx.inputs()[0].witness = bfh(witness)
            assert tx.is_complete()
            return tx
        
        
        def create_sweeptx_their_ctx_to_remote(sweep_address: str, ctx: Transaction, output_idx: int,
                                               our_payment_privkey: ecc.ECPrivkey,
       -                                       config: SimpleConfig) -> Optional[Transaction]:
       +                                       config: SimpleConfig) -> Optional[PartialTransaction]:
            our_payment_pubkey = our_payment_privkey.get_public_key_hex(compressed=True)
            val = ctx.outputs()[output_idx].value
       -    sweep_inputs = [{
       -        'type': 'p2wpkh',
       -        'x_pubkeys': [our_payment_pubkey],
       -        'num_sig': 1,
       -        'prevout_n': output_idx,
       -        'prevout_hash': ctx.txid(),
       -        'value': val,
       -        'coinbase': False,
       -        'signatures': [None],
       -    }]
       +    prevout = TxOutpoint(txid=bfh(ctx.txid()), out_idx=output_idx)
       +    txin = PartialTxInput(prevout=prevout)
       +    txin._trusted_value_sats = val
       +    txin.script_type = 'p2wpkh'
       +    txin.pubkeys = [bfh(our_payment_pubkey)]
       +    txin.num_sig = 1
       +    sweep_inputs = [txin]
            tx_size_bytes = 110  # approx size of p2wpkh->p2wpkh
            fee = config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True)
            outvalue = val - fee
            if outvalue <= dust_threshold(): return None
       -    sweep_outputs = [TxOutput(TYPE_ADDRESS, sweep_address, outvalue)]
       -    sweep_tx = Transaction.from_io(sweep_inputs, sweep_outputs)
       +    sweep_outputs = [PartialTxOutput.from_address_and_value(sweep_address, outvalue)]
       +    sweep_tx = PartialTransaction.from_io(sweep_inputs, sweep_outputs)
            sweep_tx.set_rbf(True)
            sweep_tx.sign({our_payment_pubkey: (our_payment_privkey.get_secret_bytes(), True)})
            if not sweep_tx.is_complete():
       t@@ -502,7 +495,7 @@ def create_sweeptx_their_ctx_to_remote(sweep_address: str, ctx: Transaction, out
        
        def create_sweeptx_ctx_to_local(*, sweep_address: str, ctx: Transaction, output_idx: int, witness_script: str,
                                        privkey: bytes, is_revocation: bool, config: SimpleConfig,
       -                                to_self_delay: int=None) -> Optional[Transaction]:
       +                                to_self_delay: int=None) -> Optional[PartialTransaction]:
            """Create a txn that sweeps the 'to_local' output of a commitment
            transaction into our wallet.
        
       t@@ -510,61 +503,51 @@ def create_sweeptx_ctx_to_local(*, sweep_address: str, ctx: Transaction, output_
            is_revocation: tells us which ^
            """
            val = ctx.outputs()[output_idx].value
       -    sweep_inputs = [{
       -        'scriptSig': '',
       -        'type': 'p2wsh',
       -        'signatures': [],
       -        'num_sig': 0,
       -        'prevout_n': output_idx,
       -        'prevout_hash': ctx.txid(),
       -        'value': val,
       -        'coinbase': False,
       -        'preimage_script': witness_script,
       -    }]
       +    prevout = TxOutpoint(txid=bfh(ctx.txid()), out_idx=output_idx)
       +    txin = PartialTxInput(prevout=prevout)
       +    txin._trusted_value_sats = val
       +    txin.script_sig = b''
       +    txin.witness_script = bfh(witness_script)
       +    sweep_inputs = [txin]
            if not is_revocation:
                assert isinstance(to_self_delay, int)
       -        sweep_inputs[0]['sequence'] = to_self_delay
       +        sweep_inputs[0].nsequence = to_self_delay
            tx_size_bytes = 121  # approx size of to_local -> p2wpkh
            fee = config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True)
            outvalue = val - fee
            if outvalue <= dust_threshold():
                return None
       -    sweep_outputs = [TxOutput(TYPE_ADDRESS, sweep_address, outvalue)]
       -    sweep_tx = Transaction.from_io(sweep_inputs, sweep_outputs, version=2)
       +    sweep_outputs = [PartialTxOutput.from_address_and_value(sweep_address, outvalue)]
       +    sweep_tx = PartialTransaction.from_io(sweep_inputs, sweep_outputs, version=2)
            sig = sweep_tx.sign_txin(0, privkey)
            witness = construct_witness([sig, int(is_revocation), witness_script])
       -    sweep_tx.inputs()[0]['witness'] = witness
       +    sweep_tx.inputs()[0].witness = bfh(witness)
            return sweep_tx
        
        
        def create_sweeptx_that_spends_htlctx_that_spends_htlc_in_ctx(*,
                htlc_tx: Transaction, htlctx_witness_script: bytes, sweep_address: str,
                privkey: bytes, is_revocation: bool, to_self_delay: int,
       -        config: SimpleConfig) -> Optional[Transaction]:
       +        config: SimpleConfig) -> Optional[PartialTransaction]:
            val = htlc_tx.outputs()[0].value
       -    sweep_inputs = [{
       -        'scriptSig': '',
       -        'type': 'p2wsh',
       -        'signatures': [],
       -        'num_sig': 0,
       -        'prevout_n': 0,
       -        'prevout_hash': htlc_tx.txid(),
       -        'value': val,
       -        'coinbase': False,
       -        'preimage_script': bh2u(htlctx_witness_script),
       -    }]
       +    prevout = TxOutpoint(txid=bfh(htlc_tx.txid()), out_idx=0)
       +    txin = PartialTxInput(prevout=prevout)
       +    txin._trusted_value_sats = val
       +    txin.script_sig = b''
       +    txin.witness_script = htlctx_witness_script
       +    sweep_inputs = [txin]
            if not is_revocation:
                assert isinstance(to_self_delay, int)
       -        sweep_inputs[0]['sequence'] = to_self_delay
       +        sweep_inputs[0].nsequence = to_self_delay
            tx_size_bytes = 200  # TODO
            fee = config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True)
            outvalue = val - fee
            if outvalue <= dust_threshold(): return None
       -    sweep_outputs = [TxOutput(TYPE_ADDRESS, sweep_address, outvalue)]
       -    tx = Transaction.from_io(sweep_inputs, sweep_outputs, version=2)
       +    sweep_outputs = [PartialTxOutput.from_address_and_value(sweep_address, outvalue)]
       +    tx = PartialTransaction.from_io(sweep_inputs, sweep_outputs, version=2)
        
            sig = bfh(tx.sign_txin(0, privkey))
            witness = construct_witness([sig, int(is_revocation), htlctx_witness_script])
       -    tx.inputs()[0]['witness'] = witness
       +    tx.inputs()[0].witness = bfh(witness)
            assert tx.is_complete()
            return tx
   DIR diff --git a/electrum/lnutil.py b/electrum/lnutil.py
       t@@ -10,11 +10,11 @@ import re
        
        from .util import bfh, bh2u, inv_dict
        from .crypto import sha256
       -from .transaction import Transaction
       +from .transaction import (Transaction, PartialTransaction, PartialTxInput, TxOutpoint,
       +                          PartialTxOutput, opcodes, TxOutput)
        from .ecc import CURVE_ORDER, sig_string_from_der_sig, ECPubkey, string_to_number
        from . import ecc, bitcoin, crypto, transaction
       -from .transaction import opcodes, TxOutput, Transaction
       -from .bitcoin import push_script, redeem_script_to_address, TYPE_ADDRESS
       +from .bitcoin import push_script, redeem_script_to_address, address_to_script
        from . import segwit_addr
        from .i18n import _
        from .lnaddr import lndecode
       t@@ -97,6 +97,7 @@ class ScriptHtlc(NamedTuple):
            htlc: 'UpdateAddHtlc'
        
        
       +# FIXME duplicate of TxOutpoint in transaction.py??
        class Outpoint(NamedTuple("Outpoint", [('txid', str), ('output_index', int)])):
            def to_str(self):
                return "{}:{}".format(self.txid, self.output_index)
       t@@ -287,7 +288,7 @@ def make_htlc_tx_output(amount_msat, local_feerate, revocationpubkey, local_dela
            fee = fee // 1000 * 1000
            final_amount_sat = (amount_msat - fee) // 1000
            assert final_amount_sat > 0, final_amount_sat
       -    output = TxOutput(bitcoin.TYPE_ADDRESS, p2wsh, final_amount_sat)
       +    output = PartialTxOutput.from_address_and_value(p2wsh, final_amount_sat)
            return script, output
        
        def make_htlc_tx_witness(remotehtlcsig: bytes, localhtlcsig: bytes,
       t@@ -299,29 +300,23 @@ def make_htlc_tx_witness(remotehtlcsig: bytes, localhtlcsig: bytes,
            return bfh(transaction.construct_witness([0, remotehtlcsig, localhtlcsig, payment_preimage, witness_script]))
        
        def make_htlc_tx_inputs(htlc_output_txid: str, htlc_output_index: int,
       -                        amount_msat: int, witness_script: str) -> List[dict]:
       +                        amount_msat: int, witness_script: str) -> List[PartialTxInput]:
            assert type(htlc_output_txid) is str
            assert type(htlc_output_index) is int
            assert type(amount_msat) is int
            assert type(witness_script) is str
       -    c_inputs = [{
       -        'scriptSig': '',
       -        'type': 'p2wsh',
       -        'signatures': [],
       -        'num_sig': 0,
       -        'prevout_n': htlc_output_index,
       -        'prevout_hash': htlc_output_txid,
       -        'value': amount_msat // 1000,
       -        'coinbase': False,
       -        'sequence': 0x0,
       -        'preimage_script': witness_script,
       -    }]
       +    txin = PartialTxInput(prevout=TxOutpoint(txid=bfh(htlc_output_txid), out_idx=htlc_output_index),
       +                          nsequence=0)
       +    txin.witness_script = bfh(witness_script)
       +    txin.script_sig = b''
       +    txin._trusted_value_sats = amount_msat // 1000
       +    c_inputs = [txin]
            return c_inputs
        
       -def make_htlc_tx(*, cltv_expiry: int, inputs, output) -> Transaction:
       +def make_htlc_tx(*, cltv_expiry: int, inputs: List[PartialTxInput], output: PartialTxOutput) -> PartialTransaction:
            assert type(cltv_expiry) is int
            c_outputs = [output]
       -    tx = Transaction.from_io(inputs, c_outputs, locktime=cltv_expiry, version=2)
       +    tx = PartialTransaction.from_io(inputs, c_outputs, locktime=cltv_expiry, version=2)
            return tx
        
        def make_offered_htlc(revocation_pubkey: bytes, remote_htlcpubkey: bytes,
       t@@ -437,7 +432,7 @@ def map_htlcs_to_ctx_output_idxs(*, chan: 'Channel', ctx: Transaction, pcp: byte
        
        def make_htlc_tx_with_open_channel(*, chan: 'Channel', pcp: bytes, subject: 'HTLCOwner',
                                           htlc_direction: 'Direction', commit: Transaction, ctx_output_idx: int,
       -                                   htlc: 'UpdateAddHtlc', name: str = None) -> Tuple[bytes, Transaction]:
       +                                   htlc: 'UpdateAddHtlc', name: str = None) -> Tuple[bytes, PartialTransaction]:
            amount_msat, cltv_expiry, payment_hash = htlc.amount_msat, htlc.cltv_expiry, htlc.payment_hash
            for_us = subject == LOCAL
            conf, other_conf = get_ordered_channel_configs(chan=chan, for_us=for_us)
       t@@ -472,19 +467,15 @@ def make_htlc_tx_with_open_channel(*, chan: 'Channel', pcp: bytes, subject: 'HTL
            return witness_script_of_htlc_tx_output, htlc_tx
        
        def make_funding_input(local_funding_pubkey: bytes, remote_funding_pubkey: bytes,
       -        funding_pos: int, funding_txid: bytes, funding_sat: int):
       +        funding_pos: int, funding_txid: str, funding_sat: int) -> PartialTxInput:
            pubkeys = sorted([bh2u(local_funding_pubkey), bh2u(remote_funding_pubkey)])
            # commitment tx input
       -    c_input = {
       -        'type': 'p2wsh',
       -        'x_pubkeys': pubkeys,
       -        'signatures': [None, None],
       -        'num_sig': 2,
       -        'prevout_n': funding_pos,
       -        'prevout_hash': funding_txid,
       -        'value': funding_sat,
       -        'coinbase': False,
       -    }
       +    prevout = TxOutpoint(txid=bfh(funding_txid), out_idx=funding_pos)
       +    c_input = PartialTxInput(prevout=prevout)
       +    c_input.script_type = 'p2wsh'
       +    c_input.pubkeys = [bfh(pk) for pk in pubkeys]
       +    c_input.num_sig = 2
       +    c_input._trusted_value_sats = funding_sat
            return c_input
        
        class HTLCOwner(IntFlag):
       t@@ -504,18 +495,18 @@ RECEIVED = Direction.RECEIVED
        LOCAL = HTLCOwner.LOCAL
        REMOTE = HTLCOwner.REMOTE
        
       -def make_commitment_outputs(fees_per_participant: Mapping[HTLCOwner, int], local_amount: int, remote_amount: int,
       -        local_tupl, remote_tupl, htlcs: List[ScriptHtlc], dust_limit_sat: int) -> Tuple[List[TxOutput], List[TxOutput]]:
       -    to_local_amt = local_amount - fees_per_participant[LOCAL]
       -    to_local = TxOutput(*local_tupl, to_local_amt // 1000)
       -    to_remote_amt = remote_amount - fees_per_participant[REMOTE]
       -    to_remote = TxOutput(*remote_tupl, to_remote_amt // 1000)
       +def make_commitment_outputs(*, fees_per_participant: Mapping[HTLCOwner, int], local_amount_msat: int, remote_amount_msat: int,
       +        local_script: str, remote_script: str, htlcs: List[ScriptHtlc], dust_limit_sat: int) -> Tuple[List[PartialTxOutput], List[PartialTxOutput]]:
       +    to_local_amt = local_amount_msat - fees_per_participant[LOCAL]
       +    to_local = PartialTxOutput(scriptpubkey=bfh(local_script), value=to_local_amt // 1000)
       +    to_remote_amt = remote_amount_msat - fees_per_participant[REMOTE]
       +    to_remote = PartialTxOutput(scriptpubkey=bfh(remote_script), value=to_remote_amt // 1000)
            non_htlc_outputs = [to_local, to_remote]
            htlc_outputs = []
            for script, htlc in htlcs:
       -        htlc_outputs.append(TxOutput(bitcoin.TYPE_ADDRESS,
       -                               bitcoin.redeem_script_to_address('p2wsh', bh2u(script)),
       -                               htlc.amount_msat // 1000))
       +        addr = bitcoin.redeem_script_to_address('p2wsh', bh2u(script))
       +        htlc_outputs.append(PartialTxOutput(scriptpubkey=bfh(address_to_script(addr)),
       +                                            value=htlc.amount_msat // 1000))
        
            # trim outputs
            c_outputs_filtered = list(filter(lambda x: x.value >= dust_limit_sat, non_htlc_outputs + htlc_outputs))
       t@@ -533,13 +524,13 @@ def make_commitment(ctn, local_funding_pubkey, remote_funding_pubkey,
                            delayed_pubkey, to_self_delay, funding_txid,
                            funding_pos, funding_sat, local_amount, remote_amount,
                            dust_limit_sat, fees_per_participant,
       -                    htlcs: List[ScriptHtlc]) -> Transaction:
       +                    htlcs: List[ScriptHtlc]) -> PartialTransaction:
            c_input = make_funding_input(local_funding_pubkey, remote_funding_pubkey,
                                         funding_pos, funding_txid, funding_sat)
            obs = get_obscured_ctn(ctn, funder_payment_basepoint, fundee_payment_basepoint)
            locktime = (0x20 << 24) + (obs & 0xffffff)
            sequence = (0x80 << 24) + (obs >> 24)
       -    c_input['sequence'] = sequence
       +    c_input.nsequence = sequence
        
            c_inputs = [c_input]
        
       t@@ -555,13 +546,19 @@ def make_commitment(ctn, local_funding_pubkey, remote_funding_pubkey,
            htlcs = list(htlcs)
            htlcs.sort(key=lambda x: x.htlc.cltv_expiry)
        
       -    htlc_outputs, c_outputs_filtered = make_commitment_outputs(fees_per_participant, local_amount, remote_amount,
       -        (bitcoin.TYPE_ADDRESS, local_address), (bitcoin.TYPE_ADDRESS, remote_address), htlcs, dust_limit_sat)
       +    htlc_outputs, c_outputs_filtered = make_commitment_outputs(
       +        fees_per_participant=fees_per_participant,
       +        local_amount_msat=local_amount,
       +        remote_amount_msat=remote_amount,
       +        local_script=address_to_script(local_address),
       +        remote_script=address_to_script(remote_address),
       +        htlcs=htlcs,
       +        dust_limit_sat=dust_limit_sat)
        
            assert sum(x.value for x in c_outputs_filtered) <= funding_sat, (c_outputs_filtered, funding_sat)
        
            # create commitment tx
       -    tx = Transaction.from_io(c_inputs, c_outputs_filtered, locktime=locktime, version=2)
       +    tx = PartialTransaction.from_io(c_inputs, c_outputs_filtered, locktime=locktime, version=2)
            return tx
        
        def make_commitment_output_to_local_witness_script(
       t@@ -578,11 +575,9 @@ def make_commitment_output_to_local_address(
        def make_commitment_output_to_remote_address(remote_payment_pubkey: bytes) -> str:
            return bitcoin.pubkey_to_address('p2wpkh', bh2u(remote_payment_pubkey))
        
       -def sign_and_get_sig_string(tx, local_config, remote_config):
       -    pubkeys = sorted([bh2u(local_config.multisig_key.pubkey), bh2u(remote_config.multisig_key.pubkey)])
       +def sign_and_get_sig_string(tx: PartialTransaction, local_config, remote_config):
            tx.sign({bh2u(local_config.multisig_key.pubkey): (local_config.multisig_key.privkey, True)})
       -    sig_index = pubkeys.index(bh2u(local_config.multisig_key.pubkey))
       -    sig = bytes.fromhex(tx.inputs()[0]["signatures"][sig_index])
       +    sig = tx.inputs()[0].part_sigs[local_config.multisig_key.pubkey]
            sig_64 = sig_string_from_der_sig(sig[:-1])
            return sig_64
        
       t@@ -598,11 +593,11 @@ def get_obscured_ctn(ctn: int, funder: bytes, fundee: bytes) -> int:
            mask = int.from_bytes(sha256(funder + fundee)[-6:], 'big')
            return ctn ^ mask
        
       -def extract_ctn_from_tx(tx, txin_index: int, funder_payment_basepoint: bytes,
       +def extract_ctn_from_tx(tx: Transaction, txin_index: int, funder_payment_basepoint: bytes,
                                fundee_payment_basepoint: bytes) -> int:
            tx.deserialize()
            locktime = tx.locktime
       -    sequence = tx.inputs()[txin_index]['sequence']
       +    sequence = tx.inputs()[txin_index].nsequence
            obs = ((sequence & 0xffffff) << 24) + (locktime & 0xffffff)
            return get_obscured_ctn(obs, funder_payment_basepoint, fundee_payment_basepoint)
        
       t@@ -671,12 +666,12 @@ def get_compressed_pubkey_from_bech32(bech32_pubkey: str) -> bytes:
        
        
        def make_closing_tx(local_funding_pubkey: bytes, remote_funding_pubkey: bytes,
       -                    funding_txid: bytes, funding_pos: int, funding_sat: int,
       -                    outputs: List[TxOutput]) -> Transaction:
       +                    funding_txid: str, funding_pos: int, funding_sat: int,
       +                    outputs: List[PartialTxOutput]) -> PartialTransaction:
            c_input = make_funding_input(local_funding_pubkey, remote_funding_pubkey,
                funding_pos, funding_txid, funding_sat)
       -    c_input['sequence'] = 0xFFFF_FFFF
       -    tx = Transaction.from_io([c_input], outputs, locktime=0, version=2)
       +    c_input.nsequence = 0xFFFF_FFFF
       +    tx = PartialTransaction.from_io([c_input], outputs, locktime=0, version=2)
            return tx
        
        
   DIR diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py
       t@@ -77,9 +77,11 @@ class SweepStore(SqlDB):
                return set([r[0] for r in c.fetchall()])
        
            @sql
       -    def add_sweep_tx(self, funding_outpoint, ctn, prevout, tx):
       +    def add_sweep_tx(self, funding_outpoint, ctn, prevout, tx: Transaction):
                c = self.conn.cursor()
       -        c.execute("""INSERT INTO sweep_txs (funding_outpoint, ctn, prevout, tx) VALUES (?,?,?,?)""", (funding_outpoint, ctn, prevout, bfh(str(tx))))
       +        assert tx.is_complete()
       +        raw_tx = bfh(tx.serialize())
       +        c.execute("""INSERT INTO sweep_txs (funding_outpoint, ctn, prevout, tx) VALUES (?,?,?,?)""", (funding_outpoint, ctn, prevout, raw_tx))
                self.conn.commit()
        
            @sql
   DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py
       t@@ -375,7 +375,7 @@ class LNWallet(LNWorker):
                for ctn in range(watchtower_ctn + 1, current_ctn):
                    sweeptxs = chan.create_sweeptxs(ctn)
                    for tx in sweeptxs:
       -                await watchtower.add_sweep_tx(outpoint, ctn, tx.prevout(0), str(tx))
       +                await watchtower.add_sweep_tx(outpoint, ctn, tx.inputs()[0].prevout.to_str(), tx.serialize())
        
            def start_network(self, network: 'Network'):
                self.lnwatcher = LNWatcher(network)
   DIR diff --git a/electrum/network.py b/electrum/network.py
       t@@ -64,6 +64,7 @@ if TYPE_CHECKING:
            from .channel_db import ChannelDB
            from .lnworker import LNGossip
            from .lnwatcher import WatchTower
       +    from .transaction import Transaction
        
        
        _logger = get_logger(__name__)
       t@@ -887,11 +888,11 @@ class Network(Logger):
                return await self.interface.session.send_request('blockchain.transaction.get_merkle', [tx_hash, tx_height])
        
            @best_effort_reliable
       -    async def broadcast_transaction(self, tx, *, timeout=None) -> None:
       +    async def broadcast_transaction(self, tx: 'Transaction', *, timeout=None) -> None:
                if timeout is None:
                    timeout = self.get_network_timeout_seconds(NetworkTimeout.Urgent)
                try:
       -            out = await self.interface.session.send_request('blockchain.transaction.broadcast', [str(tx)], timeout=timeout)
       +            out = await self.interface.session.send_request('blockchain.transaction.broadcast', [tx.serialize()], timeout=timeout)
                    # note: both 'out' and exception messages are untrusted input from the server
                except (RequestTimedOut, asyncio.CancelledError, asyncio.TimeoutError):
                    raise  # pass-through
   DIR diff --git a/electrum/paymentrequest.py b/electrum/paymentrequest.py
       t@@ -25,7 +25,7 @@
        import hashlib
        import sys
        import time
       -from typing import Optional
       +from typing import Optional, List
        import asyncio
        import urllib.parse
        
       t@@ -42,8 +42,8 @@ from . import bitcoin, ecc, util, transaction, x509, rsakey
        from .util import bh2u, bfh, export_meta, import_meta, make_aiohttp_session
        from .util import PR_UNPAID, PR_EXPIRED, PR_PAID, PR_UNKNOWN, PR_INFLIGHT
        from .crypto import sha256
       -from .bitcoin import TYPE_ADDRESS
       -from .transaction import TxOutput
       +from .bitcoin import address_to_script
       +from .transaction import PartialTxOutput
        from .network import Network
        from .logging import get_logger, Logger
        
       t@@ -128,7 +128,7 @@ class PaymentRequest:
                return str(self.raw)
        
            def parse(self, r):
       -        self.outputs = []
       +        self.outputs = []  # type: List[PartialTxOutput]
                if self.error:
                    return
                self.id = bh2u(sha256(r)[0:16])
       t@@ -141,12 +141,12 @@ class PaymentRequest:
                self.details = pb2.PaymentDetails()
                self.details.ParseFromString(self.data.serialized_payment_details)
                for o in self.details.outputs:
       -            type_, addr = transaction.get_address_from_output_script(o.script)
       -            if type_ != TYPE_ADDRESS:
       +            addr = transaction.get_address_from_output_script(o.script)
       +            if not addr:
                        # TODO maybe rm restriction but then get_requestor and get_id need changes
                        self.error = "only addresses are allowed as outputs"
                        return
       -            self.outputs.append(TxOutput(type_, addr, o.amount))
       +            self.outputs.append(PartialTxOutput.from_address_and_value(addr, o.amount))
                self.memo = self.details.memo
                self.payment_url = self.details.payment_url
        
       t@@ -252,8 +252,9 @@ class PaymentRequest:
        
            def get_address(self):
                o = self.outputs[0]
       -        assert o.type == TYPE_ADDRESS
       -        return o.address
       +        addr = o.address
       +        assert addr
       +        return addr
        
            def get_requestor(self):
                return self.requestor if self.requestor else self.get_address()
       t@@ -278,7 +279,7 @@ class PaymentRequest:
                paymnt.merchant_data = pay_det.merchant_data
                paymnt.transactions.append(bfh(raw_tx))
                ref_out = paymnt.refund_to.add()
       -        ref_out.script = util.bfh(transaction.Transaction.pay_script(TYPE_ADDRESS, refund_addr))
       +        ref_out.script = util.bfh(address_to_script(refund_addr))
                paymnt.memo = "Paid using Electrum"
                pm = paymnt.SerializeToString()
                payurl = urllib.parse.urlparse(pay_det.payment_url)
       t@@ -326,7 +327,7 @@ def make_unsigned_request(req):
            if amount is None:
                amount = 0
            memo = req['memo']
       -    script = bfh(Transaction.pay_script(TYPE_ADDRESS, addr))
       +    script = bfh(address_to_script(addr))
            outputs = [(script, amount)]
            pd = pb2.PaymentDetails()
            for script, amount in outputs:
   DIR diff --git a/electrum/plugin.py b/electrum/plugin.py
       t@@ -39,6 +39,7 @@ from .logging import get_logger, Logger
        
        if TYPE_CHECKING:
            from .plugins.hw_wallet import HW_PluginBase
       +    from .keystore import Hardware_KeyStore
        
        
        _logger = get_logger(__name__)
       t@@ -442,20 +443,23 @@ class DeviceMgr(ThreadJob):
                self.scan_devices()
                return self.client_lookup(id_)
        
       -    def client_for_keystore(self, plugin, handler, keystore, force_pair):
       +    def client_for_keystore(self, plugin, handler, keystore: 'Hardware_KeyStore', force_pair):
                self.logger.info("getting client for keystore")
                if handler is None:
                    raise Exception(_("Handler not found for") + ' ' + plugin.name + '\n' + _("A library is probably missing."))
                handler.update_status(False)
                devices = self.scan_devices()
                xpub = keystore.xpub
       -        derivation = keystore.get_derivation()
       +        derivation = keystore.get_derivation_prefix()
       +        assert derivation is not None
                client = self.client_by_xpub(plugin, xpub, handler, devices)
                if client is None and force_pair:
                    info = self.select_device(plugin, handler, keystore, devices)
                    client = self.force_pair_xpub(plugin, handler, info, xpub, derivation, devices)
                if client:
                    handler.update_status(True)
       +        if client:
       +            keystore.opportunistically_fill_in_missing_info_from_device(client)
                self.logger.info("end client for keystore")
                return client
        
   DIR diff --git a/electrum/plugins/audio_modem/qt.py b/electrum/plugins/audio_modem/qt.py
       t@@ -4,6 +4,7 @@ import json
        from io import BytesIO
        import sys
        import platform
       +from typing import TYPE_CHECKING
        
        from PyQt5.QtWidgets import (QComboBox, QGridLayout, QLabel, QPushButton)
        
       t@@ -12,6 +13,9 @@ from electrum.gui.qt.util import WaitingDialog, EnterButton, WindowModalDialog, 
        from electrum.i18n import _
        from electrum.logging import get_logger
        
       +if TYPE_CHECKING:
       +    from electrum.gui.qt.transaction_dialog import TxDialog
       +
        
        _logger = get_logger(__name__)
        
       t@@ -71,12 +75,12 @@ class Plugin(BasePlugin):
                return bool(d.exec_())
        
            @hook
       -    def transaction_dialog(self, dialog):
       +    def transaction_dialog(self, dialog: 'TxDialog'):
                b = QPushButton()
                b.setIcon(read_QIcon("speaker.png"))
        
                def handler():
       -            blob = json.dumps(dialog.tx.as_dict())
       +            blob = dialog.tx.serialize()
                    self._send(parent=dialog, blob=blob)
                b.clicked.connect(handler)
                dialog.sharing_buttons.insert(-1, b)
   DIR diff --git a/electrum/plugins/coldcard/basic_psbt.py b/electrum/plugins/coldcard/basic_psbt.py
       t@@ -1,313 +0,0 @@
       -#
       -# 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@@ -1,397 +0,0 @@
       -#
       -# 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@@ -2,16 +2,18 @@
        # Coldcard Electrum plugin main code.
        #
        #
       -from struct import pack, unpack
       -import os, sys, time, io
       +import os, time, io
        import traceback
       +from typing import TYPE_CHECKING
       +import struct
        
       +from electrum import bip32
        from electrum.bip32 import BIP32Node, InvalidMasterKeyVersionBytes
        from electrum.i18n import _
        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.transaction import PartialTransaction
       +from electrum.wallet import Standard_Wallet, Multisig_Wallet, Abstract_Wallet
        from electrum.util import bfh, bh2u, versiontuple, UserFacingException
        from electrum.base_wizard import ScriptTypeNotSupported
        from electrum.logging import get_logger
       t@@ -19,9 +21,9 @@ from electrum.logging import get_logger
        from ..hw_wallet import HW_PluginBase
        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)
       +if TYPE_CHECKING:
       +    from electrum.keystore import Xpub
       +
        
        _logger = get_logger(__name__)
        
       t@@ -86,7 +88,7 @@ class CKCCClient:
                return '<CKCCClient: xfp=%s label=%r>' % (xfp2str(self.dev.master_fingerprint),
                                                                self.label())
        
       -    def verify_connection(self, expected_xfp, expected_xpub=None):
       +    def verify_connection(self, expected_xfp: int, expected_xpub=None):
                ex = (expected_xfp, expected_xpub)
        
                if self._expected_device == ex:
       t@@ -213,7 +215,7 @@ class CKCCClient:
                # 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):
       +    def sign_transaction_start(self, raw_psbt: bytes, *, finalize: bool = False):
                # Multiple steps to sign:
                # - upload binary
                # - start signing UX
       t@@ -242,6 +244,8 @@ class Coldcard_KeyStore(Hardware_KeyStore):
            hw_type = 'coldcard'
            device = 'Coldcard'
        
       +    plugin: 'ColdcardPlugin'
       +
            def __init__(self, d):
                Hardware_KeyStore.__init__(self, d)
                # Errors and other user interaction is done through the wallet's
       t@@ -250,39 +254,22 @@ 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 BE32 is more natural way to view it
       +        # we need to know at least the fingerprint of the master xpub to verify against MiTM
                # - 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 = getattr(lab, 'xpub', None)
       -        else:
       -            # wallet load: fatal if missing, we need them!
       -            self.ckcc_xfp = d['ckcc_xfp']
       -            self.ckcc_xpub = d.get('ckcc_xpub', None)
       +        self.ckcc_xpub = getattr(lab, 'xpub', None) or d.get('ckcc_xpub', None)
        
            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_xfp_int(self) -> int:
       +        xfp = self.get_root_fingerprint()
       +        assert xfp is not None
       +        return xfp_int_from_xfp_bytes(bfh(xfp))
        
            def get_client(self):
                # called when user tries to do something like view address, sign somthing.
       t@@ -290,7 +277,8 @@ class Coldcard_KeyStore(Hardware_KeyStore):
                # - 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)
       +            xfp_int = self.get_xfp_int()
       +            rv.verify_connection(xfp_int, self.ckcc_xpub)
        
                return rv
        
       t@@ -332,7 +320,7 @@ class Coldcard_KeyStore(Hardware_KeyStore):
                    return b''
        
                client = self.get_client()
       -        path = self.get_derivation() + ("/%d/%d" % sequence)
       +        path = self.get_derivation_prefix() + ("/%d/%d" % sequence)
                try:
                    cl = self.get_client()
                    try:
       t@@ -372,28 +360,23 @@ class Coldcard_KeyStore(Hardware_KeyStore):
                return b''
        
            @wrap_busy
       -    def sign_transaction(self, tx: Transaction, password):
       -        # Build a PSBT in memory, upload it for signing.
       +    def sign_transaction(self, tx, password):
       +        # Upload PSBT 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
       +        assert client.dev.master_fingerprint == self.get_xfp_int()
        
       -        # makes PSBT required
       -        raw_psbt = build_psbt(tx, self.my_wallet)
       -
       -        cc_finalize = not (type(self.my_wallet) is Multisig_Wallet)
       +        raw_psbt = tx.serialize_as_bytes()
        
                try:
                    try:
                        self.handler.show_message("Authorize Transaction...")
        
       -                client.sign_transaction_start(raw_psbt, cc_finalize)
       +                client.sign_transaction_start(raw_psbt)
        
                        while 1:
                            # How to kill some time, without locking UI?
       t@@ -420,18 +403,11 @@ class Coldcard_KeyStore(Hardware_KeyStore):
                    self.give_error(e, True)
                    return
        
       -        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.
       +        tx2 = PartialTransaction.from_raw_psbt(raw_resp)
       +        # apply partial signatures back into txn
       +        tx.combine_with_other_psbt(tx2)
       +        # 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@@ -447,7 +423,7 @@ class Coldcard_KeyStore(Hardware_KeyStore):
            @wrap_busy
            def show_address(self, sequence, txin_type):
                client = self.get_client()
       -        address_path = self.get_derivation()[2:] + "/%d/%d"%sequence
       +        address_path = self.get_derivation_prefix()[2:] + "/%d/%d"%sequence
                addr_fmt = self._encode_txin_type(txin_type)
                try:
                    try:
       t@@ -573,7 +549,7 @@ class ColdcardPlugin(HW_PluginBase):
                xpub = client.get_xpub(derivation, xtype)
                return xpub
        
       -    def get_client(self, keystore, force_pair=True):
       +    def get_client(self, keystore, force_pair=True) -> 'CKCCClient':
                # Acquire a connection to the hardware device (via USB)
                devmgr = self.device_manager()
                handler = keystore.handler
       t@@ -586,9 +562,10 @@ class ColdcardPlugin(HW_PluginBase):
                return client
        
            @staticmethod
       -    def export_ms_wallet(wallet, fp, name):
       +    def export_ms_wallet(wallet: Multisig_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.
       +        assert isinstance(wallet, Multisig_Wallet)
        
                print('# Exported from Electrum', file=fp)
                print(f'Name: {name:.20s}', file=fp)
       t@@ -597,12 +574,12 @@ class ColdcardPlugin(HW_PluginBase):
        
                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)
       +        for xpub, ks in zip(wallet.get_master_public_keys(), wallet.get_keystores()):
       +            fp_bytes, der_full = ks.get_fp_and_derivation_to_be_used_in_partial_tx(der_suffix=[], only_der_suffix=False)
       +            fp_hex = fp_bytes.hex().upper()
       +            der_prefix_str = bip32.convert_bip32_intpath_to_strpath(der_full)
       +            xpubs.append( (fp_hex, xpub, der_prefix_str) )
       +            derivs.add(der_prefix_str)
        
                # Derivation doesn't matter too much to the Coldcard, since it
                # uses key path data from PSBT or USB request as needed. However,
       t@@ -613,14 +590,14 @@ class ColdcardPlugin(HW_PluginBase):
                print('', file=fp)
        
                assert len(xpubs) == wallet.n
       -        for xfp, xp, dd in xpubs:
       +        for xfp, xpub, der_prefix in xpubs:
                    if derivs:
                        # show as a comment if unclear
       -                print(f'# derivation: {dd}', file=fp)
       +                print(f'# derivation: {der_prefix}', file=fp)
        
       -            print(f'{xfp}: {xp}\n', file=fp)
       +            print(f'{xfp}: {xpub}\n', file=fp)
        
       -    def show_address(self, wallet, address, keystore=None):
       +    def show_address(self, wallet, address, keystore: 'Coldcard_KeyStore' = None):
                if keystore is None:
                    keystore = wallet.get_keystore()
                if not self.show_address_helper(wallet, address, keystore):
       t@@ -633,50 +610,36 @@ class ColdcardPlugin(HW_PluginBase):
                    sequence = wallet.get_address_index(address)
                    keystore.show_address(sequence, txin_type)
                elif type(wallet) is Multisig_Wallet:
       +            assert isinstance(wallet, Multisig_Wallet)  # only here for type-hints in IDE
                    # 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))
       +            pubkey_deriv_info = wallet.get_public_keys_with_deriv_info(address)
       +            pubkeys = sorted([pk for pk in list(pubkey_deriv_info)])
       +            xfp_paths = []
       +            for pubkey_hex in pubkey_deriv_info:
       +                ks, der_suffix = pubkey_deriv_info[pubkey_hex]
       +                fp_bytes, der_full = ks.get_fp_and_derivation_to_be_used_in_partial_tx(der_suffix, only_der_suffix=False)
       +                xfp_int = xfp_int_from_xfp_bytes(fp_bytes)
       +                xfp_paths.append([xfp_int] + list(der_full))
        
       -            # put into BIP45 (sorted) order
       -            pkx = list(sorted(zip(pubkeys, xfps)))
       +            script = bfh(wallet.pubkeys_to_scriptcode(pubkeys))
        
       -            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)
       +            keystore.show_p2sh_address(wallet.m, script, xfp_paths, txin_type)
        
                else:
                    keystore.handler.show_error(_('This function is only available for standard wallets when using {}.').format(self.device))
                    return
        
       -    @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)
       +
       +def xfp_int_from_xfp_bytes(fp_bytes: bytes) -> int:
       +    return int.from_bytes(fp_bytes, byteorder="little", signed=False)
       +
       +
       +def xfp2str(xfp: int) -> str:
       +    # 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 struct.pack('<I', xfp).hex().lower()
        
        # EOF
   DIR diff --git a/electrum/plugins/coldcard/qt.py b/electrum/plugins/coldcard/qt.py
       t@@ -1,25 +1,22 @@
        import time, os
        from functools import partial
       +import copy
        
        from PyQt5.QtCore import Qt, pyqtSignal
        from PyQt5.QtWidgets import QPushButton, QLabel, QVBoxLayout, QWidget, QGridLayout
       -from PyQt5.QtWidgets import QFileDialog
       +
       +from electrum.gui.qt.util import WindowModalDialog, CloseButton, get_parent_main_window, Buttons
       +from electrum.gui.qt.transaction_dialog import TxDialog
        
        from electrum.i18n import _
        from electrum.plugin import hook
       -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 electrum.wallet import Multisig_Wallet
       +from electrum.transaction import PartialTransaction
        
        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
        
       t@@ -73,135 +70,29 @@ class Plugin(ColdcardPlugin, QtPluginBase):
                        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
       -
       +    def transaction_dialog(self, dia: TxDialog):
                # 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"
       -        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
       -
       -        # convert to PSBT
       -        build_psbt(tx, dia.wallet)
       +        def gettx_for_coldcard_export() -> PartialTransaction:
       +            if not isinstance(dia.tx, PartialTransaction):
       +                raise Exception("Can only export partial transactions for coinjoins.")
       +            tx = copy.deepcopy(dia.tx)
       +            tx.add_info_from_wallet(dia.wallet, include_xpubs_and_full_paths=True)
       +            return tx
        
       -        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(tx.raw_psbt)
       -            dia.show_message(_("Transaction exported successfully"))
       -            dia.saved = True
       +        # add a new "export" option
       +        if isinstance(dia.tx, PartialTransaction):
       +            export_submenu = dia.export_actions_menu.addMenu(_("For {}; include xpubs").format(self.device))
       +            dia.add_export_actions_to_menu(export_submenu, gettx=gettx_for_coldcard_export)
        
            def show_settings_dialog(self, window, keystore):
                # When they click on the icon for CC we come here.
                # - 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, tx_desc=desc)
        
        class Coldcard_Handler(QtHandlerBase):
            setup_signal = pyqtSignal()
       t@@ -307,7 +198,7 @@ class CKCCSettingsDialog(WindowModalDialog):
        
            def show_placeholders(self, unclear_arg):
                # device missing, so hide lots of detail.
       -        self.xfp.setText('<tt>%s' % xfp2str(self.keystore.ckcc_xfp))
       +        self.xfp.setText('<tt>%s' % self.keystore.get_root_fingerprint())
                self.serial.setText('(not connected)')
                self.fw_version.setText('')
                self.fw_built.setText('')
   DIR diff --git a/electrum/plugins/cosigner_pool/qt.py b/electrum/plugins/cosigner_pool/qt.py
       t@@ -25,23 +25,25 @@
        
        import time
        from xmlrpc.client import ServerProxy
       +from typing import TYPE_CHECKING, Union, List, Tuple
        
        from PyQt5.QtCore import QObject, pyqtSignal
        from PyQt5.QtWidgets import QPushButton
        
        from electrum import util, keystore, ecc, crypto
        from electrum import transaction
       +from electrum.transaction import Transaction, PartialTransaction, tx_from_any
        from electrum.bip32 import BIP32Node
        from electrum.plugin import BasePlugin, hook
        from electrum.i18n import _
        from electrum.wallet import Multisig_Wallet
        from electrum.util import bh2u, bfh
        
       -from electrum.gui.qt.transaction_dialog import show_transaction
       +from electrum.gui.qt.transaction_dialog import show_transaction, TxDialog
        from electrum.gui.qt.util import WaitingDialog
        
       -import sys
       -import traceback
       +if TYPE_CHECKING:
       +    from electrum.gui.qt.main_window import ElectrumWindow
        
        
        server = ServerProxy('https://cosigner.electrum.org/', allow_none=True)
       t@@ -97,8 +99,8 @@ class Plugin(BasePlugin):
                self.listener = None
                self.obj = QReceiveSignalObject()
                self.obj.cosigner_receive_signal.connect(self.on_receive)
       -        self.keys = []
       -        self.cosigner_list = []
       +        self.keys = []  # type: List[Tuple[str, str, ElectrumWindow]]
       +        self.cosigner_list = []  # type: List[Tuple[ElectrumWindow, str, bytes, str]]
        
            @hook
            def init_qt(self, gui):
       t@@ -116,10 +118,11 @@ class Plugin(BasePlugin):
            def is_available(self):
                return True
        
       -    def update(self, window):
       +    def update(self, window: 'ElectrumWindow'):
                wallet = window.wallet
                if type(wallet) != Multisig_Wallet:
                    return
       +        assert isinstance(wallet, Multisig_Wallet)  # only here for type-hints in IDE
                if self.listener is None:
                    self.logger.info("starting listener")
                    self.listener = Listener(self)
       t@@ -131,7 +134,7 @@ class Plugin(BasePlugin):
                self.keys = []
                self.cosigner_list = []
                for key, keystore in wallet.keystores.items():
       -            xpub = keystore.get_master_public_key()
       +            xpub = keystore.get_master_public_key()  # type: str
                    pubkey = BIP32Node.from_xkey(xpub).eckey.get_public_key_bytes(compressed=True)
                    _hash = bh2u(crypto.sha256d(pubkey))
                    if not keystore.is_watching_only():
       t@@ -142,14 +145,14 @@ class Plugin(BasePlugin):
                    self.listener.set_keyhashes([t[1] for t in self.keys])
        
            @hook
       -    def transaction_dialog(self, d):
       +    def transaction_dialog(self, d: 'TxDialog'):
                d.cosigner_send_button = b = QPushButton(_("Send to cosigner"))
                b.clicked.connect(lambda: self.do_send(d.tx))
                d.buttons.insert(0, b)
                self.transaction_dialog_update(d)
        
            @hook
       -    def transaction_dialog_update(self, d):
       +    def transaction_dialog_update(self, d: 'TxDialog'):
                if d.tx.is_complete() or d.wallet.can_sign(d.tx):
                    d.cosigner_send_button.hide()
                    return
       t@@ -160,17 +163,14 @@ class Plugin(BasePlugin):
                else:
                    d.cosigner_send_button.hide()
        
       -    def cosigner_can_sign(self, tx, cosigner_xpub):
       -        from electrum.keystore import is_xpubkey, parse_xpubkey
       -        xpub_set = set([])
       -        for txin in tx.inputs():
       -            for x_pubkey in txin['x_pubkeys']:
       -                if is_xpubkey(x_pubkey):
       -                    xpub, s = parse_xpubkey(x_pubkey)
       -                    xpub_set.add(xpub)
       -        return cosigner_xpub in xpub_set
       -
       -    def do_send(self, tx):
       +    def cosigner_can_sign(self, tx: Transaction, cosigner_xpub: str) -> bool:
       +        if not isinstance(tx, PartialTransaction):
       +            return False
       +        if tx.is_complete():
       +            return False
       +        return cosigner_xpub in {bip32node.to_xpub() for bip32node in tx.xpubs}
       +
       +    def do_send(self, tx: Union[Transaction, PartialTransaction]):
                def on_success(result):
                    window.show_message(_("Your transaction was sent to the cosigning pool.") + '\n' +
                                        _("Open your cosigner wallet to retrieve it."))
       t@@ -184,7 +184,7 @@ class Plugin(BasePlugin):
                    if not self.cosigner_can_sign(tx, xpub):
                        continue
                    # construct message
       -            raw_tx_bytes = bfh(str(tx))
       +            raw_tx_bytes = tx.serialize_as_bytes()
                    public_key = ecc.ECPubkey(K)
                    message = public_key.encrypt_message(raw_tx_bytes).decode('ascii')
                    # send message
       t@@ -223,12 +223,12 @@ class Plugin(BasePlugin):
                    return
                try:
                    privkey = BIP32Node.from_xkey(xprv).eckey
       -            message = bh2u(privkey.decrypt_message(message))
       +            message = privkey.decrypt_message(message)
                except Exception as e:
                    self.logger.exception('')
                    window.show_error(_('Error decrypting message') + ':\n' + repr(e))
                    return
        
                self.listener.clear(keyhash)
       -        tx = transaction.Transaction(message)
       -        show_transaction(tx, window, prompt_if_unsaved=True)
       +        tx = tx_from_any(message)
       +        show_transaction(tx, parent=window, prompt_if_unsaved=True)
   DIR diff --git a/electrum/plugins/digitalbitbox/digitalbitbox.py b/electrum/plugins/digitalbitbox/digitalbitbox.py
       t@@ -14,20 +14,21 @@ import re
        import struct
        import sys
        import time
       +import copy
        
        from electrum.crypto import sha256d, EncodeAES_base64, EncodeAES_bytes, DecodeAES_bytes, hmac_oneshot
        from electrum.bitcoin import (TYPE_ADDRESS, push_script, var_int, public_key_to_p2pkh,
                                      is_address)
       -from electrum.bip32 import BIP32Node
       +from electrum.bip32 import BIP32Node, convert_bip32_intpath_to_strpath, is_all_public_derivation
        from electrum import ecc
        from electrum.ecc import msg_magic
        from electrum.wallet import Standard_Wallet
        from electrum import constants
       -from electrum.transaction import Transaction
       +from electrum.transaction import Transaction, PartialTransaction, PartialTxInput
        from electrum.i18n import _
        from electrum.keystore import Hardware_KeyStore
        from ..hw_wallet import HW_PluginBase
       -from electrum.util import to_string, UserCancelled, UserFacingException
       +from electrum.util import to_string, UserCancelled, UserFacingException, bfh
        from electrum.base_wizard import ScriptTypeNotSupported, HWD_SETUP_NEW_WALLET
        from electrum.network import Network
        from electrum.logging import get_logger
       t@@ -449,21 +450,13 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore):
            hw_type = 'digitalbitbox'
            device = 'DigitalBitbox'
        
       +    plugin: 'DigitalBitboxPlugin'
        
            def __init__(self, d):
                Hardware_KeyStore.__init__(self, d)
                self.force_watching_only = False
                self.maxInputs = 14 # maximum inputs per single sign command
        
       -
       -    def get_derivation(self):
       -        return str(self.derivation)
       -
       -
       -    def is_p2pkh(self):
       -        return self.derivation.startswith("m/44'/")
       -
       -
            def give_error(self, message, clear_client = False):
                if clear_client:
                    self.client = None
       t@@ -478,7 +471,7 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore):
                sig = None
                try:
                    message = message.encode('utf8')
       -            inputPath = self.get_derivation() + "/%d/%d" % sequence
       +            inputPath = self.get_derivation_prefix() + "/%d/%d" % sequence
                    msg_hash = sha256d(msg_magic(message))
                    inputHash = to_hexstr(msg_hash)
                    hasharray = []
       t@@ -540,58 +533,50 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore):
        
                try:
                    p2pkhTransaction = True
       -            derivations = self.get_tx_derivations(tx)
                    inputhasharray = []
                    hasharray = []
                    pubkeyarray = []
        
                    # Build hasharray from inputs
                    for i, txin in enumerate(tx.inputs()):
       -                if txin['type'] == 'coinbase':
       +                if txin.is_coinbase():
                            self.give_error("Coinbase not supported") # should never happen
        
       -                if txin['type'] != 'p2pkh':
       +                if txin.script_type != 'p2pkh':
                            p2pkhTransaction = False
        
       -                for x_pubkey in txin['x_pubkeys']:
       -                    if x_pubkey in derivations:
       -                        index = derivations.get(x_pubkey)
       -                        inputPath = "%s/%d/%d" % (self.get_derivation(), index[0], index[1])
       -                        inputHash = sha256d(binascii.unhexlify(tx.serialize_preimage(i)))
       -                        hasharray_i = {'hash': to_hexstr(inputHash), 'keypath': inputPath}
       -                        hasharray.append(hasharray_i)
       -                        inputhasharray.append(inputHash)
       -                        break
       -                else:
       -                    self.give_error("No matching x_key for sign_transaction") # should never happen
       +                my_pubkey, inputPath = self.find_my_pubkey_in_txinout(txin)
       +                if not inputPath:
       +                    self.give_error("No matching pubkey for sign_transaction")  # should never happen
       +                inputPath = convert_bip32_intpath_to_strpath(inputPath)
       +                inputHash = sha256d(bfh(tx.serialize_preimage(i)))
       +                hasharray_i = {'hash': to_hexstr(inputHash), 'keypath': inputPath}
       +                hasharray.append(hasharray_i)
       +                inputhasharray.append(inputHash)
        
                    # Build pubkeyarray from outputs
       -            for o in tx.outputs():
       -                assert o.type == TYPE_ADDRESS
       -                info = tx.output_info.get(o.address)
       -                if info is not None:
       -                    if info.is_change:
       -                        index = info.address_index
       -                        changePath = self.get_derivation() + "/%d/%d" % index
       -                        changePubkey = self.derive_pubkey(index[0], index[1])
       -                        pubkeyarray_i = {'pubkey': changePubkey, 'keypath': changePath}
       -                        pubkeyarray.append(pubkeyarray_i)
       +            for txout in tx.outputs():
       +                assert txout.address
       +                if txout.is_change:
       +                    changePubkey, changePath = self.find_my_pubkey_in_txinout(txout)
       +                    assert changePath
       +                    changePath = convert_bip32_intpath_to_strpath(changePath)
       +                    changePubkey = changePubkey.hex()
       +                    pubkeyarray_i = {'pubkey': changePubkey, 'keypath': changePath}
       +                    pubkeyarray.append(pubkeyarray_i)
        
                    # Special serialization of the unsigned transaction for
                    # the mobile verification app.
                    # At the moment, verification only works for p2pkh transactions.
                    if p2pkhTransaction:
       -                class CustomTXSerialization(Transaction):
       -                    @classmethod
       -                    def input_script(self, txin, estimate_size=False):
       -                        if txin['type'] == 'p2pkh':
       -                            return Transaction.get_preimage_script(txin)
       -                        if txin['type'] == 'p2sh':
       -                            # Multisig verification has partial support, but is disabled. This is the
       -                            # expected serialization though, so we leave it here until we activate it.
       -                            return '00' + push_script(Transaction.get_preimage_script(txin))
       -                        raise Exception("unsupported type %s" % txin['type'])
       -                tx_dbb_serialized = CustomTXSerialization(tx.serialize()).serialize_to_network()
       +                tx_copy = copy.deepcopy(tx)
       +                # monkey-patch method of tx_copy instance to change serialization
       +                def input_script(self, txin: PartialTxInput, *, estimate_size=False):
       +                    if txin.script_type == 'p2pkh':
       +                        return Transaction.get_preimage_script(txin)
       +                    raise Exception("unsupported type %s" % txin.script_type)
       +                tx_copy.input_script = input_script.__get__(tx_copy, PartialTransaction)
       +                tx_dbb_serialized = tx_copy.serialize_to_network()
                    else:
                        # We only need this for the signing echo / verification.
                        tx_dbb_serialized = None
       t@@ -656,12 +641,9 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore):
                    if len(dbb_signatures) != len(tx.inputs()):
                        raise Exception("Incorrect number of transactions signed.") # Should never occur
                    for i, txin in enumerate(tx.inputs()):
       -                num = txin['num_sig']
       -                for pubkey in txin['pubkeys']:
       -                    signatures = list(filter(None, txin['signatures']))
       -                    if len(signatures) == num:
       -                        break # txin is complete
       -                    ii = txin['pubkeys'].index(pubkey)
       +                for pubkey_bytes in txin.pubkeys:
       +                    if txin.is_complete():
       +                        break
                            signed = dbb_signatures[i]
                            if 'recid' in signed:
                                # firmware > v2.1.1
       t@@ -673,20 +655,19 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore):
                            elif 'pubkey' in signed:
                                # firmware <= v2.1.1
                                pk = signed['pubkey']
       -                    if pk != pubkey:
       +                    if pk != pubkey_bytes.hex():
                                continue
                            sig_r = int(signed['sig'][:64], 16)
                            sig_s = int(signed['sig'][64:], 16)
                            sig = ecc.der_sig_from_r_and_s(sig_r, sig_s)
                            sig = to_hexstr(sig) + '01'
       -                    tx.add_signature_to_txin(i, ii, sig)
       +                    tx.add_signature_to_txin(txin_idx=i, signing_pubkey=pubkey_bytes.hex(), sig=sig)
                except UserCancelled:
                    raise
                except BaseException as e:
                    self.give_error(e, True)
                else:
       -            _logger.info("Transaction is_complete {tx.is_complete()}")
       -            tx.raw = tx.serialize()
       +            _logger.info(f"Transaction is_complete {tx.is_complete()}")
        
        
        class DigitalBitboxPlugin(HW_PluginBase):
       t@@ -760,6 +741,8 @@ class DigitalBitboxPlugin(HW_PluginBase):
            def get_xpub(self, device_id, derivation, xtype, wizard):
                if xtype not in self.SUPPORTED_XTYPES:
                    raise ScriptTypeNotSupported(_('This type of script is not supported with {}.').format(self.device))
       +        if is_all_public_derivation(derivation):
       +            raise Exception(f"The {self.device} does not reveal xpubs corresponding to non-hardened paths. (path: {derivation})")
                devmgr = self.device_manager()
                client = devmgr.client_by_id(device_id)
                client.handler = self.create_handler(wizard)
       t@@ -788,11 +771,11 @@ class DigitalBitboxPlugin(HW_PluginBase):
                if not self.is_mobile_paired():
                    keystore.handler.show_error(_('This function is only available after pairing your {} with a mobile device.').format(self.device))
                    return
       -        if not keystore.is_p2pkh():
       +        if wallet.get_txin_type(address) != 'p2pkh':
                    keystore.handler.show_error(_('This function is only available for p2pkh keystores when using {}.').format(self.device))
                    return
                change, index = wallet.get_address_index(address)
       -        keypath = '%s/%d/%d' % (keystore.derivation, change, index)
       +        keypath = '%s/%d/%d' % (keystore.get_derivation_prefix(), change, index)
                xpub = self.get_client(keystore)._get_xpub(keypath)
                verify_request_payload = {
                    "type": 'p2pkh',
   DIR diff --git a/electrum/plugins/digitalbitbox/qt.py b/electrum/plugins/digitalbitbox/qt.py
       t@@ -2,7 +2,7 @@ from functools import partial
        
        from electrum.i18n import _
        from electrum.plugin import hook
       -from electrum.wallet import Standard_Wallet
       +from electrum.wallet import Standard_Wallet, Abstract_Wallet
        
        from ..hw_wallet.qt import QtHandlerBase, QtPluginBase
        from ..hw_wallet.plugin import only_hook_if_libraries_available
       t@@ -18,7 +18,7 @@ class Plugin(DigitalBitboxPlugin, QtPluginBase):
        
            @only_hook_if_libraries_available
            @hook
       -    def receive_menu(self, menu, addrs, wallet):
       +    def receive_menu(self, menu, addrs, wallet: Abstract_Wallet):
                if type(wallet) is not Standard_Wallet:
                    return
        
       t@@ -29,12 +29,12 @@ class Plugin(DigitalBitboxPlugin, QtPluginBase):
                if not self.is_mobile_paired():
                    return
        
       -        if not keystore.is_p2pkh():
       -            return
       -
                if len(addrs) == 1:
       +            addr = addrs[0]
       +            if wallet.get_txin_type(addr) != 'p2pkh':
       +                return
                    def show_address():
       -                keystore.thread.add(partial(self.show_address, wallet, addrs[0], keystore))
       +                keystore.thread.add(partial(self.show_address, wallet, addr, keystore))
        
                    menu.addAction(_("Show on {}").format(self.device), show_address)
        
   DIR diff --git a/electrum/plugins/greenaddress_instant/qt.py b/electrum/plugins/greenaddress_instant/qt.py
       t@@ -36,6 +36,7 @@ from electrum.network import Network
        
        if TYPE_CHECKING:
            from aiohttp import ClientResponse
       +    from electrum.gui.qt.transaction_dialog import TxDialog
        
        
        class Plugin(BasePlugin):
       t@@ -43,13 +44,13 @@ class Plugin(BasePlugin):
            button_label = _("Verify GA instant")
        
            @hook
       -    def transaction_dialog(self, d):
       +    def transaction_dialog(self, d: 'TxDialog'):
                d.verify_button = QPushButton(self.button_label)
                d.verify_button.clicked.connect(lambda: self.do_verify(d))
                d.buttons.insert(0, d.verify_button)
                self.transaction_dialog_update(d)
        
       -    def get_my_addr(self, d):
       +    def get_my_addr(self, d: 'TxDialog'):
                """Returns the address for given tx which can be used to request
                instant confirmation verification from GreenAddress"""
                for o in d.tx.outputs():
       t@@ -58,13 +59,13 @@ class Plugin(BasePlugin):
                return None
        
            @hook
       -    def transaction_dialog_update(self, d):
       +    def transaction_dialog_update(self, d: 'TxDialog'):
                if d.tx.is_complete() and self.get_my_addr(d):
                    d.verify_button.show()
                else:
                    d.verify_button.hide()
        
       -    def do_verify(self, d):
       +    def do_verify(self, d: 'TxDialog'):
                tx = d.tx
                wallet = d.wallet
                window = d.main_window
   DIR diff --git a/electrum/plugins/hw_wallet/plugin.py b/electrum/plugins/hw_wallet/plugin.py
       t@@ -24,11 +24,18 @@
        # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        # SOFTWARE.
        
       +from typing import TYPE_CHECKING, Dict, List, Union, Tuple, Sequence
       +
        from electrum.plugin import BasePlugin, hook
        from electrum.i18n import _
        from electrum.bitcoin import is_address, TYPE_SCRIPT, opcodes
        from electrum.util import bfh, versiontuple, UserFacingException
       -from electrum.transaction import TxOutput, Transaction
       +from electrum.transaction import TxOutput, Transaction, PartialTransaction, PartialTxInput, PartialTxOutput
       +from electrum.bip32 import BIP32Node
       +
       +if TYPE_CHECKING:
       +    from electrum.wallet import Abstract_Wallet
       +    from electrum.keystore import Hardware_KeyStore
        
        
        class HW_PluginBase(BasePlugin):
       t@@ -65,7 +72,10 @@ class HW_PluginBase(BasePlugin):
                """
                raise NotImplementedError()
        
       -    def show_address(self, wallet, address, keystore=None):
       +    def get_client(self, keystore: 'Hardware_KeyStore', force_pair: bool = True):
       +        raise NotImplementedError()
       +
       +    def show_address(self, wallet: 'Abstract_Wallet', address, keystore: 'Hardware_KeyStore' = None):
                pass  # implemented in child classes
        
            def show_address_helper(self, wallet, address, keystore=None):
       t@@ -132,20 +142,12 @@ class HW_PluginBase(BasePlugin):
                return self._ignore_outdated_fw
        
        
       -def is_any_tx_output_on_change_branch(tx: Transaction) -> bool:
       -    if not tx.output_info:
       -        return False
       -    for o in tx.outputs():
       -        info = tx.output_info.get(o.address)
       -        if info is not None:
       -            return info.is_change
       -    return False
       +def is_any_tx_output_on_change_branch(tx: PartialTransaction) -> bool:
       +    return any([txout.is_change for txout in tx.outputs()])
        
        
        def trezor_validate_op_return_output_and_get_data(output: TxOutput) -> bytes:
       -    if output.type != TYPE_SCRIPT:
       -        raise Exception("Unexpected output type: {}".format(output.type))
       -    script = bfh(output.address)
       +    script = output.scriptpubkey
            if not (script[0] == opcodes.OP_RETURN and
                    script[1] == len(script) - 2 and script[1] <= 75):
                raise UserFacingException(_("Only OP_RETURN scripts, with one constant push, are supported."))
       t@@ -154,6 +156,25 @@ def trezor_validate_op_return_output_and_get_data(output: TxOutput) -> bytes:
            return script[2:]
        
        
       +def get_xpubs_and_der_suffixes_from_txinout(tx: PartialTransaction,
       +                                            txinout: Union[PartialTxInput, PartialTxOutput]) \
       +        -> List[Tuple[str, List[int]]]:
       +    xfp_to_xpub_map = {xfp: bip32node for bip32node, (xfp, path)
       +                       in tx.xpubs.items()}  # type: Dict[bytes, BIP32Node]
       +    xfps = [txinout.bip32_paths[pubkey][0] for pubkey in txinout.pubkeys]
       +    try:
       +        xpubs = [xfp_to_xpub_map[xfp] for xfp in xfps]
       +    except KeyError as e:
       +        raise Exception(f"Partial transaction is missing global xpub for "
       +                        f"fingerprint ({str(e)}) in input/output") from e
       +    xpubs_and_deriv_suffixes = []
       +    for bip32node, pubkey in zip(xpubs, txinout.pubkeys):
       +        xfp, path = txinout.bip32_paths[pubkey]
       +        der_suffix = list(path)[bip32node.depth:]
       +        xpubs_and_deriv_suffixes.append((bip32node.to_xpub(), der_suffix))
       +    return xpubs_and_deriv_suffixes
       +
       +
        def only_hook_if_libraries_available(func):
            # note: this decorator must wrap @hook, not the other way around,
            # as 'hook' uses the name of the function it wraps
   DIR diff --git a/electrum/plugins/keepkey/keepkey.py b/electrum/plugins/keepkey/keepkey.py
       t@@ -1,19 +1,23 @@
        from binascii import hexlify, unhexlify
        import traceback
        import sys
       +from typing import NamedTuple, Any, Optional, Dict, Union, List, Tuple, TYPE_CHECKING
        
        from electrum.util import bfh, bh2u, UserCancelled, UserFacingException
        from electrum.bitcoin import TYPE_ADDRESS, TYPE_SCRIPT
        from electrum.bip32 import BIP32Node
        from electrum import constants
        from electrum.i18n import _
       -from electrum.transaction import deserialize, Transaction
       -from electrum.keystore import Hardware_KeyStore, is_xpubkey, parse_xpubkey
       +from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput
       +from electrum.keystore import Hardware_KeyStore
        from electrum.base_wizard import ScriptTypeNotSupported
        
        from ..hw_wallet import HW_PluginBase
       -from ..hw_wallet.plugin import is_any_tx_output_on_change_branch, trezor_validate_op_return_output_and_get_data
       +from ..hw_wallet.plugin import (is_any_tx_output_on_change_branch, trezor_validate_op_return_output_and_get_data,
       +                                get_xpubs_and_der_suffixes_from_txinout)
        
       +if TYPE_CHECKING:
       +    from .client import KeepKeyClient
        
        # TREZOR initialization methods
        TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY = range(0, 4)
       t@@ -23,8 +27,7 @@ class KeepKey_KeyStore(Hardware_KeyStore):
            hw_type = 'keepkey'
            device = 'KeepKey'
        
       -    def get_derivation(self):
       -        return self.derivation
       +    plugin: 'KeepKeyPlugin'
        
            def get_client(self, force_pair=True):
                return self.plugin.get_client(self, force_pair)
       t@@ -34,7 +37,7 @@ class KeepKey_KeyStore(Hardware_KeyStore):
        
            def sign_message(self, sequence, message, password):
                client = self.get_client()
       -        address_path = self.get_derivation() + "/%d/%d"%sequence
       +        address_path = self.get_derivation_prefix() + "/%d/%d"%sequence
                address_n = client.expand_path(address_path)
                msg_sig = client.sign_message(self.plugin.get_coin_name(), address_n, message)
                return msg_sig.signature
       t@@ -44,22 +47,13 @@ class KeepKey_KeyStore(Hardware_KeyStore):
                    return
                # previous transactions used as inputs
                prev_tx = {}
       -        # path of the xpubs that are involved
       -        xpub_path = {}
                for txin in tx.inputs():
       -            pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin)
       -            tx_hash = txin['prevout_hash']
       -            if txin.get('prev_tx') is None and not Transaction.is_segwit_input(txin):
       -                raise UserFacingException(_('Offline signing with {} is not supported for legacy inputs.').format(self.device))
       -            prev_tx[tx_hash] = txin['prev_tx']
       -            for x_pubkey in x_pubkeys:
       -                if not is_xpubkey(x_pubkey):
       -                    continue
       -                xpub, s = parse_xpubkey(x_pubkey)
       -                if xpub == self.get_master_public_key():
       -                    xpub_path[xpub] = self.get_derivation()
       +            tx_hash = txin.prevout.txid.hex()
       +            if txin.utxo is None and not Transaction.is_segwit_input(txin):
       +                raise UserFacingException(_('Missing previous tx for legacy input.'))
       +            prev_tx[tx_hash] = txin.utxo
        
       -        self.plugin.sign_transaction(self, tx, prev_tx, xpub_path)
       +        self.plugin.sign_transaction(self, tx, prev_tx)
        
        
        class KeepKeyPlugin(HW_PluginBase):
       t@@ -164,7 +158,7 @@ class KeepKeyPlugin(HW_PluginBase):
        
                return client
        
       -    def get_client(self, keystore, force_pair=True):
       +    def get_client(self, keystore, force_pair=True) -> Optional['KeepKeyClient']:
                devmgr = self.device_manager()
                handler = keystore.handler
                with devmgr.hid_lock:
       t@@ -306,12 +300,11 @@ class KeepKeyPlugin(HW_PluginBase):
                    return self.types.PAYTOMULTISIG
                raise ValueError('unexpected txin type: {}'.format(electrum_txin_type))
        
       -    def sign_transaction(self, keystore, tx, prev_tx, xpub_path):
       +    def sign_transaction(self, keystore, tx: PartialTransaction, prev_tx):
                self.prev_tx = prev_tx
       -        self.xpub_path = xpub_path
                client = self.get_client(keystore)
       -        inputs = self.tx_inputs(tx, True)
       -        outputs = self.tx_outputs(keystore.get_derivation(), tx)
       +        inputs = self.tx_inputs(tx, for_sig=True, keystore=keystore)
       +        outputs = self.tx_outputs(tx, keystore=keystore)
                signatures = client.sign_tx(self.get_coin_name(), inputs, outputs,
                                            lock_time=tx.locktime, version=tx.version)[0]
                signatures = [(bh2u(x) + '01') for x in signatures]
       t@@ -326,137 +319,118 @@ class KeepKeyPlugin(HW_PluginBase):
                if not client.atleast_version(1, 3):
                    keystore.handler.show_error(_("Your device firmware is too old"))
                    return
       -        change, index = wallet.get_address_index(address)
       -        derivation = keystore.derivation
       -        address_path = "%s/%d/%d"%(derivation, change, index)
       +        deriv_suffix = wallet.get_address_index(address)
       +        derivation = keystore.get_derivation_prefix()
       +        address_path = "%s/%d/%d"%(derivation, *deriv_suffix)
                address_n = client.expand_path(address_path)
       +        script_type = self.get_keepkey_input_script_type(wallet.txin_type)
       +
       +        # prepare multisig, if available:
                xpubs = wallet.get_master_public_keys()
       -        if len(xpubs) == 1:
       -            script_type = self.get_keepkey_input_script_type(wallet.txin_type)
       -            client.get_address(self.get_coin_name(), address_n, True, script_type=script_type)
       -        else:
       -            def f(xpub):
       -                return self._make_node_path(xpub, [change, index])
       +        if len(xpubs) > 1:
                    pubkeys = wallet.get_public_keys(address)
                    # sort xpubs using the order of pubkeys
       -            sorted_pubkeys, sorted_xpubs = zip(*sorted(zip(pubkeys, xpubs)))
       -            pubkeys = list(map(f, sorted_xpubs))
       -            multisig = self.types.MultisigRedeemScriptType(
       -               pubkeys=pubkeys,
       -               signatures=[b''] * wallet.n,
       -               m=wallet.m,
       -            )
       -            script_type = self.get_keepkey_input_script_type(wallet.txin_type)
       -            client.get_address(self.get_coin_name(), address_n, True, multisig=multisig, script_type=script_type)
       -
       -    def tx_inputs(self, tx, for_sig=False):
       +            sorted_pairs = sorted(zip(pubkeys, xpubs))
       +            multisig = self._make_multisig(
       +                wallet.m,
       +                [(xpub, deriv_suffix) for pubkey, xpub in sorted_pairs])
       +        else:
       +            multisig = None
       +
       +        client.get_address(self.get_coin_name(), address_n, True, multisig=multisig, script_type=script_type)
       +
       +    def tx_inputs(self, tx: Transaction, *, for_sig=False, keystore: 'KeepKey_KeyStore' = None):
                inputs = []
                for txin in tx.inputs():
                    txinputtype = self.types.TxInputType()
       -            if txin['type'] == 'coinbase':
       +            if txin.is_coinbase():
                        prev_hash = b"\x00"*32
                        prev_index = 0xffffffff  # signed int -1
                    else:
                        if for_sig:
       -                    x_pubkeys = txin['x_pubkeys']
       -                    if len(x_pubkeys) == 1:
       -                        x_pubkey = x_pubkeys[0]
       -                        xpub, s = parse_xpubkey(x_pubkey)
       -                        xpub_n = self.client_class.expand_path(self.xpub_path[xpub])
       -                        txinputtype.address_n.extend(xpub_n + s)
       -                        txinputtype.script_type = self.get_keepkey_input_script_type(txin['type'])
       +                    assert isinstance(tx, PartialTransaction)
       +                    assert isinstance(txin, PartialTxInput)
       +                    assert keystore
       +                    if len(txin.pubkeys) > 1:
       +                        xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txin)
       +                        multisig = self._make_multisig(txin.num_sig, xpubs_and_deriv_suffixes)
                            else:
       -                        def f(x_pubkey):
       -                            xpub, s = parse_xpubkey(x_pubkey)
       -                            return self._make_node_path(xpub, s)
       -                        pubkeys = list(map(f, x_pubkeys))
       -                        multisig = self.types.MultisigRedeemScriptType(
       -                            pubkeys=pubkeys,
       -                            signatures=map(lambda x: bfh(x)[:-1] if x else b'', txin.get('signatures')),
       -                            m=txin.get('num_sig'),
       -                        )
       -                        script_type = self.get_keepkey_input_script_type(txin['type'])
       -                        txinputtype = self.types.TxInputType(
       -                            script_type=script_type,
       -                            multisig=multisig
       -                        )
       -                        # find which key is mine
       -                        for x_pubkey in x_pubkeys:
       -                            if is_xpubkey(x_pubkey):
       -                                xpub, s = parse_xpubkey(x_pubkey)
       -                                if xpub in self.xpub_path:
       -                                    xpub_n = self.client_class.expand_path(self.xpub_path[xpub])
       -                                    txinputtype.address_n.extend(xpub_n + s)
       -                                    break
       -
       -                prev_hash = unhexlify(txin['prevout_hash'])
       -                prev_index = txin['prevout_n']
       -
       -            if 'value' in txin:
       -                txinputtype.amount = txin['value']
       +                        multisig = None
       +                    script_type = self.get_keepkey_input_script_type(txin.script_type)
       +                    txinputtype = self.types.TxInputType(
       +                        script_type=script_type,
       +                        multisig=multisig)
       +                    my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txin)
       +                    if full_path:
       +                        txinputtype.address_n.extend(full_path)
       +
       +                prev_hash = txin.prevout.txid
       +                prev_index = txin.prevout.out_idx
       +
       +            if txin.value_sats() is not None:
       +                txinputtype.amount = txin.value_sats()
                    txinputtype.prev_hash = prev_hash
                    txinputtype.prev_index = prev_index
        
       -            if txin.get('scriptSig') is not None:
       -                script_sig = bfh(txin['scriptSig'])
       -                txinputtype.script_sig = script_sig
       +            if txin.script_sig is not None:
       +                txinputtype.script_sig = txin.script_sig
        
       -            txinputtype.sequence = txin.get('sequence', 0xffffffff - 1)
       +            txinputtype.sequence = txin.nsequence
        
                    inputs.append(txinputtype)
        
                return inputs
        
       -    def tx_outputs(self, derivation, tx: Transaction):
       +    def _make_multisig(self, m, xpubs):
       +        if len(xpubs) == 1:
       +            return None
       +        pubkeys = [self._make_node_path(xpub, deriv) for xpub, deriv in xpubs]
       +        return self.types.MultisigRedeemScriptType(
       +            pubkeys=pubkeys,
       +            signatures=[b''] * len(pubkeys),
       +            m=m)
       +
       +    def tx_outputs(self, tx: PartialTransaction, *, keystore: 'KeepKey_KeyStore'):
        
                def create_output_by_derivation():
       -            script_type = self.get_keepkey_output_script_type(info.script_type)
       -            if len(xpubs) == 1:
       -                address_n = self.client_class.expand_path(derivation + "/%d/%d" % index)
       -                txoutputtype = self.types.TxOutputType(
       -                    amount=amount,
       -                    script_type=script_type,
       -                    address_n=address_n,
       -                )
       +            script_type = self.get_keepkey_output_script_type(txout.script_type)
       +            if len(txout.pubkeys) > 1:
       +                xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txout)
       +                multisig = self._make_multisig(txout.num_sig, xpubs_and_deriv_suffixes)
                    else:
       -                address_n = self.client_class.expand_path("/%d/%d" % index)
       -                pubkeys = [self._make_node_path(xpub, address_n) for xpub in xpubs]
       -                multisig = self.types.MultisigRedeemScriptType(
       -                    pubkeys=pubkeys,
       -                    signatures=[b''] * len(pubkeys),
       -                    m=m)
       -                txoutputtype = self.types.TxOutputType(
       -                    multisig=multisig,
       -                    amount=amount,
       -                    address_n=self.client_class.expand_path(derivation + "/%d/%d" % index),
       -                    script_type=script_type)
       +                multisig = None
       +            my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txout)
       +            assert full_path
       +            txoutputtype = self.types.TxOutputType(
       +                multisig=multisig,
       +                amount=txout.value,
       +                address_n=full_path,
       +                script_type=script_type)
                    return txoutputtype
        
                def create_output_by_address():
                    txoutputtype = self.types.TxOutputType()
       -            txoutputtype.amount = amount
       -            if _type == TYPE_SCRIPT:
       -                txoutputtype.script_type = self.types.PAYTOOPRETURN
       -                txoutputtype.op_return_data = trezor_validate_op_return_output_and_get_data(o)
       -            elif _type == TYPE_ADDRESS:
       +            txoutputtype.amount = txout.value
       +            if address:
                        txoutputtype.script_type = self.types.PAYTOADDRESS
                        txoutputtype.address = address
       +            else:
       +                txoutputtype.script_type = self.types.PAYTOOPRETURN
       +                txoutputtype.op_return_data = trezor_validate_op_return_output_and_get_data(txout)
                    return txoutputtype
        
                outputs = []
                has_change = False
                any_output_on_change_branch = is_any_tx_output_on_change_branch(tx)
        
       -        for o in tx.outputs():
       -            _type, address, amount = o.type, o.address, o.value
       +        for txout in tx.outputs():
       +            address = txout.address
                    use_create_by_derivation = False
        
       -            info = tx.output_info.get(address)
       -            if info is not None and not has_change:
       -                index, xpubs, m = info.address_index, info.sorted_xpubs, info.num_sig
       +            if txout.is_mine and not has_change:
                        # prioritise hiding outputs on the 'change' branch from user
                        # because no more than one change address allowed
       -                if info.is_change == any_output_on_change_branch:
       +                if txout.is_change == any_output_on_change_branch:
                            use_create_by_derivation = True
                            has_change = True
        
       t@@ -468,20 +442,20 @@ class KeepKeyPlugin(HW_PluginBase):
        
                return outputs
        
       -    def electrum_tx_to_txtype(self, tx):
       +    def electrum_tx_to_txtype(self, tx: Optional[Transaction]):
                t = self.types.TransactionType()
                if tx is None:
                    # probably for segwit input and we don't need this prev txn
                    return t
       -        d = deserialize(tx.raw)
       -        t.version = d['version']
       -        t.lock_time = d['lockTime']
       +        tx.deserialize()
       +        t.version = tx.version
       +        t.lock_time = tx.locktime
                inputs = self.tx_inputs(tx)
                t.inputs.extend(inputs)
       -        for vout in d['outputs']:
       +        for out in tx.outputs():
                    o = t.bin_outputs.add()
       -            o.amount = vout['value']
       -            o.script_pubkey = bfh(vout['scriptPubKey'])
       +            o.amount = out.value
       +            o.script_pubkey = out.scriptpubkey
                return t
        
            # This function is called from the TREZOR libraries (via tx_api)
   DIR diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py
       t@@ -4,11 +4,13 @@ import sys
        import traceback
        
        from electrum import ecc
       -from electrum.bitcoin import TYPE_ADDRESS, int_to_hex, var_int, is_segwit_script_type
       -from electrum.bip32 import BIP32Node
       +from electrum import bip32
       +from electrum.crypto import hash_160
       +from electrum.bitcoin import int_to_hex, var_int, is_segwit_script_type
       +from electrum.bip32 import BIP32Node, convert_bip32_intpath_to_strpath
        from electrum.i18n import _
        from electrum.keystore import Hardware_KeyStore
       -from electrum.transaction import Transaction
       +from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput
        from electrum.wallet import Standard_Wallet
        from electrum.util import bfh, bh2u, versiontuple, UserFacingException
        from electrum.base_wizard import ScriptTypeNotSupported
       t@@ -78,9 +80,6 @@ class Ledger_Client():
            def label(self):
                return ""
        
       -    def i4b(self, x):
       -        return pack('>I', x)
       -
            def has_usable_connection_with_device(self):
                try:
                    self.dongleObject.getFirmwareVersion()
       t@@ -101,29 +100,27 @@ class Ledger_Client():
                    raise UserFacingException(MSG_NEEDS_FW_UPDATE_SEGWIT)
                if xtype in ['p2wpkh-p2sh', 'p2wsh-p2sh'] and not self.supports_segwit():
                    raise UserFacingException(MSG_NEEDS_FW_UPDATE_SEGWIT)
       -        splitPath = bip32_path.split('/')
       -        if splitPath[0] == 'm':
       -            splitPath = splitPath[1:]
       -            bip32_path = bip32_path[2:]
       -        fingerprint = 0
       -        if len(splitPath) > 1:
       -            prevPath = "/".join(splitPath[0:len(splitPath) - 1])
       +        bip32_path = bip32.normalize_bip32_derivation(bip32_path)
       +        bip32_intpath = bip32.convert_bip32_path_to_list_of_uint32(bip32_path)
       +        bip32_path = bip32_path[2:]  # cut off "m/"
       +        if len(bip32_intpath) >= 1:
       +            prevPath = bip32.convert_bip32_intpath_to_strpath(bip32_intpath[:-1])[2:]
                    nodeData = self.dongleObject.getWalletPublicKey(prevPath)
                    publicKey = compress_public_key(nodeData['publicKey'])
       -            h = hashlib.new('ripemd160')
       -            h.update(hashlib.sha256(publicKey).digest())
       -            fingerprint = unpack(">I", h.digest()[0:4])[0]
       +            fingerprint_bytes = hash_160(publicKey)[0:4]
       +            childnum_bytes = bip32_intpath[-1].to_bytes(length=4, byteorder="big")
       +        else:
       +            fingerprint_bytes = bytes(4)
       +            childnum_bytes = bytes(4)
                nodeData = self.dongleObject.getWalletPublicKey(bip32_path)
                publicKey = compress_public_key(nodeData['publicKey'])
       -        depth = len(splitPath)
       -        lastChild = splitPath[len(splitPath) - 1].split('\'')
       -        childnum = int(lastChild[0]) if len(lastChild) == 1 else 0x80000000 | int(lastChild[0])
       +        depth = len(bip32_intpath)
                return BIP32Node(xtype=xtype,
                                 eckey=ecc.ECPubkey(publicKey),
                                 chaincode=nodeData['chainCode'],
                                 depth=depth,
       -                         fingerprint=self.i4b(fingerprint),
       -                         child_number=self.i4b(childnum)).to_xpub()
       +                         fingerprint=fingerprint_bytes,
       +                         child_number=childnum_bytes).to_xpub()
        
            def has_detached_pin_support(self, client):
                try:
       t@@ -217,6 +214,8 @@ class Ledger_KeyStore(Hardware_KeyStore):
            hw_type = 'ledger'
            device = 'Ledger'
        
       +    plugin: 'LedgerPlugin'
       +
            def __init__(self, d):
                Hardware_KeyStore.__init__(self, d)
                # Errors and other user interaction is done through the wallet's
       t@@ -231,9 +230,6 @@ class Ledger_KeyStore(Hardware_KeyStore):
                obj['cfg'] = self.cfg
                return obj
        
       -    def get_derivation(self):
       -        return self.derivation
       -
            def get_client(self):
                return self.plugin.get_client(self).dongleObject
        
       t@@ -260,13 +256,6 @@ class Ledger_KeyStore(Hardware_KeyStore):
                        self.signing = False
                return wrapper
        
       -    def address_id_stripped(self, address):
       -        # Strip the leading "m/"
       -        change, index = self.get_address_index(address)
       -        derivation = self.derivation
       -        address_path = "%s/%d/%d"%(derivation, change, index)
       -        return address_path[2:]
       -
            def decrypt_message(self, pubkey, message, password):
                raise UserFacingException(_('Encryption and decryption are currently not supported for {}').format(self.device))
        
       t@@ -277,7 +266,7 @@ class Ledger_KeyStore(Hardware_KeyStore):
                message_hash = hashlib.sha256(message).hexdigest().upper()
                # prompt for the PIN before displaying the dialog if necessary
                client = self.get_client()
       -        address_path = self.get_derivation()[2:] + "/%d/%d"%sequence
       +        address_path = self.get_derivation_prefix()[2:] + "/%d/%d"%sequence
                self.handler.show_message("Signing message ...\r\nMessage hash: "+message_hash)
                try:
                    info = self.get_client().signMessagePrepare(address_path, message)
       t@@ -318,16 +307,13 @@ class Ledger_KeyStore(Hardware_KeyStore):
        
            @test_pin_unlocked
            @set_and_unset_signing
       -    def sign_transaction(self, tx: Transaction, password):
       +    def sign_transaction(self, tx, password):
                if tx.is_complete():
                    return
       -        client = self.get_client()
                inputs = []
                inputsPaths = []
       -        pubKeys = []
                chipInputs = []
                redeemScripts = []
       -        signatures = []
                changePath = ""
                output = None
                p2shTransaction = False
       t@@ -336,60 +322,52 @@ class Ledger_KeyStore(Hardware_KeyStore):
                self.get_client() # prompt for the PIN before displaying the dialog if necessary
        
                # Fetch inputs of the transaction to sign
       -        derivations = self.get_tx_derivations(tx)
                for txin in tx.inputs():
       -            if txin['type'] == 'coinbase':
       +            if txin.is_coinbase():
                        self.give_error("Coinbase not supported")     # should never happen
        
       -            if txin['type'] in ['p2sh']:
       +            if txin.script_type in ['p2sh']:
                        p2shTransaction = True
        
       -            if txin['type'] in ['p2wpkh-p2sh', 'p2wsh-p2sh']:
       +            if txin.script_type in ['p2wpkh-p2sh', 'p2wsh-p2sh']:
                        if not self.get_client_electrum().supports_segwit():
                            self.give_error(MSG_NEEDS_FW_UPDATE_SEGWIT)
                        segwitTransaction = True
        
       -            if txin['type'] in ['p2wpkh', 'p2wsh']:
       +            if txin.script_type in ['p2wpkh', 'p2wsh']:
                        if not self.get_client_electrum().supports_native_segwit():
                            self.give_error(MSG_NEEDS_FW_UPDATE_SEGWIT)
                        segwitTransaction = True
        
       -            pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin)
       -            for i, x_pubkey in enumerate(x_pubkeys):
       -                if x_pubkey in derivations:
       -                    signingPos = i
       -                    s = derivations.get(x_pubkey)
       -                    hwAddress = "%s/%d/%d" % (self.get_derivation()[2:], s[0], s[1])
       -                    break
       -            else:
       -                self.give_error("No matching x_key for sign_transaction") # should never happen
       +            my_pubkey, full_path = self.find_my_pubkey_in_txinout(txin)
       +            if not full_path:
       +                self.give_error("No matching pubkey for sign_transaction")  # should never happen
       +            full_path = convert_bip32_intpath_to_strpath(full_path)[2:]
        
                    redeemScript = Transaction.get_preimage_script(txin)
       -            txin_prev_tx = txin.get('prev_tx')
       +            txin_prev_tx = txin.utxo
                    if txin_prev_tx is None and not Transaction.is_segwit_input(txin):
       -                raise UserFacingException(_('Offline signing with {} is not supported for legacy inputs.').format(self.device))
       -            txin_prev_tx_raw = txin_prev_tx.raw if txin_prev_tx else None
       +                raise UserFacingException(_('Missing previous tx for legacy input.'))
       +            txin_prev_tx_raw = txin_prev_tx.serialize() if txin_prev_tx else None
                    inputs.append([txin_prev_tx_raw,
       -                           txin['prevout_n'],
       +                           txin.prevout.out_idx,
                                   redeemScript,
       -                           txin['prevout_hash'],
       -                           signingPos,
       -                           txin.get('sequence', 0xffffffff - 1),
       -                           txin.get('value')])
       -            inputsPaths.append(hwAddress)
       -            pubKeys.append(pubkeys)
       +                           txin.prevout.txid.hex(),
       +                           my_pubkey,
       +                           txin.nsequence,
       +                           txin.value_sats()])
       +            inputsPaths.append(full_path)
        
                # Sanity check
                if p2shTransaction:
                    for txin in tx.inputs():
       -                if txin['type'] != 'p2sh':
       +                if txin.script_type != 'p2sh':
                            self.give_error("P2SH / regular input mixed in same transaction not supported") # should never happen
        
                txOutput = var_int(len(tx.outputs()))
                for o in tx.outputs():
       -            output_type, addr, amount = o.type, o.address, o.value
       -            txOutput += int_to_hex(amount, 8)
       -            script = tx.pay_script(output_type, addr)
       +            txOutput += int_to_hex(o.value, 8)
       +            script = o.scriptpubkey.hex()
                    txOutput += var_int(len(script)//2)
                    txOutput += script
                txOutput = bfh(txOutput)
       t@@ -403,21 +381,21 @@ class Ledger_KeyStore(Hardware_KeyStore):
                            self.give_error("Transaction with more than 2 outputs not supported")
                    has_change = False
                    any_output_on_change_branch = is_any_tx_output_on_change_branch(tx)
       -            for o in tx.outputs():
       -                assert o.type == TYPE_ADDRESS
       -                info = tx.output_info.get(o.address)
       -                if (info is not None) and len(tx.outputs()) > 1 \
       +            for txout in tx.outputs():
       +                assert txout.address
       +                if txout.is_mine and len(tx.outputs()) > 1 \
                                and not has_change:
       -                    index = info.address_index
                            # prioritise hiding outputs on the 'change' branch from user
                            # because no more than one change address allowed
       -                    if info.is_change == any_output_on_change_branch:
       -                        changePath = self.get_derivation()[2:] + "/%d/%d"%index
       +                    if txout.is_change == any_output_on_change_branch:
       +                        my_pubkey, changePath = self.find_my_pubkey_in_txinout(txout)
       +                        assert changePath
       +                        changePath = convert_bip32_intpath_to_strpath(changePath)[2:]
                                has_change = True
                            else:
       -                        output = o.address
       +                        output = txout.address
                        else:
       -                    output = o.address
       +                    output = txout.address
        
                self.handler.show_message(_("Confirm Transaction on your Ledger device..."))
                try:
       t@@ -467,7 +445,10 @@ class Ledger_KeyStore(Hardware_KeyStore):
                                                                    singleInput, redeemScripts[inputIndex], version=tx.version)
                            inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime)
                            inputSignature[0] = 0x30 # force for 1.4.9+
       -                    signatures.append(inputSignature)
       +                    my_pubkey = inputs[inputIndex][4]
       +                    tx.add_signature_to_txin(txin_idx=inputIndex,
       +                                             signing_pubkey=my_pubkey.hex(),
       +                                             sig=inputSignature.hex())
                            inputIndex = inputIndex + 1
                    else:
                        while inputIndex < len(inputs):
       t@@ -488,7 +469,10 @@ class Ledger_KeyStore(Hardware_KeyStore):
                                # Sign input with the provided PIN
                                inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime)
                                inputSignature[0] = 0x30 # force for 1.4.9+
       -                        signatures.append(inputSignature)
       +                        my_pubkey = inputs[inputIndex][4]
       +                        tx.add_signature_to_txin(txin_idx=inputIndex,
       +                                                 signing_pubkey=my_pubkey.hex(),
       +                                                 sig=inputSignature.hex())
                                inputIndex = inputIndex + 1
                            firstTransaction = False
                except UserWarning:
       t@@ -508,16 +492,11 @@ class Ledger_KeyStore(Hardware_KeyStore):
                finally:
                    self.handler.finished()
        
       -        for i, txin in enumerate(tx.inputs()):
       -            signingPos = inputs[i][4]
       -            tx.add_signature_to_txin(i, signingPos, bh2u(signatures[i]))
       -        tx.raw = tx.serialize()
       -
            @test_pin_unlocked
            @set_and_unset_signing
            def show_address(self, sequence, txin_type):
                client = self.get_client()
       -        address_path = self.get_derivation()[2:] + "/%d/%d"%sequence
       +        address_path = self.get_derivation_prefix()[2:] + "/%d/%d"%sequence
                self.handler.show_message(_("Showing address ..."))
                segwit = is_segwit_script_type(txin_type)
                segwitNative = txin_type == 'p2wpkh'
   DIR diff --git a/electrum/plugins/safe_t/safe_t.py b/electrum/plugins/safe_t/safe_t.py
       t@@ -1,6 +1,7 @@
        from binascii import hexlify, unhexlify
        import traceback
        import sys
       +from typing import NamedTuple, Any, Optional, Dict, Union, List, Tuple, TYPE_CHECKING
        
        from electrum.util import bfh, bh2u, versiontuple, UserCancelled, UserFacingException
        from electrum.bitcoin import TYPE_ADDRESS, TYPE_SCRIPT
       t@@ -8,13 +9,16 @@ from electrum.bip32 import BIP32Node
        from electrum import constants
        from electrum.i18n import _
        from electrum.plugin import Device
       -from electrum.transaction import deserialize, Transaction
       -from electrum.keystore import Hardware_KeyStore, is_xpubkey, parse_xpubkey
       +from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput
       +from electrum.keystore import Hardware_KeyStore
        from electrum.base_wizard import ScriptTypeNotSupported
        
        from ..hw_wallet import HW_PluginBase
       -from ..hw_wallet.plugin import is_any_tx_output_on_change_branch, trezor_validate_op_return_output_and_get_data
       +from ..hw_wallet.plugin import (is_any_tx_output_on_change_branch, trezor_validate_op_return_output_and_get_data,
       +                                get_xpubs_and_der_suffixes_from_txinout)
        
       +if TYPE_CHECKING:
       +    from .client import SafeTClient
        
        # Safe-T mini initialization methods
        TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY = range(0, 4)
       t@@ -24,8 +28,7 @@ class SafeTKeyStore(Hardware_KeyStore):
            hw_type = 'safe_t'
            device = 'Safe-T mini'
        
       -    def get_derivation(self):
       -        return self.derivation
       +    plugin: 'SafeTPlugin'
        
            def get_client(self, force_pair=True):
                return self.plugin.get_client(self, force_pair)
       t@@ -35,7 +38,7 @@ class SafeTKeyStore(Hardware_KeyStore):
        
            def sign_message(self, sequence, message, password):
                client = self.get_client()
       -        address_path = self.get_derivation() + "/%d/%d"%sequence
       +        address_path = self.get_derivation_prefix() + "/%d/%d"%sequence
                address_n = client.expand_path(address_path)
                msg_sig = client.sign_message(self.plugin.get_coin_name(), address_n, message)
                return msg_sig.signature
       t@@ -45,22 +48,13 @@ class SafeTKeyStore(Hardware_KeyStore):
                    return
                # previous transactions used as inputs
                prev_tx = {}
       -        # path of the xpubs that are involved
       -        xpub_path = {}
                for txin in tx.inputs():
       -            pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin)
       -            tx_hash = txin['prevout_hash']
       -            if txin.get('prev_tx') is None and not Transaction.is_segwit_input(txin):
       -                raise UserFacingException(_('Offline signing with {} is not supported for legacy inputs.').format(self.device))
       -            prev_tx[tx_hash] = txin['prev_tx']
       -            for x_pubkey in x_pubkeys:
       -                if not is_xpubkey(x_pubkey):
       -                    continue
       -                xpub, s = parse_xpubkey(x_pubkey)
       -                if xpub == self.get_master_public_key():
       -                    xpub_path[xpub] = self.get_derivation()
       +            tx_hash = txin.prevout.txid.hex()
       +            if txin.utxo is None and not Transaction.is_segwit_input(txin):
       +                raise UserFacingException(_('Missing previous tx for legacy input.'))
       +            prev_tx[tx_hash] = txin.utxo
        
       -        self.plugin.sign_transaction(self, tx, prev_tx, xpub_path)
       +        self.plugin.sign_transaction(self, tx, prev_tx)
        
        
        class SafeTPlugin(HW_PluginBase):
       t@@ -148,7 +142,7 @@ class SafeTPlugin(HW_PluginBase):
        
                return client
        
       -    def get_client(self, keystore, force_pair=True):
       +    def get_client(self, keystore, force_pair=True) -> Optional['SafeTClient']:
                devmgr = self.device_manager()
                handler = keystore.handler
                with devmgr.hid_lock:
       t@@ -302,12 +296,11 @@ class SafeTPlugin(HW_PluginBase):
                    return self.types.OutputScriptType.PAYTOMULTISIG
                raise ValueError('unexpected txin type: {}'.format(electrum_txin_type))
        
       -    def sign_transaction(self, keystore, tx, prev_tx, xpub_path):
       +    def sign_transaction(self, keystore, tx: PartialTransaction, prev_tx):
                self.prev_tx = prev_tx
       -        self.xpub_path = xpub_path
                client = self.get_client(keystore)
       -        inputs = self.tx_inputs(tx, True)
       -        outputs = self.tx_outputs(keystore.get_derivation(), tx)
       +        inputs = self.tx_inputs(tx, for_sig=True, keystore=keystore)
       +        outputs = self.tx_outputs(tx, keystore=keystore)
                signatures = client.sign_tx(self.get_coin_name(), inputs, outputs,
                                            lock_time=tx.locktime, version=tx.version)[0]
                signatures = [(bh2u(x) + '01') for x in signatures]
       t@@ -322,139 +315,120 @@ class SafeTPlugin(HW_PluginBase):
                if not client.atleast_version(1, 0):
                    keystore.handler.show_error(_("Your device firmware is too old"))
                    return
       -        change, index = wallet.get_address_index(address)
       -        derivation = keystore.derivation
       -        address_path = "%s/%d/%d"%(derivation, change, index)
       +        deriv_suffix = wallet.get_address_index(address)
       +        derivation = keystore.get_derivation_prefix()
       +        address_path = "%s/%d/%d"%(derivation, *deriv_suffix)
                address_n = client.expand_path(address_path)
       +        script_type = self.get_safet_input_script_type(wallet.txin_type)
       +
       +        # prepare multisig, if available:
                xpubs = wallet.get_master_public_keys()
       -        if len(xpubs) == 1:
       -            script_type = self.get_safet_input_script_type(wallet.txin_type)
       -            client.get_address(self.get_coin_name(), address_n, True, script_type=script_type)
       -        else:
       -            def f(xpub):
       -                return self._make_node_path(xpub, [change, index])
       +        if len(xpubs) > 1:
                    pubkeys = wallet.get_public_keys(address)
                    # sort xpubs using the order of pubkeys
       -            sorted_pubkeys, sorted_xpubs = zip(*sorted(zip(pubkeys, xpubs)))
       -            pubkeys = list(map(f, sorted_xpubs))
       -            multisig = self.types.MultisigRedeemScriptType(
       -               pubkeys=pubkeys,
       -               signatures=[b''] * wallet.n,
       -               m=wallet.m,
       -            )
       -            script_type = self.get_safet_input_script_type(wallet.txin_type)
       -            client.get_address(self.get_coin_name(), address_n, True, multisig=multisig, script_type=script_type)
       -
       -    def tx_inputs(self, tx, for_sig=False):
       +            sorted_pairs = sorted(zip(pubkeys, xpubs))
       +            multisig = self._make_multisig(
       +                wallet.m,
       +                [(xpub, deriv_suffix) for pubkey, xpub in sorted_pairs])
       +        else:
       +            multisig = None
       +
       +        client.get_address(self.get_coin_name(), address_n, True, multisig=multisig, script_type=script_type)
       +
       +    def tx_inputs(self, tx: Transaction, *, for_sig=False, keystore: 'SafeTKeyStore' = None):
                inputs = []
                for txin in tx.inputs():
                    txinputtype = self.types.TxInputType()
       -            if txin['type'] == 'coinbase':
       +            if txin.is_coinbase():
                        prev_hash = b"\x00"*32
                        prev_index = 0xffffffff  # signed int -1
                    else:
                        if for_sig:
       -                    x_pubkeys = txin['x_pubkeys']
       -                    if len(x_pubkeys) == 1:
       -                        x_pubkey = x_pubkeys[0]
       -                        xpub, s = parse_xpubkey(x_pubkey)
       -                        xpub_n = self.client_class.expand_path(self.xpub_path[xpub])
       -                        txinputtype._extend_address_n(xpub_n + s)
       -                        txinputtype.script_type = self.get_safet_input_script_type(txin['type'])
       +                    assert isinstance(tx, PartialTransaction)
       +                    assert isinstance(txin, PartialTxInput)
       +                    assert keystore
       +                    if len(txin.pubkeys) > 1:
       +                        xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txin)
       +                        multisig = self._make_multisig(txin.num_sig, xpubs_and_deriv_suffixes)
                            else:
       -                        def f(x_pubkey):
       -                            xpub, s = parse_xpubkey(x_pubkey)
       -                            return self._make_node_path(xpub, s)
       -                        pubkeys = list(map(f, x_pubkeys))
       -                        multisig = self.types.MultisigRedeemScriptType(
       -                            pubkeys=pubkeys,
       -                            signatures=list(map(lambda x: bfh(x)[:-1] if x else b'', txin.get('signatures'))),
       -                            m=txin.get('num_sig'),
       -                        )
       -                        script_type = self.get_safet_input_script_type(txin['type'])
       -                        txinputtype = self.types.TxInputType(
       -                            script_type=script_type,
       -                            multisig=multisig
       -                        )
       -                        # find which key is mine
       -                        for x_pubkey in x_pubkeys:
       -                            if is_xpubkey(x_pubkey):
       -                                xpub, s = parse_xpubkey(x_pubkey)
       -                                if xpub in self.xpub_path:
       -                                    xpub_n = self.client_class.expand_path(self.xpub_path[xpub])
       -                                    txinputtype._extend_address_n(xpub_n + s)
       -                                    break
       -
       -                prev_hash = unhexlify(txin['prevout_hash'])
       -                prev_index = txin['prevout_n']
       -
       -            if 'value' in txin:
       -                txinputtype.amount = txin['value']
       +                        multisig = None
       +                    script_type = self.get_safet_input_script_type(txin.script_type)
       +                    txinputtype = self.types.TxInputType(
       +                        script_type=script_type,
       +                        multisig=multisig)
       +                    my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txin)
       +                    if full_path:
       +                        txinputtype._extend_address_n(full_path)
       +
       +                prev_hash = txin.prevout.txid
       +                prev_index = txin.prevout.out_idx
       +
       +            if txin.value_sats() is not None:
       +                txinputtype.amount = txin.value_sats()
                    txinputtype.prev_hash = prev_hash
                    txinputtype.prev_index = prev_index
        
       -            if txin.get('scriptSig') is not None:
       -                script_sig = bfh(txin['scriptSig'])
       -                txinputtype.script_sig = script_sig
       +            if txin.script_sig is not None:
       +                txinputtype.script_sig = txin.script_sig
        
       -            txinputtype.sequence = txin.get('sequence', 0xffffffff - 1)
       +            txinputtype.sequence = txin.nsequence
        
                    inputs.append(txinputtype)
        
                return inputs
        
       -    def tx_outputs(self, derivation, tx: Transaction):
       +    def _make_multisig(self, m, xpubs):
       +        if len(xpubs) == 1:
       +            return None
       +        pubkeys = [self._make_node_path(xpub, deriv) for xpub, deriv in xpubs]
       +        return self.types.MultisigRedeemScriptType(
       +            pubkeys=pubkeys,
       +            signatures=[b''] * len(pubkeys),
       +            m=m)
       +
       +    def tx_outputs(self, tx: PartialTransaction, *, keystore: 'SafeTKeyStore'):
        
                def create_output_by_derivation():
       -            script_type = self.get_safet_output_script_type(info.script_type)
       -            if len(xpubs) == 1:
       -                address_n = self.client_class.expand_path(derivation + "/%d/%d" % index)
       -                txoutputtype = self.types.TxOutputType(
       -                    amount=amount,
       -                    script_type=script_type,
       -                    address_n=address_n,
       -                )
       +            script_type = self.get_safet_output_script_type(txout.script_type)
       +            if len(txout.pubkeys) > 1:
       +                xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txout)
       +                multisig = self._make_multisig(txout.num_sig, xpubs_and_deriv_suffixes)
                    else:
       -                address_n = self.client_class.expand_path("/%d/%d" % index)
       -                pubkeys = [self._make_node_path(xpub, address_n) for xpub in xpubs]
       -                multisig = self.types.MultisigRedeemScriptType(
       -                    pubkeys=pubkeys,
       -                    signatures=[b''] * len(pubkeys),
       -                    m=m)
       -                txoutputtype = self.types.TxOutputType(
       -                    multisig=multisig,
       -                    amount=amount,
       -                    address_n=self.client_class.expand_path(derivation + "/%d/%d" % index),
       -                    script_type=script_type)
       +                multisig = None
       +            my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txout)
       +            assert full_path
       +            txoutputtype = self.types.TxOutputType(
       +                multisig=multisig,
       +                amount=txout.value,
       +                address_n=full_path,
       +                script_type=script_type)
                    return txoutputtype
        
                def create_output_by_address():
                    txoutputtype = self.types.TxOutputType()
       -            txoutputtype.amount = amount
       -            if _type == TYPE_SCRIPT:
       -                txoutputtype.script_type = self.types.OutputScriptType.PAYTOOPRETURN
       -                txoutputtype.op_return_data = trezor_validate_op_return_output_and_get_data(o)
       -            elif _type == TYPE_ADDRESS:
       +            txoutputtype.amount = txout.value
       +            if address:
                        txoutputtype.script_type = self.types.OutputScriptType.PAYTOADDRESS
                        txoutputtype.address = address
       +            else:
       +                txoutputtype.script_type = self.types.OutputScriptType.PAYTOOPRETURN
       +                txoutputtype.op_return_data = trezor_validate_op_return_output_and_get_data(txout)
                    return txoutputtype
        
                outputs = []
                has_change = False
                any_output_on_change_branch = is_any_tx_output_on_change_branch(tx)
        
       -        for o in tx.outputs():
       -            _type, address, amount = o.type, o.address, o.value
       +        for txout in tx.outputs():
       +            address = txout.address
                    use_create_by_derivation = False
        
       -            info = tx.output_info.get(address)
       -            if info is not None and not has_change:
       -                index, xpubs, m = info.address_index, info.sorted_xpubs, info.num_sig
       +            if txout.is_mine and not has_change:
                        # prioritise hiding outputs on the 'change' branch from user
                        # because no more than one change address allowed
                        # note: ^ restriction can be removed once we require fw
                        # that has https://github.com/trezor/trezor-mcu/pull/306
       -                if info.is_change == any_output_on_change_branch:
       +                if txout.is_change == any_output_on_change_branch:
                            use_create_by_derivation = True
                            has_change = True
        
       t@@ -466,20 +440,20 @@ class SafeTPlugin(HW_PluginBase):
        
                return outputs
        
       -    def electrum_tx_to_txtype(self, tx):
       +    def electrum_tx_to_txtype(self, tx: Optional[Transaction]):
                t = self.types.TransactionType()
                if tx is None:
                    # probably for segwit input and we don't need this prev txn
                    return t
       -        d = deserialize(tx.raw)
       -        t.version = d['version']
       -        t.lock_time = d['lockTime']
       +        tx.deserialize()
       +        t.version = tx.version
       +        t.lock_time = tx.locktime
                inputs = self.tx_inputs(tx)
                t._extend_inputs(inputs)
       -        for vout in d['outputs']:
       +        for out in tx.outputs():
                    o = t._add_bin_outputs()
       -            o.amount = vout['value']
       -            o.script_pubkey = bfh(vout['scriptPubKey'])
       +            o.amount = out.value
       +            o.script_pubkey = out.scriptpubkey
                return t
        
            # This function is called from the TREZOR libraries (via tx_api)
   DIR diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py
       t@@ -1,6 +1,6 @@
        import traceback
        import sys
       -from typing import NamedTuple, Any
       +from typing import NamedTuple, Any, Optional, Dict, Union, List, Tuple, TYPE_CHECKING
        
        from electrum.util import bfh, bh2u, versiontuple, UserCancelled, UserFacingException
        from electrum.bitcoin import TYPE_ADDRESS, TYPE_SCRIPT
       t@@ -8,14 +8,15 @@ from electrum.bip32 import BIP32Node, convert_bip32_path_to_list_of_uint32 as pa
        from electrum import constants
        from electrum.i18n import _
        from electrum.plugin import Device
       -from electrum.transaction import deserialize, Transaction
       -from electrum.keystore import Hardware_KeyStore, is_xpubkey, parse_xpubkey
       +from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput
       +from electrum.keystore import Hardware_KeyStore
        from electrum.base_wizard import ScriptTypeNotSupported, HWD_SETUP_NEW_WALLET
        from electrum.logging import get_logger
        
        from ..hw_wallet import HW_PluginBase
        from ..hw_wallet.plugin import (is_any_tx_output_on_change_branch, trezor_validate_op_return_output_and_get_data,
       -                                LibraryFoundButUnusable, OutdatedHwFirmwareException)
       +                                LibraryFoundButUnusable, OutdatedHwFirmwareException,
       +                                get_xpubs_and_der_suffixes_from_txinout)
        
        _logger = get_logger(__name__)
        
       t@@ -53,8 +54,7 @@ class TrezorKeyStore(Hardware_KeyStore):
            hw_type = 'trezor'
            device = TREZOR_PRODUCT_KEY
        
       -    def get_derivation(self):
       -        return self.derivation
       +    plugin: 'TrezorPlugin'
        
            def get_client(self, force_pair=True):
                return self.plugin.get_client(self, force_pair)
       t@@ -64,7 +64,7 @@ class TrezorKeyStore(Hardware_KeyStore):
        
            def sign_message(self, sequence, message, password):
                client = self.get_client()
       -        address_path = self.get_derivation() + "/%d/%d"%sequence
       +        address_path = self.get_derivation_prefix() + "/%d/%d"%sequence
                msg_sig = client.sign_message(address_path, message)
                return msg_sig.signature
        
       t@@ -73,22 +73,13 @@ class TrezorKeyStore(Hardware_KeyStore):
                    return
                # previous transactions used as inputs
                prev_tx = {}
       -        # path of the xpubs that are involved
       -        xpub_path = {}
                for txin in tx.inputs():
       -            pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin)
       -            tx_hash = txin['prevout_hash']
       -            if txin.get('prev_tx') is None and not Transaction.is_segwit_input(txin):
       -                raise UserFacingException(_('Offline signing with {} is not supported for legacy inputs.').format(self.device))
       -            prev_tx[tx_hash] = txin['prev_tx']
       -            for x_pubkey in x_pubkeys:
       -                if not is_xpubkey(x_pubkey):
       -                    continue
       -                xpub, s = parse_xpubkey(x_pubkey)
       -                if xpub == self.get_master_public_key():
       -                    xpub_path[xpub] = self.get_derivation()
       +            tx_hash = txin.prevout.txid.hex()
       +            if txin.utxo is None and not Transaction.is_segwit_input(txin):
       +                raise UserFacingException(_('Missing previous tx for legacy input.'))
       +            prev_tx[tx_hash] = txin.utxo
        
       -        self.plugin.sign_transaction(self, tx, prev_tx, xpub_path)
       +        self.plugin.sign_transaction(self, tx, prev_tx)
        
        
        class TrezorInitSettings(NamedTuple):
       t@@ -172,7 +163,7 @@ class TrezorPlugin(HW_PluginBase):
                # note that this call can still raise!
                return TrezorClientBase(transport, handler, self)
        
       -    def get_client(self, keystore, force_pair=True):
       +    def get_client(self, keystore, force_pair=True) -> Optional['TrezorClientBase']:
                devmgr = self.device_manager()
                handler = keystore.handler
                with devmgr.hid_lock:
       t@@ -327,11 +318,11 @@ class TrezorPlugin(HW_PluginBase):
                    return OutputScriptType.PAYTOMULTISIG
                raise ValueError('unexpected txin type: {}'.format(electrum_txin_type))
        
       -    def sign_transaction(self, keystore, tx, prev_tx, xpub_path):
       -        prev_tx = { bfh(txhash): self.electrum_tx_to_txtype(tx, xpub_path) for txhash, tx in prev_tx.items() }
       +    def sign_transaction(self, keystore, tx: PartialTransaction, prev_tx):
       +        prev_tx = { bfh(txhash): self.electrum_tx_to_txtype(tx) for txhash, tx in prev_tx.items() }
                client = self.get_client(keystore)
       -        inputs = self.tx_inputs(tx, xpub_path, True)
       -        outputs = self.tx_outputs(keystore.get_derivation(), tx)
       +        inputs = self.tx_inputs(tx, for_sig=True, keystore=keystore)
       +        outputs = self.tx_outputs(tx, keystore=keystore)
                details = SignTx(lock_time=tx.locktime, version=tx.version)
                signatures, _ = client.sign_tx(self.get_coin_name(), inputs, outputs, details=details, prev_txes=prev_tx)
                signatures = [(bh2u(x) + '01') for x in signatures]
       t@@ -343,7 +334,7 @@ class TrezorPlugin(HW_PluginBase):
                if not self.show_address_helper(wallet, address, keystore):
                    return
                deriv_suffix = wallet.get_address_index(address)
       -        derivation = keystore.derivation
       +        derivation = keystore.get_derivation_prefix()
                address_path = "%s/%d/%d"%(derivation, *deriv_suffix)
                script_type = self.get_trezor_input_script_type(wallet.txin_type)
        
       t@@ -355,111 +346,107 @@ class TrezorPlugin(HW_PluginBase):
                    sorted_pairs = sorted(zip(pubkeys, xpubs))
                    multisig = self._make_multisig(
                        wallet.m,
       -                [(xpub, deriv_suffix) for _, xpub in sorted_pairs])
       +                [(xpub, deriv_suffix) for pubkey, xpub in sorted_pairs])
                else:
                    multisig = None
        
                client = self.get_client(keystore)
                client.show_address(address_path, script_type, multisig)
        
       -    def tx_inputs(self, tx, xpub_path, for_sig=False):
       +    def tx_inputs(self, tx: Transaction, *, for_sig=False, keystore: 'TrezorKeyStore' = None):
                inputs = []
                for txin in tx.inputs():
                    txinputtype = TxInputType()
       -            if txin['type'] == 'coinbase':
       +            if txin.is_coinbase():
                        prev_hash = b"\x00"*32
                        prev_index = 0xffffffff  # signed int -1
                    else:
                        if for_sig:
       -                    x_pubkeys = txin['x_pubkeys']
       -                    xpubs = [parse_xpubkey(x) for x in x_pubkeys]
       -                    multisig = self._make_multisig(txin.get('num_sig'), xpubs, txin.get('signatures'))
       -                    script_type = self.get_trezor_input_script_type(txin['type'])
       +                    assert isinstance(tx, PartialTransaction)
       +                    assert isinstance(txin, PartialTxInput)
       +                    assert keystore
       +                    if len(txin.pubkeys) > 1:
       +                        xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txin)
       +                        multisig = self._make_multisig(txin.num_sig, xpubs_and_deriv_suffixes)
       +                    else:
       +                        multisig = None
       +                    script_type = self.get_trezor_input_script_type(txin.script_type)
                            txinputtype = TxInputType(
                                script_type=script_type,
                                multisig=multisig)
       -                    # find which key is mine
       -                    for xpub, deriv in xpubs:
       -                        if xpub in xpub_path:
       -                            xpub_n = parse_path(xpub_path[xpub])
       -                            txinputtype.address_n = xpub_n + deriv
       -                            break
       -
       -                prev_hash = bfh(txin['prevout_hash'])
       -                prev_index = txin['prevout_n']
       -
       -            if 'value' in txin:
       -                txinputtype.amount = txin['value']
       +                    my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txin)
       +                    if full_path:
       +                        txinputtype.address_n = full_path
       +
       +                prev_hash = txin.prevout.txid
       +                prev_index = txin.prevout.out_idx
       +
       +            if txin.value_sats() is not None:
       +                txinputtype.amount = txin.value_sats()
                    txinputtype.prev_hash = prev_hash
                    txinputtype.prev_index = prev_index
        
       -            if txin.get('scriptSig') is not None:
       -                script_sig = bfh(txin['scriptSig'])
       -                txinputtype.script_sig = script_sig
       +            if txin.script_sig is not None:
       +                txinputtype.script_sig = txin.script_sig
        
       -            txinputtype.sequence = txin.get('sequence', 0xffffffff - 1)
       +            txinputtype.sequence = txin.nsequence
        
                    inputs.append(txinputtype)
        
                return inputs
        
       -    def _make_multisig(self, m, xpubs, signatures=None):
       +    def _make_multisig(self, m, xpubs):
                if len(xpubs) == 1:
                    return None
       -
                pubkeys = [self._make_node_path(xpub, deriv) for xpub, deriv in xpubs]
       -        if signatures is None:
       -            signatures = [b''] * len(pubkeys)
       -        elif len(signatures) != len(pubkeys):
       -            raise RuntimeError('Mismatched number of signatures')
       -        else:
       -            signatures = [bfh(x)[:-1] if x else b'' for x in signatures]
       -
                return MultisigRedeemScriptType(
                    pubkeys=pubkeys,
       -            signatures=signatures,
       +            signatures=[b''] * len(pubkeys),
                    m=m)
        
       -    def tx_outputs(self, derivation, tx: Transaction):
       +    def tx_outputs(self, tx: PartialTransaction, *, keystore: 'TrezorKeyStore'):
        
                def create_output_by_derivation():
       -            script_type = self.get_trezor_output_script_type(info.script_type)
       -            deriv = parse_path("/%d/%d" % index)
       -            multisig = self._make_multisig(m, [(xpub, deriv) for xpub in xpubs])
       +            script_type = self.get_trezor_output_script_type(txout.script_type)
       +            if len(txout.pubkeys) > 1:
       +                xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txout)
       +                multisig = self._make_multisig(txout.num_sig, xpubs_and_deriv_suffixes)
       +            else:
       +                multisig = None
       +            my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txout)
       +            assert full_path
                    txoutputtype = TxOutputType(
                        multisig=multisig,
       -                amount=amount,
       -                address_n=parse_path(derivation + "/%d/%d" % index),
       +                amount=txout.value,
       +                address_n=full_path,
                        script_type=script_type)
                    return txoutputtype
        
                def create_output_by_address():
                    txoutputtype = TxOutputType()
       -            txoutputtype.amount = amount
       -            if _type == TYPE_SCRIPT:
       -                txoutputtype.script_type = OutputScriptType.PAYTOOPRETURN
       -                txoutputtype.op_return_data = trezor_validate_op_return_output_and_get_data(o)
       -            elif _type == TYPE_ADDRESS:
       +            txoutputtype.amount = txout.value
       +            if address:
                        txoutputtype.script_type = OutputScriptType.PAYTOADDRESS
                        txoutputtype.address = address
       +            else:
       +                txoutputtype.script_type = OutputScriptType.PAYTOOPRETURN
       +                txoutputtype.op_return_data = trezor_validate_op_return_output_and_get_data(txout)
                    return txoutputtype
        
                outputs = []
                has_change = False
                any_output_on_change_branch = is_any_tx_output_on_change_branch(tx)
        
       -        for o in tx.outputs():
       -            _type, address, amount = o.type, o.address, o.value
       +        for txout in tx.outputs():
       +            address = txout.address
                    use_create_by_derivation = False
        
       -            info = tx.output_info.get(address)
       -            if info is not None and not has_change:
       -                index, xpubs, m = info.address_index, info.sorted_xpubs, info.num_sig
       +            if txout.is_mine and not has_change:
                        # prioritise hiding outputs on the 'change' branch from user
                        # because no more than one change address allowed
                        # note: ^ restriction can be removed once we require fw
                        # that has https://github.com/trezor/trezor-mcu/pull/306
       -                if info.is_change == any_output_on_change_branch:
       +                if txout.is_change == any_output_on_change_branch:
                            use_create_by_derivation = True
                            has_change = True
        
       t@@ -471,17 +458,17 @@ class TrezorPlugin(HW_PluginBase):
        
                return outputs
        
       -    def electrum_tx_to_txtype(self, tx, xpub_path):
       +    def electrum_tx_to_txtype(self, tx: Optional[Transaction]):
                t = TransactionType()
                if tx is None:
                    # probably for segwit input and we don't need this prev txn
                    return t
       -        d = deserialize(tx.raw)
       -        t.version = d['version']
       -        t.lock_time = d['lockTime']
       -        t.inputs = self.tx_inputs(tx, xpub_path)
       +        tx.deserialize()
       +        t.version = tx.version
       +        t.lock_time = tx.locktime
       +        t.inputs = self.tx_inputs(tx)
                t.bin_outputs = [
       -            TxOutputBinType(amount=vout['value'], script_pubkey=bfh(vout['scriptPubKey']))
       -            for vout in d['outputs']
       +            TxOutputBinType(amount=o.value, script_pubkey=o.scriptpubkey)
       +            for o in tx.outputs()
                ]
                return t
   DIR diff --git a/electrum/plugins/trustedcoin/cmdline.py b/electrum/plugins/trustedcoin/cmdline.py
       t@@ -30,7 +30,7 @@ from .trustedcoin import TrustedCoinPlugin
        
        class Plugin(TrustedCoinPlugin):
        
       -    def prompt_user_for_otp(self, wallet, tx):
       +    def prompt_user_for_otp(self, wallet, tx):  # FIXME this is broken
                if not isinstance(wallet, self.wallet_class):
                    return
                if not wallet.can_sign_without_server():
   DIR diff --git a/electrum/plugins/trustedcoin/legacy_tx_format.py b/electrum/plugins/trustedcoin/legacy_tx_format.py
       t@@ -0,0 +1,106 @@
       +# Copyright (C) 2018 The Electrum developers
       +# Distributed under the MIT software license, see the accompanying
       +# file LICENCE or http://www.opensource.org/licenses/mit-license.php
       +
       +import copy
       +from typing import Union
       +
       +from electrum import bitcoin
       +from electrum.bitcoin import push_script, int_to_hex, var_int
       +from electrum.transaction import (Transaction, PartialTransaction, PartialTxInput,
       +                                  multisig_script, construct_witness)
       +from electrum.keystore import BIP32_KeyStore
       +from electrum.wallet import Multisig_Wallet
       +
       +
       +ELECTRUM_PARTIAL_TXN_HEADER_MAGIC = b'EPTF\xff'
       +PARTIAL_FORMAT_VERSION = b'\x00'
       +NO_SIGNATURE = b'\xff'
       +
       +
       +def get_xpubkey(keystore: BIP32_KeyStore, c, i) -> str:
       +    def encode_path_int(path_int) -> str:
       +        if path_int < 0xffff:
       +            hex = bitcoin.int_to_hex(path_int, 2)
       +        else:
       +            hex = 'ffff' + bitcoin.int_to_hex(path_int, 4)
       +        return hex
       +
       +    s = ''.join(map(encode_path_int, (c, i)))
       +    return 'ff' + bitcoin.DecodeBase58Check(keystore.xpub).hex() + s
       +
       +
       +def serialize_tx_in_legacy_format(tx: PartialTransaction, *, wallet: Multisig_Wallet) -> str:
       +    assert isinstance(tx, PartialTransaction)
       +
       +    # copy tx so we don't mutate the input arg
       +    # monkey-patch method of tx instance to change serialization
       +    tx = copy.deepcopy(tx)
       +
       +    def get_siglist(txin: 'PartialTxInput', *, estimate_size=False):
       +        if txin.prevout.is_coinbase():
       +            return [], []
       +        if estimate_size:
       +            try:
       +                pubkey_size = len(txin.pubkeys[0])
       +            except IndexError:
       +                pubkey_size = 33  # guess it is compressed
       +            num_pubkeys = max(1, len(txin.pubkeys))
       +            pk_list = ["00" * pubkey_size] * num_pubkeys
       +            # we assume that signature will be 0x48 bytes long
       +            num_sig = max(txin.num_sig, num_pubkeys)
       +            sig_list = [ "00" * 0x48 ] * num_sig
       +        else:
       +            pk_list = ["" for pk in txin.pubkeys]
       +            for ks in wallet.get_keystores():
       +                my_pubkey, full_path = ks.find_my_pubkey_in_txinout(txin)
       +                x_pubkey = get_xpubkey(ks, full_path[-2], full_path[-1])
       +                pubkey_index = txin.pubkeys.index(my_pubkey)
       +                pk_list[pubkey_index] = x_pubkey
       +            assert all(pk_list)
       +            sig_list = [txin.part_sigs.get(pubkey, NO_SIGNATURE).hex() for pubkey in txin.pubkeys]
       +        return pk_list, sig_list
       +
       +    def input_script(self, txin: PartialTxInput, *, estimate_size=False) -> str:
       +        assert estimate_size is False
       +        pubkeys, sig_list = get_siglist(txin, estimate_size=estimate_size)
       +        script = ''.join(push_script(x) for x in sig_list)
       +        if txin.script_type == 'p2sh':
       +            # put op_0 before script
       +            script = '00' + script
       +            redeem_script = multisig_script(pubkeys, txin.num_sig)
       +            script += push_script(redeem_script)
       +            return script
       +        elif txin.script_type == 'p2wsh':
       +            return ''
       +        raise Exception(f"unexpected type {txin.script_type}")
       +    tx.input_script = input_script.__get__(tx, PartialTransaction)
       +
       +    def serialize_witness(self, txin: PartialTxInput, *, estimate_size=False):
       +        assert estimate_size is False
       +        if txin.witness is not None:
       +            return txin.witness.hex()
       +        if txin.prevout.is_coinbase():
       +            return ''
       +        assert isinstance(txin, PartialTxInput)
       +        if not self.is_segwit_input(txin):
       +            return '00'
       +        pubkeys, sig_list = get_siglist(txin, estimate_size=estimate_size)
       +        if txin.script_type == 'p2wsh':
       +            witness_script = multisig_script(pubkeys, txin.num_sig)
       +            witness = construct_witness([0] + sig_list + [witness_script])
       +        else:
       +            raise Exception(f"unexpected type {txin.script_type}")
       +        if txin.is_complete() or estimate_size:
       +            partial_format_witness_prefix = ''
       +        else:
       +            input_value = int_to_hex(txin.value_sats(), 8)
       +            witness_version = int_to_hex(0, 2)
       +            partial_format_witness_prefix = var_int(0xffffffff) + input_value + witness_version
       +        return partial_format_witness_prefix + witness
       +    tx.serialize_witness = serialize_witness.__get__(tx, PartialTransaction)
       +
       +    buf = ELECTRUM_PARTIAL_TXN_HEADER_MAGIC.hex()
       +    buf += PARTIAL_FORMAT_VERSION.hex()
       +    buf += tx.serialize_to_network()
       +    return buf
   DIR diff --git a/electrum/plugins/trustedcoin/trustedcoin.py b/electrum/plugins/trustedcoin/trustedcoin.py
       t@@ -29,7 +29,7 @@ import base64
        import time
        import hashlib
        from collections import defaultdict
       -from typing import Dict, Union
       +from typing import Dict, Union, Sequence, List
        
        from urllib.parse import urljoin
        from urllib.parse import quote
       t@@ -39,7 +39,7 @@ from electrum import ecc, constants, keystore, version, bip32, bitcoin
        from electrum.bitcoin import TYPE_ADDRESS
        from electrum.bip32 import BIP32Node, xpub_type
        from electrum.crypto import sha256
       -from electrum.transaction import TxOutput
       +from electrum.transaction import PartialTxOutput, PartialTxInput, PartialTransaction, Transaction
        from electrum.mnemonic import Mnemonic, seed_type, is_any_2fa_seed_type
        from electrum.wallet import Multisig_Wallet, Deterministic_Wallet
        from electrum.i18n import _
       t@@ -50,6 +50,8 @@ from electrum.network import Network
        from electrum.base_wizard import BaseWizard, WizardWalletPasswordSetting
        from electrum.logging import Logger
        
       +from .legacy_tx_format import serialize_tx_in_legacy_format
       +
        
        def get_signing_xpub(xtype):
            if not constants.net.TESTNET:
       t@@ -259,6 +261,8 @@ server = TrustedCoinCosignerClient(user_agent="Electrum/" + version.ELECTRUM_VER
        
        class Wallet_2fa(Multisig_Wallet):
        
       +    plugin: 'TrustedCoinPlugin'
       +
            wallet_type = '2fa'
        
            def __init__(self, storage, *, config):
       t@@ -314,34 +318,35 @@ class Wallet_2fa(Multisig_Wallet):
                    raise Exception('too high trustedcoin fee ({} for {} txns)'.format(price, n))
                return price
        
       -    def make_unsigned_transaction(self, coins, outputs, fixed_fee=None,
       -                                  change_addr=None, is_sweep=False):
       +    def make_unsigned_transaction(self, *, coins: Sequence[PartialTxInput],
       +                                  outputs: List[PartialTxOutput], fee=None,
       +                                  change_addr: str = None, is_sweep=False) -> PartialTransaction:
                mk_tx = lambda o: Multisig_Wallet.make_unsigned_transaction(
       -            self, coins, o, fixed_fee, change_addr)
       -        fee = self.extra_fee() if not is_sweep else 0
       -        if fee:
       +            self, coins=coins, outputs=o, fee=fee, change_addr=change_addr)
       +        extra_fee = self.extra_fee() if not is_sweep else 0
       +        if extra_fee:
                    address = self.billing_info['billing_address_segwit']
       -            fee_output = TxOutput(TYPE_ADDRESS, address, fee)
       +            fee_output = PartialTxOutput.from_address_and_value(address, extra_fee)
                    try:
                        tx = mk_tx(outputs + [fee_output])
                    except NotEnoughFunds:
                        # TrustedCoin won't charge if the total inputs is
                        # lower than their fee
                        tx = mk_tx(outputs)
       -                if tx.input_value() >= fee:
       +                if tx.input_value() >= extra_fee:
                            raise
                        self.logger.info("not charging for this tx")
                else:
                    tx = mk_tx(outputs)
                return tx
        
       -    def on_otp(self, tx, otp):
       +    def on_otp(self, tx: PartialTransaction, otp):
                if not otp:
                    self.logger.info("sign_transaction: no auth code")
                    return
                otp = int(otp)
                long_user_id, short_id = self.get_user_id()
       -        raw_tx = tx.serialize()
       +        raw_tx = serialize_tx_in_legacy_format(tx, wallet=self)
                try:
                    r = server.sign(short_id, raw_tx, otp)
                except TrustedCoinException as e:
       t@@ -350,8 +355,9 @@ class Wallet_2fa(Multisig_Wallet):
                    else:
                        raise
                if r:
       -            raw_tx = r.get('transaction')
       -            tx.update(raw_tx)
       +            received_raw_tx = r.get('transaction')
       +            received_tx = Transaction(received_raw_tx)
       +            tx.combine_with_other_psbt(received_tx)
                self.logger.info(f"twofactor: is complete {tx.is_complete()}")
                # reset billing_info
                self.billing_info = None
       t@@ -457,15 +463,16 @@ class TrustedCoinPlugin(BasePlugin):
                    self.logger.info("twofactor: xpub3 not needed")
                    return
                def wrapper(tx):
       +            assert tx
                    self.prompt_user_for_otp(wallet, tx, on_success, on_failure)
                return wrapper
        
            @hook
       -    def get_tx_extra_fee(self, wallet, tx):
       +    def get_tx_extra_fee(self, wallet, tx: Transaction):
                if type(wallet) != Wallet_2fa:
                    return
                for o in tx.outputs():
       -            if o.type == TYPE_ADDRESS and wallet.is_billing_address(o.address):
       +            if wallet.is_billing_address(o.address):
                        return o.address, o.value
        
            def finish_requesting(func):
   DIR diff --git a/electrum/scripts/bip70.py b/electrum/scripts/bip70.py
       t@@ -7,6 +7,7 @@ import tlslite
        from electrum.transaction import Transaction
        from electrum import paymentrequest
        from electrum import paymentrequest_pb2 as pb2
       +from electrum.bitcoin import address_to_script
        
        chain_file = 'mychain.pem'
        cert_file = 'mycert.pem'
       t@@ -26,7 +27,7 @@ certificates.certificate.extend(map(lambda x: str(x.bytes), chain.x509List))
        with open(cert_file, 'r') as f:
            rsakey = tlslite.utils.python_rsakey.Python_RSAKey.parsePEM(f.read())
        
       -script = Transaction.pay_script('address', address).decode('hex')
       +script = address_to_script(address)
        
        pr_string = paymentrequest.make_payment_request(amount, script, memo, rsakey)
        
   DIR diff --git a/electrum/segwit_addr.py b/electrum/segwit_addr.py
       t@@ -103,6 +103,8 @@ def convertbits(data, frombits, tobits, pad=True):
        
        def decode(hrp, addr):
            """Decode a segwit address."""
       +    if addr is None:
       +        return (None, None)
            hrpgot, data = bech32_decode(addr)
            if hrpgot != hrp:
                return (None, None)
   DIR diff --git a/electrum/synchronizer.py b/electrum/synchronizer.py
       t@@ -209,7 +209,7 @@ class Synchronizer(SynchronizerBase):
            async def _get_transaction(self, tx_hash, *, allow_server_not_finding_tx=False):
                self._requests_sent += 1
                try:
       -            result = await self.network.get_transaction(tx_hash)
       +            raw_tx = await self.network.get_transaction(tx_hash)
                except UntrustedServerReturnedError as e:
                    # most likely, "No such mempool or blockchain transaction"
                    if allow_server_not_finding_tx:
       t@@ -219,7 +219,7 @@ class Synchronizer(SynchronizerBase):
                        raise
                finally:
                    self._requests_answered += 1
       -        tx = Transaction(result)
       +        tx = Transaction(raw_tx)
                try:
                    tx.deserialize()  # see if raises
                except Exception as e:
       t@@ -233,7 +233,7 @@ class Synchronizer(SynchronizerBase):
                    raise SynchronizerFailure(f"received tx does not match expected txid ({tx_hash} != {tx.txid()})")
                tx_height = self.requested_tx.pop(tx_hash)
                self.wallet.receive_tx_callback(tx_hash, tx, tx_height)
       -        self.logger.info(f"received tx {tx_hash} height: {tx_height} bytes: {len(tx.raw)}")
       +        self.logger.info(f"received tx {tx_hash} height: {tx_height} bytes: {len(raw_tx)}")
                # callbacks
                self.wallet.network.trigger_callback('new_transaction', self.wallet, tx)
        
   DIR diff --git a/electrum/tests/regtest/regtest.sh b/electrum/tests/regtest/regtest.sh
       t@@ -147,7 +147,7 @@ if [[ $1 == "breach" ]]; then
            echo "alice pays"
            $alice lnpay $request
            sleep 2
       -    ctx=$($alice get_channel_ctx $channel | jq '.hex' | tr -d '"')
       +    ctx=$($alice get_channel_ctx $channel)
            request=$($bob add_lightning_request 0.01 -m "blah2")
            echo "alice pays again"
            $alice lnpay $request
       t@@ -224,7 +224,7 @@ if [[ $1 == "breach_with_unspent_htlc" ]]; then
                echo "SETTLE_DELAY did not work, $settled != 0"
                exit 1
            fi
       -    ctx=$($alice get_channel_ctx $channel | jq '.hex' | tr -d '"')
       +    ctx=$($alice get_channel_ctx $channel)
            sleep 5
            settled=$($alice list_channels | jq '.[] | .local_htlcs | .settles | length')
            if [[ "$settled" != "1" ]]; then
       t@@ -251,7 +251,7 @@ if [[ $1 == "breach_with_spent_htlc" ]]; then
            echo "alice pays bob"
            invoice=$($bob add_lightning_request 0.05 -m "test")
            $alice lnpay $invoice --timeout=1 || true
       -    ctx=$($alice get_channel_ctx $channel | jq '.hex' | tr -d '"')
       +    ctx=$($alice get_channel_ctx $channel)
            settled=$($alice list_channels | jq '.[] | .local_htlcs | .settles | length')
            if [[ "$settled" != "0" ]]; then
                echo "SETTLE_DELAY did not work, $settled != 0"
   DIR diff --git a/electrum/tests/test_bitcoin.py b/electrum/tests/test_bitcoin.py
       t@@ -12,7 +12,7 @@ from electrum.bitcoin import (public_key_to_p2pkh, address_from_private_key,
        from electrum.bip32 import (BIP32Node, convert_bip32_intpath_to_strpath,
                                    xpub_from_xprv, xpub_type, is_xprv, is_bip32_derivation,
                                    is_xpub, convert_bip32_path_to_list_of_uint32,
       -                            normalize_bip32_derivation)
       +                            normalize_bip32_derivation, is_all_public_derivation)
        from electrum.crypto import sha256d, SUPPORTED_PW_HASH_VERSIONS
        from electrum import ecc, crypto, constants
        from electrum.ecc import number_to_string, string_to_number
       t@@ -494,6 +494,14 @@ class Test_xprv_xpub(ElectrumTestCase):
                self.assertEqual("m/0/2/1'", normalize_bip32_derivation("m/0/2/-1/"))
                self.assertEqual("m/0/1'/1'/5'", normalize_bip32_derivation("m/0//-1/1'///5h"))
        
       +    def test_is_all_public_derivation(self):
       +        self.assertFalse(is_all_public_derivation("m/0/1'/1'"))
       +        self.assertFalse(is_all_public_derivation("m/0/2/1'"))
       +        self.assertFalse(is_all_public_derivation("m/0/1'/1'/5"))
       +        self.assertTrue(is_all_public_derivation("m"))
       +        self.assertTrue(is_all_public_derivation("m/0"))
       +        self.assertTrue(is_all_public_derivation("m/75/22/3"))
       +
            def test_xtype_from_derivation(self):
                self.assertEqual('standard', xtype_from_derivation("m/44'"))
                self.assertEqual('standard', xtype_from_derivation("m/44'/"))
   DIR diff --git a/electrum/tests/test_commands.py b/electrum/tests/test_commands.py
       t@@ -159,3 +159,24 @@ class TestCommandsTestnet(TestCaseForTestnet):
                for xkey1, xtype1 in xprvs:
                    for xkey2, xtype2 in xprvs:
                        self.assertEqual(xkey2, cmds._run('convert_xkey', (xkey1, xtype2)))
       +
       +    def test_serialize(self):
       +        cmds = Commands(config=self.config)
       +        jsontx = {
       +            "inputs": [
       +                {
       +                    "prevout_hash": "9d221a69ca3997cbeaf5624d723e7dc5f829b1023078c177d37bdae95f37c539",
       +                    "prevout_n": 1,
       +                    "value": 1000000,
       +                    "privkey": "p2wpkh:cVDXzzQg6RoCTfiKpe8MBvmm5d5cJc6JLuFApsFDKwWa6F5TVHpD"
       +                }
       +            ],
       +            "outputs": [
       +                {
       +                    "address": "tb1q4s8z6g5jqzllkgt8a4har94wl8tg0k9m8kv5zd",
       +                    "value": 990000
       +                }
       +            ]
       +        }
       +        self.assertEqual("0200000000010139c5375fe9da7bd377c1783002b129f8c57d3e724d62f5eacb9739ca691a229d0100000000feffffff01301b0f0000000000160014ac0e2d229200bffb2167ed6fd196aef9d687d8bb02483045022100fa88a9e7930b2af269fd0a5cb7fbbc3d0a05606f3ac6ea8a40686ebf02fdd85802203dd19603b4ee8fdb81d40185572027686f70ea299c6a3e22bc2545e1396398b20121021f110909ded653828a254515b58498a6bafc96799fb0851554463ed44ca7d9da00000000",
       +                         cmds._run('serialize', (jsontx,)))
   DIR diff --git a/electrum/tests/test_lnchannel.py b/electrum/tests/test_lnchannel.py
       t@@ -170,7 +170,7 @@ class TestFee(ElectrumTestCase):
            """
            def test_fee(self):
                alice_channel, bob_channel = create_test_channels(253, 10000000000, 5000000000)
       -        self.assertIn(9999817, [x[2] for x in alice_channel.get_latest_commitment(LOCAL).outputs()])
       +        self.assertIn(9999817, [x.value for x in alice_channel.get_latest_commitment(LOCAL).outputs()])
        
        class TestChannel(ElectrumTestCase):
            maxDiff = 999
   DIR diff --git a/electrum/tests/test_lnutil.py b/electrum/tests/test_lnutil.py
       t@@ -9,7 +9,7 @@ from electrum.lnutil import (RevocationStore, get_per_commitment_secret_from_see
                                     get_compressed_pubkey_from_bech32, split_host_port, ConnStringFormatError,
                                     ScriptHtlc, extract_nodeid, calc_onchain_fees, UpdateAddHtlc)
        from electrum.util import bh2u, bfh
       -from electrum.transaction import Transaction
       +from electrum.transaction import Transaction, PartialTransaction
        
        from . import ElectrumTestCase
        
       t@@ -570,7 +570,7 @@ class TestLNUtil(ElectrumTestCase):
                    localhtlcsig=bfh(local_sig),
                    payment_preimage=htlc_payment_preimage if success else b'',  # will put 00 on witness if timeout
                    witness_script=htlc)
       -        our_htlc_tx._inputs[0]['witness'] = bh2u(our_htlc_tx_witness)
       +        our_htlc_tx._inputs[0].witness = our_htlc_tx_witness
                return str(our_htlc_tx)
        
            def test_commitment_tx_with_one_output(self):
       t@@ -669,7 +669,7 @@ class TestLNUtil(ElectrumTestCase):
                ref_commit_tx_str = '02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8002c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de84311054a56a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400473044022051b75c73198c6deee1a875871c3961832909acd297c6b908d59e3319e5185a46022055c419379c5051a78d00dbbce11b5b664a0c22815fbcc6fcef6b1937c383693901483045022100f51d2e566a70ba740fc5d8c0f07b9b93d2ed741c3c0860c613173de7d39e7968022041376d520e9c0e1ad52248ddf4b22e12be8763007df977253ef45a4ca3bdb7c001475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220'
                self.assertEqual(str(our_commit_tx), ref_commit_tx_str)
        
       -    def sign_and_insert_remote_sig(self, tx, remote_pubkey, remote_signature, pubkey, privkey):
       +    def sign_and_insert_remote_sig(self, tx: PartialTransaction, remote_pubkey, remote_signature, pubkey, privkey):
                assert type(remote_pubkey) is bytes
                assert len(remote_pubkey) == 33
                assert type(remote_signature) is str
       t@@ -678,10 +678,7 @@ class TestLNUtil(ElectrumTestCase):
                assert len(pubkey) == 33
                assert len(privkey) == 33
                tx.sign({bh2u(pubkey): (privkey[:-1], True)})
       -        pubkeys, _x_pubkeys = tx.get_sorted_pubkeys(tx.inputs()[0])
       -        index_of_pubkey = pubkeys.index(bh2u(remote_pubkey))
       -        tx._inputs[0]["signatures"][index_of_pubkey] = remote_signature + "01"
       -        tx.raw = None
       +        tx.add_signature_to_txin(txin_idx=0, signing_pubkey=remote_pubkey.hex(), sig=remote_signature + "01")
        
            def test_get_compressed_pubkey_from_bech32(self):
                self.assertEqual(b'\x03\x84\xef\x87\xd9d\xa2\xaaa7=\xff\xb8\xfe=t8[}>;\n\x13\xa8e\x8eo:\xf5Mi\xb5H',
   DIR diff --git a/electrum/tests/test_psbt.py b/electrum/tests/test_psbt.py
       t@@ -0,0 +1,269 @@
       +from pprint import pprint
       +import unittest
       +
       +from electrum import constants
       +from electrum.transaction import (tx_from_any, PartialTransaction, BadHeaderMagic, UnexpectedEndOfStream,
       +                                  SerializationError, PSBTInputConsistencyFailure)
       +
       +from . import ElectrumTestCase, TestCaseForTestnet
       +
       +
       +class TestValidPSBT(TestCaseForTestnet):
       +    # test cases from BIP-0174
       +
       +    def test_valid_psbt_001(self):
       +        # Case: PSBT with one P2PKH input. Outputs are empty
parazyd.org:70 /git/electrum/commit/707b74d22b28d942c445754311736f158e505990.gph:7173: line too long