tMerge pull request #5721 from SomberNight/201910_psbt - electrum - Electrum Bitcoin wallet
 (HTM) git clone https://git.parazyd.org/electrum
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) Submodules
       ---
 (DIR) commit 707b74d22b28d942c445754311736f158e505990
 (DIR) parent 6d12ebabbb686bcd028b659c6a6ce2cb4782bc14
 (HTM) 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
       +        tx1 = tx_from_any(bytes.fromhex('70736274ff0100750200000001268171371edff285e937adeea4b37b78000c0566cbb3ad64641713ca42171bf60000000000feffffff02d3dff505000000001976a914d0c59903c5bac2868760e90fd521a4665aa7652088ac00e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787b32e1300000100fda5010100000000010289a3c71eab4d20e0371bbba4cc698fa295c9463afa2e397f8533ccb62f9567e50100000017160014be18d152a9b012039daf3da7de4f53349eecb985ffffffff86f8aa43a71dff1448893a530a7237ef6b4608bbb2dd2d0171e63aec6a4890b40100000017160014fe3e9ef1a745e974d902c4355943abcb34bd5353ffffffff0200c2eb0b000000001976a91485cff1097fd9e008bb34af709c62197b38978a4888ac72fef84e2c00000017a914339725ba21efd62ac753a9bcd067d6c7a6a39d05870247304402202712be22e0270f394f568311dc7ca9a68970b8025fdd3b240229f07f8a5f3a240220018b38d7dcd314e734c9276bd6fb40f673325bc4baa144c800d2f2f02db2765c012103d2e15674941bad4a996372cb87e1856d3652606d98562fe39c5e9e7e413f210502483045022100d12b852d85dcd961d2f5f4ab660654df6eedcc794c0c33ce5cc309ffb5fce58d022067338a8e0e1725c197fb1a88af59f51e44e4255b20167c8684031c05d1f2592a01210223b72beef0965d10be0778efecd61fcac6f79a4ea169393380734464f84f2ab300000000000000'))
       +        tx2 = tx_from_any('cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAAAA')
       +        for tx in (tx1, tx2):
       +            self.assertEqual(1, len(tx.inputs()))
       +            self.assertFalse(tx.inputs()[0].is_complete())
       +
       +    def test_valid_psbt_002(self):
       +        # Case: PSBT with one P2PKH input and one P2SH-P2WPKH input. First input is signed and finalized. Outputs are empty
       +        tx1 = tx_from_any(bytes.fromhex('70736274ff0100a00200000002ab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40000000000feffffffab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40100000000feffffff02603bea0b000000001976a914768a40bbd740cbe81d988e71de2a4d5c71396b1d88ac8e240000000000001976a9146f4620b553fa095e721b9ee0efe9fa039cca459788ac000000000001076a47304402204759661797c01b036b25928948686218347d89864b719e1f7fcf57d1e511658702205309eabf56aa4d8891ffd111fdf1336f3a29da866d7f8486d75546ceedaf93190121035cdc61fc7ba971c0b501a646a2a83b102cb43881217ca682dc86e2d73fa882920001012000e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787010416001485d13537f2e265405a34dbafa9e3dda01fb82308000000'))
       +        tx2 = tx_from_any('cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEHakcwRAIgR1lmF5fAGwNrJZKJSGhiGDR9iYZLcZ4ff89X0eURZYcCIFMJ6r9Wqk2Ikf/REf3xM286KdqGbX+EhtdVRs7tr5MZASEDXNxh/HupccC1AaZGoqg7ECy0OIEhfKaC3Ibi1z+ogpIAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIAAAA')
       +        for tx in (tx1, tx2):
       +            self.assertEqual(2, len(tx.inputs()))
       +            self.assertTrue(tx.inputs()[0].is_complete())
       +            self.assertFalse(tx.inputs()[1].is_complete())
       +
       +    def test_valid_psbt_003(self):
       +        # Case: PSBT with one P2PKH input which has a non-final scriptSig and has a sighash type specified. Outputs are empty
       +        tx1 = tx_from_any(bytes.fromhex('70736274ff0100750200000001268171371edff285e937adeea4b37b78000c0566cbb3ad64641713ca42171bf60000000000feffffff02d3dff505000000001976a914d0c59903c5bac2868760e90fd521a4665aa7652088ac00e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787b32e1300000100fda5010100000000010289a3c71eab4d20e0371bbba4cc698fa295c9463afa2e397f8533ccb62f9567e50100000017160014be18d152a9b012039daf3da7de4f53349eecb985ffffffff86f8aa43a71dff1448893a530a7237ef6b4608bbb2dd2d0171e63aec6a4890b40100000017160014fe3e9ef1a745e974d902c4355943abcb34bd5353ffffffff0200c2eb0b000000001976a91485cff1097fd9e008bb34af709c62197b38978a4888ac72fef84e2c00000017a914339725ba21efd62ac753a9bcd067d6c7a6a39d05870247304402202712be22e0270f394f568311dc7ca9a68970b8025fdd3b240229f07f8a5f3a240220018b38d7dcd314e734c9276bd6fb40f673325bc4baa144c800d2f2f02db2765c012103d2e15674941bad4a996372cb87e1856d3652606d98562fe39c5e9e7e413f210502483045022100d12b852d85dcd961d2f5f4ab660654df6eedcc794c0c33ce5cc309ffb5fce58d022067338a8e0e1725c197fb1a88af59f51e44e4255b20167c8684031c05d1f2592a01210223b72beef0965d10be0778efecd61fcac6f79a4ea169393380734464f84f2ab30000000001030401000000000000'))
       +        tx2 = tx_from_any('cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAQMEAQAAAAAAAA==')
       +        for tx in (tx1, tx2):
       +            self.assertEqual(1, len(tx.inputs()))
       +            self.assertEqual(1, tx.inputs()[0].sighash)
       +            self.assertFalse(tx.inputs()[0].is_complete())
       +
       +    def test_valid_psbt_004(self):
       +        # Case: PSBT with one P2PKH input and one P2SH-P2WPKH input both with non-final scriptSigs. P2SH-P2WPKH input's redeemScript is available. Outputs filled.
       +        tx1 = tx_from_any(bytes.fromhex('70736274ff0100a00200000002ab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40000000000feffffffab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40100000000feffffff02603bea0b000000001976a914768a40bbd740cbe81d988e71de2a4d5c71396b1d88ac8e240000000000001976a9146f4620b553fa095e721b9ee0efe9fa039cca459788ac00000000000100df0200000001268171371edff285e937adeea4b37b78000c0566cbb3ad64641713ca42171bf6000000006a473044022070b2245123e6bf474d60c5b50c043d4c691a5d2435f09a34a7662a9dc251790a022001329ca9dacf280bdf30740ec0390422422c81cb45839457aeb76fc12edd95b3012102657d118d3357b8e0f4c2cd46db7b39f6d9c38d9a70abcb9b2de5dc8dbfe4ce31feffffff02d3dff505000000001976a914d0c59903c5bac2868760e90fd521a4665aa7652088ac00e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787b32e13000001012000e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787010416001485d13537f2e265405a34dbafa9e3dda01fb8230800220202ead596687ca806043edc3de116cdf29d5e9257c196cd055cf698c8d02bf24e9910b4a6ba670000008000000080020000800022020394f62be9df19952c5587768aeb7698061ad2c4a25c894f47d8c162b4d7213d0510b4a6ba6700000080010000800200008000'))
       +        tx2 = tx_from_any('cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEA3wIAAAABJoFxNx7f8oXpN63upLN7eAAMBWbLs61kZBcTykIXG/YAAAAAakcwRAIgcLIkUSPmv0dNYMW1DAQ9TGkaXSQ18Jo0p2YqncJReQoCIAEynKnazygL3zB0DsA5BCJCLIHLRYOUV663b8Eu3ZWzASECZX0RjTNXuOD0ws1G23s59tnDjZpwq8ubLeXcjb/kzjH+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIACICAurVlmh8qAYEPtw94RbN8p1eklfBls0FXPaYyNAr8k6ZELSmumcAAACAAAAAgAIAAIAAIgIDlPYr6d8ZlSxVh3aK63aYBhrSxKJciU9H2MFitNchPQUQtKa6ZwAAAIABAACAAgAAgAA=')
       +        for tx in (tx1, tx2):
       +            self.assertEqual(2, len(tx.inputs()))
       +            self.assertFalse(tx.inputs()[0].is_complete())
       +            self.assertFalse(tx.inputs()[1].is_complete())
       +            self.assertTrue(tx.inputs()[1].redeem_script is not None)
       +
       +    def test_valid_psbt_005(self):
       +        # Case: PSBT with one P2SH-P2WSH input of a 2-of-2 multisig, redeemScript, witnessScript, and keypaths are available. Contains one signature.
       +        tx1 = tx_from_any(bytes.fromhex('70736274ff0100550200000001279a2323a5dfb51fc45f220fa58b0fc13e1e3342792a85d7e36cd6333b5cbc390000000000ffffffff01a05aea0b000000001976a914ffe9c0061097cc3b636f2cb0460fa4fc427d2b4588ac0000000000010120955eea0b0000000017a9146345200f68d189e1adc0df1c4d16ea8f14c0dbeb87220203b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4646304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa94b99bdf86151db9a9a010104220020771fd18ad459666dd49f3d564e3dbc42f4c84774e360ada16816a8ed488d5681010547522103b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd462103de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd52ae220603b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4610b4a6ba67000000800000008004000080220603de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd10b4a6ba670000008000000080050000800000'))
       +        tx2 = tx_from_any('cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQEEIgAgdx/RitRZZm3Unz1WTj28QvTIR3TjYK2haBao7UiNVoEBBUdSIQOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RiED3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg71SriIGA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GELSmumcAAACAAAAAgAQAAIAiBgPeVdHh2sgF4/iljB+/m5TALz26r+En/vykmV8m+CCDvRC0prpnAAAAgAAAAIAFAACAAAA=')
       +        for tx in (tx1, tx2):
       +            self.assertEqual(1, len(tx.inputs()))
       +            self.assertFalse(tx.inputs()[0].is_complete())
       +            self.assertTrue(tx.inputs()[0].redeem_script is not None)
       +            self.assertTrue(tx.inputs()[0].witness_script is not None)
       +            self.assertEqual(2, len(tx.inputs()[0].bip32_paths))
       +            self.assertEqual(1, len(tx.inputs()[0].part_sigs))
       +
       +    def test_valid_psbt_006(self):
       +        # Case: PSBT with one P2WSH input of a 2-of-2 multisig. witnessScript, keypaths, and global xpubs are available. Contains no signatures. Outputs filled.
       +        tx1 = tx_from_any(bytes.fromhex('70736274ff01005202000000019dfc6628c26c5899fe1bd3dc338665bfd55d7ada10f6220973df2d386dec12760100000000ffffffff01f03dcd1d000000001600147b3a00bfdc14d27795c2b74901d09da6ef133579000000004f01043587cf02da3fd0088000000097048b1ad0445b1ec8275517727c87b4e4ebc18a203ffa0f94c01566bd38e9000351b743887ee1d40dc32a6043724f2d6459b3b5a4d73daec8fbae0472f3bc43e20cd90c6a4fae000080000000804f01043587cf02da3fd00880000001b90452427139cd78c2cff2444be353cd58605e3e513285e528b407fae3f6173503d30a5e97c8adbc557dac2ad9a7e39c1722ebac69e668b6f2667cc1d671c83cab0cd90c6a4fae000080010000800001012b0065cd1d000000002200202c5486126c4978079a814e13715d65f36459e4d6ccaded266d0508645bafa6320105475221029da12cdb5b235692b91536afefe5c91c3ab9473d8e43b533836ab456299c88712103372b34234ed7cf9c1fea5d05d441557927be9542b162eb02e1ab2ce80224c00b52ae2206029da12cdb5b235692b91536afefe5c91c3ab9473d8e43b533836ab456299c887110d90c6a4fae0000800000008000000000220603372b34234ed7cf9c1fea5d05d441557927be9542b162eb02e1ab2ce80224c00b10d90c6a4fae0000800100008000000000002202039eff1f547a1d5f92dfa2ba7af6ac971a4bd03ba4a734b03156a256b8ad3a1ef910ede45cc500000080000000800100008000'))
       +        tx2 = tx_from_any('cHNidP8BAFICAAAAAZ38ZijCbFiZ/hvT3DOGZb/VXXraEPYiCXPfLTht7BJ2AQAAAAD/////AfA9zR0AAAAAFgAUezoAv9wU0neVwrdJAdCdpu8TNXkAAAAATwEENYfPAto/0AiAAAAAlwSLGtBEWx7IJ1UXcnyHtOTrwYogP/oPlMAVZr046QADUbdDiH7h1A3DKmBDck8tZFmztaTXPa7I+64EcvO8Q+IM2QxqT64AAIAAAACATwEENYfPAto/0AiAAAABuQRSQnE5zXjCz/JES+NTzVhgXj5RMoXlKLQH+uP2FzUD0wpel8itvFV9rCrZp+OcFyLrrGnmaLbyZnzB1nHIPKsM2QxqT64AAIABAACAAAEBKwBlzR0AAAAAIgAgLFSGEmxJeAeagU4TcV1l82RZ5NbMre0mbQUIZFuvpjIBBUdSIQKdoSzbWyNWkrkVNq/v5ckcOrlHPY5DtTODarRWKZyIcSEDNys0I07Xz5wf6l0F1EFVeSe+lUKxYusC4ass6AIkwAtSriIGAp2hLNtbI1aSuRU2r+/lyRw6uUc9jkO1M4NqtFYpnIhxENkMak+uAACAAAAAgAAAAAAiBgM3KzQjTtfPnB/qXQXUQVV5J76VQrFi6wLhqyzoAiTACxDZDGpPrgAAgAEAAIAAAAAAACICA57/H1R6HV+S36K6evaslxpL0DukpzSwMVaiVritOh75EO3kXMUAAACAAAAAgAEAAIAA')
       +        for tx in (tx1, tx2):
       +            self.assertEqual(1, len(tx.inputs()))
       +            self.assertFalse(tx.inputs()[0].is_complete())
       +            self.assertTrue(tx.inputs()[0].witness_script is not None)
       +            self.assertEqual(2, len(tx.inputs()[0].bip32_paths))
       +            self.assertEqual(2, len(tx.xpubs))
       +            self.assertEqual(0, len(tx.inputs()[0].part_sigs))
       +
       +    def test_valid_psbt_007(self):
       +        # Case: PSBT with unknown types in the inputs.
       +        tx1 = tx_from_any(bytes.fromhex('70736274ff01003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a010000000000000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f0000'))
       +        tx2 = tx_from_any('cHNidP8BAD8CAAAAAf//////////////////////////////////////////AAAAAAD/////AQAAAAAAAAAAA2oBAAAAAAAACg8BAgMEBQYHCAkPAQIDBAUGBwgJCgsMDQ4PAAA=')
       +        for tx in (tx1, tx2):
       +            self.assertEqual(1, len(tx.inputs()))
       +            self.assertEqual(1, len(tx.inputs()[0]._unknown))
       +
       +    def test_valid_psbt_008(self):
       +        # Case: PSBT with `PSBT_GLOBAL_XPUB`.
       +        constants.set_mainnet()
       +        try:
       +            tx1 = tx_from_any(bytes.fromhex('70736274ff01009d0100000002710ea76ab45c5cb6438e607e59cc037626981805ae9e0dfd9089012abb0be5350100000000ffffffff190994d6a8b3c8c82ccbcfb2fba4106aa06639b872a8d447465c0d42588d6d670000000000ffffffff0200e1f505000000001976a914b6bc2c0ee5655a843d79afedd0ccc3f7dd64340988ac605af405000000001600141188ef8e4ce0449eaac8fb141cbf5a1176e6a088000000004f010488b21e039e530cac800000003dbc8a5c9769f031b17e77fea1518603221a18fd18f2b9a54c6c8c1ac75cbc3502f230584b155d1c7f1cd45120a653c48d650b431b67c5b2c13f27d7142037c1691027569c503100008000000080000000800001011f00e1f5050000000016001433b982f91b28f160c920b4ab95e58ce50dda3a4a220203309680f33c7de38ea6a47cd4ecd66f1f5a49747c6ffb8808ed09039243e3ad5c47304402202d704ced830c56a909344bd742b6852dccd103e963bae92d38e75254d2bb424502202d86c437195df46c0ceda084f2a291c3da2d64070f76bf9b90b195e7ef28f77201220603309680f33c7de38ea6a47cd4ecd66f1f5a49747c6ffb8808ed09039243e3ad5c1827569c5031000080000000800000008000000000010000000001011f00e1f50500000000160014388fb944307eb77ef45197d0b0b245e079f011de220202c777161f73d0b7c72b9ee7bde650293d13f095bc7656ad1f525da5fd2e10b11047304402204cb1fb5f869c942e0e26100576125439179ae88dca8a9dc3ba08f7953988faa60220521f49ca791c27d70e273c9b14616985909361e25be274ea200d7e08827e514d01220602c777161f73d0b7c72b9ee7bde650293d13f095bc7656ad1f525da5fd2e10b1101827569c5031000080000000800000008000000000000000000000220202d20ca502ee289686d21815bd43a80637b0698e1fbcdbe4caed445f6c1a0a90ef1827569c50310000800000008000000080000000000400000000'))
       +            tx2 = tx_from_any('cHNidP8BAJ0BAAAAAnEOp2q0XFy2Q45gflnMA3YmmBgFrp4N/ZCJASq7C+U1AQAAAAD/////GQmU1qizyMgsy8+y+6QQaqBmObhyqNRHRlwNQliNbWcAAAAAAP////8CAOH1BQAAAAAZdqkUtrwsDuVlWoQ9ea/t0MzD991kNAmIrGBa9AUAAAAAFgAUEYjvjkzgRJ6qyPsUHL9aEXbmoIgAAAAATwEEiLIeA55TDKyAAAAAPbyKXJdp8DGxfnf+oVGGAyIaGP0Y8rmlTGyMGsdcvDUC8jBYSxVdHH8c1FEgplPEjWULQxtnxbLBPyfXFCA3wWkQJ1acUDEAAIAAAACAAAAAgAABAR8A4fUFAAAAABYAFDO5gvkbKPFgySC0q5XljOUN2jpKIgIDMJaA8zx9446mpHzU7NZvH1pJdHxv+4gI7QkDkkPjrVxHMEQCIC1wTO2DDFapCTRL10K2hS3M0QPpY7rpLTjnUlTSu0JFAiAthsQ3GV30bAztoITyopHD2i1kBw92v5uQsZXn7yj3cgEiBgMwloDzPH3jjqakfNTs1m8fWkl0fG/7iAjtCQOSQ+OtXBgnVpxQMQAAgAAAAIAAAACAAAAAAAEAAAAAAQEfAOH1BQAAAAAWABQ4j7lEMH63fvRRl9CwskXgefAR3iICAsd3Fh9z0LfHK57nveZQKT0T8JW8dlatH1Jdpf0uELEQRzBEAiBMsftfhpyULg4mEAV2ElQ5F5rojcqKncO6CPeVOYj6pgIgUh9JynkcJ9cOJzybFGFphZCTYeJb4nTqIA1+CIJ+UU0BIgYCx3cWH3PQt8crnue95lApPRPwlbx2Vq0fUl2l/S4QsRAYJ1acUDEAAIAAAACAAAAAgAAAAAAAAAAAAAAiAgLSDKUC7iiWhtIYFb1DqAY3sGmOH7zb5MrtRF9sGgqQ7xgnVpxQMQAAgAAAAIAAAACAAAAAAAQAAAAA')
       +            for tx in (tx1, tx2):
       +                self.assertEqual(1, len(tx.xpubs))
       +        finally:
       +            constants.set_testnet()
       +
       +
       +class TestInvalidPSBT(TestCaseForTestnet):
       +    # test cases from BIP-0174
       +
       +    def test_invalid_psbt_001(self):
       +        # Case: Network transaction, not PSBT format
       +        with self.assertRaises(BadHeaderMagic):
       +            tx1 = PartialTransaction.from_raw_psbt(bytes.fromhex('0200000001268171371edff285e937adeea4b37b78000c0566cbb3ad64641713ca42171bf6000000006a473044022070b2245123e6bf474d60c5b50c043d4c691a5d2435f09a34a7662a9dc251790a022001329ca9dacf280bdf30740ec0390422422c81cb45839457aeb76fc12edd95b3012102657d118d3357b8e0f4c2cd46db7b39f6d9c38d9a70abcb9b2de5dc8dbfe4ce31feffffff02d3dff505000000001976a914d0c59903c5bac2868760e90fd521a4665aa7652088ac00e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787b32e1300'))
       +        with self.assertRaises(BadHeaderMagic):
       +            tx2 = PartialTransaction.from_raw_psbt('AgAAAAEmgXE3Ht/yhek3re6ks3t4AAwFZsuzrWRkFxPKQhcb9gAAAABqRzBEAiBwsiRRI+a/R01gxbUMBD1MaRpdJDXwmjSnZiqdwlF5CgIgATKcqdrPKAvfMHQOwDkEIkIsgctFg5RXrrdvwS7dlbMBIQJlfRGNM1e44PTCzUbbezn22cONmnCry5st5dyNv+TOMf7///8C09/1BQAAAAAZdqkU0MWZA8W6woaHYOkP1SGkZlqnZSCIrADh9QUAAAAAF6kUNUXm4zuDLEcFDyTT7rk8nAOUi8eHsy4TAA==')
       +
       +    def test_invalid_psbt_002(self):
       +        # Case: PSBT missing outputs
       +        with self.assertRaises(UnexpectedEndOfStream):
       +            tx1 = tx_from_any(bytes.fromhex('70736274ff0100750200000001268171371edff285e937adeea4b37b78000c0566cbb3ad64641713ca42171bf60000000000feffffff02d3dff505000000001976a914d0c59903c5bac2868760e90fd521a4665aa7652088ac00e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787b32e1300000100fda5010100000000010289a3c71eab4d20e0371bbba4cc698fa295c9463afa2e397f8533ccb62f9567e50100000017160014be18d152a9b012039daf3da7de4f53349eecb985ffffffff86f8aa43a71dff1448893a530a7237ef6b4608bbb2dd2d0171e63aec6a4890b40100000017160014fe3e9ef1a745e974d902c4355943abcb34bd5353ffffffff0200c2eb0b000000001976a91485cff1097fd9e008bb34af709c62197b38978a4888ac72fef84e2c00000017a914339725ba21efd62ac753a9bcd067d6c7a6a39d05870247304402202712be22e0270f394f568311dc7ca9a68970b8025fdd3b240229f07f8a5f3a240220018b38d7dcd314e734c9276bd6fb40f673325bc4baa144c800d2f2f02db2765c012103d2e15674941bad4a996372cb87e1856d3652606d98562fe39c5e9e7e413f210502483045022100d12b852d85dcd961d2f5f4ab660654df6eedcc794c0c33ce5cc309ffb5fce58d022067338a8e0e1725c197fb1a88af59f51e44e4255b20167c8684031c05d1f2592a01210223b72beef0965d10be0778efecd61fcac6f79a4ea169393380734464f84f2ab30000000000'))
       +        with self.assertRaises(UnexpectedEndOfStream):
       +            tx2 = tx_from_any('cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAA==')
       +
       +    def test_invalid_psbt_003(self):
       +        # Case: PSBT where one input has a filled scriptSig in the unsigned tx
       +        with self.assertRaises(SerializationError):
       +            tx1 = tx_from_any(bytes.fromhex('70736274ff0100fd0a010200000002ab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be4000000006a47304402204759661797c01b036b25928948686218347d89864b719e1f7fcf57d1e511658702205309eabf56aa4d8891ffd111fdf1336f3a29da866d7f8486d75546ceedaf93190121035cdc61fc7ba971c0b501a646a2a83b102cb43881217ca682dc86e2d73fa88292feffffffab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40100000000feffffff02603bea0b000000001976a914768a40bbd740cbe81d988e71de2a4d5c71396b1d88ac8e240000000000001976a9146f4620b553fa095e721b9ee0efe9fa039cca459788ac00000000000001012000e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787010416001485d13537f2e265405a34dbafa9e3dda01fb82308000000'))
       +        with self.assertRaises(SerializationError):
       +            tx2 = tx_from_any('cHNidP8BAP0KAQIAAAACqwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QAAAAAakcwRAIgR1lmF5fAGwNrJZKJSGhiGDR9iYZLcZ4ff89X0eURZYcCIFMJ6r9Wqk2Ikf/REf3xM286KdqGbX+EhtdVRs7tr5MZASEDXNxh/HupccC1AaZGoqg7ECy0OIEhfKaC3Ibi1z+ogpL+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAABASAA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHhwEEFgAUhdE1N/LiZUBaNNuvqePdoB+4IwgAAAA=')
       +
       +    def test_invalid_psbt_004(self):
       +        # Case: PSBT where inputs and outputs are provided but without an unsigned tx
       +        with self.assertRaises(SerializationError):
       +            tx1 = tx_from_any(bytes.fromhex('70736274ff000100fda5010100000000010289a3c71eab4d20e0371bbba4cc698fa295c9463afa2e397f8533ccb62f9567e50100000017160014be18d152a9b012039daf3da7de4f53349eecb985ffffffff86f8aa43a71dff1448893a530a7237ef6b4608bbb2dd2d0171e63aec6a4890b40100000017160014fe3e9ef1a745e974d902c4355943abcb34bd5353ffffffff0200c2eb0b000000001976a91485cff1097fd9e008bb34af709c62197b38978a4888ac72fef84e2c00000017a914339725ba21efd62ac753a9bcd067d6c7a6a39d05870247304402202712be22e0270f394f568311dc7ca9a68970b8025fdd3b240229f07f8a5f3a240220018b38d7dcd314e734c9276bd6fb40f673325bc4baa144c800d2f2f02db2765c012103d2e15674941bad4a996372cb87e1856d3652606d98562fe39c5e9e7e413f210502483045022100d12b852d85dcd961d2f5f4ab660654df6eedcc794c0c33ce5cc309ffb5fce58d022067338a8e0e1725c197fb1a88af59f51e44e4255b20167c8684031c05d1f2592a01210223b72beef0965d10be0778efecd61fcac6f79a4ea169393380734464f84f2ab30000000000'))
       +        with self.assertRaises(SerializationError):
       +            tx2 = tx_from_any('cHNidP8AAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAA==')
       +
       +    def test_invalid_psbt_005(self):
       +        # Case: PSBT with duplicate keys in an input
       +        with self.assertRaises(SerializationError):
       +            tx1 = tx_from_any(bytes.fromhex('70736274ff0100750200000001268171371edff285e937adeea4b37b78000c0566cbb3ad64641713ca42171bf60000000000feffffff02d3dff505000000001976a914d0c59903c5bac2868760e90fd521a4665aa7652088ac00e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787b32e1300000100fda5010100000000010289a3c71eab4d20e0371bbba4cc698fa295c9463afa2e397f8533ccb62f9567e50100000017160014be18d152a9b012039daf3da7de4f53349eecb985ffffffff86f8aa43a71dff1448893a530a7237ef6b4608bbb2dd2d0171e63aec6a4890b40100000017160014fe3e9ef1a745e974d902c4355943abcb34bd5353ffffffff0200c2eb0b000000001976a91485cff1097fd9e008bb34af709c62197b38978a4888ac72fef84e2c00000017a914339725ba21efd62ac753a9bcd067d6c7a6a39d05870247304402202712be22e0270f394f568311dc7ca9a68970b8025fdd3b240229f07f8a5f3a240220018b38d7dcd314e734c9276bd6fb40f673325bc4baa144c800d2f2f02db2765c012103d2e15674941bad4a996372cb87e1856d3652606d98562fe39c5e9e7e413f210502483045022100d12b852d85dcd961d2f5f4ab660654df6eedcc794c0c33ce5cc309ffb5fce58d022067338a8e0e1725c197fb1a88af59f51e44e4255b20167c8684031c05d1f2592a01210223b72beef0965d10be0778efecd61fcac6f79a4ea169393380734464f84f2ab30000000001003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a010000000000000000'))
       +        with self.assertRaises(SerializationError):
       +            tx2 = tx_from_any('cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAQA/AgAAAAH//////////////////////////////////////////wAAAAAA/////wEAAAAAAAAAAANqAQAAAAAAAAAA')
       +
       +    def test_invalid_psbt_006(self):
       +        # Case: PSBT With invalid global transaction typed key
       +        with self.assertRaises(SerializationError):
       +            tx1 = tx_from_any(bytes.fromhex('70736274ff020001550200000001279a2323a5dfb51fc45f220fa58b0fc13e1e3342792a85d7e36cd6333b5cbc390000000000ffffffff01a05aea0b000000001976a914ffe9c0061097cc3b636f2cb0460fa4fc427d2b4588ac0000000000010120955eea0b0000000017a9146345200f68d189e1adc0df1c4d16ea8f14c0dbeb87220203b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4646304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa94b99bdf86151db9a9a010104220020771fd18ad459666dd49f3d564e3dbc42f4c84774e360ada16816a8ed488d5681010547522103b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd462103de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd52ae220603b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4610b4a6ba67000000800000008004000080220603de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd10b4a6ba670000008000000080050000800000'))
       +        with self.assertRaises(SerializationError):
       +            tx2 = tx_from_any('cHNidP8CAAFVAgAAAAEnmiMjpd+1H8RfIg+liw/BPh4zQnkqhdfjbNYzO1y8OQAAAAAA/////wGgWuoLAAAAABl2qRT/6cAGEJfMO2NvLLBGD6T8Qn0rRYisAAAAAAABASCVXuoLAAAAABepFGNFIA9o0YnhrcDfHE0W6o8UwNvrhyICA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GRjBDAiAEJLWO/6qmlOFVnqXJO7/UqJBkIkBVzfBwtncUaUQtBwIfXI6w/qZRbWC4rLM61k7eYOh4W/s6qUuZvfhhUduamgEBBCIAIHcf0YrUWWZt1J89Vk49vEL0yEd042CtoWgWqO1IjVaBAQVHUiEDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYhA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9Uq4iBgOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RhC0prpnAAAAgAAAAIAEAACAIgYD3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg70QtKa6ZwAAAIAAAACABQAAgAAA')
       +
       +    def test_invalid_psbt_007(self):
       +        # Case: PSBT With invalid input witness utxo typed key
       +        with self.assertRaises(SerializationError):
       +            tx1 = tx_from_any(bytes.fromhex('70736274ff0100550200000001279a2323a5dfb51fc45f220fa58b0fc13e1e3342792a85d7e36cd6333b5cbc390000000000ffffffff01a05aea0b000000001976a914ffe9c0061097cc3b636f2cb0460fa4fc427d2b4588ac000000000002010020955eea0b0000000017a9146345200f68d189e1adc0df1c4d16ea8f14c0dbeb87220203b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4646304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa94b99bdf86151db9a9a010104220020771fd18ad459666dd49f3d564e3dbc42f4c84774e360ada16816a8ed488d5681010547522103b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd462103de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd52ae220603b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4610b4a6ba67000000800000008004000080220603de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd10b4a6ba670000008000000080050000800000'))
       +        with self.assertRaises(SerializationError):
       +            tx2 = tx_from_any('cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAIBACCVXuoLAAAAABepFGNFIA9o0YnhrcDfHE0W6o8UwNvrhyICA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GRjBDAiAEJLWO/6qmlOFVnqXJO7/UqJBkIkBVzfBwtncUaUQtBwIfXI6w/qZRbWC4rLM61k7eYOh4W/s6qUuZvfhhUduamgEBBCIAIHcf0YrUWWZt1J89Vk49vEL0yEd042CtoWgWqO1IjVaBAQVHUiEDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYhA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9Uq4iBgOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RhC0prpnAAAAgAAAAIAEAACAIgYD3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg70QtKa6ZwAAAIAAAACABQAAgAAA')
       +
       +    def test_invalid_psbt_008(self):
       +        # Case: PSBT With invalid pubkey length for input partial signature typed key
       +        with self.assertRaises(SerializationError):
       +            tx1 = tx_from_any(bytes.fromhex('70736274ff0100550200000001279a2323a5dfb51fc45f220fa58b0fc13e1e3342792a85d7e36cd6333b5cbc390000000000ffffffff01a05aea0b000000001976a914ffe9c0061097cc3b636f2cb0460fa4fc427d2b4588ac0000000000010120955eea0b0000000017a9146345200f68d189e1adc0df1c4d16ea8f14c0dbeb87210203b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd46304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa94b99bdf86151db9a9a010104220020771fd18ad459666dd49f3d564e3dbc42f4c84774e360ada16816a8ed488d5681010547522103b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd462103de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd52ae220603b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4610b4a6ba67000000800000008004000080220603de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd10b4a6ba670000008000000080050000800000'))
       +        with self.assertRaises(SerializationError):
       +            tx2 = tx_from_any('cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIQIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYwQwIgBCS1jv+qppThVZ6lyTu/1KiQZCJAVc3wcLZ3FGlELQcCH1yOsP6mUW1guKyzOtZO3mDoeFv7OqlLmb34YVHbmpoBAQQiACB3H9GK1FlmbdSfPVZOPbxC9MhHdONgraFoFqjtSI1WgQEFR1IhA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GIQPeVdHh2sgF4/iljB+/m5TALz26r+En/vykmV8m+CCDvVKuIgYDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYQtKa6ZwAAAIAAAACABAAAgCIGA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9ELSmumcAAACAAAAAgAUAAIAAAA==')
       +
       +    def test_invalid_psbt_009(self):
       +        # Case: PSBT With invalid redeemscript typed key
       +        with self.assertRaises(SerializationError):
       +            tx1 = tx_from_any(bytes.fromhex('70736274ff0100550200000001279a2323a5dfb51fc45f220fa58b0fc13e1e3342792a85d7e36cd6333b5cbc390000000000ffffffff01a05aea0b000000001976a914ffe9c0061097cc3b636f2cb0460fa4fc427d2b4588ac0000000000010120955eea0b0000000017a9146345200f68d189e1adc0df1c4d16ea8f14c0dbeb87220203b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4646304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa94b99bdf86151db9a9a01020400220020771fd18ad459666dd49f3d564e3dbc42f4c84774e360ada16816a8ed488d5681010547522103b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd462103de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd52ae220603b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4610b4a6ba67000000800000008004000080220603de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd10b4a6ba670000008000000080050000800000'))
       +        with self.assertRaises(SerializationError):
       +            tx2 = tx_from_any('cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQIEACIAIHcf0YrUWWZt1J89Vk49vEL0yEd042CtoWgWqO1IjVaBAQVHUiEDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYhA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9Uq4iBgOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RhC0prpnAAAAgAAAAIAEAACAIgYD3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg70QtKa6ZwAAAIAAAACABQAAgAAA')
       +
       +    def test_invalid_psbt_010(self):
       +        # Case: PSBT With invalid witnessscript typed key
       +        with self.assertRaises(SerializationError):
       +            tx1 = tx_from_any(bytes.fromhex('70736274ff0100550200000001279a2323a5dfb51fc45f220fa58b0fc13e1e3342792a85d7e36cd6333b5cbc390000000000ffffffff01a05aea0b000000001976a914ffe9c0061097cc3b636f2cb0460fa4fc427d2b4588ac0000000000010120955eea0b0000000017a9146345200f68d189e1adc0df1c4d16ea8f14c0dbeb87220203b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4646304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa94b99bdf86151db9a9a010104220020771fd18ad459666dd49f3d564e3dbc42f4c84774e360ada16816a8ed488d568102050047522103b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd462103de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd52ae220603b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4610b4a6ba67000000800000008004000080220603de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd10b4a6ba670000008000000080050000800000'))
       +        with self.assertRaises(SerializationError):
       +            tx2 = tx_from_any('cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQEEIgAgdx/RitRZZm3Unz1WTj28QvTIR3TjYK2haBao7UiNVoECBQBHUiEDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYhA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9Uq4iBgOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RhC0prpnAAAAgAAAAIAEAACAIgYD3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg70QtKa6ZwAAAIAAAACABQAAgAAA')
       +
       +    def test_invalid_psbt_011(self):
       +        # Case: PSBT With invalid bip32 typed key
       +        with self.assertRaises(SerializationError):
       +            tx1 = tx_from_any(bytes.fromhex('70736274ff0100550200000001279a2323a5dfb51fc45f220fa58b0fc13e1e3342792a85d7e36cd6333b5cbc390000000000ffffffff01a05aea0b000000001976a914ffe9c0061097cc3b636f2cb0460fa4fc427d2b4588ac0000000000010120955eea0b0000000017a9146345200f68d189e1adc0df1c4d16ea8f14c0dbeb87220203b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4646304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa94b99bdf86151db9a9a010104220020771fd18ad459666dd49f3d564e3dbc42f4c84774e360ada16816a8ed488d5681010547522103b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd462103de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd52ae210603b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd10b4a6ba67000000800000008004000080220603de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd10b4a6ba670000008000000080050000800000'))
       +        with self.assertRaises(SerializationError):
       +            tx2 = tx_from_any('cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQEEIgAgdx/RitRZZm3Unz1WTj28QvTIR3TjYK2haBao7UiNVoEBBUdSIQOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RiED3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg71SriEGA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb0QtKa6ZwAAAIAAAACABAAAgCIGA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9ELSmumcAAACAAAAAgAUAAIAAAA==')
       +
       +    def test_invalid_psbt_012(self):
       +        # Case: PSBT With invalid non-witness utxo typed key
       +        with self.assertRaises(SerializationError):
       +            tx1 = tx_from_any(bytes.fromhex('70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f0000000000020000bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000107da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae0001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b20289030108da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000'))
       +        with self.assertRaises(SerializationError):
       +            tx2 = tx_from_any('cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAIAALsCAAAAAarXOTEBi9JfhK5AC2iEi+CdtwbqwqwYKYur7nGrZW+LAAAAAEhHMEQCIFj2/HxqM+GzFUjUgcgmwBW9MBNarULNZ3kNq2bSrSQ7AiBKHO0mBMZzW2OT5bQWkd14sA8MWUL7n3UYVvqpOBV9ugH+////AoDw+gIAAAAAF6kUD7lGNCFpa4LIM68kHHjBfdveSTSH0PIKJwEAAAAXqRQpynT4oI+BmZQoGFyXtdhS5AY/YYdlAAAAAQfaAEcwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMAUgwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gFHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4AAQEgAMLrCwAAAAAXqRS39fr0Dj1ApaRZsds1NfK3L6kh6IcBByMiACCMI1MXN0O1ld+0oHtyuo5C43l9p06H/n2ddJfjsgKJAwEI2gQARzBEAiBi63pVYQenxz9FrEq1od3fb3B1+xJ1lpp/OD7/94S8sgIgDAXbt0cNvy8IVX3TVscyXB7TCRPpls04QJRdsSIo2l8BRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBR1IhAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcIQI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc1KuACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=')
       +
       +    def test_invalid_psbt_013(self):
       +        # Case: PSBT With invalid final scriptsig typed key
       +        with self.assertRaises(SerializationError):
       +            tx1 = tx_from_any(bytes.fromhex('70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000020700da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae0001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b20289030108da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000'))
       +        with self.assertRaises(SerializationError):
       +            tx2 = tx_from_any('cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAACBwDaAEcwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMAUgwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gFHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4AAQEgAMLrCwAAAAAXqRS39fr0Dj1ApaRZsds1NfK3L6kh6IcBByMiACCMI1MXN0O1ld+0oHtyuo5C43l9p06H/n2ddJfjsgKJAwEI2gQARzBEAiBi63pVYQenxz9FrEq1od3fb3B1+xJ1lpp/OD7/94S8sgIgDAXbt0cNvy8IVX3TVscyXB7TCRPpls04QJRdsSIo2l8BRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBR1IhAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcIQI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc1KuACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=')
       +
       +    def test_invalid_psbt_014(self):
       +        # Case: PSBT With invalid final script witness typed key
       +        with self.assertRaises(SerializationError):
       +            tx1 = tx_from_any(bytes.fromhex('70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000107da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae0001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903020800da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000'))
       +        with self.assertRaises(SerializationError):
       +            tx2 = tx_from_any('cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABB9oARzBEAiB0AYrUGACXuHMyPAAVcgs2hMyBI4kQSOfbzZtVrWecmQIgc9Npt0Dj61Pc76M4I8gHBRTKVafdlUTxV8FnkTJhEYwBSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAUdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSrgABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEHIyIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAggA2gQARzBEAiBi63pVYQenxz9FrEq1od3fb3B1+xJ1lpp/OD7/94S8sgIgDAXbt0cNvy8IVX3TVscyXB7TCRPpls04QJRdsSIo2l8BRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBR1IhAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcIQI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc1KuACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=')
       +
       +    def test_invalid_psbt_015(self):
       +        # Case: PSBT With invalid pubkey in output BIP 32 derivation paths typed key
       +        with self.assertRaises(SerializationError):
       +            tx1 = tx_from_any(bytes.fromhex('70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000107da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae0001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b20289030108da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00210203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca58710d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000'))
       +        with self.assertRaises(SerializationError):
       +            tx2 = tx_from_any('cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABB9oARzBEAiB0AYrUGACXuHMyPAAVcgs2hMyBI4kQSOfbzZtVrWecmQIgc9Npt0Dj61Pc76M4I8gHBRTKVafdlUTxV8FnkTJhEYwBSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAUdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSrgABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEHIyIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQjaBABHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwFHMEQCIGX0W6WZi1mif/4ae+0BavHx+Q1Us6qPdFCqX1aiUQO9AiB/ckcDrR7blmgLKEtW1P/LiPf7dZ6rvgiqMPKbhROD0gFHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4AIQIDqaTDf1mW06ol26xrVwrwZQOUSSlCRgs1R1PtnuylhxDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA')
       +
       +    def test_invalid_psbt_016(self):
       +        # Case: PSBT With invalid input sighash type typed key
       +        with self.assertRaises(SerializationError):
       +            tx1 = tx_from_any(bytes.fromhex('70736274ff0100730200000001301ae986e516a1ec8ac5b4bc6573d32f83b465e23ad76167d68b38e730b4dbdb0000000000ffffffff02747b01000000000017a91403aa17ae882b5d0d54b25d63104e4ffece7b9ea2876043993b0000000017a914b921b1ba6f722e4bfa83b6557a3139986a42ec8387000000000001011f00ca9a3b00000000160014d2d94b64ae08587eefc8eeb187c601e939f9037c0203000100000000010016001462e9e982fff34dd8239610316b090cd2a3b747cb000100220020876bad832f1d168015ed41232a9ea65a1815d9ef13c0ef8759f64b5b2b278a65010125512103b7ce23a01c5b4bf00a642537cdfabb315b668332867478ef51309d2bd57f8a8751ae00'))
       +        with self.assertRaises(SerializationError):
       +            tx2 = tx_from_any('cHNidP8BAHMCAAAAATAa6YblFqHsisW0vGVz0y+DtGXiOtdhZ9aLOOcwtNvbAAAAAAD/////AnR7AQAAAAAAF6kUA6oXrogrXQ1Usl1jEE5P/s57nqKHYEOZOwAAAAAXqRS5IbG6b3IuS/qDtlV6MTmYakLsg4cAAAAAAAEBHwDKmjsAAAAAFgAU0tlLZK4IWH7vyO6xh8YB6Tn5A3wCAwABAAAAAAEAFgAUYunpgv/zTdgjlhAxawkM0qO3R8sAAQAiACCHa62DLx0WgBXtQSMqnqZaGBXZ7xPA74dZ9ktbKyeKZQEBJVEhA7fOI6AcW0vwCmQlN836uzFbZoMyhnR471EwnSvVf4qHUa4A')
       +
       +    def test_invalid_psbt_017(self):
       +        # Case: PSBT With invalid output redeemScript typed key
       +        with self.assertRaises(SerializationError):
       +            tx1 = tx_from_any(bytes.fromhex('70736274ff0100730200000001301ae986e516a1ec8ac5b4bc6573d32f83b465e23ad76167d68b38e730b4dbdb0000000000ffffffff02747b01000000000017a91403aa17ae882b5d0d54b25d63104e4ffece7b9ea2876043993b0000000017a914b921b1ba6f722e4bfa83b6557a3139986a42ec8387000000000001011f00ca9a3b00000000160014d2d94b64ae08587eefc8eeb187c601e939f9037c0002000016001462e9e982fff34dd8239610316b090cd2a3b747cb000100220020876bad832f1d168015ed41232a9ea65a1815d9ef13c0ef8759f64b5b2b278a65010125512103b7ce23a01c5b4bf00a642537cdfabb315b668332867478ef51309d2bd57f8a8751ae00'))
       +        with self.assertRaises(SerializationError):
       +            tx2 = tx_from_any('cHNidP8BAHMCAAAAATAa6YblFqHsisW0vGVz0y+DtGXiOtdhZ9aLOOcwtNvbAAAAAAD/////AnR7AQAAAAAAF6kUA6oXrogrXQ1Usl1jEE5P/s57nqKHYEOZOwAAAAAXqRS5IbG6b3IuS/qDtlV6MTmYakLsg4cAAAAAAAEBHwDKmjsAAAAAFgAU0tlLZK4IWH7vyO6xh8YB6Tn5A3wAAgAAFgAUYunpgv/zTdgjlhAxawkM0qO3R8sAAQAiACCHa62DLx0WgBXtQSMqnqZaGBXZ7xPA74dZ9ktbKyeKZQEBJVEhA7fOI6AcW0vwCmQlN836uzFbZoMyhnR471EwnSvVf4qHUa4A')
       +
       +    def test_invalid_psbt_018(self):
       +        # Case: PSBT With invalid output witnessScript typed key
       +        with self.assertRaises(SerializationError):
       +            tx1 = tx_from_any(bytes.fromhex('70736274ff0100730200000001301ae986e516a1ec8ac5b4bc6573d32f83b465e23ad76167d68b38e730b4dbdb0000000000ffffffff02747b01000000000017a91403aa17ae882b5d0d54b25d63104e4ffece7b9ea2876043993b0000000017a914b921b1ba6f722e4bfa83b6557a3139986a42ec8387000000000001011f00ca9a3b00000000160014d2d94b64ae08587eefc8eeb187c601e939f9037c00010016001462e9e982fff34dd8239610316b090cd2a3b747cb000100220020876bad832f1d168015ed41232a9ea65a1815d9ef13c0ef8759f64b5b2b278a6521010025512103b7ce23a01c5b4bf00a642537cdfabb315b668332867478ef51309d2bd57f8a8751ae00'))
       +        with self.assertRaises(SerializationError):
       +            tx2 = tx_from_any('cHNidP8BAHMCAAAAATAa6YblFqHsisW0vGVz0y+DtGXiOtdhZ9aLOOcwtNvbAAAAAAD/////AnR7AQAAAAAAF6kUA6oXrogrXQ1Usl1jEE5P/s57nqKHYEOZOwAAAAAXqRS5IbG6b3IuS/qDtlV6MTmYakLsg4cAAAAAAAEBHwDKmjsAAAAAFgAU0tlLZK4IWH7vyO6xh8YB6Tn5A3wAAQAWABRi6emC//NN2COWEDFrCQzSo7dHywABACIAIIdrrYMvHRaAFe1BIyqeploYFdnvE8Dvh1n2S1srJ4plIQEAJVEhA7fOI6AcW0vwCmQlN836uzFbZoMyhnR471EwnSvVf4qHUa4A')
       +
       +
       +class TestPSBTSignerChecks(TestCaseForTestnet):
       +    # test cases from BIP-0174
       +
       +    @unittest.skip("the check this test is testing is intentionally disabled in transaction.py")
       +    def test_psbt_fails_signer_checks_001(self):
       +        # Case: A Witness UTXO is provided for a non-witness input
       +        with self.assertRaises(PSBTInputConsistencyFailure):
       +            tx1 = PartialTransaction.from_raw_psbt(bytes.fromhex('70736274ff0100a00200000002ab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40000000000feffffffab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40100000000feffffff02603bea0b000000001976a914768a40bbd740cbe81d988e71de2a4d5c71396b1d88ac8e240000000000001976a9146f4620b553fa095e721b9ee0efe9fa039cca459788ac0000000000010122d3dff505000000001976a914d48ed3110b94014cb114bd32d6f4d066dc74256b88ac0001012000e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787010416001485d13537f2e265405a34dbafa9e3dda01fb8230800220202ead596687ca806043edc3de116cdf29d5e9257c196cd055cf698c8d02bf24e9910b4a6ba670000008000000080020000800022020394f62be9df19952c5587768aeb7698061ad2c4a25c894f47d8c162b4d7213d0510b4a6ba6700000080010000800200008000'))
       +            for txin in tx1.inputs():
       +                txin.validate_data(for_signing=True)
       +        with self.assertRaises(PSBTInputConsistencyFailure):
       +            tx2 = PartialTransaction.from_raw_psbt('cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEBItPf9QUAAAAAGXapFNSO0xELlAFMsRS9Mtb00GbcdCVriKwAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIACICAurVlmh8qAYEPtw94RbN8p1eklfBls0FXPaYyNAr8k6ZELSmumcAAACAAAAAgAIAAIAAIgIDlPYr6d8ZlSxVh3aK63aYBhrSxKJciU9H2MFitNchPQUQtKa6ZwAAAIABAACAAgAAgAA=')
       +            for txin in tx2.inputs():
       +                txin.validate_data(for_signing=True)
       +
       +    def test_psbt_fails_signer_checks_002(self):
       +        # Case: redeemScript with non-witness UTXO does not match the scriptPubKey
       +        with self.assertRaises(PSBTInputConsistencyFailure):
       +            tx1 = PartialTransaction.from_raw_psbt(bytes.fromhex('70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752af2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8872202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000'))
       +        with self.assertRaises(PSBTInputConsistencyFailure):
       +            tx2 = PartialTransaction.from_raw_psbt('cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU210gwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gEBAwQBAAAAAQRHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq8iBgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfxDZDGpPAAAAgAAAAIAAAACAIgYC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtcQ2QxqTwAAAIAAAACAAQAAgAABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohyICAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBAQMEAQAAAAEEIgAgjCNTFzdDtZXftKB7crqOQuN5fadOh/59nXSX47ICiQMBBUdSIQMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3CECOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnNSriIGAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zENkMak8AAACAAAAAgAMAAIAiBgMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3BDZDGpPAAAAgAAAAIACAACAACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=')
       +
       +    def test_psbt_fails_signer_checks_003(self):
       +        # Case: redeemScript with witness UTXO does not match the scriptPubKey
       +        with self.assertRaises(PSBTInputConsistencyFailure):
       +            tx1 = PartialTransaction.from_raw_psbt(bytes.fromhex('70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8872202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028900010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000'))
       +        with self.assertRaises(PSBTInputConsistencyFailure):
       +            tx2 = PartialTransaction.from_raw_psbt('cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU210gwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gEBAwQBAAAAAQRHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4iBgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfxDZDGpPAAAAgAAAAIAAAACAIgYC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtcQ2QxqTwAAAIAAAACAAQAAgAABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohyICAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBAQMEAQAAAAEEIgAgjCNTFzdDtZXftKB7crqOQuN5fadOh/59nXSX47ICiQABBUdSIQMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3CECOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnNSriIGAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zENkMak8AAACAAAAAgAMAAIAiBgMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3BDZDGpPAAAAgAAAAIACAACAACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=')
       +
       +    def test_psbt_fails_signer_checks_004(self):
       +        # Case: witnessScript with witness UTXO does not match the redeemScript
       +        with self.assertRaises(PSBTInputConsistencyFailure):
       +            tx1 = PartialTransaction.from_raw_psbt(bytes.fromhex('70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8872202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ad2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000'))
       +        with self.assertRaises(PSBTInputConsistencyFailure):
       +            tx2 = PartialTransaction.from_raw_psbt('cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU210gwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gEBAwQBAAAAAQRHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4iBgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfxDZDGpPAAAAgAAAAIAAAACAIgYC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtcQ2QxqTwAAAIAAAACAAQAAgAABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohyICAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBAQMEAQAAAAEEIgAgjCNTFzdDtZXftKB7crqOQuN5fadOh/59nXSX47ICiQMBBUdSIQMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3CECOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnNSrSIGAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zENkMak8AAACAAAAAgAMAAIAiBgMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3BDZDGpPAAAAgAAAAIACAACAACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=')
       +
       +
       +class TestPSBTComplexChecks(TestCaseForTestnet):
       +    # test cases from BIP-0174
       +
       +    def test_psbt_combiner_unknown_fields(self):
       +        tx1 = tx_from_any("70736274ff01003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a0100000000000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f00")
       +        tx2 = tx_from_any("70736274ff01003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a0100000000000a0f0102030405060708100f0102030405060708090a0b0c0d0e0f000a0f0102030405060708100f0102030405060708090a0b0c0d0e0f000a0f0102030405060708100f0102030405060708090a0b0c0d0e0f00")
       +        tx1.combine_with_other_psbt(tx2)
       +        self.assertEqual("70736274ff01003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a0100000000000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f0a0f0102030405060708100f0102030405060708090a0b0c0d0e0f000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f0a0f0102030405060708100f0102030405060708090a0b0c0d0e0f000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f0a0f0102030405060708100f0102030405060708090a0b0c0d0e0f00",
       +                         tx1.serialize_as_bytes().hex())
 (DIR) diff --git a/electrum/tests/test_transaction.py b/electrum/tests/test_transaction.py
       t@@ -1,13 +1,19 @@
        from electrum import transaction
       -from electrum.transaction import TxOutputForUI, tx_from_str
       +from electrum.transaction import convert_tx_str_to_hex, tx_from_any, Transaction, PartialTransaction
        from electrum.bitcoin import TYPE_ADDRESS
       -from electrum.keystore import xpubkey_to_address
        from electrum.util import bh2u, bfh
       +from electrum import keystore
       +from electrum import bip32
       +from electrum.mnemonic import seed_type
       +from electrum.simple_config import SimpleConfig
       +
       +
       +from electrum.plugins.trustedcoin import trustedcoin
       +from electrum.plugins.trustedcoin.legacy_tx_format import serialize_tx_in_legacy_format
        
        from . import ElectrumTestCase, TestCaseForTestnet
        from .test_bitcoin import needs_test_with_all_ecc_implementations
        
       -unsigned_blob = '45505446ff0001000000012a5c9a94fcde98f5581cd00162c60a13936ceb75389ea65bf38633b424eb4031000000005701ff4c53ff0488b21e03ef2afea18000000089689bff23e1e7fb2f161daa37270a97a3d8c2e537584b2d304ecb47b86d21fc021b010d3bd425f8cf2e04824bfdf1f1f5ff1d51fadd9a41f9e3fb8dd3403b1bfe00000000ffffffff0140420f00000000001976a914230ac37834073a42146f11ef8414ae929feaafc388ac00000000'
        signed_blob = '01000000012a5c9a94fcde98f5581cd00162c60a13936ceb75389ea65bf38633b424eb4031000000006c493046022100a82bbc57a0136751e5433f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7de89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d985012102e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6ffffffff0140420f00000000001976a914230ac37834073a42146f11ef8414ae929feaafc388ac00000000'
        v2_blob = "0200000001191601a44a81e061502b7bfbc6eaa1cef6d1e6af5308ef96c9342f71dbf4b9b5000000006b483045022100a6d44d0a651790a477e75334adfb8aae94d6612d01187b2c02526e340a7fd6c8022028bdf7a64a54906b13b145cd5dab21a26bd4b85d6044e9b97bceab5be44c2a9201210253e8e0254b0c95776786e40984c1aa32a7d03efa6bdacdea5f421b774917d346feffffff026b20fa04000000001976a914024db2e87dd7cfd0e5f266c5f212e21a31d805a588aca0860100000000001976a91421919b94ae5cefcdf0271191459157cdb41c4cbf88aca6240700"
        signed_segwit_blob = "01000000000101b66d722484f2db63e827ebf41d02684fed0c6550e85015a6c9d41ef216a8a6f00000000000fdffffff0280c3c90100000000160014b65ce60857f7e7892b983851c2a8e3526d09e4ab64bac30400000000160014c478ebbc0ab2097706a98e10db7cf101839931c4024730440220789c7d47f876638c58d98733c30ae9821c8fa82b470285dcdf6db5994210bf9f02204163418bbc44af701212ad42d884cc613f3d3d831d2d0cc886f767cca6e0235e012103083a6dc250816d771faa60737bfe78b23ad619f6b458e0a1f1688e3a0605e79c00000000"
       t@@ -58,80 +64,35 @@ class TestBCDataStream(ElectrumTestCase):
        class TestTransaction(ElectrumTestCase):
        
            @needs_test_with_all_ecc_implementations
       -    def test_tx_unsigned(self):
       -        expected = {
       -            'inputs': [{
       -                'type': 'p2pkh',
       -                'address': '1446oU3z268EeFgfcwJv6X2VBXHfoYxfuD',
       -                'num_sig': 1,
       -                'prevout_hash': '3140eb24b43386f35ba69e3875eb6c93130ac66201d01c58f598defc949a5c2a',
       -                'prevout_n': 0,
       -                'pubkeys': ['02e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6'],
       -                'scriptSig': '01ff4c53ff0488b21e03ef2afea18000000089689bff23e1e7fb2f161daa37270a97a3d8c2e537584b2d304ecb47b86d21fc021b010d3bd425f8cf2e04824bfdf1f1f5ff1d51fadd9a41f9e3fb8dd3403b1bfe00000000',
       -                'sequence': 4294967295,
       -                'signatures': [None],
       -                'x_pubkeys': ['ff0488b21e03ef2afea18000000089689bff23e1e7fb2f161daa37270a97a3d8c2e537584b2d304ecb47b86d21fc021b010d3bd425f8cf2e04824bfdf1f1f5ff1d51fadd9a41f9e3fb8dd3403b1bfe00000000']}],
       -            'lockTime': 0,
       -            'outputs': [{
       -                'address': '14CHYaaByjJZpx4oHBpfDMdqhTyXnZ3kVs',
       -                'prevout_n': 0,
       -                'scriptPubKey': '76a914230ac37834073a42146f11ef8414ae929feaafc388ac',
       -                'type': TYPE_ADDRESS,
       -                'value': 1000000}],
       -            'partial': True,
       -            'segwit_ser': False,
       -            'version': 1,
       -        }
       -        tx = transaction.Transaction(unsigned_blob)
       -        self.assertEqual(tx.deserialize(), expected)
       -        self.assertEqual(tx.deserialize(), None)
       -
       -        self.assertEqual(tx.as_dict(), {'hex': unsigned_blob, 'complete': False, 'final': True})
       -        self.assertEqual(tx.get_outputs_for_UI(), [TxOutputForUI('14CHYaaByjJZpx4oHBpfDMdqhTyXnZ3kVs', 1000000)])
       -
       -        self.assertTrue(tx.has_address('14CHYaaByjJZpx4oHBpfDMdqhTyXnZ3kVs'))
       -        self.assertTrue(tx.has_address('1446oU3z268EeFgfcwJv6X2VBXHfoYxfuD'))
       -        self.assertFalse(tx.has_address('1CQj15y1N7LDHp7wTt28eoD1QhHgFgxECH'))
       -
       -        self.assertEqual(tx.serialize(), unsigned_blob)
       -
       +    def test_tx_update_signatures(self):
       +        tx = tx_from_any("cHNidP8BAFUBAAAAASpcmpT83pj1WBzQAWLGChOTbOt1OJ6mW/OGM7Qk60AxAAAAAAD/////AUBCDwAAAAAAGXapFCMKw3g0BzpCFG8R74QUrpKf6q/DiKwAAAAAAAAA")
       +        tx.inputs()[0].script_type = 'p2pkh'
       +        tx.inputs()[0].pubkeys = [bfh('02e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6')]
       +        tx.inputs()[0].num_sig = 1
                tx.update_signatures(signed_blob_signatures)
       -        self.assertEqual(tx.raw, signed_blob)
       -
       -        tx.update(unsigned_blob)
       -        tx.raw = None
       -        blob = str(tx)
       -        self.assertEqual(transaction.deserialize(blob), expected)
       +        self.assertEqual(tx.serialize(), signed_blob)
        
            @needs_test_with_all_ecc_implementations
       -    def test_tx_signed(self):
       -        expected = {
       -            'inputs': [{'address': None,
       -                'num_sig': 0,
       -                'prevout_hash': '3140eb24b43386f35ba69e3875eb6c93130ac66201d01c58f598defc949a5c2a',
       -                'prevout_n': 0,
       -                'scriptSig': '493046022100a82bbc57a0136751e5433f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7de89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d985012102e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6',
       -                'sequence': 4294967295,
       -                'type': 'unknown'}],
       -            'lockTime': 0,
       -            'outputs': [{
       -                'address': '14CHYaaByjJZpx4oHBpfDMdqhTyXnZ3kVs',
       -                'prevout_n': 0,
       -                'scriptPubKey': '76a914230ac37834073a42146f11ef8414ae929feaafc388ac',
       -                'type': TYPE_ADDRESS,
       -                'value': 1000000}],
       -            'partial': False,
       -            'segwit_ser': False,
       -            'version': 1
       -        }
       +    def test_tx_deserialize_for_signed_network_tx(self):
                tx = transaction.Transaction(signed_blob)
       -        self.assertEqual(tx.deserialize(), expected)
       -        self.assertEqual(tx.deserialize(), None)
       -        self.assertEqual(tx.as_dict(), {'hex': signed_blob, 'complete': True, 'final': True})
       +        tx.deserialize()
       +        self.assertEqual(1, tx.version)
       +        self.assertEqual(0, tx.locktime)
       +        self.assertEqual(1, len(tx.inputs()))
       +        self.assertEqual(4294967295, tx.inputs()[0].nsequence)
       +        self.assertEqual(bfh('493046022100a82bbc57a0136751e5433f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7de89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d985012102e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6'),
       +                         tx.inputs()[0].script_sig)
       +        self.assertEqual(None, tx.inputs()[0].witness)
       +        self.assertEqual('3140eb24b43386f35ba69e3875eb6c93130ac66201d01c58f598defc949a5c2a:0', tx.inputs()[0].prevout.to_str())
       +        self.assertEqual(1, len(tx.outputs()))
       +        self.assertEqual(bfh('76a914230ac37834073a42146f11ef8414ae929feaafc388ac'), tx.outputs()[0].scriptpubkey)
       +        self.assertEqual('14CHYaaByjJZpx4oHBpfDMdqhTyXnZ3kVs', tx.outputs()[0].address)
       +        self.assertEqual(1000000, tx.outputs()[0].value)
        
                self.assertEqual(tx.serialize(), signed_blob)
        
       -        tx.update_signatures(signed_blob_signatures)
       +    def test_estimated_tx_size(self):
       +        tx = transaction.Transaction(signed_blob)
        
                self.assertEqual(tx.estimated_total_size(), 193)
                self.assertEqual(tx.estimated_base_size(), 193)
       t@@ -156,73 +117,84 @@ class TestTransaction(ElectrumTestCase):
                self.assertEqual(tx.estimated_weight(), 561)
                self.assertEqual(tx.estimated_size(), 141)
        
       -    def test_errors(self):
       -        with self.assertRaises(TypeError):
       -            transaction.Transaction.pay_script(output_type=None, addr='')
       -
       -        with self.assertRaises(BaseException):
       -            xpubkey_to_address('')
       -
       -    def test_parse_xpub(self):
       -        res = xpubkey_to_address('fe4e13b0f311a55b8a5db9a32e959da9f011b131019d4cebe6141b9e2c93edcbfc0954c358b062a9f94111548e50bde5847a3096b8b7872dcffadb0e9579b9017b01000200')
       -        self.assertEqual(res, ('04ee98d63800824486a1cf5b4376f2f574d86e0a3009a6448105703453f3368e8e1d8d090aaecdd626a45cc49876709a3bbb6dc96a4311b3cac03e225df5f63dfc', '19h943e4diLc68GXW7G75QNe2KWuMu7BaJ'))
       -
            def test_version_field(self):
                tx = transaction.Transaction(v2_blob)
                self.assertEqual(tx.txid(), "b97f9180173ab141b61b9f944d841e60feec691d6daab4d4d932b24dd36606fe")
        
       -    def test_tx_from_str(self):
       -        # json dict
       -        self.assertEqual('020000000001012005273af813ba23b0c205e4b145e525c280dd876e061f35bff7db9b2e0043640100000000fdffffff02d885010000000000160014e73f444b8767c84afb46ef4125d8b81d2542a53d00e1f5050000000017a914052ed032f5c74a636ed5059611bb90012d40316c870247304402200c628917673d75f05db893cc377b0a69127f75e10949b35da52aa1b77a14c350022055187adf9a668fdf45fc09002726ba7160e713ed79dddcd20171308273f1a2f1012103cb3e00561c3439ccbacc033a72e0513bcfabff8826de0bc651d661991ade6171049e1600',
       -                         tx_from_str("""{
       -                                        "complete": true,
       -                                        "final": false,
       -                                        "hex": "020000000001012005273af813ba23b0c205e4b145e525c280dd876e061f35bff7db9b2e0043640100000000fdffffff02d885010000000000160014e73f444b8767c84afb46ef4125d8b81d2542a53d00e1f5050000000017a914052ed032f5c74a636ed5059611bb90012d40316c870247304402200c628917673d75f05db893cc377b0a69127f75e10949b35da52aa1b77a14c350022055187adf9a668fdf45fc09002726ba7160e713ed79dddcd20171308273f1a2f1012103cb3e00561c3439ccbacc033a72e0513bcfabff8826de0bc651d661991ade6171049e1600"
       -                                    }
       -                                    """)
       -        )
       +    def test_convert_tx_str_to_hex(self):
                # raw hex
                self.assertEqual('020000000001012005273af813ba23b0c205e4b145e525c280dd876e061f35bff7db9b2e0043640100000000fdffffff02d885010000000000160014e73f444b8767c84afb46ef4125d8b81d2542a53d00e1f5050000000017a914052ed032f5c74a636ed5059611bb90012d40316c870247304402200c628917673d75f05db893cc377b0a69127f75e10949b35da52aa1b77a14c350022055187adf9a668fdf45fc09002726ba7160e713ed79dddcd20171308273f1a2f1012103cb3e00561c3439ccbacc033a72e0513bcfabff8826de0bc651d661991ade6171049e1600',
       -                         tx_from_str('020000000001012005273af813ba23b0c205e4b145e525c280dd876e061f35bff7db9b2e0043640100000000fdffffff02d885010000000000160014e73f444b8767c84afb46ef4125d8b81d2542a53d00e1f5050000000017a914052ed032f5c74a636ed5059611bb90012d40316c870247304402200c628917673d75f05db893cc377b0a69127f75e10949b35da52aa1b77a14c350022055187adf9a668fdf45fc09002726ba7160e713ed79dddcd20171308273f1a2f1012103cb3e00561c3439ccbacc033a72e0513bcfabff8826de0bc651d661991ade6171049e1600'))
       +                         convert_tx_str_to_hex('020000000001012005273af813ba23b0c205e4b145e525c280dd876e061f35bff7db9b2e0043640100000000fdffffff02d885010000000000160014e73f444b8767c84afb46ef4125d8b81d2542a53d00e1f5050000000017a914052ed032f5c74a636ed5059611bb90012d40316c870247304402200c628917673d75f05db893cc377b0a69127f75e10949b35da52aa1b77a14c350022055187adf9a668fdf45fc09002726ba7160e713ed79dddcd20171308273f1a2f1012103cb3e00561c3439ccbacc033a72e0513bcfabff8826de0bc651d661991ade6171049e1600'))
                # base43
                self.assertEqual('020000000001012005273af813ba23b0c205e4b145e525c280dd876e061f35bff7db9b2e0043640100000000fdffffff02d885010000000000160014e73f444b8767c84afb46ef4125d8b81d2542a53d00e1f5050000000017a914052ed032f5c74a636ed5059611bb90012d40316c870247304402200c628917673d75f05db893cc377b0a69127f75e10949b35da52aa1b77a14c350022055187adf9a668fdf45fc09002726ba7160e713ed79dddcd20171308273f1a2f1012103cb3e00561c3439ccbacc033a72e0513bcfabff8826de0bc651d661991ade6171049e1600',
       -                         tx_from_str('64XF-8+PM6*4IYN-QWW$B2QLNW+:C8-$I$-+T:L.6DKXTSWSFFONDP1J/MOS3SPK0-SYVW38U9.3+A1/*2HTHQTJGP79LVEK-IITQJ1H.C/X$NSOV$8DWR6JAFWXD*LX4-EN0.BDOF+PPYPH16$NM1H.-MAA$V1SCP0Q.6Y5FR822S6K-.5K5F.Z4Q:0SDRG-4GEBLAO4W9Z*H-$1-KDYAFOGF675W0:CK5M1LT92IG:3X60P3GKPM:X2$SP5A7*LT9$-TTEG0/DRZYV$7B4ADL9CVS5O7YG.J64HLZ24MVKO/-GV:V.T/L$D3VQ:MR8--44HK8W'))
       +                         convert_tx_str_to_hex('64XF-8+PM6*4IYN-QWW$B2QLNW+:C8-$I$-+T:L.6DKXTSWSFFONDP1J/MOS3SPK0-SYVW38U9.3+A1/*2HTHQTJGP79LVEK-IITQJ1H.C/X$NSOV$8DWR6JAFWXD*LX4-EN0.BDOF+PPYPH16$NM1H.-MAA$V1SCP0Q.6Y5FR822S6K-.5K5F.Z4Q:0SDRG-4GEBLAO4W9Z*H-$1-KDYAFOGF675W0:CK5M1LT92IG:3X60P3GKPM:X2$SP5A7*LT9$-TTEG0/DRZYV$7B4ADL9CVS5O7YG.J64HLZ24MVKO/-GV:V.T/L$D3VQ:MR8--44HK8W'))
        
            def test_get_address_from_output_script(self):
                # the inverse of this test is in test_bitcoin: test_address_to_script
                addr_from_script = lambda script: transaction.get_address_from_output_script(bfh(script))
       -        ADDR = transaction.TYPE_ADDRESS
       -        PUBKEY = transaction.TYPE_PUBKEY
       -        SCRIPT = transaction.TYPE_SCRIPT
        
                # bech32 native segwit
                # test vectors from BIP-0173
       -        self.assertEqual((ADDR, 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'), addr_from_script('0014751e76e8199196d454941c45d1b3a323f1433bd6'))
       -        self.assertEqual((ADDR, 'bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx'), addr_from_script('5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6'))
       -        self.assertEqual((ADDR, 'bc1sw50qa3jx3s'), addr_from_script('6002751e'))
       -        self.assertEqual((ADDR, 'bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj'), addr_from_script('5210751e76e8199196d454941c45d1b3a323'))
       +        self.assertEqual('bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4', addr_from_script('0014751e76e8199196d454941c45d1b3a323f1433bd6'))
       +        self.assertEqual('bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx', addr_from_script('5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6'))
       +        self.assertEqual('bc1sw50qa3jx3s', addr_from_script('6002751e'))
       +        self.assertEqual('bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj', addr_from_script('5210751e76e8199196d454941c45d1b3a323'))
                # almost but not quite
       -        self.assertEqual((SCRIPT, '0013751e76e8199196d454941c45d1b3a323f1433b'), addr_from_script('0013751e76e8199196d454941c45d1b3a323f1433b'))
       +        self.assertEqual(None, addr_from_script('0013751e76e8199196d454941c45d1b3a323f1433b'))
        
                # base58 p2pkh
       -        self.assertEqual((ADDR, '14gcRovpkCoGkCNBivQBvw7eso7eiNAbxG'), addr_from_script('76a91428662c67561b95c79d2257d2a93d9d151c977e9188ac'))
       -        self.assertEqual((ADDR, '1BEqfzh4Y3zzLosfGhw1AsqbEKVW6e1qHv'), addr_from_script('76a914704f4b81cadb7bf7e68c08cd3657220f680f863c88ac'))
       +        self.assertEqual('14gcRovpkCoGkCNBivQBvw7eso7eiNAbxG', addr_from_script('76a91428662c67561b95c79d2257d2a93d9d151c977e9188ac'))
       +        self.assertEqual('1BEqfzh4Y3zzLosfGhw1AsqbEKVW6e1qHv', addr_from_script('76a914704f4b81cadb7bf7e68c08cd3657220f680f863c88ac'))
                # almost but not quite
       -        self.assertEqual((SCRIPT, '76a9130000000000000000000000000000000000000088ac'), addr_from_script('76a9130000000000000000000000000000000000000088ac'))
       +        self.assertEqual(None, addr_from_script('76a9130000000000000000000000000000000000000088ac'))
        
                # base58 p2sh
       -        self.assertEqual((ADDR, '35ZqQJcBQMZ1rsv8aSuJ2wkC7ohUCQMJbT'), addr_from_script('a9142a84cf00d47f699ee7bbc1dea5ec1bdecb4ac15487'))
       -        self.assertEqual((ADDR, '3PyjzJ3im7f7bcV724GR57edKDqoZvH7Ji'), addr_from_script('a914f47c8954e421031ad04ecd8e7752c9479206b9d387'))
       +        self.assertEqual('35ZqQJcBQMZ1rsv8aSuJ2wkC7ohUCQMJbT', addr_from_script('a9142a84cf00d47f699ee7bbc1dea5ec1bdecb4ac15487'))
       +        self.assertEqual('3PyjzJ3im7f7bcV724GR57edKDqoZvH7Ji', addr_from_script('a914f47c8954e421031ad04ecd8e7752c9479206b9d387'))
                # almost but not quite
       -        self.assertEqual((SCRIPT, 'a912f47c8954e421031ad04ecd8e7752c947920687'), addr_from_script('a912f47c8954e421031ad04ecd8e7752c947920687'))
       +        self.assertEqual(None, addr_from_script('a912f47c8954e421031ad04ecd8e7752c947920687'))
        
                # p2pk
       -        self.assertEqual((PUBKEY, '0289e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751c8b'), addr_from_script('210289e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751c8bac'))
       -        self.assertEqual((PUBKEY, '045485b0b076848af1209e788c893522a90f3df77c1abac2ca545846a725e6c3da1f7743f55a1bc3b5f0c7e0ee4459954ec0307022742d60032b13432953eb7120'), addr_from_script('41045485b0b076848af1209e788c893522a90f3df77c1abac2ca545846a725e6c3da1f7743f55a1bc3b5f0c7e0ee4459954ec0307022742d60032b13432953eb7120ac'))
       +        self.assertEqual(None, addr_from_script('210289e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751c8bac'))
       +        self.assertEqual(None, addr_from_script('41045485b0b076848af1209e788c893522a90f3df77c1abac2ca545846a725e6c3da1f7743f55a1bc3b5f0c7e0ee4459954ec0307022742d60032b13432953eb7120ac'))
                # almost but not quite
       -        self.assertEqual((SCRIPT, '200289e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751cac'), addr_from_script('200289e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751cac'))
       -        self.assertEqual((SCRIPT, '210589e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751c8bac'), addr_from_script('210589e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751c8bac'))
       -
       +        self.assertEqual(None, addr_from_script('200289e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751cac'))
       +        self.assertEqual(None, addr_from_script('210589e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751c8bac'))
       +
       +    def test_tx_serialize_methods_for_psbt(self):
       +        raw_hex = "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000"
       +        raw_base64 = "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABAwQBAAAAAQRHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4iBgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfxDZDGpPAAAAgAAAAIAAAACAIgYC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtcQ2QxqTwAAAIAAAACAAQAAgAABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEDBAEAAAABBCIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQVHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4iBgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8OcxDZDGpPAAAAgAAAAIADAACAIgYDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwQ2QxqTwAAAIAAAACAAgAAgAAiAgOppMN/WZbTqiXbrGtXCvBlA5RJKUJGCzVHU+2e7KWHcRDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA"
       +        partial_tx = tx_from_any(raw_hex)
       +        self.assertEqual(PartialTransaction, type(partial_tx))
       +        self.assertEqual(raw_base64,
       +                         partial_tx.serialize())
       +        self.assertEqual(raw_hex,
       +                         partial_tx.serialize_as_bytes().hex())
       +        self.assertEqual(raw_base64,
       +                         partial_tx._serialize_as_base64())
       +
       +    def test_tx_serialize_methods_for_network_tx(self):
       +        raw_hex = "0200000000010258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7500000000da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752aeffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d01000000232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00000000"
       +        tx = tx_from_any(raw_hex)
       +        self.assertEqual(Transaction, type(tx))
       +        self.assertEqual(raw_hex,
       +                         tx.serialize())
       +        self.assertEqual(raw_hex,
       +                         tx.serialize_as_bytes().hex())
       +
       +    def test_tx_serialize_methods_for_psbt_that_is_ready_to_be_finalized(self):
       +        raw_hex_psbt = "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000107da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae0001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b20289030108da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000"
       +        raw_hex_network_tx = "0200000000010258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7500000000da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752aeffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d01000000232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00000000"
       +        partial_tx = tx_from_any(raw_hex_psbt)
       +        self.assertEqual(PartialTransaction, type(partial_tx))
       +        self.assertEqual(raw_hex_network_tx,
       +                         partial_tx.serialize())
       +        self.assertEqual(raw_hex_network_tx,
       +                         partial_tx.serialize_as_bytes().hex())
       +        # note: the diff between the following, and raw_hex_psbt, is that we added
       +        #       an extra FINAL_SCRIPTWITNESS field in finalize_psbt()
       +        self.assertEqual("70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000107da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae010801000001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b20289030108da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000",
       +                         partial_tx.serialize_as_bytes(force_psbt=True).hex())
        
        #####
        
       t@@ -811,45 +783,54 @@ class TestTransaction(ElectrumTestCase):
        # txns from Bitcoin Core ends <---
        
        
       -class TestTransactionTestnet(TestCaseForTestnet):
       -
       -    def _run_naive_tests_on_tx(self, raw_tx, txid):
       -        tx = transaction.Transaction(raw_tx)
       -        self.assertEqual(txid, tx.txid())
       -        self.assertEqual(raw_tx, tx.serialize())
       -        self.assertTrue(tx.estimated_size() >= 0)
       -
       -# partial txns using our partial format --->
       -    # NOTE: our partial format contains xpubs, and xpubs have version bytes,
       -    # and version bytes encode the network as well; so these are network-sensitive!
       -
       -    def test_txid_partial_segwit_p2wpkh(self):
       -        raw_tx = '45505446ff000100000000010115a847356cbb44be67f345965bb3f2589e2fec1c9a0ada21fd28225dcc602e8f0100000000fdffffff02f6fd1200000000001600149c756aa33f4f89418b33872a973274b5445c727b80969800000000001600140f9de573bc679d040e763d13f0250bd03e625f6ffeffffffff9095ab000000000000000201ff53ff045f1cf6014af5fa07800000002fa3f450ba41799b9b62642979505817783a9b6c656dc11cd0bb4fa362096808026adc616c25a4d0a877d1741eb1db9cef65c15118bd7d5f31bf65f319edda81840100c8000f391400'
       -        txid = '63ff7e99d85d8e33f683e6ec84574bdf8f5111078a5fe900893e019f9a7f95c3'
       -        self._run_naive_tests_on_tx(raw_tx, txid)
       -
       -    def test_txid_partial_segwit_p2wpkh_p2sh_simple(self):
       -        raw_tx = '45505446ff0001000000000101d0d23a6fbddb21cc664cb81cca96715baa4d6dbe5b7b9bcc6632f1005a7b0b840100000017160014a78a91261e71a681b6312cd184b14503a21f856afdffffff0134410f000000000017a914d6514ca17ecc31952c990daf96e307fbc58529cd87feffffffff40420f000000000000000201ff53ff044a5262033601222e800000001618aa51e49a961f63fd111f64cd4a7e792c1d7168be7a07703de505ebed2cf70286ebbe755767adaa5835f4d78dec1ee30849d69eacfe80b7ee6b1585279536c30000020011391400'
       -        txid = '2739f2e7fde9b8ec73fce4aee53722cc7683312d1321ded073284c51fadf44df'
       -        self._run_naive_tests_on_tx(raw_tx, txid)
       -
       -    def test_txid_partial_segwit_p2wpkh_p2sh_mixed_outputs(self):
       -        raw_tx = '45505446ff00010000000001011dcac788f24b84d771b60c44e1f9b6b83429e50f06e1472d47241922164013b00100000017160014801d28ca6e2bde551112031b6cb75de34f10851ffdffffff0440420f00000000001600140f9de573bc679d040e763d13f0250bd03e625f6fc0c62d000000000017a9142899f6484e477233ce60072fc185ef4c1f2c654487809698000000000017a914d40f85ba3c8fa0f3615bcfa5d6603e36dfc613ef87712d19040000000017a914e38c0cffde769cb65e72cda1c234052ae8d2254187feffffffff6ad1ee040000000000000201ff53ff044a5262033601222e800000001618aa51e49a961f63fd111f64cd4a7e792c1d7168be7a07703de505ebed2cf70286ebbe755767adaa5835f4d78dec1ee30849d69eacfe80b7ee6b1585279536c301000c000f391400'
       -        txid = 'ba5c88e07a4025a39ad3b85247cbd4f556a70d6312b18e04513c7cec9d45d6ac'
       -        self._run_naive_tests_on_tx(raw_tx, txid)
       -
       -    def test_txid_partial_issue_5366(self):
       -        raw_tx = '45505446ff000200000000010127523d70642dabd999fb43191ff6763f5b04150ba4cf38d2cfb53edf6a40ac4f0100000000fdffffff013286010000000000160014e79c7ac0b390a9caf52dc002e1095a5fbc042a18feffffffffa08601000000000000000201ff57ff045f1cf60157e9eb7a8000000038fa0b3a9c155ff3390ca0d639783d97af3b3bf66ebb69a31dfe8317fae0a7fe0324bc048fc0002253dfec9d6299711d708175f950ecee8e09db3518a5685741830000ffffcf01010043281700'
       -        txid = 'a0c159616073dc7a4a482092dab4e8516c83dddb769b65919f23f6df63d33eb8'
       -        self._run_naive_tests_on_tx(raw_tx, txid)
       -
       -# end partial txns <---
       -
       -
       -class NetworkMock(object):
       -
       -    def __init__(self, unspent):
       -        self.unspent = unspent
       -
       -    def synchronous_send(self, arg):
       -        return self.unspent
       +class TestLegacyPartialTxFormat(TestCaseForTestnet):
       +
       +    def setUp(self):
       +        super().setUp()
       +        self.config = SimpleConfig({'electrum_path': self.electrum_path})
       +
       +    def test_trustedcoin_legacy_2fa_psbt_to_legacy_partial_tx(self):
       +        from .test_wallet_vertical import WalletIntegrityHelper
       +        seed_words = 'kiss live scene rude gate step hip quarter bunker oxygen motor glove'
       +        self.assertEqual(seed_type(seed_words), '2fa')
       +
       +        xprv1, xpub1, xprv2, xpub2 = trustedcoin.TrustedCoinPlugin.xkeys_from_seed(seed_words, '')
       +        ks1 = keystore.from_xprv(xprv1)
       +        ks2 = keystore.from_xprv(xprv2)
       +        long_user_id, short_id = trustedcoin.get_user_id(
       +            {'x1/': {'xpub': xpub1},
       +             'x2/': {'xpub': xpub2}})
       +        xtype = bip32.xpub_type(xpub1)
       +        xpub3 = trustedcoin.make_xpub(trustedcoin.get_signing_xpub(xtype), long_user_id)
       +        ks3 = keystore.from_xpub(xpub3)
       +
       +        wallet = WalletIntegrityHelper.create_multisig_wallet([ks1, ks2, ks3], '2of3', config=self.config)
       +
       +        tx = tx_from_any('cHNidP8BAJQCAAAAAcqqxrXrkW4wZ9AiT5QvszHOHc+0Axz7R555Qdz5XkCYAQAAAAD9////A6CGAQAAAAAAFgAU+fBLRlKk9v89xVEm2xJ0kG1wcvNMCwMAAAAAABepFPKffLiXEB3Gmv1Y35uy5bTUM59Nh0ANAwAAAAAAGXapFPriyJZefiOenIisUU3nDewLDxYIiKwSKxgATwEENYfPAAAAAAAAAAAAnOMnCVq57ruCJ7c38H6PtmrwS48+kcQJPEh70w/ofCQCDSEN062A0pw2JKkYltX2G3th8zLexPfEVDGu74BeD6cEcH3xxE8BBDWHzwGCB4l2gAAAAJOfYJjOAH6kksFOokIboP3+8Gwhlzlxhl5uY7zokvfcAmGy8e8txy0wkx69/TgZFOMe1aZc2g1HCwrRQ9M9+Ph7CIIHiXYAAACATwEENYfPAYIHiXaAAAABb6EovcClpG/Hrxr9IF22IHGR1MQFG27b0GQTzcCxot8Dak5MvnvEZt1lN4TIazd0m+w+goApzqNMFWkJVv1hV28IggeJdgEAAIAAAQDfAgAAAAGcKHw7enlMh6IibIkEeKQlL5pUR2wKv6GC1NTd6KY8ggEAAABqRzBEAiBNHsG9H5z10eHHsIOe4kFdvnZK38E7Jx+Cmru14SdQ/gIgWngNYj/F8qHAhkdlU+BgY5ktAL2MeIUoqIJKXudcFRMBIQNU856KX8nmKx8+nbIRwjpRAvyMWroJGz+F6ADwzYv/GP3///8CnJ0HAAAAAAAZdqkUsQVAb+BbDci+RMeDa6WBLb9nTOiIrCChBwAAAAAAF6kULBYX0k+TbkDRSw3ylOy3u6rXUzeHEisYACICA/5C2rWHGOoEE/fI3mk83u4izhmx3DTAu916SCRUZcWiSDBFAiEA0Dw1yyk7Adp74Ndxztr6iR7V1wpnfPzNaWcTVva+vtwCIAsqV2xM0cZCSAdWzh/WYKyvC6UmGTowmeH4HN0BrSCTAQEEaVIhAgkfC02KswAWpdHAiCSeAog/rYFg8G+lNYithZhlCj5iIQNfL4JjuzYI1sxO4DvUy41lxNcK9xBJ8F+/7kl4gyof0iED/kLatYcY6gQT98jeaTze7iLOGbHcNMC73XpIJFRlxaJTriIGA/5C2rWHGOoEE/fI3mk83u4izhmx3DTAu916SCRUZcWiEIIHiXYAAACAAAAAAAAAAAAiBgIJHwtNirMAFqXRwIgkngKIP62BYPBvpTWIrYWYZQo+YhCCB4l2AQAAgAAAAAAAAAAAIgYDXy+CY7s2CNbMTuA71MuNZcTXCvcQSfBfv+5JeIMqH9IMcH3xxAAAAAAAAAAAAAABAGlSIQIqtnn9ouM3xAq7wIID09cdKpb9u/OMkI97kuU3wcTv/yEDTNFHFnZ1xmKGRQzFaUAT9DeDk2NdeWSrilc8w9BKjU4hA1joWoIBRYfeqDPrX/uT45hWkO5Lph7zLVsorVqhYXN/U64iAgNM0UcWdnXGYoZFDMVpQBP0N4OTY115ZKuKVzzD0EqNThCCB4l2AAAAgAEAAAAAAAAAIgICKrZ5/aLjN8QKu8CCA9PXHSqW/bvzjJCPe5LlN8HE7/8QggeJdgEAAIABAAAAAAAAACICA1joWoIBRYfeqDPrX/uT45hWkO5Lph7zLVsorVqhYXN/DHB98cQBAAAAAAAAAAAA')
       +        tx.add_info_from_wallet(wallet)
       +        raw_tx = serialize_tx_in_legacy_format(tx, wallet=wallet)
       +        self.assertEqual('45505446ff000200000001caaac6b5eb916e3067d0224f942fb331ce1dcfb4031cfb479e7941dcf95e409801000000fd53010001ff01ff483045022100d03c35cb293b01da7be0d771cedafa891ed5d70a677cfccd69671356f6bebedc02200b2a576c4cd1c642480756ce1fd660acaf0ba526193a3099e1f81cdd01ad2093014d0201524c53ff043587cf0182078976800000016fa128bdc0a5a46fc7af1afd205db6207191d4c4051b6edbd06413cdc0b1a2df036a4e4cbe7bc466dd653784c86b37749bec3e828029cea34c15690956fd61576f000000004c53ff043587cf0000000000000000009ce327095ab9eebb8227b737f07e8fb66af04b8f3e91c4093c487bd30fe87c24020d210dd3ad80d29c3624a91896d5f61b7b61f332dec4f7c45431aeef805e0fa7000000004c53ff043587cf018207897680000000939f6098ce007ea492c14ea2421ba0fdfef06c21973971865e6e63bce892f7dc0261b2f1ef2dc72d30931ebdfd381914e31ed5a65cda0d470b0ad143d33df8f87b0000000053aefdffffff03a086010000000000160014f9f04b4652a4f6ff3dc55126db1274906d7072f34c0b03000000000017a914f29f7cb897101dc69afd58df9bb2e5b4d4339f4d87400d0300000000001976a914fae2c8965e7e239e9c88ac514de70dec0b0f160888ac122b1800',
       +                         raw_tx)
       +
       +    def test_trustedcoin_segwit_2fa_psbt_to_legacy_partial_tx(self):
       +        from .test_wallet_vertical import WalletIntegrityHelper
       +        seed_words = 'universe topic remind silver february ranch shine worth innocent cattle enhance wise'
       +        self.assertEqual(seed_type(seed_words), '2fa_segwit')
       +
       +        xprv1, xpub1, xprv2, xpub2 = trustedcoin.TrustedCoinPlugin.xkeys_from_seed(seed_words, '')
       +        ks1 = keystore.from_xprv(xprv1)
       +        ks2 = keystore.from_xprv(xprv2)
       +        long_user_id, short_id = trustedcoin.get_user_id(
       +            {'x1/': {'xpub': xpub1},
       +             'x2/': {'xpub': xpub2}})
       +        xtype = bip32.xpub_type(xpub1)
       +        xpub3 = trustedcoin.make_xpub(trustedcoin.get_signing_xpub(xtype), long_user_id)
       +        ks3 = keystore.from_xpub(xpub3)
       +
       +        wallet = WalletIntegrityHelper.create_multisig_wallet([ks1, ks2, ks3], '2of3', config=self.config)
       +
       +        tx = tx_from_any('70736274ff01009f020000000187c4646ca690b397e357b23b2137030691a90a068a6690834d340b4be84acd6a0100000000fdffffff03a0860100000000001600148bc9d947e4d0addc2f4c34b8371034eb47b3d305140c030000000000220020a6087c4f84a55dc39014729a18e08955139d4384559d1fd2a48a1d95d746a425400d0300000000001976a914c13fd6294d1be7b9410a5538f4b4ef10fc594ee788ac132b18004f0102575483000000000000000000d644dcdd7a4acb432355e010ac4a5f955940c82a9767731b7d89dc42eb3cdac40272faa2f98b76cc9d655ef281c07acda75c30a990dc3a527b6cf92a8f5a6b8a80044bf212094f010257548301d24cd5918000000199c4d6c893fa1addec6b0121181137e25c38194333b8c57215b3cf1e6d7e7af102a23ddae698bb6095b8b8035bdd1e94479f66c29679d81931e0fe07aa436149ee08d24cd591010000804f010257548301d24cd59180000000b60a85089ad0850a6ffe8590a3e15064e63eefd020813df2e4a6b3209ce3df5f02c8b14f2917d557cd482970c114c3e3457e09d256397802277a2e4d1519ab9f8c08d24cd591000000800001012b20a1070000000000220020a948d7fa6abbb97e31779ae54383012b413d53821c7fd394900f6b443c61deee22020307a3c41d07ed976d65e213e823d02840937475e709b41253e85e970e3cb1667447304402202f7be5fad398f1a3576293339f2274d227af17798690aa1ad00ff83d96725cc5022000cb2e9fff5be23a862718c665a5035f172783e9b3158eae83673f6c59adc3c001010569522102a9dcb570e8280c741f09032c158095b7aa3b0ce401ada030f2d47b999f020606210307a3c41d07ed976d65e213e823d02840937475e709b41253e85e970e3cb166742103521b0a45e042f08ccd03af47fd88bb207b5414e0e30bb8799fca311a06323a1953ae22060307a3c41d07ed976d65e213e823d02840937475e709b41253e85e970e3cb1667410d24cd591000000800000000001000000220603521b0a45e042f08ccd03af47fd88bb207b5414e0e30bb8799fca311a06323a1910d24cd591010000800000000001000000220602a9dcb570e8280c741f09032c158095b7aa3b0ce401ada030f2d47b999f0206060c4bf212090000000001000000000001016952210205829f9522577122ca9ca9beb67f94cf2fe0ad0d17e052a110701cf4128d339c21022cca282cfdd9cc1f387d3098661d68bc9c8a39ac6bd72c30db579a7857d0859b2102d90e3c8973844b9cc0b38494c48515a319eec3ac2489f96f6f55e6aa6912a60853ae220202d90e3c8973844b9cc0b38494c48515a319eec3ac2489f96f6f55e6aa6912a60810d24cd5910000008001000000000000002202022cca282cfdd9cc1f387d3098661d68bc9c8a39ac6bd72c30db579a7857d0859b10d24cd59101000080010000000000000022020205829f9522577122ca9ca9beb67f94cf2fe0ad0d17e052a110701cf4128d339c0c4bf2120901000000000000000000')
       +        tx.add_info_from_wallet(wallet)
       +        raw_tx = serialize_tx_in_legacy_format(tx, wallet=wallet)
       +        self.assertEqual('45505446ff000200000000010187c4646ca690b397e357b23b2137030691a90a068a6690834d340b4be84acd6a0100000000fdffffff03a0860100000000001600148bc9d947e4d0addc2f4c34b8371034eb47b3d305140c030000000000220020a6087c4f84a55dc39014729a18e08955139d4384559d1fd2a48a1d95d746a425400d0300000000001976a914c13fd6294d1be7b9410a5538f4b4ef10fc594ee788acfeffffffff20a10700000000000000050001ff47304402202f7be5fad398f1a3576293339f2274d227af17798690aa1ad00ff83d96725cc5022000cb2e9fff5be23a862718c665a5035f172783e9b3158eae83673f6c59adc3c00101fffd0201524c53ff02575483000000000000000000d644dcdd7a4acb432355e010ac4a5f955940c82a9767731b7d89dc42eb3cdac40272faa2f98b76cc9d655ef281c07acda75c30a990dc3a527b6cf92a8f5a6b8a80000001004c53ff0257548301d24cd59180000000b60a85089ad0850a6ffe8590a3e15064e63eefd020813df2e4a6b3209ce3df5f02c8b14f2917d557cd482970c114c3e3457e09d256397802277a2e4d1519ab9f8c000001004c53ff0257548301d24cd5918000000199c4d6c893fa1addec6b0121181137e25c38194333b8c57215b3cf1e6d7e7af102a23ddae698bb6095b8b8035bdd1e94479f66c29679d81931e0fe07aa436149ee0000010053ae132b1800',
       +                         raw_tx)
 (DIR) diff --git a/electrum/tests/test_wallet.py b/electrum/tests/test_wallet.py
       t@@ -222,7 +222,7 @@ class TestCreateRestoreWallet(WalletTestCase):
                addr0 = wallet.get_receiving_addresses()[0]
                self.assertEqual('bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', addr0)
                self.assertEqual('p2wpkh:L4jkdiXszG26SUYvwwJhzGwg37H2nLhrbip7u6crmgNeJysv5FHL',
       -                         wallet.export_private_key(addr0, password=None)[0])
       +                         wallet.export_private_key(addr0, password=None))
                self.assertEqual(2, len(wallet.get_receiving_addresses()))
                # also test addr deletion
                wallet.delete_address('bc1qnp78h78vp92pwdwq5xvh8eprlga5q8gu66960c')
 (DIR) diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py
       t@@ -11,7 +11,7 @@ from electrum import SimpleConfig
        from electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT
        from electrum.wallet import sweep, Multisig_Wallet, Standard_Wallet, Imported_Wallet, restore_wallet_from_text
        from electrum.util import bfh, bh2u
       -from electrum.transaction import TxOutput
       +from electrum.transaction import TxOutput, Transaction, PartialTransaction, PartialTxOutput, PartialTxInput, tx_from_any
        from electrum.mnemonic import seed_type
        
        from electrum.plugins.trustedcoin import trustedcoin
       t@@ -573,14 +573,14 @@ class TestWalletSending(TestCaseForTestnet):
                wallet1.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
        
                # wallet1 -> wallet2
       -        outputs = [TxOutput(bitcoin.TYPE_ADDRESS, wallet2.get_receiving_address(), 250000)]
       +        outputs = [PartialTxOutput.from_address_and_value(wallet2.get_receiving_address(), 250000)]
                tx = wallet1.mktx(outputs=outputs, password=None, fee=5000, tx_version=1)
        
                self.assertTrue(tx.is_complete())
                self.assertTrue(tx.is_segwit())
                self.assertEqual(1, len(tx.inputs()))
       -        self.assertEqual(wallet1.txin_type, tx.inputs()[0]['type'])
       -        tx_copy = Transaction(tx.serialize())
       +        self.assertEqual(wallet1.txin_type, tx.inputs()[0].script_type)
       +        tx_copy = tx_from_any(tx.serialize())
                self.assertTrue(wallet1.is_mine(wallet1.get_txin_address(tx_copy.inputs()[0])))
        
                self.assertEqual('010000000001010392c1940e2ec9f2372919ca3887327fe5b98b866022cc79bab5cbed5a53d2ad0000000000feffffff0290d00300000000001976a914ea7804a2c266063572cc009a63dc25dcc0e9d9b588ac285e0b0000000000160014690b59a8140602fb23cc2904ece9cc4daf361052024730440220608a5339ca894592da82119e1e4a1d09335d70a552c683687223b8ed724465e902201b3f0feccf391b1b6257e4b18970ae57d7ca060af2dae519b3690baad2b2a34e0121030faee9b4a25b7db82023ca989192712cdd4cb53d3d9338591c7909e581ae1c0c00000000',
       t@@ -593,14 +593,14 @@ class TestWalletSending(TestCaseForTestnet):
                wallet2.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
        
                # wallet2 -> wallet1
       -        outputs = [TxOutput(bitcoin.TYPE_ADDRESS, wallet1.get_receiving_address(), 100000)]
       +        outputs = [PartialTxOutput.from_address_and_value(wallet1.get_receiving_address(), 100000)]
                tx = wallet2.mktx(outputs=outputs, password=None, fee=5000, tx_version=1)
        
                self.assertTrue(tx.is_complete())
                self.assertFalse(tx.is_segwit())
                self.assertEqual(1, len(tx.inputs()))
       -        self.assertEqual(wallet2.txin_type, tx.inputs()[0]['type'])
       -        tx_copy = Transaction(tx.serialize())
       +        self.assertEqual(wallet2.txin_type, tx.inputs()[0].script_type)
       +        tx_copy = tx_from_any(tx.serialize())
                self.assertTrue(wallet2.is_mine(wallet2.get_txin_address(tx_copy.inputs()[0])))
        
                self.assertEqual('0100000001e228327e4c0bb80661d258d625f516307e7c127c7f3e2b476a22e89b4dae063c000000006b483045022100d3895b31e7c9766987c6f53794c7394f534f4acecefda5479d963236f9703d0b022026dd4e40700ceb788f136faf54bf85b966648dc7c2a608d8110604f2d22d59070121030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cffeffffff02a0860100000000001600148a28bddb7f61864bdcf58b2ad13d5aeb3abc3c4268360200000000001976a914ca4c60999c46c2108326590b125aefd476dcb11888ac00000000',
       t@@ -648,17 +648,20 @@ class TestWalletSending(TestCaseForTestnet):
                wallet1a.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
        
                # wallet1 -> wallet2
       -        outputs = [TxOutput(bitcoin.TYPE_ADDRESS, wallet2.get_receiving_address(), 370000)]
       +        outputs = [PartialTxOutput.from_address_and_value(wallet2.get_receiving_address(), 370000)]
                tx = wallet1a.mktx(outputs=outputs, password=None, fee=5000, tx_version=1)
       -        tx = Transaction(tx.serialize())  # simulates moving partial txn between cosigners
       +        partial_tx = tx.serialize_as_bytes().hex()
       +        self.assertEqual("70736274ff01007501000000017120d4e1f2cdfe7df000d632cff74167fb354f0546d5cfc228e5c98756d55cb20100000000feffffff0250a50500000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac2862b1000000000017a9142e517854aa54668128c0e9a3fdd4dec13ad571368700000000000100e0010000000001014121f99dc02f0364d2dab3d08905ff4c36fc76c55437fd90b769c35cc18618280100000000fdffffff02d4c22d00000000001600143fd1bc5d32245850c8cb5be5b09c73ccbb9a0f75001bb7000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e4887024830450221008781c78df0c9d4b5ea057333195d5d76bc29494d773f14fa80e27d2f288b2c360220762531614799b6f0fb8d539b18cb5232ab4253dd4385435157b28a44ff63810d0121033de77d21926e09efd04047ae2d39dbd3fb9db446e8b7ed53e0f70f9c9478f735dac11300220202afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f28483045022100f9ce5616683e613ae14b98d56436454b003348a8172e2ed598018e3d206e57d7022030c65c6551e839f9e9409812be624dbb4e36bd4152c9ed9b0988c10fd8201d1401010469522102afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f2821030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf2103e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce53ae220602afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f280c0036e9ac00000000000000002206030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf0c48adc7a00000000000000000220603e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce0cdb692427000000000000000000000100695221022ec6f62b0f3b7c2446f44346bff0a6f06b5fdbc27368be8a36478e0287fe47be21024238f21f90527dc87e945f389f3d1711943b06f0a738d5baab573fc0ab6c98582102b7139e93747d7c77f62af5a38b8a2b009f3456aa94dea9bf21f73a6298c867a253ae2202022ec6f62b0f3b7c2446f44346bff0a6f06b5fdbc27368be8a36478e0287fe47be0cdb69242701000000000000002202024238f21f90527dc87e945f389f3d1711943b06f0a738d5baab573fc0ab6c98580c0036e9ac0100000000000000220202b7139e93747d7c77f62af5a38b8a2b009f3456aa94dea9bf21f73a6298c867a20c48adc7a0010000000000000000",
       +                         partial_tx)
       +        tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners
                self.assertFalse(tx.is_complete())
                wallet1b.sign_transaction(tx, password=None)
        
                self.assertTrue(tx.is_complete())
                self.assertFalse(tx.is_segwit())
                self.assertEqual(1, len(tx.inputs()))
       -        self.assertEqual(wallet1a.txin_type, tx.inputs()[0]['type'])
       -        tx_copy = Transaction(tx.serialize())
       +        self.assertEqual(wallet1a.txin_type, tx.inputs()[0].script_type)
       +        tx_copy = tx_from_any(tx.serialize())
                self.assertTrue(wallet1a.is_mine(wallet1a.get_txin_address(tx_copy.inputs()[0])))
        
                self.assertEqual('01000000017120d4e1f2cdfe7df000d632cff74167fb354f0546d5cfc228e5c98756d55cb201000000fdfe0000483045022100f9ce5616683e613ae14b98d56436454b003348a8172e2ed598018e3d206e57d7022030c65c6551e839f9e9409812be624dbb4e36bd4152c9ed9b0988c10fd8201d1401483045022100d5cb94d4d1dcf01bb9e9280e8178a7e9ada3ad14378ca543afcc9f5667b27cb2022018e76b74800a21934e73b226b34cbbe45c877fba64693da8a20d3cb330f2eafd014c69522102afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f2821030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf2103e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce53aefeffffff0250a50500000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac2862b1000000000017a9142e517854aa54668128c0e9a3fdd4dec13ad571368700000000',
       t@@ -671,14 +674,14 @@ class TestWalletSending(TestCaseForTestnet):
                wallet2.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
        
                # wallet2 -> wallet1
       -        outputs = [TxOutput(bitcoin.TYPE_ADDRESS, wallet1a.get_receiving_address(), 100000)]
       +        outputs = [PartialTxOutput.from_address_and_value(wallet1a.get_receiving_address(), 100000)]
                tx = wallet2.mktx(outputs=outputs, password=None, fee=5000, tx_version=1)
        
                self.assertTrue(tx.is_complete())
                self.assertFalse(tx.is_segwit())
                self.assertEqual(1, len(tx.inputs()))
       -        self.assertEqual(wallet2.txin_type, tx.inputs()[0]['type'])
       -        tx_copy = Transaction(tx.serialize())
       +        self.assertEqual(wallet2.txin_type, tx.inputs()[0].script_type)
       +        tx_copy = tx_from_any(tx.serialize())
                self.assertTrue(wallet2.is_mine(wallet2.get_txin_address(tx_copy.inputs()[0])))
        
                self.assertEqual('01000000015df26ee0f55487ca29727c50dbf0ce2227d3e3eb44621219ff1c2e40d0bdf326000000008b483045022100bd9f61ba82507d3a28922fb8be129e14699dfa54ddd03cc9494f696d38ac4121022071afca6fad5bc5c09b0a675e6444be3e97dbbdbc283764ee5f4e27a032d933d80141045f7ba332df2a7b4f5d13f246e307c9174cfa9b8b05f3b83410a3c23ef8958d610be285963d67c7bc1feb082f168fa9877c25999963ff8b56b242a852b23e25edfeffffff02a08601000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e4887280b0400000000001976a914ca14915184a2662b5d1505ce7142c8ca066c70e288ac00000000',
       t@@ -743,10 +746,13 @@ class TestWalletSending(TestCaseForTestnet):
                wallet1a.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
        
                # wallet1 -> wallet2
       -        outputs = [TxOutput(bitcoin.TYPE_ADDRESS, wallet2a.get_receiving_address(), 165000)]
       +        outputs = [PartialTxOutput.from_address_and_value(wallet2a.get_receiving_address(), 165000)]
                tx = wallet1a.mktx(outputs=outputs, password=None, fee=5000, tx_version=1)
                txid = tx.txid()
       -        tx = Transaction(tx.serialize())  # simulates moving partial txn between cosigners
       +        partial_tx = tx.serialize_as_bytes().hex()
       +        self.assertEqual("70736274ff01007e0100000001213e1012a461e056752fab5a6414a2fb63f950cd21a50ac5e2b82d339d6cbdd20000000000feffffff023075000000000000220020cc5e4cc05a76d0648cd0742768556317e9f8cc729aed077134287909035dba88888402000000000017a914187842cea9c15989a51ce7ca889a08b824bf874387000000000001012b400d0300000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c22020223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa483045022100ea2fbd3d8681cfafdcae1bdaaa64f92fb9872fb8f6bf03a2b7effcf7390b66c8022021a79eff7975479934f958f3766d6ac61d708c79b785e398b3bcd84b1039e9b50101056952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153ae22060223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa0cb5c37672000000000000000022060273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e0c043b02bf0000000000000000220602aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae9410c3b1da3ef000000000000000000010169522102174696a58a8dcd6c6455bd25e0749e9a6fc7d84ee09e192ab37b0d0b18c2de1a2102c807a19ca6783261f8c198ffcc437622e7ecba8d6c5692f3a5e7f1e45af53fd52102eee40c7e24d89639182db32f5e9188613e4bc212da2ee9b4ccc85d9b82e1a98053ae220202174696a58a8dcd6c6455bd25e0749e9a6fc7d84ee09e192ab37b0d0b18c2de1a0c043b02bf0100000000000000220202c807a19ca6783261f8c198ffcc437622e7ecba8d6c5692f3a5e7f1e45af53fd50c3b1da3ef0100000000000000220202eee40c7e24d89639182db32f5e9188613e4bc212da2ee9b4ccc85d9b82e1a9800cb5c3767201000000000000000000",
       +                         partial_tx)
       +        tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners
                self.assertEqual(txid, tx.txid())
                self.assertFalse(tx.is_complete())
                wallet1b.sign_transaction(tx, password=None)
       t@@ -754,8 +760,8 @@ class TestWalletSending(TestCaseForTestnet):
                self.assertTrue(tx.is_complete())
                self.assertTrue(tx.is_segwit())
                self.assertEqual(1, len(tx.inputs()))
       -        self.assertEqual(wallet1a.txin_type, tx.inputs()[0]['type'])
       -        tx_copy = Transaction(tx.serialize())
       +        self.assertEqual(wallet1a.txin_type, tx.inputs()[0].script_type)
       +        tx_copy = tx_from_any(tx.serialize())
                self.assertTrue(wallet1a.is_mine(wallet1a.get_txin_address(tx_copy.inputs()[0])))
        
                self.assertEqual('01000000000101213e1012a461e056752fab5a6414a2fb63f950cd21a50ac5e2b82d339d6cbdd20000000000feffffff023075000000000000220020cc5e4cc05a76d0648cd0742768556317e9f8cc729aed077134287909035dba88888402000000000017a914187842cea9c15989a51ce7ca889a08b824bf8743870400483045022100ea2fbd3d8681cfafdcae1bdaaa64f92fb9872fb8f6bf03a2b7effcf7390b66c8022021a79eff7975479934f958f3766d6ac61d708c79b785e398b3bcd84b1039e9b501483045022100dbc4f1ec18f0e0deb4ff88d7d5b3d3b7b500a80d0c0f33efbd3262f0c8689095022074fd226c0b52e3716ad907d14cba9c79aca482a8f4a51662ca83a5b9db49e15b016952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153ae00000000',
       t@@ -769,10 +775,13 @@ class TestWalletSending(TestCaseForTestnet):
                wallet2a.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
        
                # wallet2 -> wallet1
       -        outputs = [TxOutput(bitcoin.TYPE_ADDRESS, wallet1a.get_receiving_address(), 100000)]
       +        outputs = [PartialTxOutput.from_address_and_value(wallet1a.get_receiving_address(), 100000)]
                tx = wallet2a.mktx(outputs=outputs, password=None, fee=5000, tx_version=1)
                txid = tx.txid()
       -        tx = Transaction(tx.serialize())  # simulates moving partial txn between cosigners
       +        partial_tx = tx.serialize_as_bytes().hex()
       +        self.assertEqual("70736274ff01007e010000000149d077be0ee9d52776211e9b4fec1cc02bd53661a04e120a97db8b78d83c9c6e0100000000feffffff0260ea00000000000017a9143025051b6b5ccd4baf30dfe2de8aa84f0dd567ed87a0860100000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c0000000000010120888402000000000017a914187842cea9c15989a51ce7ca889a08b824bf874387220202119f899075a131d4d519d4cdcf5de5907dc2df3b93d54b53ded852211d2b6cb1483045022100c254468bbe6b8bd1c8c01b6a223e46cc5c6b56fbba87d59575385ad249133b0e02207139688f8d6ae8076c92a266d98454d25c040d04c8e513a37bf7c32dad3e48210101042200204311edae835c7a5aa712c8ca644180f13a3b2f3b420fa879b181474724d6163c010547522102119f899075a131d4d519d4cdcf5de5907dc2df3b93d54b53ded852211d2b6cb12102fdb0f6775d4b6619257c43343ba5e7807b0164f1eb3f00f2b594ab9e53ab812652ae220602119f899075a131d4d519d4cdcf5de5907dc2df3b93d54b53ded852211d2b6cb10cd1dbcc210000000000000000220602fdb0f6775d4b6619257c43343ba5e7807b0164f1eb3f00f2b594ab9e53ab81260c17cea9140000000000000000000100220020717ab7037b81797cb3e192a8a1b4d88083444bbfcd26934cadf3bcf890f14e05010147522102987c184fcd8ace2e2a314250e04a15a4b8c885fb4eb778ab82c45838bcbcbdde21034084c4a0493c248783e60d8415cd30b3ba2c3b7a79201e38b953adea2bc44f9952ae220202987c184fcd8ace2e2a314250e04a15a4b8c885fb4eb778ab82c45838bcbcbdde0c17cea91401000000000000002202034084c4a0493c248783e60d8415cd30b3ba2c3b7a79201e38b953adea2bc44f990cd1dbcc2101000000000000000000",
       +                         partial_tx)
       +        tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners
                self.assertEqual(txid, tx.txid())
                self.assertFalse(tx.is_complete())
                wallet2b.sign_transaction(tx, password=None)
       t@@ -780,8 +789,8 @@ class TestWalletSending(TestCaseForTestnet):
                self.assertTrue(tx.is_complete())
                self.assertTrue(tx.is_segwit())
                self.assertEqual(1, len(tx.inputs()))
       -        self.assertEqual(wallet2a.txin_type, tx.inputs()[0]['type'])
       -        tx_copy = Transaction(tx.serialize())
       +        self.assertEqual(wallet2a.txin_type, tx.inputs()[0].script_type)
       +        tx_copy = tx_from_any(tx.serialize())
                self.assertTrue(wallet2a.is_mine(wallet2a.get_txin_address(tx_copy.inputs()[0])))
        
                self.assertEqual('0100000000010149d077be0ee9d52776211e9b4fec1cc02bd53661a04e120a97db8b78d83c9c6e01000000232200204311edae835c7a5aa712c8ca644180f13a3b2f3b420fa879b181474724d6163cfeffffff0260ea00000000000017a9143025051b6b5ccd4baf30dfe2de8aa84f0dd567ed87a0860100000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c0400483045022100c254468bbe6b8bd1c8c01b6a223e46cc5c6b56fbba87d59575385ad249133b0e02207139688f8d6ae8076c92a266d98454d25c040d04c8e513a37bf7c32dad3e48210147304402204af5edbab2d674f6a9edef8c97b2f7fdf8ababedc7b287710cc7a64d4699358b022064e2d07f4bb32373be31b2003dc56b7b831a7c01419326efb3011c64b898b3f00147522102119f899075a131d4d519d4cdcf5de5907dc2df3b93d54b53ded852211d2b6cb12102fdb0f6775d4b6619257c43343ba5e7807b0164f1eb3f00f2b594ab9e53ab812652ae00000000',
       t@@ -825,14 +834,14 @@ class TestWalletSending(TestCaseForTestnet):
                wallet1a.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
        
                # wallet1 -> wallet2
       -        outputs = [TxOutput(bitcoin.TYPE_ADDRESS, wallet2.get_receiving_address(), 1000000)]
       +        outputs = [PartialTxOutput.from_address_and_value(wallet2.get_receiving_address(), 1000000)]
                tx = wallet1a.mktx(outputs=outputs, password=None, fee=5000, tx_version=1)
        
                self.assertTrue(tx.is_complete())
                self.assertFalse(tx.is_segwit())
                self.assertEqual(1, len(tx.inputs()))
       -        self.assertEqual(wallet1a.txin_type, tx.inputs()[0]['type'])
       -        tx_copy = Transaction(tx.serialize())
       +        self.assertEqual(wallet1a.txin_type, tx.inputs()[0].script_type)
       +        tx_copy = tx_from_any(tx.serialize())
                self.assertTrue(wallet1a.is_mine(wallet1a.get_txin_address(tx_copy.inputs()[0])))
        
                self.assertEqual('0100000001a391c8b3d4a551eac85714f3f0a7514381c014ba4688de085b0fcee42dc13711010000009200483045022100fcf03aeb97b66791372c18aa0dd651817cf458d941dd628c966f0305a023360f022016c534530e267b6a52f90e62aa9fb50ace609ffb21e472d3ba7b29db9b30050e014751210245c90e040d4f9d1fc136b3d4d6b7535bbb5df2bd27666c21977042cc1e05b5b02103c9a6bebfce6294488315e58137a279b2efe09f1f528ecf93b40675ded3cf0e5f52aefeffffff0240420f000000000017a9149573eb50f3136dff141ac304190f41c8becc92ce8738b32d000000000017a914b815d1b430ae9b632e3834ed537f7956325ee2a98700000000',
       t@@ -845,14 +854,14 @@ class TestWalletSending(TestCaseForTestnet):
                wallet2.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
        
                # wallet2 -> wallet1
       -        outputs = [TxOutput(bitcoin.TYPE_ADDRESS, wallet1a.get_receiving_address(), 300000)]
       +        outputs = [PartialTxOutput.from_address_and_value(wallet1a.get_receiving_address(), 300000)]
                tx = wallet2.mktx(outputs=outputs, password=None, fee=5000, tx_version=1)
        
                self.assertTrue(tx.is_complete())
                self.assertTrue(tx.is_segwit())
                self.assertEqual(1, len(tx.inputs()))
       -        self.assertEqual(wallet2.txin_type, tx.inputs()[0]['type'])
       -        tx_copy = Transaction(tx.serialize())
       +        self.assertEqual(wallet2.txin_type, tx.inputs()[0].script_type)
       +        tx_copy = tx_from_any(tx.serialize())
                self.assertTrue(wallet2.is_mine(wallet2.get_txin_address(tx_copy.inputs()[0])))
        
                self.assertEqual('010000000001012a4a7e0487c839f211a2f174aa91e94146bdfd408d9271e3d481960b86947e1b00000000171600149fad840ed174584ee054bd26f3e411817338c5edfeffffff02e09304000000000017a914b0b9f31bace76cdfae2c14abc03e223403d7dc4b87d89a0a000000000017a9148ccd0efb2be5b412c4033715f560ed8f446c8ceb87024830450221009c816c3e0c40b37085244f0976f65635b8d711952bad9843c5f51e386fd37cc402202c34a4a7227182742d9f93e9f28c4bd30ded6514550f39614cb5ad00e46690070121038362bbf0b4918b37e9d7c75930ed3a78e3d445724cb5c37ade4a59b6e411fe4e00000000',
       t@@ -871,6 +880,7 @@ class TestWalletSending(TestCaseForTestnet):
            @needs_test_with_all_ecc_implementations
            @mock.patch.object(storage.WalletStorage, '_write')
            def test_rbf(self, mock_write):
       +        self.maxDiff = None
                for simulate_moving_txs in (False, True):
                    with self.subTest(msg="_bump_fee_p2pkh_when_there_is_a_change_address", simulate_moving_txs=simulate_moving_txs):
                        self._bump_fee_p2pkh_when_there_is_a_change_address(simulate_moving_txs=simulate_moving_txs)
       t@@ -896,20 +906,23 @@ class TestWalletSending(TestCaseForTestnet):
                wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
        
                # create tx
       -        outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', 2500000)]
       +        outputs = [PartialTxOutput.from_address_and_value('2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', 2500000)]
                coins = wallet.get_spendable_coins(domain=None)
       -        tx = wallet.make_unsigned_transaction(coins, outputs, fixed_fee=5000)
       +        tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=5000)
                tx.set_rbf(True)
                tx.locktime = 1325501
                tx.version = 1
                if simulate_moving_txs:
       -            tx = Transaction(str(tx))
       +            partial_tx = tx.serialize_as_bytes().hex()
       +            self.assertEqual("70736274ff01007501000000016207d958dc46508d706e4cd7d3bc46c5c2b02160e2578e5fad2efafc392705030000000000fdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987585d7200000000001976a914aab9af3fbee0ab4e5c00d53e92f66d4bcb44f1bd88acbd391400000100fa010000000001011f4db0ecd81f4388db316bc16efb4e9daf874cf4950d54ecb4c0fb372433d68500000000171600143d57fd9e88ef0e70cddb0d8b75ef86698cab0d44fdffffff0280969800000000001976a91472e34cebab371967b038ce41d0e8fa1fb983795e88ac86a0ae020000000017a9149188bc82bdcae077060ebb4f02201b73c806edc887024830450221008e0725d531bd7dee4d8d38a0f921d7b1213e5b16c05312a80464ecc2b649598d0220596d309cf66d5f47cb3df558dbb43c5023a7796a80f5a88b023287e45a4db6b9012102c34d61ceafa8c216f01e05707672354f8119334610f7933a3f80dd7fb6290296bd391400220602a807c07bd7975211078e916bdda061d97e98d59a3631a804aada2f9a3f5b587a0c8296e57100000000000000000000220203aa6a5d43c6de66d60f50942cf34f20e02c2c6f55349548fbf2cde5dd5d69b9180c8296e571010000000000000000",
       +                             partial_tx)
       +            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners
                wallet.sign_transaction(tx, password=None)
        
                self.assertTrue(tx.is_complete())
                self.assertFalse(tx.is_segwit())
                self.assertEqual(1, len(tx.inputs()))
       -        tx_copy = Transaction(tx.serialize())
       +        tx_copy = tx_from_any(tx.serialize())
                self.assertTrue(wallet.is_mine(wallet.get_txin_address(tx_copy.inputs()[0])))
        
                self.assertEqual(tx.txid(), tx_copy.txid())
       t@@ -923,17 +936,20 @@ class TestWalletSending(TestCaseForTestnet):
                self.assertEqual((0, funding_output_value - 2500000 - 5000, 0), wallet.get_balance())
        
                # bump tx
       -        tx = wallet.bump_fee(tx=Transaction(tx.serialize()), new_fee_rate=70.0)
       +        tx = wallet.bump_fee(tx=tx_from_any(tx.serialize()), new_fee_rate=70.0)
                tx.locktime = 1325501
                tx.version = 1
                if simulate_moving_txs:
       -            tx = Transaction(str(tx))
       +            partial_tx = tx.serialize_as_bytes().hex()
       +            self.assertEqual("70736274ff01007501000000016207d958dc46508d706e4cd7d3bc46c5c2b02160e2578e5fad2efafc392705030000000000fdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987a0337200000000001976a914aab9af3fbee0ab4e5c00d53e92f66d4bcb44f1bd88acbd391400000100fa010000000001011f4db0ecd81f4388db316bc16efb4e9daf874cf4950d54ecb4c0fb372433d68500000000171600143d57fd9e88ef0e70cddb0d8b75ef86698cab0d44fdffffff0280969800000000001976a91472e34cebab371967b038ce41d0e8fa1fb983795e88ac86a0ae020000000017a9149188bc82bdcae077060ebb4f02201b73c806edc887024830450221008e0725d531bd7dee4d8d38a0f921d7b1213e5b16c05312a80464ecc2b649598d0220596d309cf66d5f47cb3df558dbb43c5023a7796a80f5a88b023287e45a4db6b9012102c34d61ceafa8c216f01e05707672354f8119334610f7933a3f80dd7fb6290296bd391400220602a807c07bd7975211078e916bdda061d97e98d59a3631a804aada2f9a3f5b587a0c8296e5710000000000000000000000",
       +                             partial_tx)
       +            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners
                self.assertFalse(tx.is_complete())
        
                wallet.sign_transaction(tx, password=None)
                self.assertTrue(tx.is_complete())
                self.assertFalse(tx.is_segwit())
       -        tx_copy = Transaction(tx.serialize())
       +        tx_copy = tx_from_any(tx.serialize())
                self.assertEqual('01000000016207d958dc46508d706e4cd7d3bc46c5c2b02160e2578e5fad2efafc39270503000000006b483045022100a30c21d1ba8cf751b1b78b5a41684cbab6e39687fa188a4295881c7b06f10a6202204ba4f56cbfdeb8ed948d8a18e34112c256c48e921db048f134819b2ca7ed85fd012102a807c07bd7975211078e916bdda061d97e98d59a3631a804aada2f9a3f5b587afdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987a0337200000000001976a914aab9af3fbee0ab4e5c00d53e92f66d4bcb44f1bd88acbd391400',
                                 str(tx_copy))
                self.assertEqual('40768e1e418f8e851d496448c9627ee29f04c33f67a59ac49d2bbc66288d2077', tx_copy.txid())
       t@@ -964,7 +980,7 @@ class TestWalletSending(TestCaseForTestnet):
                self.assertTrue(tx.is_complete())
                self.assertFalse(tx.is_segwit())
                self.assertEqual(1, len(tx.inputs()))
       -        tx_copy = Transaction(tx.serialize())
       +        tx_copy = tx_from_any(tx.serialize())
        
                self.assertEqual(tx.txid(), tx_copy.txid())
                self.assertEqual(tx.wtxid(), tx_copy.wtxid())
       t@@ -987,20 +1003,23 @@ class TestWalletSending(TestCaseForTestnet):
                wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
        
                # create tx
       -        outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', 2500000)]
       +        outputs = [PartialTxOutput.from_address_and_value('2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', 2500000)]
                coins = wallet.get_spendable_coins(domain=None)
       -        tx = wallet.make_unsigned_transaction(coins, outputs, fixed_fee=5000)
       +        tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=5000)
                tx.set_rbf(True)
                tx.locktime = 1325499
                tx.version = 1
                if simulate_moving_txs:
       -            tx = Transaction(str(tx))
       +            partial_tx = tx.serialize_as_bytes().hex()
       +            self.assertEqual("70736274ff0100720100000001c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987585d720000000000160014f0fe5c1867a174a12e70165e728a072619455ed5bb3914000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b72206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c50c3c14aede00000000000000000000220202105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea0c3c14aede010000000000000000",
       +                             partial_tx)
       +            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners
                wallet.sign_transaction(tx, password=None)
        
                self.assertTrue(tx.is_complete())
                self.assertTrue(tx.is_segwit())
                self.assertEqual(1, len(tx.inputs()))
       -        tx_copy = Transaction(tx.serialize())
       +        tx_copy = tx_from_any(tx.serialize())
                self.assertTrue(wallet.is_mine(wallet.get_txin_address(tx_copy.inputs()[0])))
        
                self.assertEqual(tx.txid(), tx_copy.txid())
       t@@ -1014,17 +1033,20 @@ class TestWalletSending(TestCaseForTestnet):
                self.assertEqual((0, funding_output_value - 2500000 - 5000, 0), wallet.get_balance())
        
                # bump tx
       -        tx = wallet.bump_fee(tx=Transaction(tx.serialize()), new_fee_rate=70.0)
       +        tx = wallet.bump_fee(tx=tx_from_any(tx.serialize()), new_fee_rate=70.0)
                tx.locktime = 1325500
                tx.version = 1
                if simulate_moving_txs:
       -            tx = Transaction(str(tx))
       +            partial_tx = tx.serialize_as_bytes().hex()
       +            self.assertEqual("70736274ff0100720100000001c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f9870c4a720000000000160014f0fe5c1867a174a12e70165e728a072619455ed5bc3914000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b72206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c50c3c14aede0000000000000000000000",
       +                             partial_tx)
       +            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners
                self.assertFalse(tx.is_complete())
        
                wallet.sign_transaction(tx, password=None)
                self.assertTrue(tx.is_complete())
                self.assertTrue(tx.is_segwit())
       -        tx_copy = Transaction(tx.serialize())
       +        tx_copy = tx_from_any(tx.serialize())
                self.assertEqual('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f9870c4a720000000000160014f0fe5c1867a174a12e70165e728a072619455ed50247304402202a7e412d37f7a54f7ede0f85e58c7f9dc0f7244d222a4f50a90f87b05badeed40220788d4a4a13f660de7d5464dce5e79419361fdd5d1853c7da65469cd32f7981a90121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5bc391400',
                                 str(tx_copy))
                self.assertEqual('dad75ab7078b9ce9698a83e7a954c1c38b235d3a4ab79bcb340245e3d9b62b93', tx_copy.txid())
       t@@ -1044,19 +1066,22 @@ class TestWalletSending(TestCaseForTestnet):
                wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
        
                # create tx
       -        outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1q7rl9cxr85962ztnsze089zs8ycv52hk43f3m9n', '!')]
       +        outputs = [PartialTxOutput.from_address_and_value('tb1q7rl9cxr85962ztnsze089zs8ycv52hk43f3m9n', '!')]
                coins = wallet.get_spendable_coins(domain=None)
       -        tx = wallet.make_unsigned_transaction(coins, outputs, fixed_fee=5000)
       +        tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=5000)
                tx.set_rbf(True)
                tx.locktime = 1325499
                if simulate_moving_txs:
       -            tx = Transaction(str(tx))
       +            partial_tx = tx.serialize_as_bytes().hex()
       +            self.assertEqual("70736274ff0100520200000001c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff01f882980000000000160014f0fe5c1867a174a12e70165e728a072619455ed5bb3914000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b72206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c50c3c14aede000000000000000000220202105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea0c3c14aede010000000000000000",
       +                             partial_tx)
       +            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners
                wallet.sign_transaction(tx, password=None)
        
                self.assertTrue(tx.is_complete())
                self.assertTrue(tx.is_segwit())
                self.assertEqual(1, len(tx.inputs()))
       -        tx_copy = Transaction(tx.serialize())
       +        tx_copy = tx_from_any(tx.serialize())
                self.assertTrue(wallet.is_mine(wallet.get_txin_address(tx_copy.inputs()[0])))
        
                self.assertEqual(tx.txid(), tx_copy.txid())
       t@@ -1070,16 +1095,19 @@ class TestWalletSending(TestCaseForTestnet):
                self.assertEqual((0, funding_output_value - 5000, 0), wallet.get_balance())
        
                # bump tx
       -        tx = wallet.bump_fee(tx=Transaction(tx.serialize()), new_fee_rate=75)
       +        tx = wallet.bump_fee(tx=tx_from_any(tx.serialize()), new_fee_rate=75)
                tx.locktime = 1325500
                if simulate_moving_txs:
       -            tx = Transaction(str(tx))
       +            partial_tx = tx.serialize_as_bytes().hex()
       +            self.assertEqual("70736274ff0100520200000001c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff014676980000000000160014f0fe5c1867a174a12e70165e728a072619455ed5bc3914000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b72206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c50c3c14aede000000000000000000220202105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea0c3c14aede010000000000000000",
       +                             partial_tx)
       +            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners
                self.assertFalse(tx.is_complete())
        
                wallet.sign_transaction(tx, password=None)
                self.assertTrue(tx.is_complete())
                self.assertTrue(tx.is_segwit())
       -        tx_copy = Transaction(tx.serialize())
       +        tx_copy = tx_from_any(tx.serialize())
                self.assertEqual('02000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff014676980000000000160014f0fe5c1867a174a12e70165e728a072619455ed502483045022100ffe74cde6fabd27cecca29e77b863cd901188cb7870e39c237b193e7532d6ef502203efdf069482ce5f89a4a273166ff319f1e122f2e9d56f37696dc15a1b933df420121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5bc391400',
                                 str(tx_copy))
                self.assertEqual('0787da6829907ede8a322273d19ba47943ac234ad7fd1cb1821f6a0e78fcc003', tx_copy.txid())
       t@@ -1098,20 +1126,23 @@ class TestWalletSending(TestCaseForTestnet):
                wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
        
                # create tx
       -        outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', '!')]
       +        outputs = [PartialTxOutput.from_address_and_value('2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', '!')]
                coins = wallet.get_spendable_coins(domain=None)
       -        tx = wallet.make_unsigned_transaction(coins, outputs, fixed_fee=5000)
       +        tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=5000)
                tx.set_rbf(True)
                tx.locktime = 1325499
                tx.version = 1
                if simulate_moving_txs:
       -            tx = Transaction(str(tx))
       +            partial_tx = tx.serialize_as_bytes().hex()
       +            self.assertEqual("70736274ff0100530100000001c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff01f88298000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987bb3914000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b72206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c50c3c14aede00000000000000000000",
       +                             partial_tx)
       +            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners
                wallet.sign_transaction(tx, password=None)
        
                self.assertTrue(tx.is_complete())
                self.assertTrue(tx.is_segwit())
                self.assertEqual(1, len(tx.inputs()))
       -        tx_copy = Transaction(tx.serialize())
       +        tx_copy = tx_from_any(tx.serialize())
                self.assertTrue(wallet.is_mine(wallet.get_txin_address(tx_copy.inputs()[0])))
        
                self.assertEqual(tx.txid(), tx_copy.txid())
       t@@ -1125,17 +1156,20 @@ class TestWalletSending(TestCaseForTestnet):
                self.assertEqual((0, 0, 0), wallet.get_balance())
        
                # bump tx
       -        tx = wallet.bump_fee(tx=Transaction(tx.serialize()), new_fee_rate=70.0)
       +        tx = wallet.bump_fee(tx=tx_from_any(tx.serialize()), new_fee_rate=70.0)
                tx.locktime = 1325500
                tx.version = 1
                if simulate_moving_txs:
       -            tx = Transaction(str(tx))
       +            partial_tx = tx.serialize_as_bytes().hex()
       +            self.assertEqual("70736274ff0100530100000001c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff01267898000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987bc3914000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b72206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c50c3c14aede00000000000000000000",
       +                             partial_tx)
       +            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners
                self.assertFalse(tx.is_complete())
        
                wallet.sign_transaction(tx, password=None)
                self.assertTrue(tx.is_complete())
                self.assertTrue(tx.is_segwit())
       -        tx_copy = Transaction(tx.serialize())
       +        tx_copy = tx_from_any(tx.serialize())
                self.assertEqual('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff01267898000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f98702473044022069412007c3a6509fdfcfbe90679395c202c973740b0530b8ff366bc86ebff99d02206a02e3c0beb0921fa7d30379db4999d685d4b97239a2b8c7dd839531c72863110121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5bc391400',
                                 str(tx_copy))
                self.assertEqual('53824cc67e8fe973b0dfa1b8cc10f4e2441b9b4b2b1eb92576fbba7000c2908a', tx_copy.txid())
       t@@ -1155,20 +1189,23 @@ class TestWalletSending(TestCaseForTestnet):
                wallet.receive_tx_callback(funding_txid1, funding_tx1, TX_HEIGHT_UNCONFIRMED)
        
                # create tx
       -        outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', '!')]
       +        outputs = [PartialTxOutput.from_address_and_value('2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', '!')]
                coins = wallet.get_spendable_coins(domain=None)
       -        tx = wallet.make_unsigned_transaction(coins, outputs, fixed_fee=5000)
       +        tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=5000)
                tx.set_rbf(True)
                tx.locktime = 1325499
                tx.version = 1
                if simulate_moving_txs:
       -            tx = Transaction(str(tx))
       +            partial_tx = tx.serialize_as_bytes().hex()
       +            self.assertEqual("70736274ff0100530100000001c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff01f88298000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987bb3914000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b72206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c50c3c14aede00000000000000000000",
       +                             partial_tx)
       +            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners
                wallet.sign_transaction(tx, password=None)
        
                self.assertTrue(tx.is_complete())
                self.assertTrue(tx.is_segwit())
                self.assertEqual(1, len(tx.inputs()))
       -        tx_copy = Transaction(tx.serialize())
       +        tx_copy = tx_from_any(tx.serialize())
                self.assertTrue(wallet.is_mine(wallet.get_txin_address(tx_copy.inputs()[0])))
        
                self.assertEqual(tx.txid(), tx_copy.txid())
       t@@ -1190,17 +1227,20 @@ class TestWalletSending(TestCaseForTestnet):
                self.assertEqual((0, 5_000_000, 0), wallet.get_balance())
        
                # bump tx
       -        tx = wallet.bump_fee(tx=Transaction(tx.serialize()), new_fee_rate=70.0)
       +        tx = wallet.bump_fee(tx=tx_from_any(tx.serialize()), new_fee_rate=70.0)
                tx.locktime = 1325500
                tx.version = 1
                if simulate_moving_txs:
       -            tx = Transaction(str(tx))
       +            partial_tx = tx.serialize_as_bytes().hex()
       +            self.assertEqual("70736274ff01009b0100000002c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff4a5d2593658f7feb9fadcf70dced3bc18db8c90bf77495e608f14dd51c6e6ac30100000000feffffff025c254c0000000000160014f0fe5c1867a174a12e70165e728a072619455ed5f88298000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987bc3914000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b72206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c50c3c14aede00000000000000000001011f404b4c000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab90220602a6ff1ffc189b4776b78e20edca969cc45da3e610cc0cc79925604be43fee469f0c3c14aede0000000001000000000000",
       +                             partial_tx)
       +            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners
                self.assertFalse(tx.is_complete())
        
                wallet.sign_transaction(tx, password=None)
                self.assertTrue(tx.is_complete())
                self.assertTrue(tx.is_segwit())
       -        tx_copy = Transaction(tx.serialize())
       +        tx_copy = tx_from_any(tx.serialize())
                self.assertEqual('01000000000102c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff4a5d2593658f7feb9fadcf70dced3bc18db8c90bf77495e608f14dd51c6e6ac30100000000feffffff025c254c0000000000160014f0fe5c1867a174a12e70165e728a072619455ed5f88298000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987024730440220075992f2696076ca14265372c797fa5c6116ef9b8023f36fa7500442fe3e21430220252677cce7b009d8a65681e8f50b78c9a31c6461f67c995b8804041a290893660121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c502473044022018379b52ea52436eaeef1593e08aba78db1fd624b804ab747722f748203d553702204cbe4c87a010c8b67be9034014b503354e72f9c8205172269c00de20883fac61012102a6ff1ffc189b4776b78e20edca969cc45da3e610cc0cc79925604be43fee469fbc391400',
                                 str(tx_copy))
                self.assertEqual('056aaf5ec628a492742b083ad7790836e2d12e89061f32d5b517679764fdaff1', tx_copy.txid())
       t@@ -1221,20 +1261,23 @@ class TestWalletSending(TestCaseForTestnet):
                wallet.receive_tx_callback(funding_txid1, funding_tx1, TX_HEIGHT_UNCONFIRMED)
        
                # create tx
       -        outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', 2_500_000)]
       +        outputs = [PartialTxOutput.from_address_and_value('2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', 2_500_000)]
                coins = wallet.get_spendable_coins(domain=None)
       -        tx = wallet.make_unsigned_transaction(coins, outputs, fixed_fee=5000)
       +        tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=5000)
                tx.set_rbf(True)
                tx.locktime = 1325499
                tx.version = 1
                if simulate_moving_txs:
       -            tx = Transaction(str(tx))
       +            partial_tx = tx.serialize_as_bytes().hex()
       +            self.assertEqual("70736274ff0100720100000001c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987585d720000000000160014f0fe5c1867a174a12e70165e728a072619455ed5bb3914000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b72206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c50c3c14aede00000000000000000000220202105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea0c3c14aede010000000000000000",
       +                             partial_tx)
       +            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners
                wallet.sign_transaction(tx, password=None)
        
                self.assertTrue(tx.is_complete())
                self.assertTrue(tx.is_segwit())
                self.assertEqual(1, len(tx.inputs()))
       -        tx_copy = Transaction(tx.serialize())
       +        tx_copy = tx_from_any(tx.serialize())
                self.assertTrue(wallet.is_mine(wallet.get_txin_address(tx_copy.inputs()[0])))
        
                self.assertEqual(tx.txid(), tx_copy.txid())
       t@@ -1257,20 +1300,23 @@ class TestWalletSending(TestCaseForTestnet):
        
                # create new tx (output should be batched with existing!)
                # no new input will be needed. just a new output, and change decreased.
       -        outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1qy6xmdj96v5dzt3j08hgc05yk3kltqsnmw4r6ry', 2_500_000)]
       +        outputs = [PartialTxOutput.from_address_and_value('tb1qy6xmdj96v5dzt3j08hgc05yk3kltqsnmw4r6ry', 2_500_000)]
                coins = wallet.get_spendable_coins(domain=None)
       -        tx = wallet.make_unsigned_transaction(coins, outputs, fixed_fee=20000)
       +        tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=20000)
                tx.set_rbf(True)
                tx.locktime = 1325499
                tx.version = 1
                if simulate_moving_txs:
       -            tx = Transaction(str(tx))
       +            partial_tx = tx.serialize_as_bytes().hex()
       +            self.assertEqual("70736274ff0100910100000001c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff03a025260000000000160014268db6c8ba651a25c64f3dd187d0968dbeb0427ba02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f98720fd4b0000000000160014f0fe5c1867a174a12e70165e728a072619455ed5bb3914000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b72206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c50c3c14aede0000000000000000000000220202105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea0c3c14aede010000000000000000",
       +                             partial_tx)
       +            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners
                wallet.sign_transaction(tx, password=None)
        
                self.assertTrue(tx.is_complete())
                self.assertTrue(tx.is_segwit())
                self.assertEqual(1, len(tx.inputs()))
       -        tx_copy = Transaction(tx.serialize())
       +        tx_copy = tx_from_any(tx.serialize())
                self.assertTrue(wallet.is_mine(wallet.get_txin_address(tx_copy.inputs()[0])))
        
                self.assertEqual(tx.txid(), tx_copy.txid())
       t@@ -1285,20 +1331,23 @@ class TestWalletSending(TestCaseForTestnet):
        
                # create new tx (output should be batched with existing!)
                # new input will be needed!
       -        outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2NCVwbmEpvaXKHpXUGJfJr9iB5vtRN3vcut', 6_000_000)]
       +        outputs = [PartialTxOutput.from_address_and_value('2NCVwbmEpvaXKHpXUGJfJr9iB5vtRN3vcut', 6_000_000)]
                coins = wallet.get_spendable_coins(domain=None)
       -        tx = wallet.make_unsigned_transaction(coins, outputs, fixed_fee=100_000)
       +        tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=100_000)
                tx.set_rbf(True)
                tx.locktime = 1325499
                tx.version = 1
                if simulate_moving_txs:
       -            tx = Transaction(str(tx))
       +            partial_tx = tx.serialize_as_bytes().hex()
       +            self.assertEqual("70736274ff0100da0100000002c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff4a5d2593658f7feb9fadcf70dced3bc18db8c90bf77495e608f14dd51c6e6ac30100000000fdffffff04a025260000000000160014268db6c8ba651a25c64f3dd187d0968dbeb0427ba02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f98760823b0000000000160014f0fe5c1867a174a12e70165e728a072619455ed5808d5b000000000017a914d332f2f63019da6f2d23ee77bbe30eed7739790587bb3914000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b72206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c50c3c14aede00000000000000000001011f404b4c000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab90220602a6ff1ffc189b4776b78e20edca969cc45da3e610cc0cc79925604be43fee469f0c3c14aede0000000001000000000000220202105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea0c3c14aede01000000000000000000",
       +                             partial_tx)
       +            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners
                wallet.sign_transaction(tx, password=None)
        
                self.assertTrue(tx.is_complete())
                self.assertTrue(tx.is_segwit())
                self.assertEqual(2, len(tx.inputs()))
       -        tx_copy = Transaction(tx.serialize())
       +        tx_copy = tx_from_any(tx.serialize())
                self.assertTrue(wallet.is_mine(wallet.get_txin_address(tx_copy.inputs()[0])))
        
                self.assertEqual(tx.txid(), tx_copy.txid())
       t@@ -1333,7 +1382,7 @@ class TestWalletSending(TestCaseForTestnet):
                self.assertTrue(tx.is_complete())
                self.assertTrue(tx.is_segwit())
                self.assertEqual(1, len(tx.inputs()))
       -        tx_copy = Transaction(tx.serialize())
       +        tx_copy = tx_from_any(tx.serialize())
        
                self.assertEqual(tx.txid(), tx_copy.txid())
                self.assertEqual(tx.wtxid(), tx_copy.wtxid())
       t@@ -1362,14 +1411,99 @@ class TestWalletSending(TestCaseForTestnet):
                privkeys = ['93NQ7CFbwTPyKDJLXe97jczw33fiLijam2SCZL3Uinz1NSbHrTu', ]
                network = NetworkMock()
                dest_addr = 'tb1q3ws2p0qjk5vrravv065xqlnkckvzcpclk79eu2'
       -        tx = sweep(privkeys, network, config=None, recipient=dest_addr, fee=5000, locktime=1325785, tx_version=1)
       +        tx = sweep(privkeys, network=network, config=None, to_address=dest_addr, fee=5000, locktime=1325785, tx_version=1)
        
       -        tx_copy = Transaction(tx.serialize())
       +        tx_copy = tx_from_any(tx.serialize())
                self.assertEqual('010000000129349e5641d79915e9d0282fdbaee8c3df0b6731bab9d70bf626e8588bde24ac010000004847304402206bf0d0a93abae0d5873a62ebf277a5dd2f33837821e8b93e74d04e19d71b578002201a6d729bc159941ef5c4c9e5fe13ece9fc544351ba531b00f68ba549c8b38a9a01fdffffff01b82e0f00000000001600148ba0a0bc12b51831f58c7ea8607e76c5982c071fd93a1400',
                                 str(tx_copy))
                self.assertEqual('7f827fc5256c274fd1094eb7e020c8ded0baf820356f61aa4f14a9093b0ea0ee', tx_copy.txid())
                self.assertEqual('7f827fc5256c274fd1094eb7e020c8ded0baf820356f61aa4f14a9093b0ea0ee', tx_copy.wtxid())
        
       +    @needs_test_with_all_ecc_implementations
       +    @mock.patch.object(storage.WalletStorage, '_write')
       +    def test_coinjoin_between_two_p2wpkh_electrum_seeds(self, mock_write):
       +        wallet1 = WalletIntegrityHelper.create_standard_wallet(
       +            keystore.from_seed('humor argue expand gain goat shiver remove morning security casual leopard degree', ''),
       +            gap_limit=2,
       +            config=self.config
       +        )
       +        wallet2 = WalletIntegrityHelper.create_standard_wallet(
       +            keystore.from_seed('couple fade lift useless text thank badge act august roof drastic violin', ''),
       +            gap_limit=2,
       +            config=self.config
       +        )
       +
       +        # bootstrap wallet1
       +        funding_tx = Transaction('0200000000010162ecbac2f0c8662f53505d9410fdc56c84c5642ddbd3358d9a27d564e26731130200000000fdffffff02c0d8a70000000000160014aba1c9faecc3f8882e641583e8734a3f9d01b15ab89ed5000000000016001470afbd97b2dc351bd167f714e294b2fd3b60aedf02483045022100c93449989510e279eb14a0193d5c262ae93034b81376a1f6be259c6080d3ba5d0220536ab394f7c20f301d7ec2ef11be6e7b6d492053dce56458931c1b54218ec0fd012103b8f5a11df8e68cf335848e83a41fdad3c7413dc42148248a3799b58c93919ca010851800')
       +        funding_txid = funding_tx.txid()
       +        self.assertEqual('d8f8186379085cffc9a3fd747e7a7527435db974d1e2941f52f063be8e4fbdd5', funding_txid)
       +        wallet1.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
       +
       +        # bootstrap wallet2
       +        funding_tx = Transaction('02000000000101d5bd4f8ebe63f0521f94e2d174b95d4327757a7e74fda3c9ff5c08796318f8d80100000000fdffffff025066350000000000160014e3aa82aa2e754507d5585c0b6db06cc0cb4927b7a037a000000000001600140719d12228c61cab793ecd659c09cfe565a845c302483045022100f42e27519bd2379c22951c16b038fa6d49164fe6802854f2fdc7ee87fe31a8bc02204ea71e9324781b44bf7fea2f318caf3bedc5b497cbd1b4313fa71f833500bcb7012103a7853e1ee02a1629c8e870ec694a1420aeb98e6f5d071815257028f62d6f784169851800')
       +        funding_txid = funding_tx.txid()
       +        self.assertEqual('934f26a72c840293f06c37dc10a358df056dfe245cdf072ae836977c0abc46e5', funding_txid)
       +        wallet2.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
       +
       +        # wallet1 creates tx1, with output back to himself
       +        outputs = [PartialTxOutput.from_address_and_value("tb1qhye4wfp26kn0l7ynpn5a4hvt539xc3zf0n76t3", 10_000_000)]
       +        tx1 = wallet1.mktx(outputs=outputs, fee=5000, tx_version=2, rbf=True, sign=False)
       +        tx1.locktime = 1607022
       +        partial_tx1 = tx1.serialize_as_bytes().hex()
       +        self.assertEqual("70736274ff0100710200000001d5bd4f8ebe63f0521f94e2d174b95d4327757a7e74fda3c9ff5c08796318f8d80000000000fdffffff02b82e0f0000000000160014250dbabd5761d7e0773d6147699938dd08ec2eb88096980000000000160014b93357242ad5a6fff8930ce9dadd8ba44a6c44496e8518000001011fc0d8a70000000000160014aba1c9faecc3f8882e641583e8734a3f9d01b15a22060205e8db1b1906219782fadb18e763c0874a3118a17ce931e01707cbde194e04150ca6046cbd00000000000000000022020240ef5d2efee3b04b313a254df1b13a0b155451581e73943b21f3346bf6e1ba350ca6046cbd0100000000000000002202024a410b1212e88573561887b2bc38c90c074e4be425b9f3d971a9207825d9d3c80ca6046cbd000000000100000000",
       +                         partial_tx1)
       +        tx1.prepare_for_export_for_coinjoin()
       +        partial_tx1 = tx1.serialize_as_bytes().hex()
       +        self.assertEqual("70736274ff0100710200000001d5bd4f8ebe63f0521f94e2d174b95d4327757a7e74fda3c9ff5c08796318f8d80000000000fdffffff02b82e0f0000000000160014250dbabd5761d7e0773d6147699938dd08ec2eb88096980000000000160014b93357242ad5a6fff8930ce9dadd8ba44a6c44496e8518000001011fc0d8a70000000000160014aba1c9faecc3f8882e641583e8734a3f9d01b15a000000",
       +                         partial_tx1)
       +
       +        # wallet2 creates tx2, with output back to himself
       +        outputs = [PartialTxOutput.from_address_and_value("tb1qufnj5k2rrsnpjq7fg6d2pq3q9um6skdyyehw5m", 10_000_000)]
       +        tx2 = wallet2.mktx(outputs=outputs, fee=5000, tx_version=2, rbf=True, sign=False)
       +        tx2.locktime = 1607023
       +        partial_tx2 = tx2.serialize_as_bytes().hex()
       +        self.assertEqual("70736274ff0100710200000001e546bc0a7c9736e82a07df5c24fe6d05df58a310dc376cf09302842ca7264f930100000000fdffffff02988d07000000000016001453675a59be834aa6d139c3ebea56646a9b160c4c8096980000000000160014e2672a59431c261903c9469aa082202f37a859a46f8518000001011fa037a000000000001600140719d12228c61cab793ecd659c09cfe565a845c3220602275b4fba18bb34e5198a9cfb3e940306658839079b3bda50d504a9cf2bae36f40c467882350000000001000000002202036e4d0a5fb845b2f1c3c868c2ce7212b155b73e91c05be1b7a77c48830831ba4f0c4678823501000000000000000022020200062fdea2b0a056b17fa6b91dd87f5b5d838fe1ee84d636a5022f9a340eebcc0c46788235000000000000000000",
       +                         partial_tx2)
       +
       +        # wallet2 gets raw partial tx1, merges it into his own tx2
       +        tx2.join_with_other_psbt(tx_from_any(partial_tx1))
       +        partial_tx2 = tx2.serialize_as_bytes().hex()
       +        self.assertEqual("70736274ff0100d80200000002e546bc0a7c9736e82a07df5c24fe6d05df58a310dc376cf09302842ca7264f930100000000fdffffffd5bd4f8ebe63f0521f94e2d174b95d4327757a7e74fda3c9ff5c08796318f8d80000000000fdffffff04988d07000000000016001453675a59be834aa6d139c3ebea56646a9b160c4cb82e0f0000000000160014250dbabd5761d7e0773d6147699938dd08ec2eb88096980000000000160014b93357242ad5a6fff8930ce9dadd8ba44a6c44498096980000000000160014e2672a59431c261903c9469aa082202f37a859a46f8518000001011fa037a000000000001600140719d12228c61cab793ecd659c09cfe565a845c3220602275b4fba18bb34e5198a9cfb3e940306658839079b3bda50d504a9cf2bae36f40c4678823500000000010000000001011fc0d8a70000000000160014aba1c9faecc3f8882e641583e8734a3f9d01b15a002202036e4d0a5fb845b2f1c3c868c2ce7212b155b73e91c05be1b7a77c48830831ba4f0c46788235010000000000000000000022020200062fdea2b0a056b17fa6b91dd87f5b5d838fe1ee84d636a5022f9a340eebcc0c46788235000000000000000000",
       +                         partial_tx2)
       +        tx2.prepare_for_export_for_coinjoin()
       +        partial_tx2 = tx2.serialize_as_bytes().hex()
       +        self.assertEqual("70736274ff0100d80200000002e546bc0a7c9736e82a07df5c24fe6d05df58a310dc376cf09302842ca7264f930100000000fdffffffd5bd4f8ebe63f0521f94e2d174b95d4327757a7e74fda3c9ff5c08796318f8d80000000000fdffffff04988d07000000000016001453675a59be834aa6d139c3ebea56646a9b160c4cb82e0f0000000000160014250dbabd5761d7e0773d6147699938dd08ec2eb88096980000000000160014b93357242ad5a6fff8930ce9dadd8ba44a6c44498096980000000000160014e2672a59431c261903c9469aa082202f37a859a46f8518000001011fa037a000000000001600140719d12228c61cab793ecd659c09cfe565a845c30001011fc0d8a70000000000160014aba1c9faecc3f8882e641583e8734a3f9d01b15a0000000000",
       +                         partial_tx2)
       +
       +        # wallet2 signs
       +        wallet2.sign_transaction(tx2, password=None)
       +        partial_tx2 = tx2.serialize_as_bytes().hex()
       +        self.assertEqual("70736274ff0100d80200000002e546bc0a7c9736e82a07df5c24fe6d05df58a310dc376cf09302842ca7264f930100000000fdffffffd5bd4f8ebe63f0521f94e2d174b95d4327757a7e74fda3c9ff5c08796318f8d80000000000fdffffff04988d07000000000016001453675a59be834aa6d139c3ebea56646a9b160c4cb82e0f0000000000160014250dbabd5761d7e0773d6147699938dd08ec2eb88096980000000000160014b93357242ad5a6fff8930ce9dadd8ba44a6c44498096980000000000160014e2672a59431c261903c9469aa082202f37a859a46f8518000001011fa037a000000000001600140719d12228c61cab793ecd659c09cfe565a845c301070001086c02483045022100b87a557003acf04e2b328df6e4fa2bc387ba1a072d9325bd84f162d495720b24022042513f12244318cf94ad7944e8c135c6f47dcfe13ad08a77dca63a76facbe0b8012102275b4fba18bb34e5198a9cfb3e940306658839079b3bda50d504a9cf2bae36f40001011fc0d8a70000000000160014aba1c9faecc3f8882e641583e8734a3f9d01b15a002202036e4d0a5fb845b2f1c3c868c2ce7212b155b73e91c05be1b7a77c48830831ba4f0c46788235010000000000000000000022020200062fdea2b0a056b17fa6b91dd87f5b5d838fe1ee84d636a5022f9a340eebcc0c46788235000000000000000000",
       +                         partial_tx2)
       +        tx2.prepare_for_export_for_coinjoin()
       +        partial_tx2 = tx2.serialize_as_bytes().hex()
       +        self.assertEqual("70736274ff0100d80200000002e546bc0a7c9736e82a07df5c24fe6d05df58a310dc376cf09302842ca7264f930100000000fdffffffd5bd4f8ebe63f0521f94e2d174b95d4327757a7e74fda3c9ff5c08796318f8d80000000000fdffffff04988d07000000000016001453675a59be834aa6d139c3ebea56646a9b160c4cb82e0f0000000000160014250dbabd5761d7e0773d6147699938dd08ec2eb88096980000000000160014b93357242ad5a6fff8930ce9dadd8ba44a6c44498096980000000000160014e2672a59431c261903c9469aa082202f37a859a46f8518000001011fa037a000000000001600140719d12228c61cab793ecd659c09cfe565a845c301070001086c02483045022100b87a557003acf04e2b328df6e4fa2bc387ba1a072d9325bd84f162d495720b24022042513f12244318cf94ad7944e8c135c6f47dcfe13ad08a77dca63a76facbe0b8012102275b4fba18bb34e5198a9cfb3e940306658839079b3bda50d504a9cf2bae36f40001011fc0d8a70000000000160014aba1c9faecc3f8882e641583e8734a3f9d01b15a0000000000",
       +                         partial_tx2)
       +
       +        # wallet1 gets raw partial tx2, and signs
       +        tx2 = tx_from_any(partial_tx2)
       +        wallet1.sign_transaction(tx2, password=None)
       +        tx = tx_from_any(tx2.serialize_as_bytes().hex())  # simulates moving partial txn between cosigners
       +
       +        self.assertTrue(tx.is_complete())
       +        self.assertTrue(tx.is_segwit())
       +        self.assertEqual("02000000000102e546bc0a7c9736e82a07df5c24fe6d05df58a310dc376cf09302842ca7264f930100000000fdffffffd5bd4f8ebe63f0521f94e2d174b95d4327757a7e74fda3c9ff5c08796318f8d80000000000fdffffff04988d07000000000016001453675a59be834aa6d139c3ebea56646a9b160c4cb82e0f0000000000160014250dbabd5761d7e0773d6147699938dd08ec2eb88096980000000000160014b93357242ad5a6fff8930ce9dadd8ba44a6c44498096980000000000160014e2672a59431c261903c9469aa082202f37a859a402483045022100b87a557003acf04e2b328df6e4fa2bc387ba1a072d9325bd84f162d495720b24022042513f12244318cf94ad7944e8c135c6f47dcfe13ad08a77dca63a76facbe0b8012102275b4fba18bb34e5198a9cfb3e940306658839079b3bda50d504a9cf2bae36f402473044022003010ece3471f7a23f31b2a0fd157f88f7d436c0c73ec408043c7f5dd2b7ccbb02204bd21f5829555c3f94fbd0b5295d1071f739c6b8f2682f8a688e34d0ad26c90101210205e8db1b1906219782fadb18e763c0874a3118a17ce931e01707cbde194e04156f851800",
       +                         str(tx))
       +        self.assertEqual('4a33546eeaed0e25f9e6a58968be92a804a7e70a5332360dabc79f93cd059752', tx.txid())
       +        self.assertEqual('868c9f396cc8f998b80e47f199cc85eab971638ecf892b5024cb218bc2025137', tx.wtxid())
       +
       +        wallet1.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
       +        wallet2.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
       +
       +        # wallet level checks
       +        self.assertEqual((0, 10995000, 0), wallet1.get_balance())
       +        self.assertEqual((0, 10495000, 0), wallet2.get_balance())
       +
        
        class TestWalletOfflineSigning(TestCaseForTestnet):
        
       t@@ -1398,7 +1532,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
                wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
        
                # create unsigned tx
       -        outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1qyw3c0rvn6kk2c688y3dygvckn57525y8qnxt3a', 2500000)]
       +        outputs = [PartialTxOutput.from_address_and_value('tb1qyw3c0rvn6kk2c688y3dygvckn57525y8qnxt3a', 2500000)]
                tx = wallet_online.mktx(outputs=outputs, password=None, fee=5000)
                tx.set_rbf(True)
                tx.locktime = 1446655
       t@@ -1407,7 +1541,10 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
                self.assertFalse(tx.is_complete())
                self.assertFalse(tx.is_segwit())
                self.assertEqual(1, len(tx.inputs()))
       -        tx_copy = Transaction(tx.serialize())
       +        partial_tx = tx.serialize_as_bytes().hex()
       +        self.assertEqual("70736274ff01007401000000015608436ec7dc01c95ca1ca91519f2dc62b6613ac3d6304cb56462f6081059e3b0200000000fdffffff02a02526000000000016001423a3878d93d5acac68e7245a4433169d3d455087585d7200000000001976a914b6a6bbbc4cf9da58786a8acc58291e218d52130688acff121600000100fd000101000000000101161115f8d8110001aa0883989487f9c7a2faf4451038e4305c7594c5236cbb490100000000fdffffff0338117a0000000000160014c1d7b2ded7017cbde837aab36c1e7b2a3952a57800127a00000000001600143e2ab71fc9738ce16fbe6b3b1c210a68c12db84180969800000000001976a91424b64d981d621c227716b51479faf33019371f4688ac0247304402207a5efc6d970f6a5fdcd1933f68b353b4bf2904743f9f1dc3e9177d8754074baf02202eed707e661493bc450357f12cd7a8b8c610c7cb32ded10516c2933a2ba4346a01210287dce03f594fd889726b13a12970237992a0094a5c9f4eebcca6d50d454b39e9ff121600420604e79eb77f2f3f989f5e9d090bc0af50afeb0d5bd6ec916f2022c5629ed022e84a87584ef647d69f073ea314a0f0c110ebe24ad64bc1922a10819ea264fc3f35f50c343ddcab000000000100000000004202048e2004ca581afcc54a5d9b3b47affdf48b3f89e16d5bd96774fc0f167f2d7873bac6264e3d1f1bb96f64d1530a54e026e0bd7d674151d146fba582e79f4ef5e80c343ddcab010000000000000000",
       +                         partial_tx)
       +        tx_copy = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners
                self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0])))
        
                self.assertEqual(tx.txid(), tx_copy.txid())
       t@@ -1443,7 +1580,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
                wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
        
                # create unsigned tx
       -        outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)]
       +        outputs = [PartialTxOutput.from_address_and_value('tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)]
                tx = wallet_online.mktx(outputs=outputs, password=None, fee=5000)
                tx.set_rbf(True)
                tx.locktime = 1325340
       t@@ -1452,7 +1589,10 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
                self.assertFalse(tx.is_complete())
                self.assertFalse(tx.is_segwit())
                self.assertEqual(1, len(tx.inputs()))
       -        tx_copy = Transaction(tx.serialize())
       +        partial_tx = tx.serialize_as_bytes().hex()
       +        self.assertEqual("70736274ff010074010000000162878508bdcd372fc4c33ce6145cd1fb1dcc5314d4930ceb6957e7f6c54b57980200000000fdffffff02a0252600000000001600140bf6c540d0218c99511f7c62a49784c88b1870b8585d7200000000001976a9149b308d0b3efd4e3469441bc83c3521afde4072b988ac1c391400000100fd4c0d01000000000116e9c9dac2651672316aab3b9553257b6942c5f762c5d795776d9cfa504f183c000000000000fdffffff8085019852fada9da84b58dcf753d292dde314a19f5a5527f6588fa2566142130000000000fdffffffa4154a48db20ce538b28722a89c6b578bd5b5d60d6d7b52323976339e39405230000000000fdffffff0b5ef43f843a96364aebd708e25ea1bdcf2c7df7d0d995560b8b1be5f357b64f0100000000fdffffffd41dfe1199c76fdb3f20e9947ea31136d032d9da48c5e45d85c8f440e2351a510100000000fdffffff5bd015d17e4a1837b01c24ebb4a6b394e3da96a85442bd7dc6abddfbf16f20510000000000fdffffff13a3e7f80b1bd46e38f2abc9e2f335c18a4b0af1778133c7f1c3caae9504345c0200000000fdffffffdf4fc1ab21bca69d18544ddb10a913cd952dbc730ab3d236dd9471445ff405680100000000fdffffffe0424d78a30d5e60ac6b26e2274d7d6e7c6b78fe0b49bdc3ac4dd2147c9535750100000000fdffffff7ab6dd6b3c0d44b0fef0fdc9ab0ad6eee23eef799eee29c005d52bc4461998760000000000fdffffff48a77e5053a21acdf4f235ce00c82c9bc1704700f54d217f6a30704711b9737d0000000000fdffffff86918b39c1d9bb6f34d9b082182f73cedd15504331164dc2b186e95c568ccb870000000000fdffffff15a847356cbb44be67f345965bb3f2589e2fec1c9a0ada21fd28225dcc602e8f0100000000fdffffff9a2875297f81dfd3b77426d63f621db350c270cc28c634ad86b9969ee33ac6960000000000fdffffffd6eeb1d1833e00967083d1ab86fa5a2e44355bd613d9277135240fe6f60148a20100000000fdffffffd8a6e5a9b68a65ff88220ca33e36faf6f826ae8c5c8a13fe818a5e63828b68a40100000000fdffffff73aab8471f82092e45ed1b1afeffdb49ea1ec74ce4853f971812f6a72a7e85aa0000000000fdffffffacd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba0000000000fdffffff1eddd5e13bef1aba1ff151762b5860837daa9b39db1eae8ea8227c81a5a1c8ba0000000000fdffffff67a096ff7c343d39e96929798097f6d7a61156bbdb905fbe534ba36f273271d40100000000fdffffff109a671eb7daf6dcd07c0ceff99f2de65864ab36d64fb3a890bab951569adeee0100000000fdffffff4f1bdc64da8056d08f79db7f5348d1de55946e57aa7c8279499c703889b6e0fd0200000000fdffffff042f280000000000001600149c756aa33f4f89418b33872a973274b5445c727b80969800000000001600146c540c1c9f546004539f45318b8d9f4d7b4857ef80969800000000001976a91422a6daa4a7b695c8a2dd104d47c5dc73d655c96f88ac809698000000000017a914a6885437e0762013facbda93894202a0fe86e35f8702473044022075ef5f04d7a63347064938e15a0c74277a79e5c9d32a26e39e8a517a44d565cc022015246790fb5b29c9bf3eded1b95699b1635bcfc6d521886fddf1135ba1b988ec012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe02473044022061aa9b0d9649ffd7259bc54b35f678565dbbe11507d348dd8885522eaf1fa70c02202cc79de09e8e63e8d57fde6ef66c079ddac4d9828e1936a9db833d4c142615c3012103a8f58fc1f5625f18293403104874f2d38c9279f777e512570e4199c7d292b81b0247304402207744dc1ab0bf77c081b58540c4321d090c0a24a32742a361aa55ad86f0c7c24e02201a9b0dd78b63b495ab5a0b5b161c54cb085d70683c90e188bb4dc2e41e142f6601210361fb354f8259abfcbfbdda36b7cb4c3b05a3ca3d68dd391fd8376e920d93870d0247304402204803e423c321acc6c12cb0ebf196d2906842fdfed6de977cc78277052ee5f15002200634670c1dc25e6b1787a65d3e09c8e6bb0340238d90b9d98887e8fd53944e080121031104c60d027123bf8676bcaefaa66c001a0d3d379dc4a9492a567a9e1004452d02473044022050e4b5348d30011a22b6ae8b43921d29249d88ea71b1fbaa2d9c22dfdef58b7002201c5d5e143aa8835454f61b0742226ebf8cd466bcc2cdcb1f77b92e473d3b13190121030496b9d49aa8efece4f619876c60a77d2c0dc846390ecdc5d9acbfa1bb3128760247304402204d6a9b986e1a0e3473e8aef84b3eb7052442a76dfd7631e35377f141496a55490220131ab342853c01e31f111436f8461e28bc95883b871ca0e01b5f57146e79d7bb012103262ffbc88e25296056a3c65c880e3686297e07f360e6b80f1219d65b0900e84e02483045022100c8ffacf92efa1dddef7e858a241af7a80adcc2489bcc325195970733b1f35fac022076f40c26023a228041a9665c5290b9918d06f03b716e4d8f6d47e79121c7eb37012102d9ba7e02d7cd7dd24302f823b3114c99da21549c663f72440dc87e8ba412120902483045022100b55545d84e43d001bbc10a981f184e7d3b98a7ed6689863716cab053b3655a2f0220537eb76a695fbe86bf020b4b6f7ae93b506d778bbd0885f0a61067616a2c8bce0121034a57f2fa2c32c9246691f6a922fb1ebdf1468792bae7eff253a99fc9f2a5023902483045022100f1d4408463dbfe257f9f778d5e9c8cdb97c8b1d395dbd2e180bc08cad306492c022002a024e19e1a406eaa24467f033659de09ab58822987281e28bb6359288337bd012103e91daa18d924eea62011ce596e15b6d683975cf724ea5bf69a8e2022c26fc12f0247304402204f1e12b923872f396e5e1a3aa94b0b2e86b4ce448f4349a017631db26d7dff8a022069899a05de2ad2bbd8e0202c56ab1025a7db9a4998eea70744e3c367d2a7eb71012103b0eee86792dbef1d4a49bc4ea32d197c8c15d27e6e0c5c33e58e409e26d4a39a0247304402201787dacdb92e0df6ad90226649f0e8321287d0bd8fddc536a297dd19b5fc103e022001fe89300a76e5b46d0e3f7e39e0ee26cc83b71d59a2a5da1dd7b13350cd0c07012103afb1e43d7ec6b7999ef0f1093069e68fe1dfe5d73fc6cfb4f7a5022f7098758c02483045022100acc1212bba0fe4fcc6c3ae5cf8e25f221f140c8444d3c08dfc53a93630ac25da02203f12982847244bd9421ef340293f3a38d2ab5d028af60769e46fcc7d81312e7e012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024830450221009c04934102402949484b21899271c3991c007b783b8efc85a3c3d24641ac7c24022006fb1895ce969d08a2cb29413e1a85427c7e85426f7a185108ca44b5a0328cb301210360248db4c7d7f76fe231998d2967104fee04df8d8da34f10101cc5523e82648c02483045022100b11fe61b393fa5dbe18ab98f65c249345b429b13f69ee2d1b1335725b24a0e73022010960cdc5565cbc81885c8ed95142435d3c202dfa5a3dc5f50f3914c106335ce0121029c878610c34c21381cda12f6f36ab88bf60f5f496c1b82c357b8ac448713e7b50247304402200ca080db069c15bbf98e1d4dff68d0aea51227ff5d17a8cf67ceae464c22bbb0022051e7331c0918cbb71bb2cef29ca62411454508a16180b0fb5df94248890840df0121028f0be0cde43ff047edbda42c91c37152449d69789eb812bb2e148e4f22472c0f0247304402201fefe258938a2c481d5a745ef3aa8d9f8124bbe7f1f8c693e2ddce4ddc9a927c02204049e0060889ede8fda975edf896c03782d71ba53feb51b04f5ae5897d7431dc012103946730b480f52a43218a9edce240e8b234790e21df5e96482703d81c3c19d3f1024730440220126a6a56dbe69af78d156626fc9cf41d6aac0c07b8b5f0f8491f68db5e89cb5002207ee6ed6f2f41da256f3c1e79679a3de6cf34cc08b940b82be14aefe7da031a6b012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024730440220363204a1586d7f13c148295122cbf9ec7939685e3cadab81d6d9e921436d21b7022044626b8c2bd4aa7c167d74bc4e9eb9d0744e29ce0ad906d78e10d6d854f23d170121037fb9c51716739bb4c146857fab5a783372f72a65987d61f3b58c74360f4328dd0247304402207925a4c2a3a6b76e10558717ee28fcb8c6fde161b9dc6382239af9f372ace99902204a58e31ce0b4a4804a42d2224331289311ded2748062c92c8aca769e81417a4c012102e18a8c235b48e41ef98265a8e07fa005d2602b96d585a61ad67168d74e7391cb02483045022100bbfe060479174a8d846b5a897526003eb2220ba307a5fee6e1e8de3e4e8b38fd02206723857301d447f67ac98a5a5c2b80ef6820e98fae213db1720f93d91161803b01210386728e2ac3ecee15f58d0505ee26f86a68f08c702941ffaf2fb7213e5026aea10247304402203a2613ae68f697eb02b5b7d18e3c4236966dac2b3a760e3021197d76e9ad4239022046f9067d3df650fcabbdfd250308c64f90757dec86f0b08813c979a42d06a6ec012102a1d7ee1cb4dc502f899aaafae0a2eb6cbf80d9a1073ae60ddcaabc3b1d1f15df02483045022100ab1bea2cc5388428fd126c7801550208701e21564bd4bd00cfd4407cfafc1acd0220508ee587f080f3c80a5c0b2175b58edd84b755e659e2135b3152044d75ebc4b501210236dd1b7f27a296447d0eb3750e1bdb2d53af50b31a72a45511dc1ec3fe7a684a19391400220602ab053d10eda769fab03ab52ee4f1692730288751369643290a8506e31d1e80f00c233d2ae40000000002000000000022020327295144ffff9943356c2d6625f5e2d6411bab77fd56dce571fda6234324e3d90c233d2ae4010000000000000000",
       +                         partial_tx)
       +        tx_copy = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners
                self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0])))
        
                self.assertEqual(tx.txid(), tx_copy.txid())
       t@@ -1486,7 +1626,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
                wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
        
                # create unsigned tx
       -        outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)]
       +        outputs = [PartialTxOutput.from_address_and_value('tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)]
                tx = wallet_online.mktx(outputs=outputs, password=None, fee=5000)
                tx.set_rbf(True)
                tx.locktime = 1325341
       t@@ -1495,7 +1635,10 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
                self.assertFalse(tx.is_complete())
                self.assertTrue(tx.is_segwit())
                self.assertEqual(1, len(tx.inputs()))
       -        tx_copy = Transaction(tx.serialize())
       +        partial_tx = tx.serialize_as_bytes().hex()
       +        self.assertEqual("70736274ff010072010000000162878508bdcd372fc4c33ce6145cd1fb1dcc5314d4930ceb6957e7f6c54b57980300000000fdffffff02a0252600000000001600140bf6c540d0218c99511f7c62a49784c88b1870b8585d72000000000017a914191e7373ae7b4829532220e8f281f4581ed52638871d39140000010120809698000000000017a914a6885437e0762013facbda93894202a0fe86e35f870104160014105db4dae7e5b8dd4dda7b7d3b1e588c9bf26f192206030dddd5d3c31738ca2d8b25391f648af6a8b08e6961e8f56d4173d03e9db82d3e0c105d19280000000002000000000001001600144f485261505d5cbd33dce02a723776c99240c28722020211ab9359cc49c95b3b9a87ee95fd4edf0cecce862f9e9f86ff63e10880baaba80c105d1928010000000000000000",
       +                         partial_tx)
       +        tx_copy = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners
                self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0])))
        
                self.assertEqual('3f0d188519237478258ad2bf881643618635d11c2bb95512e830fcf2eda3c522', tx_copy.txid())
       t@@ -1530,7 +1673,54 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
                wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
        
                # create unsigned tx
       -        outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)]
       +        outputs = [PartialTxOutput.from_address_and_value('tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)]
       +        tx = wallet_online.mktx(outputs=outputs, password=None, fee=5000)
       +        tx.set_rbf(True)
       +        tx.locktime = 1325341
       +        tx.version = 1
       +
       +        self.assertFalse(tx.is_complete())
       +        self.assertTrue(tx.is_segwit())
       +        self.assertEqual(1, len(tx.inputs()))
       +        partial_tx = tx.serialize_as_bytes().hex()
       +        self.assertEqual("70736274ff010071010000000162878508bdcd372fc4c33ce6145cd1fb1dcc5314d4930ceb6957e7f6c54b57980100000000fdffffff02a0252600000000001600140bf6c540d0218c99511f7c62a49784c88b1870b8585d7200000000001600145543fe1a1364b806b27a5c9dc92ac9bbf0d42aa31d3914000001011f80969800000000001600146c540c1c9f546004539f45318b8d9f4d7b4857ef220603fd88f32a81e812af0187677fc0e7ac9b7fb63ca68c2d98c2afbcf99aa311ac060cdf758ae500000000020000000000220202ac05f54ef082ac98302d57d532e728653565bd55f46fcf03cacbddb168fd6c760cdf758ae5010000000000000000",
       +                         partial_tx)
       +        tx_copy = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners
       +        self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0])))
       +
       +        self.assertEqual('ee76c0c6da87f0eb5ab4d1ae05d3942512dcd3c4c42518f9d3619e74400cfc1f', tx_copy.txid())
       +        self.assertEqual(tx.txid(), tx_copy.txid())
       +
       +        # sign tx
       +        tx = wallet_offline.sign_transaction(tx_copy, password=None)
       +        self.assertTrue(tx.is_complete())
       +        self.assertTrue(tx.is_segwit())
       +        self.assertEqual('ee76c0c6da87f0eb5ab4d1ae05d3942512dcd3c4c42518f9d3619e74400cfc1f', tx.txid())
       +        self.assertEqual('729c2e40a2fccd6b731407c01ed304119c1ac329bdf9baae5b642d916c5f3272', tx.wtxid())
       +
       +    @needs_test_with_all_ecc_implementations
       +    @mock.patch.object(storage.WalletStorage, '_write')
       +    def test_offline_signing_beyond_gap_limit(self, mock_write):
       +        wallet_offline = WalletIntegrityHelper.create_standard_wallet(
       +            # bip39: "qwe", der: m/84'/1'/0'
       +            keystore.from_xprv('vprv9K9hbuA23Bidgj1KRSHUZMa59jJLeZBpXPVn4RP7sBLArNhZxJjw4AX7aQmVTErDt4YFC11ptMLjbwxgrsH8GLQ1cx77KggWeVPeDBjr9xM'),
       +            gap_limit=1,  # gap limit of offline wallet intentionally set too low
       +            config=self.config
       +        )
       +        wallet_online = WalletIntegrityHelper.create_standard_wallet(
       +            keystore.from_xpub('vpub5Y941QgusZGvuD5nXTpUvVWohm8q41uftcRNronjRWs9jB2iVr4BbxqbRfAoQjWHgJtDCQEXChgfsPbEuBnidtkFztZSD3zDKTrtwXa2LCa'),
       +            gap_limit=4,
       +            config=self.config
       +        )
       +
       +        # bootstrap wallet_online
       +        funding_tx = Transaction('01000000000116e9c9dac2651672316aab3b9553257b6942c5f762c5d795776d9cfa504f183c000000000000fdffffff8085019852fada9da84b58dcf753d292dde314a19f5a5527f6588fa2566142130000000000fdffffffa4154a48db20ce538b28722a89c6b578bd5b5d60d6d7b52323976339e39405230000000000fdffffff0b5ef43f843a96364aebd708e25ea1bdcf2c7df7d0d995560b8b1be5f357b64f0100000000fdffffffd41dfe1199c76fdb3f20e9947ea31136d032d9da48c5e45d85c8f440e2351a510100000000fdffffff5bd015d17e4a1837b01c24ebb4a6b394e3da96a85442bd7dc6abddfbf16f20510000000000fdffffff13a3e7f80b1bd46e38f2abc9e2f335c18a4b0af1778133c7f1c3caae9504345c0200000000fdffffffdf4fc1ab21bca69d18544ddb10a913cd952dbc730ab3d236dd9471445ff405680100000000fdffffffe0424d78a30d5e60ac6b26e2274d7d6e7c6b78fe0b49bdc3ac4dd2147c9535750100000000fdffffff7ab6dd6b3c0d44b0fef0fdc9ab0ad6eee23eef799eee29c005d52bc4461998760000000000fdffffff48a77e5053a21acdf4f235ce00c82c9bc1704700f54d217f6a30704711b9737d0000000000fdffffff86918b39c1d9bb6f34d9b082182f73cedd15504331164dc2b186e95c568ccb870000000000fdffffff15a847356cbb44be67f345965bb3f2589e2fec1c9a0ada21fd28225dcc602e8f0100000000fdffffff9a2875297f81dfd3b77426d63f621db350c270cc28c634ad86b9969ee33ac6960000000000fdffffffd6eeb1d1833e00967083d1ab86fa5a2e44355bd613d9277135240fe6f60148a20100000000fdffffffd8a6e5a9b68a65ff88220ca33e36faf6f826ae8c5c8a13fe818a5e63828b68a40100000000fdffffff73aab8471f82092e45ed1b1afeffdb49ea1ec74ce4853f971812f6a72a7e85aa0000000000fdffffffacd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba0000000000fdffffff1eddd5e13bef1aba1ff151762b5860837daa9b39db1eae8ea8227c81a5a1c8ba0000000000fdffffff67a096ff7c343d39e96929798097f6d7a61156bbdb905fbe534ba36f273271d40100000000fdffffff109a671eb7daf6dcd07c0ceff99f2de65864ab36d64fb3a890bab951569adeee0100000000fdffffff4f1bdc64da8056d08f79db7f5348d1de55946e57aa7c8279499c703889b6e0fd0200000000fdffffff042f280000000000001600149c756aa33f4f89418b33872a973274b5445c727b80969800000000001600146c540c1c9f546004539f45318b8d9f4d7b4857ef80969800000000001976a91422a6daa4a7b695c8a2dd104d47c5dc73d655c96f88ac809698000000000017a914a6885437e0762013facbda93894202a0fe86e35f8702473044022075ef5f04d7a63347064938e15a0c74277a79e5c9d32a26e39e8a517a44d565cc022015246790fb5b29c9bf3eded1b95699b1635bcfc6d521886fddf1135ba1b988ec012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe02473044022061aa9b0d9649ffd7259bc54b35f678565dbbe11507d348dd8885522eaf1fa70c02202cc79de09e8e63e8d57fde6ef66c079ddac4d9828e1936a9db833d4c142615c3012103a8f58fc1f5625f18293403104874f2d38c9279f777e512570e4199c7d292b81b0247304402207744dc1ab0bf77c081b58540c4321d090c0a24a32742a361aa55ad86f0c7c24e02201a9b0dd78b63b495ab5a0b5b161c54cb085d70683c90e188bb4dc2e41e142f6601210361fb354f8259abfcbfbdda36b7cb4c3b05a3ca3d68dd391fd8376e920d93870d0247304402204803e423c321acc6c12cb0ebf196d2906842fdfed6de977cc78277052ee5f15002200634670c1dc25e6b1787a65d3e09c8e6bb0340238d90b9d98887e8fd53944e080121031104c60d027123bf8676bcaefaa66c001a0d3d379dc4a9492a567a9e1004452d02473044022050e4b5348d30011a22b6ae8b43921d29249d88ea71b1fbaa2d9c22dfdef58b7002201c5d5e143aa8835454f61b0742226ebf8cd466bcc2cdcb1f77b92e473d3b13190121030496b9d49aa8efece4f619876c60a77d2c0dc846390ecdc5d9acbfa1bb3128760247304402204d6a9b986e1a0e3473e8aef84b3eb7052442a76dfd7631e35377f141496a55490220131ab342853c01e31f111436f8461e28bc95883b871ca0e01b5f57146e79d7bb012103262ffbc88e25296056a3c65c880e3686297e07f360e6b80f1219d65b0900e84e02483045022100c8ffacf92efa1dddef7e858a241af7a80adcc2489bcc325195970733b1f35fac022076f40c26023a228041a9665c5290b9918d06f03b716e4d8f6d47e79121c7eb37012102d9ba7e02d7cd7dd24302f823b3114c99da21549c663f72440dc87e8ba412120902483045022100b55545d84e43d001bbc10a981f184e7d3b98a7ed6689863716cab053b3655a2f0220537eb76a695fbe86bf020b4b6f7ae93b506d778bbd0885f0a61067616a2c8bce0121034a57f2fa2c32c9246691f6a922fb1ebdf1468792bae7eff253a99fc9f2a5023902483045022100f1d4408463dbfe257f9f778d5e9c8cdb97c8b1d395dbd2e180bc08cad306492c022002a024e19e1a406eaa24467f033659de09ab58822987281e28bb6359288337bd012103e91daa18d924eea62011ce596e15b6d683975cf724ea5bf69a8e2022c26fc12f0247304402204f1e12b923872f396e5e1a3aa94b0b2e86b4ce448f4349a017631db26d7dff8a022069899a05de2ad2bbd8e0202c56ab1025a7db9a4998eea70744e3c367d2a7eb71012103b0eee86792dbef1d4a49bc4ea32d197c8c15d27e6e0c5c33e58e409e26d4a39a0247304402201787dacdb92e0df6ad90226649f0e8321287d0bd8fddc536a297dd19b5fc103e022001fe89300a76e5b46d0e3f7e39e0ee26cc83b71d59a2a5da1dd7b13350cd0c07012103afb1e43d7ec6b7999ef0f1093069e68fe1dfe5d73fc6cfb4f7a5022f7098758c02483045022100acc1212bba0fe4fcc6c3ae5cf8e25f221f140c8444d3c08dfc53a93630ac25da02203f12982847244bd9421ef340293f3a38d2ab5d028af60769e46fcc7d81312e7e012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024830450221009c04934102402949484b21899271c3991c007b783b8efc85a3c3d24641ac7c24022006fb1895ce969d08a2cb29413e1a85427c7e85426f7a185108ca44b5a0328cb301210360248db4c7d7f76fe231998d2967104fee04df8d8da34f10101cc5523e82648c02483045022100b11fe61b393fa5dbe18ab98f65c249345b429b13f69ee2d1b1335725b24a0e73022010960cdc5565cbc81885c8ed95142435d3c202dfa5a3dc5f50f3914c106335ce0121029c878610c34c21381cda12f6f36ab88bf60f5f496c1b82c357b8ac448713e7b50247304402200ca080db069c15bbf98e1d4dff68d0aea51227ff5d17a8cf67ceae464c22bbb0022051e7331c0918cbb71bb2cef29ca62411454508a16180b0fb5df94248890840df0121028f0be0cde43ff047edbda42c91c37152449d69789eb812bb2e148e4f22472c0f0247304402201fefe258938a2c481d5a745ef3aa8d9f8124bbe7f1f8c693e2ddce4ddc9a927c02204049e0060889ede8fda975edf896c03782d71ba53feb51b04f5ae5897d7431dc012103946730b480f52a43218a9edce240e8b234790e21df5e96482703d81c3c19d3f1024730440220126a6a56dbe69af78d156626fc9cf41d6aac0c07b8b5f0f8491f68db5e89cb5002207ee6ed6f2f41da256f3c1e79679a3de6cf34cc08b940b82be14aefe7da031a6b012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024730440220363204a1586d7f13c148295122cbf9ec7939685e3cadab81d6d9e921436d21b7022044626b8c2bd4aa7c167d74bc4e9eb9d0744e29ce0ad906d78e10d6d854f23d170121037fb9c51716739bb4c146857fab5a783372f72a65987d61f3b58c74360f4328dd0247304402207925a4c2a3a6b76e10558717ee28fcb8c6fde161b9dc6382239af9f372ace99902204a58e31ce0b4a4804a42d2224331289311ded2748062c92c8aca769e81417a4c012102e18a8c235b48e41ef98265a8e07fa005d2602b96d585a61ad67168d74e7391cb02483045022100bbfe060479174a8d846b5a897526003eb2220ba307a5fee6e1e8de3e4e8b38fd02206723857301d447f67ac98a5a5c2b80ef6820e98fae213db1720f93d91161803b01210386728e2ac3ecee15f58d0505ee26f86a68f08c702941ffaf2fb7213e5026aea10247304402203a2613ae68f697eb02b5b7d18e3c4236966dac2b3a760e3021197d76e9ad4239022046f9067d3df650fcabbdfd250308c64f90757dec86f0b08813c979a42d06a6ec012102a1d7ee1cb4dc502f899aaafae0a2eb6cbf80d9a1073ae60ddcaabc3b1d1f15df02483045022100ab1bea2cc5388428fd126c7801550208701e21564bd4bd00cfd4407cfafc1acd0220508ee587f080f3c80a5c0b2175b58edd84b755e659e2135b3152044d75ebc4b501210236dd1b7f27a296447d0eb3750e1bdb2d53af50b31a72a45511dc1ec3fe7a684a19391400')
       +        funding_txid = funding_tx.txid()
       +        self.assertEqual('98574bc5f6e75769eb0c93d41453cc1dfbd15c14e63cc3c42f37cdbd08858762', funding_txid)
       +        wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
       +
       +        # create unsigned tx
       +        outputs = [PartialTxOutput.from_address_and_value('tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)]
                tx = wallet_online.mktx(outputs=outputs, password=None, fee=5000)
                tx.set_rbf(True)
                tx.locktime = 1325341
       t@@ -1539,7 +1729,10 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
                self.assertFalse(tx.is_complete())
                self.assertTrue(tx.is_segwit())
                self.assertEqual(1, len(tx.inputs()))
       -        tx_copy = Transaction(tx.serialize())
       +        partial_tx = tx.serialize_as_bytes().hex()
       +        self.assertEqual("70736274ff010071010000000162878508bdcd372fc4c33ce6145cd1fb1dcc5314d4930ceb6957e7f6c54b57980100000000fdffffff02a0252600000000001600140bf6c540d0218c99511f7c62a49784c88b1870b8585d7200000000001600145543fe1a1364b806b27a5c9dc92ac9bbf0d42aa31d3914000001011f80969800000000001600146c540c1c9f546004539f45318b8d9f4d7b4857ef220603fd88f32a81e812af0187677fc0e7ac9b7fb63ca68c2d98c2afbcf99aa311ac060cdf758ae500000000020000000000220202ac05f54ef082ac98302d57d532e728653565bd55f46fcf03cacbddb168fd6c760cdf758ae5010000000000000000",
       +                         partial_tx)
       +        tx_copy = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners
                self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0])))
        
                self.assertEqual('ee76c0c6da87f0eb5ab4d1ae05d3942512dcd3c4c42518f9d3619e74400cfc1f', tx_copy.txid())
       t@@ -1567,7 +1760,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
                wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
        
                # create unsigned tx
       -        outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)]
       +        outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)]
                tx = wallet_online.mktx(outputs=outputs, password=None, fee=5000)
                tx.set_rbf(True)
                tx.locktime = 1325340
       t@@ -1575,9 +1768,13 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
        
                self.assertFalse(tx.is_complete())
                self.assertEqual(1, len(tx.inputs()))
       -        tx_copy = Transaction(tx.serialize())
       +        partial_tx = tx.serialize_as_bytes().hex()
       +        self.assertEqual("70736274ff0100740100000001626bbbb7a4ad82dbf7f6bd64ac3f40d0e2695b606d7953f2802b9ea426ea080a0100000000fdffffff02a025260000000000160014e5bddbfee3883729b48fe3385216e64e6035f6eb585d7200000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac1c391400000100fd200101000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400000000",
       +                         partial_tx)
       +        tx_copy = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners
                self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0])))
        
       +        self.assertEqual(None, tx_copy.txid())  # not segwit
                self.assertEqual(tx.txid(), tx_copy.txid())
        
                # sign tx
       t@@ -1602,7 +1799,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
                wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
        
                # create unsigned tx
       -        outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)]
       +        outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)]
                tx = wallet_online.mktx(outputs=outputs, password=None, fee=5000)
                tx.set_rbf(True)
                tx.locktime = 1325340
       t@@ -1610,9 +1807,13 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
        
                self.assertFalse(tx.is_complete())
                self.assertEqual(1, len(tx.inputs()))
       -        tx_copy = Transaction(tx.serialize())
       +        partial_tx = tx.serialize_as_bytes().hex()
       +        self.assertEqual("70736274ff0100720100000001626bbbb7a4ad82dbf7f6bd64ac3f40d0e2695b606d7953f2802b9ea426ea080a0200000000fdffffff02a025260000000000160014e5bddbfee3883729b48fe3385216e64e6035f6eb585d72000000000017a914b808938a8007bc54509cd946944c479c0fa6554f871c391400000100fd200101000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400000000",
       +                         partial_tx)
       +        tx_copy = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners
                self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0])))
        
       +        self.assertEqual(None, tx_copy.txid())  # redeem script not available
                self.assertEqual(tx.txid(), tx_copy.txid())
        
                # sign tx
       t@@ -1637,7 +1838,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
                wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
        
                # create unsigned tx
       -        outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)]
       +        outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)]
                tx = wallet_online.mktx(outputs=outputs, password=None, fee=5000)
                tx.set_rbf(True)
                tx.locktime = 1325340
       t@@ -1645,9 +1846,13 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
        
                self.assertFalse(tx.is_complete())
                self.assertEqual(1, len(tx.inputs()))
       -        tx_copy = Transaction(tx.serialize())
       +        partial_tx = tx.serialize_as_bytes().hex()
       +        self.assertEqual("70736274ff0100710100000001626bbbb7a4ad82dbf7f6bd64ac3f40d0e2695b606d7953f2802b9ea426ea080a0000000000fdffffff02a025260000000000160014e5bddbfee3883729b48fe3385216e64e6035f6eb585d720000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a1c3914000001011f8096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a000000",
       +                         partial_tx)
       +        tx_copy = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners
                self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0])))
        
       +        self.assertEqual('f8039bd85279f2b5698f15d47f2e338d067d09af391bd8a19467aa94d03f280c', tx_copy.txid())
                self.assertEqual(tx.txid(), tx_copy.txid())
        
                # sign tx
       t@@ -1676,7 +1881,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
                wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
        
                # create unsigned tx
       -        outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)]
       +        outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)]
                tx = wallet_online.mktx(outputs=outputs, password=None, fee=5000)
                tx.set_rbf(True)
                tx.locktime = 1325340
       t@@ -1684,9 +1889,13 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
        
                self.assertFalse(tx.is_complete())
                self.assertEqual(1, len(tx.inputs()))
       -        tx_copy = Transaction(tx.serialize())
       +        partial_tx = tx.serialize_as_bytes().hex()
       +        self.assertEqual("70736274ff0100740100000001626bbbb7a4ad82dbf7f6bd64ac3f40d0e2695b606d7953f2802b9ea426ea080a0100000000fdffffff02a025260000000000160014e5bddbfee3883729b48fe3385216e64e6035f6eb585d7200000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac1c391400000100fd200101000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400000000",
       +                         partial_tx)
       +        tx_copy = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners
                self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0])))
        
       +        self.assertEqual(None, tx_copy.txid())  # not segwit
                self.assertEqual(tx.txid(), tx_copy.txid())
        
                # sign tx
       t@@ -1715,7 +1924,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
                wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
        
                # create unsigned tx
       -        outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)]
       +        outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)]
                tx = wallet_online.mktx(outputs=outputs, password=None, fee=5000)
                tx.set_rbf(True)
                tx.locktime = 1325340
       t@@ -1723,9 +1932,13 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
        
                self.assertFalse(tx.is_complete())
                self.assertEqual(1, len(tx.inputs()))
       -        tx_copy = Transaction(tx.serialize())
       +        partial_tx = tx.serialize_as_bytes().hex()
       +        self.assertEqual("70736274ff0100720100000001626bbbb7a4ad82dbf7f6bd64ac3f40d0e2695b606d7953f2802b9ea426ea080a0200000000fdffffff02a025260000000000160014e5bddbfee3883729b48fe3385216e64e6035f6eb585d72000000000017a914b808938a8007bc54509cd946944c479c0fa6554f871c391400000100fd200101000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400000000",
       +                         partial_tx)
       +        tx_copy = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners
                self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0])))
        
       +        self.assertEqual(None, tx_copy.txid())  # redeem script not available
                self.assertEqual(tx.txid(), tx_copy.txid())
        
                # sign tx
       t@@ -1754,7 +1967,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
                wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
        
                # create unsigned tx
       -        outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)]
       +        outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)]
                tx = wallet_online.mktx(outputs=outputs, password=None, fee=5000)
                tx.set_rbf(True)
                tx.locktime = 1325340
       t@@ -1762,9 +1975,13 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
        
                self.assertFalse(tx.is_complete())
                self.assertEqual(1, len(tx.inputs()))
       -        tx_copy = Transaction(tx.serialize())
       +        partial_tx = tx.serialize_as_bytes().hex()
       +        self.assertEqual("70736274ff0100710100000001626bbbb7a4ad82dbf7f6bd64ac3f40d0e2695b606d7953f2802b9ea426ea080a0000000000fdffffff02a025260000000000160014e5bddbfee3883729b48fe3385216e64e6035f6eb585d720000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a1c3914000001011f8096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a000000",
       +                         partial_tx)
       +        tx_copy = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners
                self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0])))
        
       +        self.assertEqual('f8039bd85279f2b5698f15d47f2e338d067d09af391bd8a19467aa94d03f280c', tx_copy.txid())
                self.assertEqual(tx.txid(), tx_copy.txid())
        
                # sign tx
       t@@ -1806,7 +2023,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
                wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
        
                # create unsigned tx
       -        outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2MuCQQHJNnrXzQzuqfUCfAwAjPqpyEHbgue', 2500000)]
       +        outputs = [PartialTxOutput.from_address_and_value('2MuCQQHJNnrXzQzuqfUCfAwAjPqpyEHbgue', 2500000)]
                tx = wallet_online.mktx(outputs=outputs, password=None, fee=5000)
                tx.set_rbf(True)
                tx.locktime = 1325503
       t@@ -1814,20 +2031,27 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
        
                self.assertFalse(tx.is_complete())
                self.assertEqual(1, len(tx.inputs()))
       -        tx_copy = Transaction(tx.serialize())
       +        partial_tx = tx.serialize_as_bytes().hex()
       +        self.assertEqual("70736274ff010073010000000132352f6459e847e65e56aa05cbd7b9ee67be90b40d8f92f6f11e9bfaa11399c50000000000fdffffff02a02526000000000017a9141567b2578f300fa618ef0033611fd67087aff6d187585d72000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e4887bf391400000100f7010000000001016207d958dc46508d706e4cd7d3bc46c5c2b02160e2578e5fad2efafc3927050301000000171600147a4fc8cdc1c2cf7abbcd88ef6d880e59269797acfdffffff02809698000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e48870d0916020000000017a914703f83ef20f3a52d908475dcad00c5144164d5a2870247304402203b1a5cb48cadeee14fa6c7bbf2bc581ca63104762ec5c37c703df778884cc5b702203233fa53a2a0bfbd85617c636e415da72214e359282cce409019319d031766c50121021112c01a48cc7ea13cba70493c6bffebb3e805df10ff4611d2bf559d26e25c04bf391400000000",
       +                         partial_tx)
       +        tx_copy = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners
                self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0])))
        
       +        self.assertEqual(None, tx_copy.txid())  # not segwit
                self.assertEqual(tx.txid(), tx_copy.txid())
        
                # sign tx - first
                tx = wallet_offline1.sign_transaction(tx_copy, password=None)
                self.assertFalse(tx.is_complete())
       -        tx = Transaction(tx.serialize())
       +        partial_tx = tx.serialize_as_bytes().hex()
       +        self.assertEqual("70736274ff010073010000000132352f6459e847e65e56aa05cbd7b9ee67be90b40d8f92f6f11e9bfaa11399c50000000000fdffffff02a02526000000000017a9141567b2578f300fa618ef0033611fd67087aff6d187585d72000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e4887bf391400000100f7010000000001016207d958dc46508d706e4cd7d3bc46c5c2b02160e2578e5fad2efafc3927050301000000171600147a4fc8cdc1c2cf7abbcd88ef6d880e59269797acfdffffff02809698000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e48870d0916020000000017a914703f83ef20f3a52d908475dcad00c5144164d5a2870247304402203b1a5cb48cadeee14fa6c7bbf2bc581ca63104762ec5c37c703df778884cc5b702203233fa53a2a0bfbd85617c636e415da72214e359282cce409019319d031766c50121021112c01a48cc7ea13cba70493c6bffebb3e805df10ff4611d2bf559d26e25c04bf391400220202afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f28483045022100cfe41e783629a2ad0b1f17cd2dbd69db05763fa7a22691131fa321ba3140d7cb02203fbda2ccc6212315464cd814d4e909b4f80a2361e3af0f9deda06478f91a0f3901010469522102afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f2821030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf2103e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce53ae220602afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f280c0036e9ac00000000000000002206030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf0c48adc7a00000000000000000220603e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce0cdb69242700000000000000000000010069522102afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f2821030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf2103e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce53ae220202afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f280c0036e9ac00000000000000002202030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf0c48adc7a00000000000000000220203e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce0cdb692427000000000000000000",
       +                         partial_tx)
       +        tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners
        
                # sign tx - second
                tx = wallet_offline2.sign_transaction(tx, password=None)
                self.assertTrue(tx.is_complete())
       -        tx = Transaction(tx.serialize())
       +        tx = tx_from_any(tx.serialize())
        
                self.assertEqual('010000000132352f6459e847e65e56aa05cbd7b9ee67be90b40d8f92f6f11e9bfaa11399c500000000fdfe0000483045022100cfe41e783629a2ad0b1f17cd2dbd69db05763fa7a22691131fa321ba3140d7cb02203fbda2ccc6212315464cd814d4e909b4f80a2361e3af0f9deda06478f91a0f3901483045022100b84fd63e957f2409558f63962fc91ba58334efde8b88ff53ca71da3d0fe7219702206001c6caeb30e18a7525fc72de0003e12646bf815b12fb132c1aadd6ffa1989c014c69522102afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f2821030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf2103e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce53aefdffffff02a02526000000000017a9141567b2578f300fa618ef0033611fd67087aff6d187585d72000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e4887bf391400',
                                 str(tx))
       t@@ -1866,7 +2090,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
                wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
        
                # create unsigned tx
       -        outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2N8CtJRwxb2GCaiWWdSHLZHHLoZy53CCyxf', 2500000)]
       +        outputs = [PartialTxOutput.from_address_and_value('2N8CtJRwxb2GCaiWWdSHLZHHLoZy53CCyxf', 2500000)]
                tx = wallet_online.mktx(outputs=outputs, password=None, fee=5000)
                tx.set_rbf(True)
                tx.locktime = 1325504
       t@@ -1874,21 +2098,31 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
        
                self.assertFalse(tx.is_complete())
                self.assertEqual(1, len(tx.inputs()))
       -        tx_copy = Transaction(tx.serialize())
       +        partial_tx = tx.serialize_as_bytes().hex()
       +        self.assertEqual("70736274ff01007301000000013eee274625ae78394847614a8bf513558bb6bd514dfd16855cb856e1e96d35540100000000fdffffff02a02526000000000017a914a4189ef02c95cfe36f8e880c6cb54dff0837b22687585d72000000000017a91400698bd11c38f887f17c99846d9be96321fbf98987c0391400000100f70100000000010118d494d28e5c3bf61566ca0313e22c3b561b888a317d689cc8b47b947adebd440000000017160014aec84704ea8508ddb94a3c6e53f0992d33a2a529fdffffff020f0925000000000017a91409f7aae0265787a02de22839d41e9c927768230287809698000000000017a91400698bd11c38f887f17c99846d9be96321fbf989870247304402206b906369f4075ebcfc149f7429dcfc34e11e1b7bbfc85d1185d5e9c324be0d3702203ce7fc12fd3131920fbcbb733250f05dbf7d03e18a4656232ee69d5c54dd46bd0121028a4b697a37f3f57f6e53f90db077fa9696095b277454fda839c211d640d48649c0391400000000",
       +                         partial_tx)
       +        tx_copy = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners
                self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0])))
        
       +        self.assertEqual(None, tx_copy.txid())  # redeem script not available
                self.assertEqual(tx.txid(), tx_copy.txid())
        
                # sign tx - first
                tx = wallet_offline1.sign_transaction(tx_copy, password=None)
                self.assertFalse(tx.is_complete())
                self.assertEqual('6a58a51591142429203b62b6ddf6b799a6926882efac229998c51bee6c3573eb', tx.txid())
       -        tx = Transaction(tx.serialize())
       +        partial_tx = tx.serialize_as_bytes().hex()
       +        # note re PSBT: online wallet had put a NON-WITNESS UTXO for input0, as they did not know if it was segwit.
       +        #               offline wallet now replaced this with a WITNESS-UTXO.
       +        #               this switch is needed to interop with bitcoin core... https://github.com/bitcoin/bitcoin/blob/fba574c908bb61eff1a0e83c935f3526ba9035f2/src/psbt.cpp#L163
       +        self.assertEqual("70736274ff01007301000000013eee274625ae78394847614a8bf513558bb6bd514dfd16855cb856e1e96d35540100000000fdffffff02a02526000000000017a914a4189ef02c95cfe36f8e880c6cb54dff0837b22687585d72000000000017a91400698bd11c38f887f17c99846d9be96321fbf98987c039140000010120809698000000000017a91400698bd11c38f887f17c99846d9be96321fbf98987220202d3f47041b424a84898e315cc8ef58190f6aec79c178c12de0790890ba7166e9c4730440220234f6648c5741eb195f0f4cd645298a10ce02f6ef557d05df93331e21c4f58cb022058ce2af0de1c238c4a8dd3b3c7a9a0da6e381ddad7593cddfc0480f9fe5baadf0101042200206ee8d4bb1277b7dbe1d4e49b880993aa993f417a9101cb23865c7c7258732704010547522102975c00f6af579f9a1d283f1e5a43032deadbab2308aef30fb307c0cfe54777462102d3f47041b424a84898e315cc8ef58190f6aec79c178c12de0790890ba7166e9c52ae220602975c00f6af579f9a1d283f1e5a43032deadbab2308aef30fb307c0cfe54777460c17cea9140000000001000000220602d3f47041b424a84898e315cc8ef58190f6aec79c178c12de0790890ba7166e9c0cd1dbcc210000000001000000000001002200206ee8d4bb1277b7dbe1d4e49b880993aa993f417a9101cb23865c7c7258732704010147522102975c00f6af579f9a1d283f1e5a43032deadbab2308aef30fb307c0cfe54777462102d3f47041b424a84898e315cc8ef58190f6aec79c178c12de0790890ba7166e9c52ae220202975c00f6af579f9a1d283f1e5a43032deadbab2308aef30fb307c0cfe54777460c17cea9140000000001000000220202d3f47041b424a84898e315cc8ef58190f6aec79c178c12de0790890ba7166e9c0cd1dbcc21000000000100000000",
       +                         partial_tx)
       +        tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners
        
                # sign tx - second
                tx = wallet_offline2.sign_transaction(tx, password=None)
                self.assertTrue(tx.is_complete())
       -        tx = Transaction(tx.serialize())
       +        tx = tx_from_any(tx.serialize())
        
                self.assertEqual('010000000001013eee274625ae78394847614a8bf513558bb6bd514dfd16855cb856e1e96d355401000000232200206ee8d4bb1277b7dbe1d4e49b880993aa993f417a9101cb23865c7c7258732704fdffffff02a02526000000000017a914a4189ef02c95cfe36f8e880c6cb54dff0837b22687585d72000000000017a91400698bd11c38f887f17c99846d9be96321fbf98987040047304402205a9dd9eb5676196893fb08f60079a2e9f567ee39614075d8c5d9fab0f11cbbc7022039640855188ebb7bccd9e3f00b397a888766d42d00d006f1ca7457c15449285f014730440220234f6648c5741eb195f0f4cd645298a10ce02f6ef557d05df93331e21c4f58cb022058ce2af0de1c238c4a8dd3b3c7a9a0da6e381ddad7593cddfc0480f9fe5baadf0147522102975c00f6af579f9a1d283f1e5a43032deadbab2308aef30fb307c0cfe54777462102d3f47041b424a84898e315cc8ef58190f6aec79c178c12de0790890ba7166e9c52aec0391400',
                                 str(tx))
       t@@ -1928,7 +2162,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
                wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
        
                # create unsigned tx
       -        outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2MyoZVy8T1t94yLmyKu8DP1SmbWvnxbkwRA', 2500000)]
       +        outputs = [PartialTxOutput.from_address_and_value('2MyoZVy8T1t94yLmyKu8DP1SmbWvnxbkwRA', 2500000)]
                tx = wallet_online.mktx(outputs=outputs, password=None, fee=5000)
                tx.set_rbf(True)
                tx.locktime = 1325505
       t@@ -1936,21 +2170,28 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
        
                self.assertFalse(tx.is_complete())
                self.assertEqual(1, len(tx.inputs()))
       -        tx_copy = Transaction(tx.serialize())
       +        partial_tx = tx.serialize_as_bytes().hex()
       +        self.assertEqual("70736274ff01007e0100000001a36fa6d72cb8aadf795097ff18609e278db156ce14f39ddd27023d08b97a3a640000000000fdffffff02a02526000000000017a91447ee5a659f6ffb53f7e3afc1681b6415f3c00fa187585d7200000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63cc13914000001012b80969800000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c000000",
       +                         partial_tx)
       +        tx_copy = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners
                self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0])))
        
       +        self.assertEqual('32e946761b4e718c1fa8d044db9e72d5831f6395eb284faf2fb5c4af0743e501', tx_copy.txid())
                self.assertEqual(tx.txid(), tx_copy.txid())
        
                # sign tx - first
                tx = wallet_offline1.sign_transaction(tx_copy, password=None)
                self.assertFalse(tx.is_complete())
                self.assertEqual('32e946761b4e718c1fa8d044db9e72d5831f6395eb284faf2fb5c4af0743e501', tx.txid())
       -        tx = Transaction(tx.serialize())
       +        partial_tx = tx.serialize_as_bytes().hex()
       +        self.assertEqual("70736274ff01007e0100000001a36fa6d72cb8aadf795097ff18609e278db156ce14f39ddd27023d08b97a3a640000000000fdffffff02a02526000000000017a91447ee5a659f6ffb53f7e3afc1681b6415f3c00fa187585d7200000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63cc13914000001012b80969800000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c22020223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa4730440220629d89626585f563202e6b38ceddc26ccd00737e0b7ee4239b9266ef9174ea2f02200b74828399a2e35ed46c9b484af4817438d5fea890606ebb201b821944db1fdc0101056952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153ae22060223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa0cb5c37672000000000000000022060273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e0c043b02bf0000000000000000220602aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae9410c3b1da3ef0000000000000000000001016952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153ae22020223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa0cb5c37672000000000000000022020273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e0c043b02bf0000000000000000220202aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae9410c3b1da3ef000000000000000000",
       +                         partial_tx)
       +        tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners
        
                # sign tx - second
                tx = wallet_offline2.sign_transaction(tx, password=None)
                self.assertTrue(tx.is_complete())
       -        tx = Transaction(tx.serialize())
       +        tx = tx_from_any(tx.serialize())
        
                self.assertEqual('01000000000101a36fa6d72cb8aadf795097ff18609e278db156ce14f39ddd27023d08b97a3a640000000000fdffffff02a02526000000000017a91447ee5a659f6ffb53f7e3afc1681b6415f3c00fa187585d7200000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c04004730440220629d89626585f563202e6b38ceddc26ccd00737e0b7ee4239b9266ef9174ea2f02200b74828399a2e35ed46c9b484af4817438d5fea890606ebb201b821944db1fdc0147304402205d1a59c84c419992069e9764a7992abca6a812cc5dfd4f0d6515d4283e660ce802202597a38899f31545aaf305629bd488f36bf54e4a05fe983932cafbb3906efb8f016952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153aec1391400',
                                 str(tx))
 (DIR) diff --git a/electrum/transaction.py b/electrum/transaction.py
       t@@ -30,19 +30,23 @@
        import struct
        import traceback
        import sys
       +import io
       +import base64
        from typing import (Sequence, Union, NamedTuple, Tuple, Optional, Iterable,
                            Callable, List, Dict, Set, TYPE_CHECKING)
        from collections import defaultdict
       +from enum import IntEnum
       +import itertools
        
       -from . import ecc, bitcoin, constants, segwit_addr
       -from .util import profiler, to_bytes, bh2u, bfh
       -from .bitcoin import (TYPE_ADDRESS, TYPE_PUBKEY, TYPE_SCRIPT, hash_160,
       +from . import ecc, bitcoin, constants, segwit_addr, bip32
       +from .bip32 import BIP32Node
       +from .util import profiler, to_bytes, bh2u, bfh, chunks, is_hex_str
       +from .bitcoin import (TYPE_ADDRESS, TYPE_SCRIPT, hash_160,
                              hash160_to_p2sh, hash160_to_p2pkh, hash_to_segwit_addr,
       -                      hash_encode, var_int, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC, COIN,
       -                      push_script, int_to_hex, push_script, b58_address_to_hash160,
       +                      var_int, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC, COIN,
       +                      int_to_hex, push_script, b58_address_to_hash160,
                              opcodes, add_number_to_script, base_decode, is_segwit_script_type)
        from .crypto import sha256d
       -from .keystore import xpubkey_to_address, xpubkey_to_pubkey
        from .logging import get_logger
        
        if TYPE_CHECKING:
       t@@ -50,10 +54,7 @@ if TYPE_CHECKING:
        
        
        _logger = get_logger(__name__)
       -
       -
       -NO_SIGNATURE = 'ff'
       -PARTIAL_TXN_HEADER_MAGIC = b'EPTF\xff'
       +DEBUG_PSBT_PARSING = False
        
        
        class SerializationError(Exception):
       t@@ -64,7 +65,15 @@ class UnknownTxinType(Exception):
            pass
        
        
       -class NotRecognizedRedeemScript(Exception):
       +class BadHeaderMagic(SerializationError):
       +    pass
       +
       +
       +class UnexpectedEndOfStream(SerializationError):
       +    pass
       +
       +
       +class PSBTInputConsistencyFailure(SerializationError):
            pass
        
        
       t@@ -72,23 +81,83 @@ class MalformedBitcoinScript(Exception):
            pass
        
        
       -class TxOutput(NamedTuple):
       -    type: int
       -    address: str
       -    value: Union[int, str]  # str when the output is set to max: '!'
       +class MissingTxInputAmount(Exception):
       +    pass
       +
       +
       +SIGHASH_ALL = 1
       +
       +
       +class TxOutput:
       +    scriptpubkey: bytes
       +    value: Union[int, str]
       +
       +    def __init__(self, *, scriptpubkey: bytes, value: Union[int, str]):
       +        self.scriptpubkey = scriptpubkey
       +        self.value = value  # str when the output is set to max: '!'  # in satoshis
        
       +    @classmethod
       +    def from_address_and_value(cls, address: str, value: Union[int, str]) -> Union['TxOutput', 'PartialTxOutput']:
       +        return cls(scriptpubkey=bfh(bitcoin.address_to_script(address)),
       +                   value=value)
       +
       +    def serialize_to_network(self) -> bytes:
       +        buf = int.to_bytes(self.value, 8, byteorder="little", signed=False)
       +        script = self.scriptpubkey
       +        buf += bfh(var_int(len(script.hex()) // 2))
       +        buf += script
       +        return buf
       +
       +    @classmethod
       +    def from_network_bytes(cls, raw: bytes) -> 'TxOutput':
       +        vds = BCDataStream()
       +        vds.write(raw)
       +        txout = parse_output(vds)
       +        if vds.can_read_more():
       +            raise SerializationError('extra junk at the end of TxOutput bytes')
       +        return txout
       +
       +    def to_legacy_tuple(self) -> Tuple[int, str, Union[int, str]]:
       +        if self.address:
       +            return TYPE_ADDRESS, self.address, self.value
       +        return TYPE_SCRIPT, self.scriptpubkey.hex(), self.value
       +
       +    @classmethod
       +    def from_legacy_tuple(cls, _type: int, addr: str, val: Union[int, str]) -> Union['TxOutput', 'PartialTxOutput']:
       +        if _type == TYPE_ADDRESS:
       +            return cls.from_address_and_value(addr, val)
       +        if _type == TYPE_SCRIPT:
       +            return cls(scriptpubkey=bfh(addr), value=val)
       +        raise Exception(f"unexptected legacy address type: {_type}")
       +
       +    @property
       +    def address(self) -> Optional[str]:
       +        return get_address_from_output_script(self.scriptpubkey)  # TODO cache this?
       +
       +    def get_ui_address_str(self) -> str:
       +        addr = self.address
       +        if addr is not None:
       +            return addr
       +        return f"SCRIPT {self.scriptpubkey.hex()}"
        
       -class TxOutputForUI(NamedTuple):
       -    address: str
       -    value: int
       +    def __repr__(self):
       +        return f"<TxOutput script={self.scriptpubkey.hex()} address={self.address} value={self.value}>"
        
       +    def __eq__(self, other):
       +        if not isinstance(other, TxOutput):
       +            return False
       +        return self.scriptpubkey == other.scriptpubkey and self.value == other.value
       +
       +    def __ne__(self, other):
       +        return not (self == other)
        
       -class TxOutputHwInfo(NamedTuple):
       -    address_index: Tuple
       -    sorted_xpubs: Sequence[str]
       -    num_sig: Optional[int]
       -    script_type: str
       -    is_change: bool  # whether the wallet considers the output to be change
       +    def to_json(self):
       +        d = {
       +            'scriptpubkey': self.scriptpubkey.hex(),
       +            'address': self.address,
       +            'value_sats': self.value,
       +        }
       +        return d
        
        
        class BIP143SharedTxDigestFields(NamedTuple):
       t@@ -97,11 +166,67 @@ class BIP143SharedTxDigestFields(NamedTuple):
            hashOutputs: str
        
        
       +class TxOutpoint(NamedTuple):
       +    txid: bytes  # endianness same as hex string displayed; reverse of tx serialization order
       +    out_idx: int
       +
       +    @classmethod
       +    def from_str(cls, s: str) -> 'TxOutpoint':
       +        hash_str, idx_str = s.split(':')
       +        return TxOutpoint(txid=bfh(hash_str),
       +                          out_idx=int(idx_str))
       +
       +    def to_str(self) -> str:
       +        return f"{self.txid.hex()}:{self.out_idx}"
       +
       +    def serialize_to_network(self) -> bytes:
       +        return self.txid[::-1] + bfh(int_to_hex(self.out_idx, 4))
       +
       +    def is_coinbase(self) -> bool:
       +        return self.txid == bytes(32)
       +
       +
       +class TxInput:
       +    prevout: TxOutpoint
       +    script_sig: Optional[bytes]
       +    nsequence: int
       +    witness: Optional[bytes]
       +
       +    def __init__(self, *,
       +                 prevout: TxOutpoint,
       +                 script_sig: bytes = None,
       +                 nsequence: int = 0xffffffff - 1,
       +                 witness: bytes = None):
       +        self.prevout = prevout
       +        self.script_sig = script_sig
       +        self.nsequence = nsequence
       +        self.witness = witness
       +
       +    def is_coinbase(self) -> bool:
       +        return self.prevout.is_coinbase()
       +
       +    def value_sats(self) -> Optional[int]:
       +        return None
       +
       +    def to_json(self):
       +        d = {
       +            'prevout_hash': self.prevout.txid.hex(),
       +            'prevout_n': self.prevout.out_idx,
       +            'coinbase': self.is_coinbase(),
       +            'nsequence': self.nsequence,
       +        }
       +        if self.script_sig is not None:
       +            d['scriptSig'] = self.script_sig.hex()
       +        if self.witness is not None:
       +            d['witness'] = self.witness.hex()
       +        return d
       +
       +
        class BCDataStream(object):
            """Workalike python implementation of Bitcoin's CDataStream class."""
        
            def __init__(self):
       -        self.input = None
       +        self.input = None  # type: Optional[bytearray]
                self.read_cursor = 0
        
            def clear(self):
       t@@ -135,11 +260,11 @@ class BCDataStream(object):
                self.write_compact_size(len(string))
                self.write(string)
        
       -    def read_bytes(self, length):
       +    def read_bytes(self, length) -> bytes:
                try:
       -            result = self.input[self.read_cursor:self.read_cursor+length]
       +            result = self.input[self.read_cursor:self.read_cursor+length]  # type: bytearray
                    self.read_cursor += length
       -            return result
       +            return bytes(result)
                except IndexError:
                    raise SerializationError("attempt to read past end of buffer") from None
        
       t@@ -192,6 +317,8 @@ class BCDataStream(object):
                elif size < 2**64:
                    self.write(b'\xff')
                    self._write_num('<Q', size)
       +        else:
       +            raise Exception(f"size {size} too large for compact_size")
        
            def _read_num(self, format):
                try:
       t@@ -270,195 +397,44 @@ def match_decoded(decoded, to_match):
            return True
        
        
       -def parse_sig(x_sig):
       -    return [None if x == NO_SIGNATURE else x for x in x_sig]
       -
       -def safe_parse_pubkey(x):
       -    try:
       -        return xpubkey_to_pubkey(x)
       -    except:
       -        return x
       -
       -def parse_scriptSig(d, _bytes):
       -    try:
       -        decoded = [ x for x in script_GetOp(_bytes) ]
       -    except Exception as e:
       -        # coinbase transactions raise an exception
       -        _logger.info(f"parse_scriptSig: cannot find address in input script (coinbase?) {bh2u(_bytes)}")
       -        return
       -
       -    match = [OPPushDataGeneric]
       -    if match_decoded(decoded, match):
       -        item = decoded[0][1]
       -        if item[0] == 0:
       -            # segwit embedded into p2sh
       -            # witness version 0
       -            d['address'] = bitcoin.hash160_to_p2sh(hash_160(item))
       -            if len(item) == 22:
       -                d['type'] = 'p2wpkh-p2sh'
       -            elif len(item) == 34:
       -                d['type'] = 'p2wsh-p2sh'
       -            else:
       -                _logger.info(f"unrecognized txin type {bh2u(item)}")
       -        elif opcodes.OP_1 <= item[0] <= opcodes.OP_16:
       -            # segwit embedded into p2sh
       -            # witness version 1-16
       -            pass
       -        else:
       -            # assert item[0] == 0x30
       -            # pay-to-pubkey
       -            d['type'] = 'p2pk'
       -            d['address'] = "(pubkey)"
       -            d['signatures'] = [bh2u(item)]
       -            d['num_sig'] = 1
       -            d['x_pubkeys'] = ["(pubkey)"]
       -            d['pubkeys'] = ["(pubkey)"]
       -        return
       -
       -    # p2pkh TxIn transactions push a signature
       -    # (71-73 bytes) and then their public key
       -    # (33 or 65 bytes) onto the stack:
       -    match = [OPPushDataGeneric, OPPushDataGeneric]
       -    if match_decoded(decoded, match):
       -        sig = bh2u(decoded[0][1])
       -        x_pubkey = bh2u(decoded[1][1])
       -        try:
       -            signatures = parse_sig([sig])
       -            pubkey, address = xpubkey_to_address(x_pubkey)
       -        except:
       -            _logger.info(f"parse_scriptSig: cannot find address in input script (p2pkh?) {bh2u(_bytes)}")
       -            return
       -        d['type'] = 'p2pkh'
       -        d['signatures'] = signatures
       -        d['x_pubkeys'] = [x_pubkey]
       -        d['num_sig'] = 1
       -        d['pubkeys'] = [pubkey]
       -        d['address'] = address
       -        return
       -
       -    # p2sh transaction, m of n
       -    match = [opcodes.OP_0] + [OPPushDataGeneric] * (len(decoded) - 1)
       -    if match_decoded(decoded, match):
       -        x_sig = [bh2u(x[1]) for x in decoded[1:-1]]
       -        redeem_script_unsanitized = decoded[-1][1]  # for partial multisig txn, this has x_pubkeys
       -        try:
       -            m, n, x_pubkeys, pubkeys, redeem_script = parse_redeemScript_multisig(redeem_script_unsanitized)
       -        except NotRecognizedRedeemScript:
       -            _logger.info(f"parse_scriptSig: cannot find address in input script (p2sh?) {bh2u(_bytes)}")
       -
       -            # we could still guess:
       -            # d['address'] = hash160_to_p2sh(hash_160(decoded[-1][1]))
       -            return
       -        # write result in d
       -        d['type'] = 'p2sh'
       -        d['num_sig'] = m
       -        d['signatures'] = parse_sig(x_sig)
       -        d['x_pubkeys'] = x_pubkeys
       -        d['pubkeys'] = pubkeys
       -        d['redeem_script'] = redeem_script
       -        d['address'] = hash160_to_p2sh(hash_160(bfh(redeem_script)))
       -        return
       -
       -    # custom partial format for imported addresses
       -    match = [opcodes.OP_INVALIDOPCODE, opcodes.OP_0, OPPushDataGeneric]
       -    if match_decoded(decoded, match):
       -        x_pubkey = bh2u(decoded[2][1])
       -        pubkey, address = xpubkey_to_address(x_pubkey)
       -        d['type'] = 'address'
       -        d['address'] = address
       -        d['num_sig'] = 1
       -        d['x_pubkeys'] = [x_pubkey]
       -        d['pubkeys'] = None  # get_sorted_pubkeys will populate this
       -        d['signatures'] = [None]
       -        return
       -
       -    _logger.info(f"parse_scriptSig: cannot find address in input script (unknown) {bh2u(_bytes)}")
       -
       -
       -def parse_redeemScript_multisig(redeem_script: bytes):
       -    try:
       -        dec2 = [ x for x in script_GetOp(redeem_script) ]
       -    except MalformedBitcoinScript:
       -        raise NotRecognizedRedeemScript()
       -    try:
       -        m = dec2[0][0] - opcodes.OP_1 + 1
       -        n = dec2[-2][0] - opcodes.OP_1 + 1
       -    except IndexError:
       -        raise NotRecognizedRedeemScript()
       -    op_m = opcodes.OP_1 + m - 1
       -    op_n = opcodes.OP_1 + n - 1
       -    match_multisig = [op_m] + [OPPushDataGeneric] * n + [op_n, opcodes.OP_CHECKMULTISIG]
       -    if not match_decoded(dec2, match_multisig):
       -        raise NotRecognizedRedeemScript()
       -    x_pubkeys = [bh2u(x[1]) for x in dec2[1:-2]]
       -    pubkeys = [safe_parse_pubkey(x) for x in x_pubkeys]
       -    redeem_script2 = bfh(multisig_script(x_pubkeys, m))
       -    if redeem_script2 != redeem_script:
       -        raise NotRecognizedRedeemScript()
       -    redeem_script_sanitized = multisig_script(pubkeys, m)
       -    return m, n, x_pubkeys, pubkeys, redeem_script_sanitized
       -
       -
       -def get_address_from_output_script(_bytes: bytes, *, net=None) -> Tuple[int, str]:
       +def get_address_from_output_script(_bytes: bytes, *, net=None) -> Optional[str]:
            try:
                decoded = [x for x in script_GetOp(_bytes)]
            except MalformedBitcoinScript:
                decoded = None
        
       -    # p2pk
       -    match = [OPPushDataPubkey, opcodes.OP_CHECKSIG]
       -    if match_decoded(decoded, match) and ecc.ECPubkey.is_pubkey_bytes(decoded[0][1]):
       -        return TYPE_PUBKEY, bh2u(decoded[0][1])
       -
            # p2pkh
            match = [opcodes.OP_DUP, opcodes.OP_HASH160, OPPushDataGeneric(lambda x: x == 20), opcodes.OP_EQUALVERIFY, opcodes.OP_CHECKSIG]
            if match_decoded(decoded, match):
       -        return TYPE_ADDRESS, hash160_to_p2pkh(decoded[2][1], net=net)
       +        return hash160_to_p2pkh(decoded[2][1], net=net)
        
            # p2sh
            match = [opcodes.OP_HASH160, OPPushDataGeneric(lambda x: x == 20), opcodes.OP_EQUAL]
            if match_decoded(decoded, match):
       -        return TYPE_ADDRESS, hash160_to_p2sh(decoded[1][1], net=net)
       +        return hash160_to_p2sh(decoded[1][1], net=net)
        
            # segwit address (version 0)
            match = [opcodes.OP_0, OPPushDataGeneric(lambda x: x in (20, 32))]
            if match_decoded(decoded, match):
       -        return TYPE_ADDRESS, hash_to_segwit_addr(decoded[1][1], witver=0, net=net)
       +        return hash_to_segwit_addr(decoded[1][1], witver=0, net=net)
        
            # segwit address (version 1-16)
            future_witness_versions = list(range(opcodes.OP_1, opcodes.OP_16 + 1))
            for witver, opcode in enumerate(future_witness_versions, start=1):
                match = [opcode, OPPushDataGeneric(lambda x: 2 <= x <= 40)]
                if match_decoded(decoded, match):
       -            return TYPE_ADDRESS, hash_to_segwit_addr(decoded[1][1], witver=witver, net=net)
       +            return hash_to_segwit_addr(decoded[1][1], witver=witver, net=net)
        
       -    return TYPE_SCRIPT, bh2u(_bytes)
       +    return None
        
        
       -def parse_input(vds, full_parse: bool):
       -    d = {}
       -    prevout_hash = hash_encode(vds.read_bytes(32))
       +def parse_input(vds) -> TxInput:
       +    prevout_hash = vds.read_bytes(32)[::-1]
            prevout_n = vds.read_uint32()
       -    scriptSig = vds.read_bytes(vds.read_compact_size())
       -    sequence = vds.read_uint32()
       -    d['prevout_hash'] = prevout_hash
       -    d['prevout_n'] = prevout_n
       -    d['scriptSig'] = bh2u(scriptSig)
       -    d['sequence'] = sequence
       -    d['type'] = 'unknown' if prevout_hash != '00'*32 else 'coinbase'
       -    d['address'] = None
       -    d['num_sig'] = 0
       -    if not full_parse:
       -        return d
       -    d['x_pubkeys'] = []
       -    d['pubkeys'] = []
       -    d['signatures'] = {}
       -    if d['type'] != 'coinbase' and scriptSig:
       -        try:
       -            parse_scriptSig(d, scriptSig)
       -        except BaseException:
       -            _logger.exception(f'failed to parse scriptSig {bh2u(scriptSig)}')
       -    return d
       +    prevout = TxOutpoint(txid=prevout_hash, out_idx=prevout_n)
       +    script_sig = vds.read_bytes(vds.read_compact_size())
       +    nsequence = vds.read_uint32()
       +    return TxInput(prevout=prevout, script_sig=script_sig, nsequence=nsequence)
        
        
        def construct_witness(items: Sequence[Union[str, int, bytes]]) -> str:
       t@@ -467,114 +443,26 @@ def construct_witness(items: Sequence[Union[str, int, bytes]]) -> str:
            for item in items:
                if type(item) is int:
                    item = bitcoin.script_num_to_hex(item)
       -        elif type(item) is bytes:
       +        elif isinstance(item, (bytes, bytearray)):
                    item = bh2u(item)
                witness += bitcoin.witness_push(item)
            return witness
        
        
       -def parse_witness(vds, txin, full_parse: bool):
       +def parse_witness(vds: BCDataStream, txin: TxInput) -> None:
            n = vds.read_compact_size()
       -    if n == 0:
       -        txin['witness'] = '00'
       -        return
       -    if n == 0xffffffff:
       -        txin['value'] = vds.read_uint64()
       -        txin['witness_version'] = vds.read_uint16()
       -        n = vds.read_compact_size()
       -    # now 'n' is the number of items in the witness
       -    w = list(bh2u(vds.read_bytes(vds.read_compact_size())) for i in range(n))
       -    txin['witness'] = construct_witness(w)
       -    if not full_parse:
       -        return
       +    witness_elements = list(vds.read_bytes(vds.read_compact_size()) for i in range(n))
       +    txin.witness = bfh(construct_witness(witness_elements))
        
       -    try:
       -        if txin.get('witness_version', 0) != 0:
       -            raise UnknownTxinType()
       -        if txin['type'] == 'coinbase':
       -            pass
       -        elif txin['type'] == 'address':
       -            pass
       -        elif txin['type'] == 'p2wsh-p2sh' or n > 2:
       -            witness_script_unsanitized = w[-1]  # for partial multisig txn, this has x_pubkeys
       -            try:
       -                m, n, x_pubkeys, pubkeys, witness_script = parse_redeemScript_multisig(bfh(witness_script_unsanitized))
       -            except NotRecognizedRedeemScript:
       -                raise UnknownTxinType()
       -            txin['signatures'] = parse_sig(w[1:-1])
       -            txin['num_sig'] = m
       -            txin['x_pubkeys'] = x_pubkeys
       -            txin['pubkeys'] = pubkeys
       -            txin['witness_script'] = witness_script
       -            if not txin.get('scriptSig'):  # native segwit script
       -                txin['type'] = 'p2wsh'
       -                txin['address'] = bitcoin.script_to_p2wsh(witness_script)
       -        elif txin['type'] == 'p2wpkh-p2sh' or n == 2:
       -            txin['num_sig'] = 1
       -            txin['x_pubkeys'] = [w[1]]
       -            txin['pubkeys'] = [safe_parse_pubkey(w[1])]
       -            txin['signatures'] = parse_sig([w[0]])
       -            if not txin.get('scriptSig'):  # native segwit script
       -                txin['type'] = 'p2wpkh'
       -                txin['address'] = bitcoin.public_key_to_p2wpkh(bfh(txin['pubkeys'][0]))
       -        else:
       -            raise UnknownTxinType()
       -    except UnknownTxinType:
       -        txin['type'] = 'unknown'
       -    except BaseException:
       -        txin['type'] = 'unknown'
       -        _logger.exception(f"failed to parse witness {txin.get('witness')}")
       -
       -
       -def parse_output(vds, i):
       -    d = {}
       -    d['value'] = vds.read_int64()
       -    if d['value'] > TOTAL_COIN_SUPPLY_LIMIT_IN_BTC * COIN:
       +
       +def parse_output(vds: BCDataStream) -> TxOutput:
       +    value = vds.read_int64()
       +    if value > TOTAL_COIN_SUPPLY_LIMIT_IN_BTC * COIN:
                raise SerializationError('invalid output amount (too large)')
       -    if d['value'] < 0:
       +    if value < 0:
                raise SerializationError('invalid output amount (negative)')
       -    scriptPubKey = vds.read_bytes(vds.read_compact_size())
       -    d['type'], d['address'] = get_address_from_output_script(scriptPubKey)
       -    d['scriptPubKey'] = bh2u(scriptPubKey)
       -    d['prevout_n'] = i
       -    return d
       -
       -
       -def deserialize(raw: str, force_full_parse=False) -> dict:
       -    raw_bytes = bfh(raw)
       -    d = {}
       -    if raw_bytes[:5] == PARTIAL_TXN_HEADER_MAGIC:
       -        d['partial'] = is_partial = True
       -        partial_format_version = raw_bytes[5]
       -        if partial_format_version != 0:
       -            raise SerializationError('unknown tx partial serialization format version: {}'
       -                                     .format(partial_format_version))
       -        raw_bytes = raw_bytes[6:]
       -    else:
       -        d['partial'] = is_partial = False
       -    full_parse = force_full_parse or is_partial
       -    vds = BCDataStream()
       -    vds.write(raw_bytes)
       -    d['version'] = vds.read_int32()
       -    n_vin = vds.read_compact_size()
       -    is_segwit = (n_vin == 0)
       -    if is_segwit:
       -        marker = vds.read_bytes(1)
       -        if marker != b'\x01':
       -            raise ValueError('invalid txn marker byte: {}'.format(marker))
       -        n_vin = vds.read_compact_size()
       -    d['segwit_ser'] = is_segwit
       -    d['inputs'] = [parse_input(vds, full_parse=full_parse) for i in range(n_vin)]
       -    n_vout = vds.read_compact_size()
       -    d['outputs'] = [parse_output(vds, i) for i in range(n_vout)]
       -    if is_segwit:
       -        for i in range(n_vin):
       -            txin = d['inputs'][i]
       -            parse_witness(vds, txin, full_parse=full_parse)
       -    d['lockTime'] = vds.read_uint32()
       -    if vds.can_read_more():
       -        raise SerializationError('extra junk at the end')
       -    return d
       +    scriptpubkey = vds.read_bytes(vds.read_compact_size())
       +    return TxOutput(value=value, scriptpubkey=scriptpubkey)
        
        
        # pay & redeem scripts
       t@@ -591,247 +479,139 @@ def multisig_script(public_keys: Sequence[str], m: int) -> str:
        
        
        class Transaction:
       +    _cached_network_ser: Optional[str]
        
            def __str__(self):
       -        if self.raw is None:
       -            self.raw = self.serialize()
       -        return self.raw
       +        return self.serialize()
        
            def __init__(self, raw):
                if raw is None:
       -            self.raw = None
       +            self._cached_network_ser = None
                elif isinstance(raw, str):
       -            self.raw = raw.strip() if raw else None
       -        elif isinstance(raw, dict):
       -            self.raw = raw['hex']
       +            self._cached_network_ser = raw.strip() if raw else None
       +            assert is_hex_str(self._cached_network_ser)
       +        elif isinstance(raw, (bytes, bytearray)):
       +            self._cached_network_ser = bh2u(raw)
                else:
       -            raise Exception("cannot initialize transaction", raw)
       -        self._inputs = None
       +            raise Exception(f"cannot initialize transaction from {raw}")
       +        self._inputs = None  # type: List[TxInput]
                self._outputs = None  # type: List[TxOutput]
                self.locktime = 0
                self.version = 2
       -        # by default we assume this is a partial txn;
       -        # this value will get properly set when deserializing
       -        self.is_partial_originally = True
       -        self._segwit_ser = None  # None means "don't know"
       -        self.output_info = None  # type: Optional[Dict[str, TxOutputHwInfo]]
       -
       -    def update(self, raw):
       -        self.raw = raw
       -        self._inputs = None
       -        self.deserialize()
        
       -    def inputs(self):
       +        self._cached_txid = None  # type: Optional[str]
       +
       +    def to_json(self) -> dict:
       +        d = {
       +            'version': self.version,
       +            'locktime': self.locktime,
       +            'inputs': [txin.to_json() for txin in self.inputs()],
       +            'outputs': [txout.to_json() for txout in self.outputs()],
       +        }
       +        return d
       +
       +    def inputs(self) -> Sequence[TxInput]:
                if self._inputs is None:
                    self.deserialize()
       -        return self._inputs or []
       +        return self._inputs
        
       -    def outputs(self) -> List[TxOutput]:
       +    def outputs(self) -> Sequence[TxOutput]:
                if self._outputs is None:
                    self.deserialize()
       -        return self._outputs or []
       -
       -    @classmethod
       -    def get_sorted_pubkeys(self, txin):
       -        # sort pubkeys and x_pubkeys, using the order of pubkeys
       -        if txin['type'] == 'coinbase':
       -            return [], []
       -        x_pubkeys = txin['x_pubkeys']
       -        pubkeys = txin.get('pubkeys')
       -        if pubkeys is None:
       -            pubkeys = [xpubkey_to_pubkey(x) for x in x_pubkeys]
       -            pubkeys, x_pubkeys = zip(*sorted(zip(pubkeys, x_pubkeys)))
       -            txin['pubkeys'] = pubkeys = list(pubkeys)
       -            txin['x_pubkeys'] = x_pubkeys = list(x_pubkeys)
       -        return pubkeys, x_pubkeys
       -
       -    def update_signatures(self, signatures: Sequence[str]):
       -        """Add new signatures to a transaction
       -
       -        `signatures` is expected to be a list of sigs with signatures[i]
       -        intended for self._inputs[i].
       -        This is used by the Trezor, KeepKey an Safe-T plugins.
       -        """
       -        if self.is_complete():
       -            return
       -        if len(self.inputs()) != len(signatures):
       -            raise Exception('expected {} signatures; got {}'.format(len(self.inputs()), len(signatures)))
       -        for i, txin in enumerate(self.inputs()):
       -            pubkeys, x_pubkeys = self.get_sorted_pubkeys(txin)
       -            sig = signatures[i]
       -            if sig in txin.get('signatures'):
       -                continue
       -            pre_hash = sha256d(bfh(self.serialize_preimage(i)))
       -            sig_string = ecc.sig_string_from_der_sig(bfh(sig[:-2]))
       -            for recid in range(4):
       -                try:
       -                    public_key = ecc.ECPubkey.from_sig_string(sig_string, recid, pre_hash)
       -                except ecc.InvalidECPointException:
       -                    # the point might not be on the curve for some recid values
       -                    continue
       -                pubkey_hex = public_key.get_public_key_hex(compressed=True)
       -                if pubkey_hex in pubkeys:
       -                    try:
       -                        public_key.verify_message_hash(sig_string, pre_hash)
       -                    except Exception:
       -                        _logger.exception('')
       -                        continue
       -                    j = pubkeys.index(pubkey_hex)
       -                    _logger.info(f"adding sig {i} {j} {pubkey_hex} {sig}")
       -                    self.add_signature_to_txin(i, j, sig)
       -                    break
       -        # redo raw
       -        self.raw = self.serialize()
       -
       -    def add_signature_to_txin(self, i, signingPos, sig):
       -        txin = self._inputs[i]
       -        txin['signatures'][signingPos] = sig
       -        txin['scriptSig'] = None  # force re-serialization
       -        txin['witness'] = None    # force re-serialization
       -        self.raw = None
       -
       -    def add_inputs_info(self, wallet: 'Abstract_Wallet') -> None:
       -        if self.is_complete():
       -            return
       -        for txin in self.inputs():
       -            wallet.add_input_info(txin)
       +        return self._outputs
        
       -    def remove_signatures(self):
       -        for txin in self.inputs():
       -            txin['signatures'] = [None] * len(txin['signatures'])
       -            txin['scriptSig'] = None
       -            txin['witness'] = None
       -        assert not self.is_complete()
       -        self.raw = None
       -
       -    def deserialize(self, force_full_parse=False):
       -        if self.raw is None:
       +    def deserialize(self) -> None:
       +        if self._cached_network_ser is None:
                    return
       -            #self.raw = self.serialize()
                if self._inputs is not None:
                    return
       -        d = deserialize(self.raw, force_full_parse)
       -        self._inputs = d['inputs']
       -        self._outputs = [TxOutput(x['type'], x['address'], x['value']) for x in d['outputs']]
       -        self.locktime = d['lockTime']
       -        self.version = d['version']
       -        self.is_partial_originally = d['partial']
       -        self._segwit_ser = d['segwit_ser']
       -        return d
       -
       -    @classmethod
       -    def from_io(klass, inputs, outputs, *, locktime=0, version=None):
       -        self = klass(None)
       -        self._inputs = inputs
       -        self._outputs = outputs
       -        self.locktime = locktime
       -        if version is not None:
       -            self.version = version
       -        self.BIP69_sort()
       -        return self
       -
       -    @classmethod
       -    def pay_script(self, output_type, addr: str) -> str:
       -        """Returns scriptPubKey in hex form."""
       -        if output_type == TYPE_SCRIPT:
       -            return addr
       -        elif output_type == TYPE_ADDRESS:
       -            return bitcoin.address_to_script(addr)
       -        elif output_type == TYPE_PUBKEY:
       -            return bitcoin.public_key_to_p2pk_script(addr)
       -        else:
       -            raise TypeError('Unknown output type')
       -
       -    @classmethod
       -    def estimate_pubkey_size_from_x_pubkey(cls, x_pubkey):
       -        try:
       -            if x_pubkey[0:2] in ['02', '03']:  # compressed pubkey
       -                return 0x21
       -            elif x_pubkey[0:2] == '04':  # uncompressed pubkey
       -                return 0x41
       -            elif x_pubkey[0:2] == 'ff':  # bip32 extended pubkey
       -                return 0x21
       -            elif x_pubkey[0:2] == 'fe':  # old electrum extended pubkey
       -                return 0x41
       -        except Exception as e:
       -            pass
       -        return 0x21  # just guess it is compressed
        
       -    @classmethod
       -    def estimate_pubkey_size_for_txin(cls, txin):
       -        pubkeys = txin.get('pubkeys', [])
       -        x_pubkeys = txin.get('x_pubkeys', [])
       -        if pubkeys and len(pubkeys) > 0:
       -            return cls.estimate_pubkey_size_from_x_pubkey(pubkeys[0])
       -        elif x_pubkeys and len(x_pubkeys) > 0:
       -            return cls.estimate_pubkey_size_from_x_pubkey(x_pubkeys[0])
       -        else:
       -            return 0x21  # just guess it is compressed
       +        raw_bytes = bfh(self._cached_network_ser)
       +        vds = BCDataStream()
       +        vds.write(raw_bytes)
       +        self.version = vds.read_int32()
       +        n_vin = vds.read_compact_size()
       +        is_segwit = (n_vin == 0)
       +        if is_segwit:
       +            marker = vds.read_bytes(1)
       +            if marker != b'\x01':
       +                raise ValueError('invalid txn marker byte: {}'.format(marker))
       +            n_vin = vds.read_compact_size()
       +        self._inputs = [parse_input(vds) for i in range(n_vin)]
       +        n_vout = vds.read_compact_size()
       +        self._outputs = [parse_output(vds) for i in range(n_vout)]
       +        if is_segwit:
       +            for txin in self._inputs:
       +                parse_witness(vds, txin)
       +        self.locktime = vds.read_uint32()
       +        if vds.can_read_more():
       +            raise SerializationError('extra junk at the end')
        
            @classmethod
       -    def get_siglist(self, txin, estimate_size=False):
       -        # if we have enough signatures, we use the actual pubkeys
       -        # otherwise, use extended pubkeys (with bip32 derivation)
       -        if txin['type'] == 'coinbase':
       +    def get_siglist(self, txin: 'PartialTxInput', *, estimate_size=False):
       +        if txin.prevout.is_coinbase():
                    return [], []
       -        num_sig = txin.get('num_sig', 1)
       +
                if estimate_size:
       -            pubkey_size = self.estimate_pubkey_size_for_txin(txin)
       -            pk_list = ["00" * pubkey_size] * len(txin.get('x_pubkeys', [None]))
       +            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(1, txin.num_sig)
                    sig_list = [ "00" * 0x48 ] * num_sig
                else:
       -            pubkeys, x_pubkeys = self.get_sorted_pubkeys(txin)
       -            x_signatures = txin['signatures']
       -            signatures = list(filter(None, x_signatures))
       -            is_complete = len(signatures) == num_sig
       -            if is_complete:
       -                pk_list = pubkeys
       -                sig_list = signatures
       -            else:
       -                pk_list = x_pubkeys
       -                sig_list = [sig if sig else NO_SIGNATURE for sig in x_signatures]
       +            pk_list = [pubkey.hex() for pubkey in txin.pubkeys]
       +            sig_list = [txin.part_sigs.get(pubkey, b'').hex() for pubkey in txin.pubkeys]
       +            if txin.is_complete():
       +                sig_list = [sig for sig in sig_list if sig]
                return pk_list, sig_list
        
            @classmethod
       -    def serialize_witness(self, txin, estimate_size=False):
       -        _type = txin['type']
       -        if not self.is_segwit_input(txin) and not txin['type'] == 'address':
       +    def serialize_witness(cls, txin: TxInput, *, estimate_size=False) -> str:
       +        if txin.witness is not None:
       +            return txin.witness.hex()
       +        if txin.prevout.is_coinbase():
       +            return ''
       +        assert isinstance(txin, PartialTxInput)
       +
       +        _type = txin.script_type
       +        if not cls.is_segwit_input(txin):
                    return '00'
       -        if _type == 'coinbase':
       -            return txin['witness']
       -
       -        witness = txin.get('witness', None)
       -        if witness is None or estimate_size:
       -            if _type == 'address' and estimate_size:
       -                _type = self.guess_txintype_from_address(txin['address'])
       -            pubkeys, sig_list = self.get_siglist(txin, estimate_size)
       -            if _type in ['p2wpkh', 'p2wpkh-p2sh']:
       -                witness = construct_witness([sig_list[0], pubkeys[0]])
       -            elif _type in ['p2wsh', 'p2wsh-p2sh']:
       -                witness_script = multisig_script(pubkeys, txin['num_sig'])
       -                witness = construct_witness([0] + sig_list + [witness_script])
       -            else:
       -                witness = txin.get('witness', '00')
        
       -        if self.is_txin_complete(txin) or estimate_size:
       -            partial_format_witness_prefix = ''
       -        else:
       -            input_value = int_to_hex(txin['value'], 8)
       -            witness_version = int_to_hex(txin.get('witness_version', 0), 2)
       -            partial_format_witness_prefix = var_int(0xffffffff) + input_value + witness_version
       -        return partial_format_witness_prefix + witness
       +        if _type in ('address', 'unknown') and estimate_size:
       +            _type = cls.guess_txintype_from_address(txin.address)
       +        pubkeys, sig_list = cls.get_siglist(txin, estimate_size=estimate_size)
       +        if _type in ['p2wpkh', 'p2wpkh-p2sh']:
       +            return construct_witness([sig_list[0], pubkeys[0]])
       +        elif _type in ['p2wsh', 'p2wsh-p2sh']:
       +            witness_script = multisig_script(pubkeys, txin.num_sig)
       +            return construct_witness([0] + sig_list + [witness_script])
       +        elif _type in ['p2pk', 'p2pkh', 'p2sh']:
       +            return '00'
       +        raise UnknownTxinType(f'cannot construct witness for txin_type: {_type}')
        
            @classmethod
       -    def is_segwit_input(cls, txin, guess_for_address=False):
       -        _type = txin['type']
       +    def is_segwit_input(cls, txin: 'TxInput', *, guess_for_address=False) -> bool:
       +        if txin.witness not in (b'\x00', b'', None):
       +            return True
       +        if not isinstance(txin, PartialTxInput):
       +            return False
       +        if txin.is_native_segwit() or txin.is_p2sh_segwit():
       +            return True
       +        if txin.is_native_segwit() is False and txin.is_p2sh_segwit() is False:
       +            return False
       +        if txin.witness_script:
       +            return True
       +        _type = txin.script_type
                if _type == 'address' and guess_for_address:
       -            _type = cls.guess_txintype_from_address(txin['address'])
       -        has_nonzero_witness = txin.get('witness', '00') not in ('00', None)
       -        return is_segwit_script_type(_type) or has_nonzero_witness
       +            _type = cls.guess_txintype_from_address(txin.address)
       +        return is_segwit_script_type(_type)
        
            @classmethod
       -    def guess_txintype_from_address(cls, addr):
       +    def guess_txintype_from_address(cls, addr: Optional[str]) -> str:
                # It's not possible to tell the script type in general
                # just from an address.
                # - "1" addresses are of course p2pkh
       t@@ -841,6 +621,8 @@ class Transaction:
                # If we don't know the script, we _guess_ it is pubkeyhash.
                # As this method is used e.g. for tx size estimation,
                # the estimation will not be precise.
       +        if addr is None:
       +            return 'p2wpkh'
                witver, witprog = segwit_addr.decode(constants.net.SEGWIT_HRP, addr)
                if witprog is not None:
                    return 'p2wpkh'
       t@@ -849,236 +631,169 @@ class Transaction:
                    return 'p2pkh'
                elif addrtype == constants.net.ADDRTYPE_P2SH:
                    return 'p2wpkh-p2sh'
       +        raise Exception(f'unrecognized address: {repr(addr)}')
        
            @classmethod
       -    def input_script(self, txin, estimate_size=False):
       -        _type = txin['type']
       -        if _type == 'coinbase':
       -            return txin['scriptSig']
       -
       -        # If there is already a saved scriptSig, just return that.
       -        # This allows manual creation of txins of any custom type.
       -        # However, if the txin is not complete, we might have some garbage
       -        # saved from our partial txn ser format, so we re-serialize then.
       -        script_sig = txin.get('scriptSig', None)
       -        if script_sig is not None and self.is_txin_complete(txin):
       -            return script_sig
       -
       -        pubkeys, sig_list = self.get_siglist(txin, estimate_size)
       +    def input_script(self, txin: TxInput, *, estimate_size=False) -> str:
       +        if txin.script_sig is not None:
       +            return txin.script_sig.hex()
       +        if txin.prevout.is_coinbase():
       +            return ''
       +        assert isinstance(txin, PartialTxInput)
       +
       +        if txin.is_p2sh_segwit() and txin.redeem_script:
       +            return push_script(txin.redeem_script.hex())
       +        if txin.is_native_segwit():
       +            return ''
       +
       +        _type = txin.script_type
       +        pubkeys, sig_list = self.get_siglist(txin, estimate_size=estimate_size)
                script = ''.join(push_script(x) for x in sig_list)
       -        if _type == 'address' and estimate_size:
       -            _type = self.guess_txintype_from_address(txin['address'])
       +        if _type in ('address', 'unknown') and estimate_size:
       +            _type = self.guess_txintype_from_address(txin.address)
                if _type == 'p2pk':
       -            pass
       +            return script
                elif _type == 'p2sh':
                    # put op_0 before script
                    script = '00' + script
       -            redeem_script = multisig_script(pubkeys, txin['num_sig'])
       +            redeem_script = multisig_script(pubkeys, txin.num_sig)
                    script += push_script(redeem_script)
       +            return script
                elif _type == 'p2pkh':
                    script += push_script(pubkeys[0])
       +            return script
                elif _type in ['p2wpkh', 'p2wsh']:
                    return ''
                elif _type == 'p2wpkh-p2sh':
       -            pubkey = safe_parse_pubkey(pubkeys[0])
       -            scriptSig = bitcoin.p2wpkh_nested_script(pubkey)
       -            return push_script(scriptSig)
       +            redeem_script = bitcoin.p2wpkh_nested_script(pubkeys[0])
       +            return push_script(redeem_script)
                elif _type == 'p2wsh-p2sh':
                    if estimate_size:
                        witness_script = ''
                    else:
                        witness_script = self.get_preimage_script(txin)
       -            scriptSig = bitcoin.p2wsh_nested_script(witness_script)
       -            return push_script(scriptSig)
       -        elif _type == 'address':
       -            return bytes([opcodes.OP_INVALIDOPCODE, opcodes.OP_0]).hex() + push_script(pubkeys[0])
       -        elif _type == 'unknown':
       -            return txin['scriptSig']
       -        return script
       +            redeem_script = bitcoin.p2wsh_nested_script(witness_script)
       +            return push_script(redeem_script)
       +        raise UnknownTxinType(f'cannot construct scriptSig for txin_type: {_type}')
        
            @classmethod
       -    def is_txin_complete(cls, txin):
       -        if txin['type'] == 'coinbase':
       -            return True
       -        num_sig = txin.get('num_sig', 1)
       -        if num_sig == 0:
       -            return True
       -        x_signatures = txin['signatures']
       -        signatures = list(filter(None, x_signatures))
       -        return len(signatures) == num_sig
       -
       -    @classmethod
       -    def get_preimage_script(self, txin):
       -        preimage_script = txin.get('preimage_script', None)
       -        if preimage_script is not None:
       -            return preimage_script
       -
       -        pubkeys, x_pubkeys = self.get_sorted_pubkeys(txin)
       -        if txin['type'] in ['p2sh', 'p2wsh', 'p2wsh-p2sh']:
       -            return multisig_script(pubkeys, txin['num_sig'])
       -        elif txin['type'] in ['p2pkh', 'p2wpkh', 'p2wpkh-p2sh']:
       +    def get_preimage_script(cls, txin: 'PartialTxInput') -> str:
       +        if txin.witness_script:
       +            opcodes_in_witness_script = [x[0] for x in script_GetOp(txin.witness_script)]
       +            if opcodes.OP_CODESEPARATOR in opcodes_in_witness_script:
       +                raise Exception('OP_CODESEPARATOR black magic is not supported')
       +            return txin.witness_script.hex()
       +
       +        pubkeys = [pk.hex() for pk in txin.pubkeys]
       +        if txin.script_type in ['p2sh', 'p2wsh', 'p2wsh-p2sh']:
       +            return multisig_script(pubkeys, txin.num_sig)
       +        elif txin.script_type in ['p2pkh', 'p2wpkh', 'p2wpkh-p2sh']:
                    pubkey = pubkeys[0]
                    pkh = bh2u(hash_160(bfh(pubkey)))
                    return bitcoin.pubkeyhash_to_p2pkh_script(pkh)
       -        elif txin['type'] == 'p2pk':
       +        elif txin.script_type == 'p2pk':
                    pubkey = pubkeys[0]
                    return bitcoin.public_key_to_p2pk_script(pubkey)
                else:
       -            raise TypeError('Unknown txin type', txin['type'])
       -
       -    @classmethod
       -    def serialize_outpoint(self, txin):
       -        return bh2u(bfh(txin['prevout_hash'])[::-1]) + int_to_hex(txin['prevout_n'], 4)
       +            raise UnknownTxinType(f'cannot construct preimage_script for txin_type: {txin.script_type}')
        
            @classmethod
       -    def get_outpoint_from_txin(cls, txin):
       -        if txin['type'] == 'coinbase':
       -            return None
       -        prevout_hash = txin['prevout_hash']
       -        prevout_n = txin['prevout_n']
       -        return prevout_hash + ':%d' % prevout_n
       -
       -    def prevout(self, index):
       -        return self.get_outpoint_from_txin(self.inputs()[index])
       -
       -    @classmethod
       -    def serialize_input(self, txin, script):
       +    def serialize_input(self, txin: TxInput, script: str) -> str:
                # Prev hash and index
       -        s = self.serialize_outpoint(txin)
       +        s = txin.prevout.serialize_to_network().hex()
                # Script length, script, sequence
                s += var_int(len(script)//2)
                s += script
       -        s += int_to_hex(txin.get('sequence', 0xffffffff - 1), 4)
       -        return s
       -
       -    def set_rbf(self, rbf):
       -        nSequence = 0xffffffff - (2 if rbf else 1)
       -        for txin in self.inputs():
       -            txin['sequence'] = nSequence
       -
       -    def BIP69_sort(self, inputs=True, outputs=True):
       -        # NOTE: other parts of the code rely on these sorts being *stable* sorts
       -        if inputs:
       -            self._inputs.sort(key = lambda i: (i['prevout_hash'], i['prevout_n']))
       -        if outputs:
       -            self._outputs.sort(key = lambda o: (o.value, self.pay_script(o.type, o.address)))
       -
       -    @classmethod
       -    def serialize_output(cls, output: TxOutput) -> str:
       -        s = int_to_hex(output.value, 8)
       -        script = cls.pay_script(output.type, output.address)
       -        s += var_int(len(script)//2)
       -        s += script
       +        s += int_to_hex(txin.nsequence, 4)
                return s
        
            def _calc_bip143_shared_txdigest_fields(self) -> BIP143SharedTxDigestFields:
                inputs = self.inputs()
                outputs = self.outputs()
       -        hashPrevouts = bh2u(sha256d(bfh(''.join(self.serialize_outpoint(txin) for txin in inputs))))
       -        hashSequence = bh2u(sha256d(bfh(''.join(int_to_hex(txin.get('sequence', 0xffffffff - 1), 4) for txin in inputs))))
       -        hashOutputs = bh2u(sha256d(bfh(''.join(self.serialize_output(o) for o in outputs))))
       +        hashPrevouts = bh2u(sha256d(b''.join(txin.prevout.serialize_to_network() for txin in inputs)))
       +        hashSequence = bh2u(sha256d(bfh(''.join(int_to_hex(txin.nsequence, 4) for txin in inputs))))
       +        hashOutputs = bh2u(sha256d(bfh(''.join(o.serialize_to_network().hex() for o in outputs))))
                return BIP143SharedTxDigestFields(hashPrevouts=hashPrevouts,
                                                  hashSequence=hashSequence,
                                                  hashOutputs=hashOutputs)
        
       -    def serialize_preimage(self, txin_index: int, *,
       -                           bip143_shared_txdigest_fields: BIP143SharedTxDigestFields = None) -> str:
       -        nVersion = int_to_hex(self.version, 4)
       -        nHashType = int_to_hex(1, 4)  # SIGHASH_ALL
       -        nLocktime = int_to_hex(self.locktime, 4)
       -        inputs = self.inputs()
       -        outputs = self.outputs()
       -        txin = inputs[txin_index]
       -        if self.is_segwit_input(txin):
       -            if bip143_shared_txdigest_fields is None:
       -                bip143_shared_txdigest_fields = self._calc_bip143_shared_txdigest_fields()
       -            hashPrevouts = bip143_shared_txdigest_fields.hashPrevouts
       -            hashSequence = bip143_shared_txdigest_fields.hashSequence
       -            hashOutputs = bip143_shared_txdigest_fields.hashOutputs
       -            outpoint = self.serialize_outpoint(txin)
       -            preimage_script = self.get_preimage_script(txin)
       -            scriptCode = var_int(len(preimage_script) // 2) + preimage_script
       -            amount = int_to_hex(txin['value'], 8)
       -            nSequence = int_to_hex(txin.get('sequence', 0xffffffff - 1), 4)
       -            preimage = nVersion + hashPrevouts + hashSequence + outpoint + scriptCode + amount + nSequence + hashOutputs + nLocktime + nHashType
       -        else:
       -            txins = var_int(len(inputs)) + ''.join(self.serialize_input(txin, self.get_preimage_script(txin) if txin_index==k else '')
       -                                                   for k, txin in enumerate(inputs))
       -            txouts = var_int(len(outputs)) + ''.join(self.serialize_output(o) for o in outputs)
       -            preimage = nVersion + txins + txouts + nLocktime + nHashType
       -        return preimage
       +    def is_segwit(self, *, guess_for_address=False):
       +        return any(self.is_segwit_input(txin, guess_for_address=guess_for_address)
       +                   for txin in self.inputs())
        
       -    def is_segwit(self, guess_for_address=False):
       -        if not self.is_partial_originally:
       -            return self._segwit_ser
       -        return any(self.is_segwit_input(x, guess_for_address=guess_for_address) for x in self.inputs())
       +    def invalidate_ser_cache(self):
       +        self._cached_network_ser = None
       +        self._cached_txid = None
        
       -    def serialize(self, estimate_size=False, witness=True):
       -        network_ser = self.serialize_to_network(estimate_size, witness)
       -        if estimate_size:
       -            return network_ser
       -        if self.is_partial_originally and not self.is_complete():
       -            partial_format_version = '00'
       -            return bh2u(PARTIAL_TXN_HEADER_MAGIC) + partial_format_version + network_ser
       -        else:
       -            return network_ser
       +    def serialize(self) -> str:
       +        if not self._cached_network_ser:
       +            self._cached_network_ser = self.serialize_to_network(estimate_size=False, include_sigs=True)
       +        return self._cached_network_ser
       +
       +    def serialize_as_bytes(self) -> bytes:
       +        return bfh(self.serialize())
        
       -    def serialize_to_network(self, estimate_size=False, witness=True):
       +    def serialize_to_network(self, *, estimate_size=False, include_sigs=True, force_legacy=False) -> str:
       +        """Serialize the transaction as used on the Bitcoin network, into hex.
       +        `include_sigs` signals whether to include scriptSigs and witnesses.
       +        `force_legacy` signals to use the pre-segwit format
       +        note: (not include_sigs) implies force_legacy
       +        """
                self.deserialize()
                nVersion = int_to_hex(self.version, 4)
                nLocktime = int_to_hex(self.locktime, 4)
                inputs = self.inputs()
                outputs = self.outputs()
       -        txins = var_int(len(inputs)) + ''.join(self.serialize_input(txin, self.input_script(txin, estimate_size)) for txin in inputs)
       -        txouts = var_int(len(outputs)) + ''.join(self.serialize_output(o) for o in outputs)
       +
       +        def create_script_sig(txin: TxInput) -> str:
       +            if include_sigs:
       +                return self.input_script(txin, estimate_size=estimate_size)
       +            return ''
       +        txins = var_int(len(inputs)) + ''.join(self.serialize_input(txin, create_script_sig(txin))
       +                                               for txin in inputs)
       +        txouts = var_int(len(outputs)) + ''.join(o.serialize_to_network().hex() for o in outputs)
       +
                use_segwit_ser_for_estimate_size = estimate_size and self.is_segwit(guess_for_address=True)
       -        use_segwit_ser_for_actual_use = not estimate_size and \
       -                                        (self.is_segwit() or any(txin['type'] == 'address' for txin in inputs))
       +        use_segwit_ser_for_actual_use = not estimate_size and self.is_segwit()
                use_segwit_ser = use_segwit_ser_for_estimate_size or use_segwit_ser_for_actual_use
       -        if witness and use_segwit_ser:
       +        if include_sigs and not force_legacy and use_segwit_ser:
                    marker = '00'
                    flag = '01'
       -            witness = ''.join(self.serialize_witness(x, estimate_size) for x in inputs)
       +            witness = ''.join(self.serialize_witness(x, estimate_size=estimate_size) for x in inputs)
                    return nVersion + marker + flag + txins + txouts + witness + nLocktime
                else:
                    return nVersion + txins + txouts + nLocktime
        
       -    def txid(self):
       -        self.deserialize()
       -        all_segwit = all(self.is_segwit_input(x) for x in self.inputs())
       -        if not all_segwit and not self.is_complete():
       -            return None
       -        ser = self.serialize_to_network(witness=False)
       -        return bh2u(sha256d(bfh(ser))[::-1])
       -
       -    def wtxid(self):
       +    def txid(self) -> Optional[str]:
       +        if self._cached_txid is None:
       +            self.deserialize()
       +            all_segwit = all(self.is_segwit_input(x) for x in self.inputs())
       +            if not all_segwit and not self.is_complete():
       +                return None
       +            try:
       +                ser = self.serialize_to_network(force_legacy=True)
       +            except UnknownTxinType:
       +                # we might not know how to construct scriptSig for some scripts
       +                return None
       +            self._cached_txid = bh2u(sha256d(bfh(ser))[::-1])
       +        return self._cached_txid
       +
       +    def wtxid(self) -> Optional[str]:
                self.deserialize()
                if not self.is_complete():
                    return None
       -        ser = self.serialize_to_network(witness=True)
       +        try:
       +            ser = self.serialize_to_network()
       +        except UnknownTxinType:
       +            # we might not know how to construct scriptSig/witness for some scripts
       +            return None
                return bh2u(sha256d(bfh(ser))[::-1])
        
       -    def add_inputs(self, inputs):
       -        self._inputs.extend(inputs)
       -        self.raw = None
       -        self.BIP69_sort(outputs=False)
       -
       -    def add_outputs(self, outputs):
       -        self._outputs.extend(outputs)
       -        self.raw = None
       -        self.BIP69_sort(inputs=False)
       -
       -    def input_value(self) -> int:
       -        return sum(x['value'] for x in self.inputs())
       -
       -    def output_value(self) -> int:
       -        return sum(o.value for o in self.outputs())
       -
       -    def get_fee(self) -> int:
       -        return self.input_value() - self.output_value()
       +    def add_info_from_wallet(self, wallet: 'Abstract_Wallet') -> None:
       +        return  # no-op
        
            def is_final(self):
       -        return not any([x.get('sequence', 0xffffffff - 1) < 0xffffffff - 1 for x in self.inputs()])
       +        return not any([txin.nsequence < 0xffffffff - 1 for txin in self.inputs()])
        
            def estimated_size(self):
                """Return an estimated virtual tx size in vbytes.
       t@@ -1093,11 +808,11 @@ class Transaction:
            @classmethod
            def estimated_input_weight(cls, txin, is_segwit_tx):
                '''Return an estimate of serialized input weight in weight units.'''
       -        script = cls.input_script(txin, True)
       +        script = cls.input_script(txin, estimate_size=True)
                input_size = len(cls.serialize_input(txin, script)) // 2
        
                if cls.is_segwit_input(txin, guess_for_address=True):
       -            witness_size = len(cls.serialize_witness(txin, True)) // 2
       +            witness_size = len(cls.serialize_witness(txin, estimate_size=True)) // 2
                else:
                    witness_size = 1 if is_segwit_tx else 0
        
       t@@ -1116,7 +831,10 @@ class Transaction:
        
            def estimated_total_size(self):
                """Return an estimated total transaction size in bytes."""
       -        return len(self.serialize(True)) // 2 if not self.is_complete() or self.raw is None else len(self.raw) // 2  # ASCII hex string
       +        if not self.is_complete() or self._cached_network_ser is None:
       +            return len(self.serialize_to_network(estimate_size=True)) // 2
       +        else:
       +            return len(self._cached_network_ser) // 2  # ASCII hex string
        
            def estimated_witness_size(self):
                """Return an estimate of witness size in bytes."""
       t@@ -1124,7 +842,7 @@ class Transaction:
                if not self.is_segwit(guess_for_address=estimate):
                    return 0
                inputs = self.inputs()
       -        witness = ''.join(self.serialize_witness(x, estimate) for x in inputs)
       +        witness = ''.join(self.serialize_witness(x, estimate_size=estimate) for x in inputs)
                witness_size = len(witness) // 2 + 2  # include marker and flag
                return witness_size
        
       t@@ -1138,66 +856,8 @@ class Transaction:
                base_tx_size = self.estimated_base_size()
                return 3 * base_tx_size + total_tx_size
        
       -    def signature_count(self):
       -        r = 0
       -        s = 0
       -        for txin in self.inputs():
       -            if txin['type'] == 'coinbase':
       -                continue
       -            signatures = list(filter(None, txin.get('signatures',[])))
       -            s += len(signatures)
       -            r += txin.get('num_sig',-1)
       -        return s, r
       -
       -    def is_complete(self):
       -        s, r = self.signature_count()
       -        return r == s
       -
       -    def sign(self, keypairs) -> None:
       -        # keypairs:  (x_)pubkey -> secret_bytes
       -        bip143_shared_txdigest_fields = self._calc_bip143_shared_txdigest_fields()
       -        for i, txin in enumerate(self.inputs()):
       -            pubkeys, x_pubkeys = self.get_sorted_pubkeys(txin)
       -            for j, (pubkey, x_pubkey) in enumerate(zip(pubkeys, x_pubkeys)):
       -                if self.is_txin_complete(txin):
       -                    break
       -                if pubkey in keypairs:
       -                    _pubkey = pubkey
       -                elif x_pubkey in keypairs:
       -                    _pubkey = x_pubkey
       -                else:
       -                    continue
       -                _logger.info(f"adding signature for {_pubkey}")
       -                sec, compressed = keypairs.get(_pubkey)
       -                sig = self.sign_txin(i, sec, bip143_shared_txdigest_fields=bip143_shared_txdigest_fields)
       -                self.add_signature_to_txin(i, j, sig)
       -
       -        _logger.debug(f"is_complete {self.is_complete()}")
       -        self.raw = self.serialize()
       -
       -    def sign_txin(self, txin_index, privkey_bytes, *, bip143_shared_txdigest_fields=None) -> str:
       -        pre_hash = sha256d(bfh(self.serialize_preimage(txin_index,
       -                                                       bip143_shared_txdigest_fields=bip143_shared_txdigest_fields)))
       -        privkey = ecc.ECPrivkey(privkey_bytes)
       -        sig = privkey.sign_transaction(pre_hash)
       -        sig = bh2u(sig) + '01'
       -        return sig
       -
       -    def get_outputs_for_UI(self) -> Sequence[TxOutputForUI]:
       -        outputs = []
       -        for o in self.outputs():
       -            if o.type == TYPE_ADDRESS:
       -                addr = o.address
       -            elif o.type == TYPE_PUBKEY:
       -                addr = 'PUBKEY ' + o.address
       -            else:
       -                addr = 'SCRIPT ' + o.address
       -            outputs.append(TxOutputForUI(addr, o.value))      # consider using yield
       -        return outputs
       -
       -    def has_address(self, addr: str) -> bool:
       -        return (addr in (o.address for o in self.outputs())) \
       -               or (addr in (txin.get("address") for txin in self.inputs()))
       +    def is_complete(self) -> bool:
       +        return True
        
            def get_output_idxs_from_scriptpubkey(self, script: str) -> Set[int]:
                """Returns the set indices of outputs with given script."""
       t@@ -1208,7 +868,7 @@ class Transaction:
                if not hasattr(self, '_script_to_output_idx'):
                    d = defaultdict(set)
                    for output_idx, o in enumerate(self.outputs()):
       -                o_script = self.pay_script(o.type, o.address)
       +                o_script = o.scriptpubkey.hex()
                        assert isinstance(o_script, str)
                        d[o_script].add(output_idx)
                    self._script_to_output_idx = d
       t@@ -1218,27 +878,10 @@ class Transaction:
                script = bitcoin.address_to_script(addr)
                return self.get_output_idxs_from_scriptpubkey(script)
        
       -    def as_dict(self):
       -        if self.raw is None:
       -            self.raw = self.serialize()
       -        self.deserialize()
       -        out = {
       -            'hex': self.raw,
       -            'complete': self.is_complete(),
       -            'final': self.is_final(),
       -        }
       -        return out
       -
       -    @classmethod
       -    def from_dict(cls, d):
       -        tx = cls(d['hex'])
       -        tx.deserialize(True)
       -        return tx
        
       -
       -def tx_from_str(txt: str) -> str:
       -    """Sanitizes tx-describing input (json or raw hex or base43) into
       -    raw hex transaction."""
       +def convert_tx_str_to_hex(txt: str) -> str:
       +    """Sanitizes tx-describing input (hex/base43/base64) into
       +    raw tx hex string."""
            assert isinstance(txt, str), f"txt must be str, not {type(txt)}"
            txt = txt.strip()
            if not txt:
       t@@ -1254,8 +897,997 @@ def tx_from_str(txt: str) -> str:
                return base_decode(txt, length=None, base=43).hex()
            except:
                pass
       -    # try json
       -    import json
       -    tx_dict = json.loads(str(txt))
       -    assert "hex" in tx_dict.keys()
       -    return tx_dict["hex"]
       +    # try base64
       +    if txt[0:6] == 'cHNidP':  # base64 psbt
       +        try:
       +            return base64.b64decode(txt).hex()
       +        except:
       +            pass
       +    raise ValueError(f"failed to recognize transaction encoding for txt: {txt[:30]}...")
       +
       +
       +def tx_from_any(raw: Union[str, bytes]) -> Union['PartialTransaction', 'Transaction']:
       +    if isinstance(raw, (bytes, bytearray)):
       +        raw = raw.hex()
       +    raw = convert_tx_str_to_hex(raw)
       +    try:
       +        return PartialTransaction.from_raw_psbt(raw)
       +    except BadHeaderMagic:
       +        if raw[:10] == b'EPTF\xff'.hex():
       +            raise Exception("Partial transactions generated with old Electrum versions "
       +                            "(< 4.0) are no longer supported. Please upgrade Electrum on "
       +                            "the other machine where this transaction was created.")
       +    tx = Transaction(raw)
       +    tx.deserialize()
       +    return tx
       +
       +
       +class PSBTGlobalType(IntEnum):
       +    UNSIGNED_TX = 0
       +    XPUB = 1
       +    VERSION = 0xFB
       +
       +
       +class PSBTInputType(IntEnum):
       +    NON_WITNESS_UTXO = 0
       +    WITNESS_UTXO = 1
       +    PARTIAL_SIG = 2
       +    SIGHASH_TYPE = 3
       +    REDEEM_SCRIPT = 4
       +    WITNESS_SCRIPT = 5
       +    BIP32_DERIVATION = 6
       +    FINAL_SCRIPTSIG = 7
       +    FINAL_SCRIPTWITNESS = 8
       +
       +
       +class PSBTOutputType(IntEnum):
       +    REDEEM_SCRIPT = 0
       +    WITNESS_SCRIPT = 1
       +    BIP32_DERIVATION = 2
       +
       +
       +# Serialization/deserialization tools
       +def deser_compact_size(f) -> Optional[int]:
       +    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
       +
       +
       +class PSBTSection:
       +
       +    def _populate_psbt_fields_from_fd(self, fd=None):
       +        if not fd: return
       +
       +        while True:
       +            try:
       +                key_type, key, val = self.get_next_kv_from_fd(fd)
       +            except StopIteration:
       +                break
       +            self.parse_psbt_section_kv(key_type, key, val)
       +
       +    @classmethod
       +    def get_next_kv_from_fd(cls, fd) -> Tuple[int, bytes, bytes]:
       +        key_size = deser_compact_size(fd)
       +        if key_size == 0:
       +            raise StopIteration()
       +        if key_size is None:
       +            raise UnexpectedEndOfStream()
       +
       +        full_key = fd.read(key_size)
       +        key_type, key = cls.get_keytype_and_key_from_fullkey(full_key)
       +
       +        val_size = deser_compact_size(fd)
       +        if val_size is None: raise UnexpectedEndOfStream()
       +        val = fd.read(val_size)
       +
       +        return key_type, key, val
       +
       +    @classmethod
       +    def create_psbt_writer(cls, fd):
       +        def wr(key_type: int, val: bytes, key: bytes = b''):
       +            full_key = cls.get_fullkey_from_keytype_and_key(key_type, key)
       +            fd.write(bytes.fromhex(var_int(len(full_key))))  # key_size
       +            fd.write(full_key)  # key
       +            fd.write(bytes.fromhex(var_int(len(val))))  # val_size
       +            fd.write(val)  # val
       +        return wr
       +
       +    @classmethod
       +    def get_keytype_and_key_from_fullkey(cls, full_key: bytes) -> Tuple[int, bytes]:
       +        with io.BytesIO(full_key) as key_stream:
       +            key_type = deser_compact_size(key_stream)
       +            if key_type is None: raise UnexpectedEndOfStream()
       +            key = key_stream.read()
       +        return key_type, key
       +
       +    @classmethod
       +    def get_fullkey_from_keytype_and_key(cls, key_type: int, key: bytes) -> bytes:
       +        key_type_bytes = bytes.fromhex(var_int(key_type))
       +        return key_type_bytes + key
       +
       +    def _serialize_psbt_section(self, fd):
       +        wr = self.create_psbt_writer(fd)
       +        self.serialize_psbt_section_kvs(wr)
       +        fd.write(b'\x00')  # section-separator
       +
       +    def parse_psbt_section_kv(self, kt: int, key: bytes, val: bytes) -> None:
       +        raise NotImplementedError()  # implemented by subclasses
       +
       +    def serialize_psbt_section_kvs(self, wr) -> None:
       +        raise NotImplementedError()  # implemented by subclasses
       +
       +
       +class PartialTxInput(TxInput, PSBTSection):
       +    def __init__(self, *args, **kwargs):
       +        TxInput.__init__(self, *args, **kwargs)
       +        self.utxo = None  # type: Optional[Transaction]
       +        self.witness_utxo = None  # type: Optional[TxOutput]
       +        self.part_sigs = {}  # type: Dict[bytes, bytes]  # pubkey -> sig
       +        self.sighash = None  # type: Optional[int]
       +        self.bip32_paths = {}  # type: Dict[bytes, Tuple[bytes, Sequence[int]]]  # pubkey -> (xpub_fingerprint, path)
       +        self.redeem_script = None  # type: Optional[bytes]
       +        self.witness_script = None  # type: Optional[bytes]
       +        self._unknown = {}  # type: Dict[bytes, bytes]
       +
       +        self.script_type = 'unknown'
       +        self.num_sig = 0  # type: int  # num req sigs for multisig
       +        self.pubkeys = []  # type: List[bytes]  # note: order matters
       +        self._trusted_value_sats = None  # type: Optional[int]
       +        self._trusted_address = None  # type: Optional[str]
       +        self.block_height = None  # type: Optional[int]  # height at which the TXO is mined; None means unknown
       +        self._is_p2sh_segwit = None  # type: Optional[bool]  # None means unknown
       +        self._is_native_segwit = None  # type: Optional[bool]  # None means unknown
       +
       +    def to_json(self):
       +        d = super().to_json()
       +        d.update({
       +            'height': self.block_height,
       +            'value_sats': self.value_sats(),
       +            'address': self.address,
       +            'utxo': str(self.utxo) if self.utxo else None,
       +            'witness_utxo': self.witness_utxo.serialize_to_network().hex() if self.witness_utxo else None,
       +            'sighash': self.sighash,
       +            'redeem_script': self.redeem_script.hex() if self.redeem_script else None,
       +            'witness_script': self.witness_script.hex() if self.witness_script else None,
       +            'part_sigs': {pubkey.hex(): sig.hex() for pubkey, sig in self.part_sigs.items()},
       +            'bip32_paths': {pubkey.hex(): (xfp.hex(), bip32.convert_bip32_intpath_to_strpath(path))
       +                            for pubkey, (xfp, path) in self.bip32_paths.items()},
       +            'unknown_psbt_fields': {key.hex(): val.hex() for key, val in self._unknown.items()},
       +        })
       +        return d
       +
       +    @classmethod
       +    def from_txin(cls, txin: TxInput, *, strip_witness: bool = True) -> 'PartialTxInput':
       +        res = PartialTxInput(prevout=txin.prevout,
       +                             script_sig=None if strip_witness else txin.script_sig,
       +                             nsequence=txin.nsequence,
       +                             witness=None if strip_witness else txin.witness)
       +        return res
       +
       +    def validate_data(self, *, for_signing=False) -> None:
       +        if self.utxo:
       +            if self.prevout.txid.hex() != self.utxo.txid():
       +                raise PSBTInputConsistencyFailure(f"PSBT input validation: "
       +                                                  f"If a non-witness UTXO is provided, its hash must match the hash specified in the prevout")
       +        # The following test is disabled, so we are willing to sign non-segwit inputs
       +        # without verifying the input amount. This means, given a maliciously modified PSBT,
       +        # for non-segwit inputs, we might end up burning coins as miner fees.
       +        if for_signing and False:
       +            if not Transaction.is_segwit_input(self) and self.witness_utxo:
       +                raise PSBTInputConsistencyFailure(f"PSBT input validation: "
       +                                                  f"If a witness UTXO is provided, no non-witness signature may be created")
       +        if self.redeem_script and self.address:
       +            addr = hash160_to_p2sh(hash_160(self.redeem_script))
       +            if self.address != addr:
       +                raise PSBTInputConsistencyFailure(f"PSBT input validation: "
       +                                                  f"If a redeemScript is provided, the scriptPubKey must be for that redeemScript")
       +        if self.witness_script:
       +            if self.redeem_script:
       +                if self.redeem_script != bfh(bitcoin.p2wsh_nested_script(self.witness_script.hex())):
       +                    raise PSBTInputConsistencyFailure(f"PSBT input validation: "
       +                                                      f"If a witnessScript is provided, the redeemScript must be for that witnessScript")
       +            elif self.address:
       +                if self.address != bitcoin.script_to_p2wsh(self.witness_script.hex()):
       +                    raise PSBTInputConsistencyFailure(f"PSBT input validation: "
       +                                                      f"If a witnessScript is provided, the scriptPubKey must be for that witnessScript")
       +
       +    def parse_psbt_section_kv(self, kt, key, val):
       +        try:
       +            kt = PSBTInputType(kt)
       +        except ValueError:
       +            pass  # unknown type
       +        if DEBUG_PSBT_PARSING: print(f"{repr(kt)} {key.hex()} {val.hex()}")
       +        if kt == PSBTInputType.NON_WITNESS_UTXO:
       +            if self.utxo is not None:
       +                raise SerializationError(f"duplicate key: {repr(kt)}")
       +            if self.witness_utxo is not None:
       +                raise SerializationError(f"PSBT input cannot have both PSBT_IN_NON_WITNESS_UTXO and PSBT_IN_WITNESS_UTXO")
       +            self.utxo = Transaction(val)
       +            self.utxo.deserialize()
       +            if key: raise SerializationError(f"key for {repr(kt)} must be empty")
       +        elif kt == PSBTInputType.WITNESS_UTXO:
       +            if self.witness_utxo is not None:
       +                raise SerializationError(f"duplicate key: {repr(kt)}")
       +            if self.utxo is not None:
       +                raise SerializationError(f"PSBT input cannot have both PSBT_IN_NON_WITNESS_UTXO and PSBT_IN_WITNESS_UTXO")
       +            self.witness_utxo = TxOutput.from_network_bytes(val)
       +            if key: raise SerializationError(f"key for {repr(kt)} must be empty")
       +        elif kt == PSBTInputType.PARTIAL_SIG:
       +            if key in self.part_sigs:
       +                raise SerializationError(f"duplicate key: {repr(kt)}")
       +            if len(key) not in (33, 65):  # TODO also allow 32? one of the tests in the BIP is "supposed to" fail with len==32...
       +                raise SerializationError(f"key for {repr(kt)} has unexpected length: {len(key)}")
       +            self.part_sigs[key] = val
       +        elif kt == PSBTInputType.SIGHASH_TYPE:
       +            if self.sighash is not None:
       +                raise SerializationError(f"duplicate key: {repr(kt)}")
       +            if len(val) != 4:
       +                raise SerializationError(f"value for {repr(kt)} has unexpected length: {len(val)}")
       +            self.sighash = struct.unpack("<I", val)[0]
       +            if key: raise SerializationError(f"key for {repr(kt)} must be empty")
       +        elif kt == PSBTInputType.BIP32_DERIVATION:
       +            if key in self.bip32_paths:
       +                raise SerializationError(f"duplicate key: {repr(kt)}")
       +            if len(key) not in (33, 65):  # TODO also allow 32? one of the tests in the BIP is "supposed to" fail with len==32...
       +                raise SerializationError(f"key for {repr(kt)} has unexpected length: {len(key)}")
       +            self.bip32_paths[key] = unpack_bip32_root_fingerprint_and_int_path(val)
       +        elif kt == PSBTInputType.REDEEM_SCRIPT:
       +            if self.redeem_script is not None:
       +                raise SerializationError(f"duplicate key: {repr(kt)}")
       +            self.redeem_script = val
       +            if key: raise SerializationError(f"key for {repr(kt)} must be empty")
       +        elif kt == PSBTInputType.WITNESS_SCRIPT:
       +            if self.witness_script is not None:
       +                raise SerializationError(f"duplicate key: {repr(kt)}")
       +            self.witness_script = val
       +            if key: raise SerializationError(f"key for {repr(kt)} must be empty")
       +        elif kt == PSBTInputType.FINAL_SCRIPTSIG:
       +            if self.script_sig is not None:
       +                raise SerializationError(f"duplicate key: {repr(kt)}")
       +            self.script_sig = val
       +            if key: raise SerializationError(f"key for {repr(kt)} must be empty")
       +        elif kt == PSBTInputType.FINAL_SCRIPTWITNESS:
       +            if self.witness is not None:
       +                raise SerializationError(f"duplicate key: {repr(kt)}")
       +            self.witness = val
       +            if key: raise SerializationError(f"key for {repr(kt)} must be empty")
       +        else:
       +            full_key = self.get_fullkey_from_keytype_and_key(kt, key)
       +            if full_key in self._unknown:
       +                raise SerializationError(f'duplicate key. PSBT input key for unknown type: {full_key}')
       +            self._unknown[full_key] = val
       +
       +    def serialize_psbt_section_kvs(self, wr):
       +        if self.witness_utxo:
       +            wr(PSBTInputType.WITNESS_UTXO, self.witness_utxo.serialize_to_network())
       +        elif self.utxo:
       +            wr(PSBTInputType.NON_WITNESS_UTXO, bfh(self.utxo.serialize_to_network(include_sigs=True)))
       +        for pk, val in sorted(self.part_sigs.items()):
       +            wr(PSBTInputType.PARTIAL_SIG, val, pk)
       +        if self.sighash is not None:
       +            wr(PSBTInputType.SIGHASH_TYPE, struct.pack('<I', self.sighash))
       +        if self.redeem_script is not None:
       +            wr(PSBTInputType.REDEEM_SCRIPT, self.redeem_script)
       +        if self.witness_script is not None:
       +            wr(PSBTInputType.WITNESS_SCRIPT, self.witness_script)
       +        for k in sorted(self.bip32_paths):
       +            packed_path = pack_bip32_root_fingerprint_and_int_path(*self.bip32_paths[k])
       +            wr(PSBTInputType.BIP32_DERIVATION, packed_path, k)
       +        if self.script_sig is not None:
       +            wr(PSBTInputType.FINAL_SCRIPTSIG, self.script_sig)
       +        if self.witness is not None:
       +            wr(PSBTInputType.FINAL_SCRIPTWITNESS, self.witness)
       +        for full_key, val in sorted(self._unknown.items()):
       +            key_type, key = self.get_keytype_and_key_from_fullkey(full_key)
       +            wr(key_type, val, key=key)
       +
       +    def value_sats(self) -> Optional[int]:
       +        if self._trusted_value_sats is not None:
       +            return self._trusted_value_sats
       +        if self.utxo:
       +            out_idx = self.prevout.out_idx
       +            return self.utxo.outputs()[out_idx].value
       +        if self.witness_utxo:
       +            return self.witness_utxo.value
       +        return None
       +
       +    @property
       +    def address(self) -> Optional[str]:
       +        if self._trusted_address is not None:
       +            return self._trusted_address
       +        scriptpubkey = self.scriptpubkey
       +        if scriptpubkey:
       +            return get_address_from_output_script(scriptpubkey)
       +        return None
       +
       +    @property
       +    def scriptpubkey(self) -> Optional[bytes]:
       +        if self._trusted_address is not None:
       +            return bfh(bitcoin.address_to_script(self._trusted_address))
       +        if self.utxo:
       +            out_idx = self.prevout.out_idx
       +            return self.utxo.outputs()[out_idx].scriptpubkey
       +        if self.witness_utxo:
       +            return self.witness_utxo.scriptpubkey
       +        return None
       +
       +    def is_complete(self) -> bool:
       +        if self.script_sig is not None and self.witness is not None:
       +            return True
       +        if self.prevout.is_coinbase():
       +            return True
       +        if self.script_sig is not None and not Transaction.is_segwit_input(self):
       +            return True
       +        signatures = list(self.part_sigs.values())
       +        s = len(signatures)
       +        # note: The 'script_type' field is currently only set by the wallet,
       +        #       for its own addresses. This means we can only finalize inputs
       +        #       that are related to the wallet.
       +        #       The 'fix' would be adding extra logic that matches on templates,
       +        #       and figures out the script_type from available fields.
       +        if self.script_type in ('p2pk', 'p2pkh', 'p2wpkh', 'p2wpkh-p2sh'):
       +            return s >= 1
       +        if self.script_type in ('p2sh', 'p2wsh', 'p2wsh-p2sh'):
       +            return s >= self.num_sig
       +        return False
       +
       +    def finalize(self) -> None:
       +        def clear_fields_when_finalized():
       +            # BIP-174: "All other data except the UTXO and unknown fields in the
       +            #           input key-value map should be cleared from the PSBT"
       +            self.part_sigs = {}
       +            self.sighash = None
       +            self.bip32_paths = {}
       +            self.redeem_script = None
       +            self.witness_script = None
       +
       +        if self.script_sig is not None and self.witness is not None:
       +            clear_fields_when_finalized()
       +            return  # already finalized
       +        if self.is_complete():
       +            self.script_sig = bfh(Transaction.input_script(self))
       +            self.witness = bfh(Transaction.serialize_witness(self))
       +            clear_fields_when_finalized()
       +
       +    def combine_with_other_txin(self, other_txin: 'TxInput') -> None:
       +        assert self.prevout == other_txin.prevout
       +        if other_txin.script_sig is not None:
       +            self.script_sig = other_txin.script_sig
       +        if other_txin.witness is not None:
       +            self.witness = other_txin.witness
       +        if isinstance(other_txin, PartialTxInput):
       +            if other_txin.witness_utxo:
       +                self.witness_utxo = other_txin.witness_utxo
       +            if other_txin.utxo:
       +                self.utxo = other_txin.utxo
       +            self.part_sigs.update(other_txin.part_sigs)
       +            if other_txin.sighash is not None:
       +                self.sighash = other_txin.sighash
       +            self.bip32_paths.update(other_txin.bip32_paths)
       +            if other_txin.redeem_script is not None:
       +                self.redeem_script = other_txin.redeem_script
       +            if other_txin.witness_script is not None:
       +                self.witness_script = other_txin.witness_script
       +            self._unknown.update(other_txin._unknown)
       +        self.ensure_there_is_only_one_utxo()
       +        # try to finalize now
       +        self.finalize()
       +
       +    def ensure_there_is_only_one_utxo(self):
       +        if self.utxo is not None and self.witness_utxo is not None:
       +            if Transaction.is_segwit_input(self):
       +                self.utxo = None
       +            else:
       +                self.witness_utxo = None
       +
       +    def convert_utxo_to_witness_utxo(self) -> None:
       +        if self.utxo:
       +            self.witness_utxo = self.utxo.outputs()[self.prevout.out_idx]
       +            self.utxo = None  # type: Optional[Transaction]
       +
       +    def is_native_segwit(self) -> Optional[bool]:
       +        """Whether this input is native segwit. None means inconclusive."""
       +        if self._is_native_segwit is None:
       +            if self.address:
       +                self._is_native_segwit = bitcoin.is_segwit_address(self.address)
       +        return self._is_native_segwit
       +
       +    def is_p2sh_segwit(self) -> Optional[bool]:
       +        """Whether this input is p2sh-embedded-segwit. None means inconclusive."""
       +        if self._is_p2sh_segwit is None:
       +            def calc_if_p2sh_segwit_now():
       +                if not (self.address and self.redeem_script):
       +                    return None
       +                if self.address != bitcoin.hash160_to_p2sh(hash_160(self.redeem_script)):
       +                    # not p2sh address
       +                    return False
       +                try:
       +                    decoded = [x for x in script_GetOp(self.redeem_script)]
       +                except MalformedBitcoinScript:
       +                    decoded = None
       +                # witness version 0
       +                match = [opcodes.OP_0, OPPushDataGeneric(lambda x: x in (20, 32))]
       +                if match_decoded(decoded, match):
       +                    return True
       +                # witness version 1-16
       +                future_witness_versions = list(range(opcodes.OP_1, opcodes.OP_16 + 1))
       +                for witver, opcode in enumerate(future_witness_versions, start=1):
       +                    match = [opcode, OPPushDataGeneric(lambda x: 2 <= x <= 40)]
       +                    if match_decoded(decoded, match):
       +                        return True
       +                return False
       +
       +            self._is_p2sh_segwit = calc_if_p2sh_segwit_now()
       +        return self._is_p2sh_segwit
       +
       +
       +class PartialTxOutput(TxOutput, PSBTSection):
       +    def __init__(self, *args, **kwargs):
       +        TxOutput.__init__(self, *args, **kwargs)
       +        self.redeem_script = None  # type: Optional[bytes]
       +        self.witness_script = None  # type: Optional[bytes]
       +        self.bip32_paths = {}  # type: Dict[bytes, Tuple[bytes, Sequence[int]]]  # pubkey -> (xpub_fingerprint, path)
       +        self._unknown = {}  # type: Dict[bytes, bytes]
       +
       +        self.script_type = 'unknown'
       +        self.num_sig = 0  # num req sigs for multisig
       +        self.pubkeys = []  # type: List[bytes]  # note: order matters
       +        self.is_mine = False  # type: bool  # whether the wallet considers the output to be ismine
       +        self.is_change = False  # type: bool  # whether the wallet considers the output to be change
       +
       +    def to_json(self):
       +        d = super().to_json()
       +        d.update({
       +            'redeem_script': self.redeem_script.hex() if self.redeem_script else None,
       +            'witness_script': self.witness_script.hex() if self.witness_script else None,
       +            'bip32_paths': {pubkey.hex(): (xfp.hex(), bip32.convert_bip32_intpath_to_strpath(path))
       +                            for pubkey, (xfp, path) in self.bip32_paths.items()},
       +            'unknown_psbt_fields': {key.hex(): val.hex() for key, val in self._unknown.items()},
       +        })
       +        return d
       +
       +    @classmethod
       +    def from_txout(cls, txout: TxOutput) -> 'PartialTxOutput':
       +        res = PartialTxOutput(scriptpubkey=txout.scriptpubkey,
       +                              value=txout.value)
       +        return res
       +
       +    def parse_psbt_section_kv(self, kt, key, val):
       +        try:
       +            kt = PSBTOutputType(kt)
       +        except ValueError:
       +            pass  # unknown type
       +        if DEBUG_PSBT_PARSING: print(f"{repr(kt)} {key.hex()} {val.hex()}")
       +        if kt == PSBTOutputType.REDEEM_SCRIPT:
       +            if self.redeem_script is not None:
       +                raise SerializationError(f"duplicate key: {repr(kt)}")
       +            self.redeem_script = val
       +            if key: raise SerializationError(f"key for {repr(kt)} must be empty")
       +        elif kt == PSBTOutputType.WITNESS_SCRIPT:
       +            if self.witness_script is not None:
       +                raise SerializationError(f"duplicate key: {repr(kt)}")
       +            self.witness_script = val
       +            if key: raise SerializationError(f"key for {repr(kt)} must be empty")
       +        elif kt == PSBTOutputType.BIP32_DERIVATION:
       +            if key in self.bip32_paths:
       +                raise SerializationError(f"duplicate key: {repr(kt)}")
       +            if len(key) not in (33, 65):  # TODO also allow 32? one of the tests in the BIP is "supposed to" fail with len==32...
       +                raise SerializationError(f"key for {repr(kt)} has unexpected length: {len(key)}")
       +            self.bip32_paths[key] = unpack_bip32_root_fingerprint_and_int_path(val)
       +        else:
       +            full_key = self.get_fullkey_from_keytype_and_key(kt, key)
       +            if full_key in self._unknown:
       +                raise SerializationError(f'duplicate key. PSBT output key for unknown type: {full_key}')
       +            self._unknown[full_key] = val
       +
       +    def serialize_psbt_section_kvs(self, wr):
       +        if self.redeem_script is not None:
       +            wr(PSBTOutputType.REDEEM_SCRIPT, self.redeem_script)
       +        if self.witness_script is not None:
       +            wr(PSBTOutputType.WITNESS_SCRIPT, self.witness_script)
       +        for k in sorted(self.bip32_paths):
       +            packed_path = pack_bip32_root_fingerprint_and_int_path(*self.bip32_paths[k])
       +            wr(PSBTOutputType.BIP32_DERIVATION, packed_path, k)
       +        for full_key, val in sorted(self._unknown.items()):
       +            key_type, key = self.get_keytype_and_key_from_fullkey(full_key)
       +            wr(key_type, val, key=key)
       +
       +    def combine_with_other_txout(self, other_txout: 'TxOutput') -> None:
       +        assert self.scriptpubkey == other_txout.scriptpubkey
       +        if not isinstance(other_txout, PartialTxOutput):
       +            return
       +        if other_txout.redeem_script is not None:
       +            self.redeem_script = other_txout.redeem_script
       +        if other_txout.witness_script is not None:
       +            self.witness_script = other_txout.witness_script
       +        self.bip32_paths.update(other_txout.bip32_paths)
       +        self._unknown.update(other_txout._unknown)
       +
       +
       +class PartialTransaction(Transaction):
       +
       +    def __init__(self, raw_unsigned_tx):
       +        Transaction.__init__(self, raw_unsigned_tx)
       +        self.xpubs = {}  # type: Dict[BIP32Node, Tuple[bytes, Sequence[int]]]  # intermediate bip32node -> (xfp, der_prefix)
       +        self._inputs = []  # type: List[PartialTxInput]
       +        self._outputs = []  # type: List[PartialTxOutput]
       +        self._unknown = {}  # type: Dict[bytes, bytes]
       +
       +    def to_json(self) -> dict:
       +        d = super().to_json()
       +        d.update({
       +            'xpubs': {bip32node.to_xpub(): (xfp.hex(), bip32.convert_bip32_intpath_to_strpath(path))
       +                      for bip32node, (xfp, path) in self.xpubs.items()},
       +            'unknown_psbt_fields': {key.hex(): val.hex() for key, val in self._unknown.items()},
       +        })
       +        return d
       +
       +    @classmethod
       +    def from_tx(cls, tx: Transaction) -> 'PartialTransaction':
       +        res = cls(None)
       +        res._inputs = [PartialTxInput.from_txin(txin) for txin in tx.inputs()]
       +        res._outputs = [PartialTxOutput.from_txout(txout) for txout in tx.outputs()]
       +        res.version = tx.version
       +        res.locktime = tx.locktime
       +        return res
       +
       +    @classmethod
       +    def from_raw_psbt(cls, raw) -> 'PartialTransaction':
       +        # auto-detect and decode Base64 and Hex.
       +        if raw[0:10].lower() in (b'70736274ff', '70736274ff'):  # hex
       +            raw = bytes.fromhex(raw.strip())
       +        elif raw[0:6] in (b'cHNidP', 'cHNidP'):  # base64
       +            raw = base64.b64decode(raw)
       +        if not isinstance(raw, (bytes, bytearray)) or raw[0:5] != b'psbt\xff':
       +            raise BadHeaderMagic("bad magic")
       +
       +        tx = None  # type: Optional[PartialTransaction]
       +
       +        # We parse the raw stream twice. The first pass is used to find the
       +        # PSBT_GLOBAL_UNSIGNED_TX key in the global section and set 'tx'.
       +        # The second pass does everything else.
       +        with io.BytesIO(raw[5:]) as fd:  # parsing "first pass"
       +            while True:
       +                try:
       +                    kt, key, val = PSBTSection.get_next_kv_from_fd(fd)
       +                except StopIteration:
       +                    break
       +                try:
       +                    kt = PSBTGlobalType(kt)
       +                except ValueError:
       +                    pass  # unknown type
       +                if kt == PSBTGlobalType.UNSIGNED_TX:
       +                    if tx is not None:
       +                        raise SerializationError(f"duplicate key: {repr(kt)}")
       +                    if key: raise SerializationError(f"key for {repr(kt)} must be empty")
       +                    unsigned_tx = Transaction(val.hex())
       +                    for txin in unsigned_tx.inputs():
       +                        if txin.script_sig or txin.witness:
       +                            raise SerializationError(f"PSBT {repr(kt)} must have empty scriptSigs and witnesses")
       +                    tx = PartialTransaction.from_tx(unsigned_tx)
       +
       +        if tx is None:
       +            raise SerializationError(f"PSBT missing required global section PSBT_GLOBAL_UNSIGNED_TX")
       +
       +        with io.BytesIO(raw[5:]) as fd:  # parsing "second pass"
       +            # global section
       +            while True:
       +                try:
       +                    kt, key, val = PSBTSection.get_next_kv_from_fd(fd)
       +                except StopIteration:
       +                    break
       +                try:
       +                    kt = PSBTGlobalType(kt)
       +                except ValueError:
       +                    pass  # unknown type
       +                if DEBUG_PSBT_PARSING: print(f"{repr(kt)} {key.hex()} {val.hex()}")
       +                if kt == PSBTGlobalType.UNSIGNED_TX:
       +                    pass  # already handled during "first" parsing pass
       +                elif kt == PSBTGlobalType.XPUB:
       +                    bip32node = BIP32Node.from_bytes(key)
       +                    if bip32node in tx.xpubs:
       +                        raise SerializationError(f"duplicate key: {repr(kt)}")
       +                    xfp, path = unpack_bip32_root_fingerprint_and_int_path(val)
       +                    if bip32node.depth != len(path):
       +                        raise SerializationError(f"PSBT global xpub has mismatching depth ({bip32node.depth}) "
       +                                                 f"and derivation prefix len ({len(path)})")
       +                    child_number_of_xpub = int.from_bytes(bip32node.child_number, 'big')
       +                    if not ((bip32node.depth == 0 and child_number_of_xpub == 0)
       +                            or (bip32node.depth != 0 and child_number_of_xpub == path[-1])):
       +                        raise SerializationError(f"PSBT global xpub has inconsistent child_number and derivation prefix")
       +                    tx.xpubs[bip32node] = xfp, path
       +                elif kt == PSBTGlobalType.VERSION:
       +                    if len(val) > 4:
       +                        raise SerializationError(f"value for {repr(kt)} has unexpected length: {len(val)} > 4")
       +                    psbt_version = int.from_bytes(val, byteorder='little', signed=False)
       +                    if psbt_version > 0:
       +                        raise SerializationError(f"Only PSBTs with version 0 are supported. Found version: {psbt_version}")
       +                    if key: raise SerializationError(f"key for {repr(kt)} must be empty")
       +                else:
       +                    full_key = PSBTSection.get_fullkey_from_keytype_and_key(kt, key)
       +                    if full_key in tx._unknown:
       +                        raise SerializationError(f'duplicate key. PSBT global key for unknown type: {full_key}')
       +                    tx._unknown[full_key] = val
       +            try:
       +                # inputs sections
       +                for txin in tx.inputs():
       +                    if DEBUG_PSBT_PARSING: print("-> new input starts")
       +                    txin._populate_psbt_fields_from_fd(fd)
       +                # outputs sections
       +                for txout in tx.outputs():
       +                    if DEBUG_PSBT_PARSING: print("-> new output starts")
       +                    txout._populate_psbt_fields_from_fd(fd)
       +            except UnexpectedEndOfStream:
       +                raise UnexpectedEndOfStream('Unexpected end of stream. Num input and output maps provided does not match unsigned tx.') from None
       +
       +            if fd.read(1) != b'':
       +                raise SerializationError("extra junk at the end of PSBT")
       +
       +        for txin in tx.inputs():
       +            txin.validate_data()
       +
       +        return tx
       +
       +    @classmethod
       +    def from_io(cls, inputs: Sequence[PartialTxInput], outputs: Sequence[PartialTxOutput], *,
       +                locktime: int = None, version: int = None):
       +        self = cls(None)
       +        self._inputs = list(inputs)
       +        self._outputs = list(outputs)
       +        if locktime is not None:
       +            self.locktime = locktime
       +        if version is not None:
       +            self.version = version
       +        self.BIP69_sort()
       +        return self
       +
       +    def _serialize_psbt(self, fd) -> None:
       +        wr = PSBTSection.create_psbt_writer(fd)
       +        fd.write(b'psbt\xff')
       +        # global section
       +        wr(PSBTGlobalType.UNSIGNED_TX, bfh(self.serialize_to_network(include_sigs=False)))
       +        for bip32node, (xfp, path) in sorted(self.xpubs.items()):
       +            val = pack_bip32_root_fingerprint_and_int_path(xfp, path)
       +            wr(PSBTGlobalType.XPUB, val, key=bip32node.to_bytes())
       +        for full_key, val in sorted(self._unknown.items()):
       +            key_type, key = PSBTSection.get_keytype_and_key_from_fullkey(full_key)
       +            wr(key_type, val, key=key)
       +        fd.write(b'\x00')  # section-separator
       +        # input sections
       +        for inp in self._inputs:
       +            inp._serialize_psbt_section(fd)
       +        # output sections
       +        for outp in self._outputs:
       +            outp._serialize_psbt_section(fd)
       +
       +    def finalize_psbt(self) -> None:
       +        for txin in self.inputs():
       +            txin.finalize()
       +
       +    def combine_with_other_psbt(self, other_tx: 'Transaction') -> None:
       +        """Pulls in all data from other_tx we don't yet have (e.g. signatures).
       +        other_tx must be concerning the same unsigned tx.
       +        """
       +        if self.serialize_to_network(include_sigs=False) != other_tx.serialize_to_network(include_sigs=False):
       +            raise Exception('A Combiner must not combine two different PSBTs.')
       +        # BIP-174: "The resulting PSBT must contain all of the key-value pairs from each of the PSBTs.
       +        #           The Combiner must remove any duplicate key-value pairs, in accordance with the specification."
       +        # global section
       +        if isinstance(other_tx, PartialTransaction):
       +            self.xpubs.update(other_tx.xpubs)
       +            self._unknown.update(other_tx._unknown)
       +        # input sections
       +        for txin, other_txin in zip(self.inputs(), other_tx.inputs()):
       +            txin.combine_with_other_txin(other_txin)
       +        # output sections
       +        for txout, other_txout in zip(self.outputs(), other_tx.outputs()):
       +            txout.combine_with_other_txout(other_txout)
       +        self.invalidate_ser_cache()
       +
       +    def join_with_other_psbt(self, other_tx: 'PartialTransaction') -> None:
       +        """Adds inputs and outputs from other_tx into this one."""
       +        if not isinstance(other_tx, PartialTransaction):
       +            raise Exception('Can only join partial transactions.')
       +        # make sure there are no duplicate prevouts
       +        prevouts = set()
       +        for txin in itertools.chain(self.inputs(), other_tx.inputs()):
       +            prevout_str = txin.prevout.to_str()
       +            if prevout_str in prevouts:
       +                raise Exception(f"Duplicate inputs! "
       +                                f"Transactions that spend the same prevout cannot be joined.")
       +            prevouts.add(prevout_str)
       +        # copy global PSBT section
       +        self.xpubs.update(other_tx.xpubs)
       +        self._unknown.update(other_tx._unknown)
       +        # copy and add inputs and outputs
       +        self.add_inputs(list(other_tx.inputs()))
       +        self.add_outputs(list(other_tx.outputs()))
       +        self.remove_signatures()
       +        self.invalidate_ser_cache()
       +
       +    def inputs(self) -> Sequence[PartialTxInput]:
       +        return self._inputs
       +
       +    def outputs(self) -> Sequence[PartialTxOutput]:
       +        return self._outputs
       +
       +    def add_inputs(self, inputs: List[PartialTxInput]) -> None:
       +        self._inputs.extend(inputs)
       +        self.BIP69_sort(outputs=False)
       +        self.invalidate_ser_cache()
       +
       +    def add_outputs(self, outputs: List[PartialTxOutput]) -> None:
       +        self._outputs.extend(outputs)
       +        self.BIP69_sort(inputs=False)
       +        self.invalidate_ser_cache()
       +
       +    def set_rbf(self, rbf: bool) -> None:
       +        nSequence = 0xffffffff - (2 if rbf else 1)
       +        for txin in self.inputs():
       +            txin.nsequence = nSequence
       +        self.invalidate_ser_cache()
       +
       +    def BIP69_sort(self, inputs=True, outputs=True):
       +        # NOTE: other parts of the code rely on these sorts being *stable* sorts
       +        if inputs:
       +            self._inputs.sort(key = lambda i: (i.prevout.txid, i.prevout.out_idx))
       +        if outputs:
       +            self._outputs.sort(key = lambda o: (o.value, o.scriptpubkey))
       +        self.invalidate_ser_cache()
       +
       +    def input_value(self) -> int:
       +        input_values = [txin.value_sats() for txin in self.inputs()]
       +        if any([val is None for val in input_values]):
       +            raise MissingTxInputAmount()
       +        return sum(input_values)
       +
       +    def output_value(self) -> int:
       +        return sum(o.value for o in self.outputs())
       +
       +    def get_fee(self) -> Optional[int]:
       +        try:
       +            return self.input_value() - self.output_value()
       +        except MissingTxInputAmount:
       +            return None
       +
       +    def serialize_preimage(self, txin_index: int, *,
       +                           bip143_shared_txdigest_fields: BIP143SharedTxDigestFields = None) -> str:
       +        nVersion = int_to_hex(self.version, 4)
       +        nLocktime = int_to_hex(self.locktime, 4)
       +        inputs = self.inputs()
       +        outputs = self.outputs()
       +        txin = inputs[txin_index]
       +        sighash = txin.sighash if txin.sighash is not None else SIGHASH_ALL
       +        if sighash != SIGHASH_ALL:
       +            raise Exception("only SIGHASH_ALL signing is supported!")
       +        nHashType = int_to_hex(sighash, 4)
       +        preimage_script = self.get_preimage_script(txin)
       +        if self.is_segwit_input(txin):
       +            if bip143_shared_txdigest_fields is None:
       +                bip143_shared_txdigest_fields = self._calc_bip143_shared_txdigest_fields()
       +            hashPrevouts = bip143_shared_txdigest_fields.hashPrevouts
       +            hashSequence = bip143_shared_txdigest_fields.hashSequence
       +            hashOutputs = bip143_shared_txdigest_fields.hashOutputs
       +            outpoint = txin.prevout.serialize_to_network().hex()
       +            scriptCode = var_int(len(preimage_script) // 2) + preimage_script
       +            amount = int_to_hex(txin.value_sats(), 8)
       +            nSequence = int_to_hex(txin.nsequence, 4)
       +            preimage = nVersion + hashPrevouts + hashSequence + outpoint + scriptCode + amount + nSequence + hashOutputs + nLocktime + nHashType
       +        else:
       +            txins = var_int(len(inputs)) + ''.join(self.serialize_input(txin, preimage_script if txin_index==k else '')
       +                                                   for k, txin in enumerate(inputs))
       +            txouts = var_int(len(outputs)) + ''.join(o.serialize_to_network().hex() for o in outputs)
       +            preimage = nVersion + txins + txouts + nLocktime + nHashType
       +        return preimage
       +
       +    def sign(self, keypairs) -> None:
       +        # keypairs:  pubkey_hex -> (secret_bytes, is_compressed)
       +        bip143_shared_txdigest_fields = self._calc_bip143_shared_txdigest_fields()
       +        for i, txin in enumerate(self.inputs()):
       +            pubkeys = [pk.hex() for pk in txin.pubkeys]
       +            for pubkey in pubkeys:
       +                if txin.is_complete():
       +                    break
       +                if pubkey not in keypairs:
       +                    continue
       +                _logger.info(f"adding signature for {pubkey}")
       +                sec, compressed = keypairs[pubkey]
       +                sig = self.sign_txin(i, sec, bip143_shared_txdigest_fields=bip143_shared_txdigest_fields)
       +                self.add_signature_to_txin(txin_idx=i, signing_pubkey=pubkey, sig=sig)
       +
       +        _logger.debug(f"is_complete {self.is_complete()}")
       +        self.invalidate_ser_cache()
       +
       +    def sign_txin(self, txin_index, privkey_bytes, *, bip143_shared_txdigest_fields=None) -> str:
       +        txin = self.inputs()[txin_index]
       +        txin.validate_data(for_signing=True)
       +        pre_hash = sha256d(bfh(self.serialize_preimage(txin_index,
       +                                                       bip143_shared_txdigest_fields=bip143_shared_txdigest_fields)))
       +        privkey = ecc.ECPrivkey(privkey_bytes)
       +        sig = privkey.sign_transaction(pre_hash)
       +        sig = bh2u(sig) + '01'  # SIGHASH_ALL
       +        return sig
       +
       +    def is_complete(self) -> bool:
       +        return all([txin.is_complete() for txin in self.inputs()])
       +
       +    def signature_count(self) -> Tuple[int, int]:
       +        s = 0  # "num Sigs we have"
       +        r = 0  # "Required"
       +        for txin in self.inputs():
       +            if txin.prevout.is_coinbase():
       +                continue
       +            signatures = list(txin.part_sigs.values())
       +            s += len(signatures)
       +            r += txin.num_sig
       +        return s, r
       +
       +    def serialize(self) -> str:
       +        """Returns PSBT as base64 text, or raw hex of network tx (if complete)."""
       +        self.finalize_psbt()
       +        if self.is_complete():
       +            return Transaction.serialize(self)
       +        return self._serialize_as_base64()
       +
       +    def serialize_as_bytes(self, *, force_psbt: bool = False) -> bytes:
       +        """Returns PSBT as raw bytes, or raw bytes of network tx (if complete)."""
       +        self.finalize_psbt()
       +        if force_psbt or not self.is_complete():
       +            with io.BytesIO() as fd:
       +                self._serialize_psbt(fd)
       +                return fd.getvalue()
       +        else:
       +            return Transaction.serialize_as_bytes(self)
       +
       +    def _serialize_as_base64(self) -> str:
       +        raw_bytes = self.serialize_as_bytes()
       +        return base64.b64encode(raw_bytes).decode('ascii')
       +
       +    def update_signatures(self, signatures: Sequence[str]):
       +        """Add new signatures to a transaction
       +
       +        `signatures` is expected to be a list of sigs with signatures[i]
       +        intended for self._inputs[i].
       +        This is used by the Trezor, KeepKey and Safe-T plugins.
       +        """
       +        if self.is_complete():
       +            return
       +        if len(self.inputs()) != len(signatures):
       +            raise Exception('expected {} signatures; got {}'.format(len(self.inputs()), len(signatures)))
       +        for i, txin in enumerate(self.inputs()):
       +            pubkeys = [pk.hex() for pk in txin.pubkeys]
       +            sig = signatures[i]
       +            if bfh(sig) in list(txin.part_sigs.values()):
       +                continue
       +            pre_hash = sha256d(bfh(self.serialize_preimage(i)))
       +            sig_string = ecc.sig_string_from_der_sig(bfh(sig[:-2]))
       +            for recid in range(4):
       +                try:
       +                    public_key = ecc.ECPubkey.from_sig_string(sig_string, recid, pre_hash)
       +                except ecc.InvalidECPointException:
       +                    # the point might not be on the curve for some recid values
       +                    continue
       +                pubkey_hex = public_key.get_public_key_hex(compressed=True)
       +                if pubkey_hex in pubkeys:
       +                    try:
       +                        public_key.verify_message_hash(sig_string, pre_hash)
       +                    except Exception:
       +                        _logger.exception('')
       +                        continue
       +                    _logger.info(f"adding sig: txin_idx={i}, signing_pubkey={pubkey_hex}, sig={sig}")
       +                    self.add_signature_to_txin(txin_idx=i, signing_pubkey=pubkey_hex, sig=sig)
       +                    break
       +        # redo raw
       +        self.invalidate_ser_cache()
       +
       +    def add_signature_to_txin(self, *, txin_idx: int, signing_pubkey: str, sig: str):
       +        txin = self._inputs[txin_idx]
       +        txin.part_sigs[bfh(signing_pubkey)] = bfh(sig)
       +        # force re-serialization
       +        txin.script_sig = None
       +        txin.witness = None
       +        self.invalidate_ser_cache()
       +
       +    def add_info_from_wallet(self, wallet: 'Abstract_Wallet', *,
       +                             include_xpubs_and_full_paths: bool = False) -> None:
       +        if self.is_complete():
       +            return
       +        only_der_suffix = not include_xpubs_and_full_paths
       +        # only include xpubs for multisig wallets; currently only they need it in practice
       +        # note: coldcard fw have a limitation that if they are included then all
       +        #       inputs are assumed to be multisig... https://github.com/spesmilo/electrum/pull/5440#issuecomment-549504761
       +        # note: trezor plugin needs xpubs included, if there are multisig inputs/change_outputs
       +        from .wallet import Multisig_Wallet
       +        if include_xpubs_and_full_paths and isinstance(wallet, Multisig_Wallet):
       +            from .keystore import Xpub
       +            for ks in wallet.get_keystores():
       +                if isinstance(ks, Xpub):
       +                    fp_bytes, der_full = ks.get_fp_and_derivation_to_be_used_in_partial_tx(der_suffix=[],
       +                                                                                           only_der_suffix=only_der_suffix)
       +                    xpub = ks.get_xpub_to_be_used_in_partial_tx(only_der_suffix=only_der_suffix)
       +                    bip32node = BIP32Node.from_xkey(xpub)
       +                    self.xpubs[bip32node] = (fp_bytes, der_full)
       +        for txin in self.inputs():
       +            wallet.add_input_info(txin, only_der_suffix=only_der_suffix)
       +        for txout in self.outputs():
       +            wallet.add_output_info(txout, only_der_suffix=only_der_suffix)
       +
       +    def remove_xpubs_and_bip32_paths(self) -> None:
       +        self.xpubs.clear()
       +        for txin in self.inputs():
       +            txin.bip32_paths.clear()
       +        for txout in self.outputs():
       +            txout.bip32_paths.clear()
       +
       +    def prepare_for_export_for_coinjoin(self) -> None:
       +        """Removes all sensitive details."""
       +        # globals
       +        self.xpubs.clear()
       +        self._unknown.clear()
       +        # inputs
       +        for txin in self.inputs():
       +            txin.bip32_paths.clear()
       +        # outputs
       +        for txout in self.outputs():
       +            txout.redeem_script = None
       +            txout.witness_script = None
       +            txout.bip32_paths.clear()
       +            txout._unknown.clear()
       +
       +    def convert_all_utxos_to_witness_utxos(self) -> None:
       +        """Replaces all NON-WITNESS-UTXOs with WITNESS-UTXOs.
       +        This will likely make an exported PSBT invalid spec-wise,
       +        but it makes e.g. QR codes significantly smaller.
       +        """
       +        for txin in self.inputs():
       +            txin.convert_utxo_to_witness_utxo()
       +
       +    def is_there_risk_of_burning_coins_as_fees(self) -> bool:
       +        """Returns whether there is risk of burning coins as fees if we sign.
       +
       +        Note:
       +            - legacy sighash does not commit to any input amounts
       +            - BIP-0143 sighash only commits to the *corresponding* input amount
       +            - BIP-taproot sighash commits to *all* input amounts
       +        """
       +        for txin in self.inputs():
       +            # if we have full previous tx, we *know* the input amount
       +            if txin.utxo:
       +                continue
       +            # if we have just the previous output, we only have guarantees if
       +            # the sighash commits to this data
       +            if txin.witness_utxo and Transaction.is_segwit_input(txin):
       +                continue
       +            return True
       +        return False
       +
       +    def remove_signatures(self):
       +        for txin in self.inputs():
       +            txin.part_sigs = {}
       +            txin.script_sig = None
       +            txin.witness = None
       +        assert not self.is_complete()
       +        self.invalidate_ser_cache()
       +
       +
       +def pack_bip32_root_fingerprint_and_int_path(xfp: bytes, path: Sequence[int]) -> bytes:
       +    if len(xfp) != 4:
       +        raise Exception(f'unexpected xfp length. xfp={xfp}')
       +    return xfp + b''.join(i.to_bytes(4, byteorder='little', signed=False) for i in path)
       +
       +
       +def unpack_bip32_root_fingerprint_and_int_path(path: bytes) -> Tuple[bytes, Sequence[int]]:
       +    if len(path) % 4 != 0:
       +        raise Exception(f'unexpected packed path length. path={path.hex()}')
       +    xfp = path[0:4]
       +    int_path = [int.from_bytes(b, byteorder='little', signed=False) for b in chunks(path[4:], 4)]
       +    return xfp, int_path
 (DIR) diff --git a/electrum/util.py b/electrum/util.py
       t@@ -256,9 +256,11 @@ class Fiat(object):
        class MyEncoder(json.JSONEncoder):
            def default(self, obj):
                # note: this does not get called for namedtuples :(  https://bugs.python.org/issue30343
       -        from .transaction import Transaction
       +        from .transaction import Transaction, TxOutput
                if isinstance(obj, Transaction):
       -            return obj.as_dict()
       +            return obj.serialize()
       +        if isinstance(obj, TxOutput):
       +            return obj.to_legacy_tuple()
                if isinstance(obj, Satoshis):
                    return str(obj)
                if isinstance(obj, Fiat):
 (DIR) diff --git a/electrum/wallet.py b/electrum/wallet.py
       t@@ -38,7 +38,7 @@ import operator
        from functools import partial
        from numbers import Number
        from decimal import Decimal
       -from typing import TYPE_CHECKING, List, Optional, Tuple, Union, NamedTuple, Sequence
       +from typing import TYPE_CHECKING, List, Optional, Tuple, Union, NamedTuple, Sequence, Dict, Any
        
        from .i18n import _
        from .bip32 import BIP32Node
       t@@ -50,7 +50,7 @@ from .util import (NotEnoughFunds, UserCancelled, profiler,
                           Fiat, bfh, bh2u, TxMinedInfo, quantize_feerate, create_bip21_uri, OrderedDictWithIndex)
        from .util import PR_TYPE_ONCHAIN, PR_TYPE_LN
        from .simple_config import SimpleConfig
       -from .bitcoin import (COIN, TYPE_ADDRESS, is_address, address_to_script,
       +from .bitcoin import (COIN, is_address, address_to_script,
                              is_minikey, relayfee, dust_threshold)
        from .crypto import sha256d
        from . import keystore
       t@@ -58,7 +58,8 @@ from .keystore import load_keystore, Hardware_KeyStore, KeyStore
        from .util import multisig_type
        from .storage import StorageEncryptionVersion, WalletStorage
        from . import transaction, bitcoin, coinchooser, paymentrequest, ecc, bip32
       -from .transaction import Transaction, TxOutput, TxOutputHwInfo
       +from .transaction import (Transaction, TxInput, UnknownTxinType, TxOutput,
       +                          PartialTransaction, PartialTxInput, PartialTxOutput, TxOutpoint)
        from .plugin import run_hook
        from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL,
                                           TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE)
       t@@ -85,36 +86,41 @@ TX_STATUS = [
        ]
        
        
       -def append_utxos_to_inputs(inputs, network: 'Network', pubkey, txin_type, imax):
       -    if txin_type != 'p2pk':
       +def _append_utxos_to_inputs(inputs: List[PartialTxInput], network: 'Network', pubkey, txin_type, imax):
       +    if txin_type in ('p2pkh', 'p2wpkh', 'p2wpkh-p2sh'):
                address = bitcoin.pubkey_to_address(txin_type, pubkey)
                scripthash = bitcoin.address_to_scripthash(address)
       -    else:
       +    elif txin_type == 'p2pk':
                script = bitcoin.public_key_to_p2pk_script(pubkey)
                scripthash = bitcoin.script_to_scripthash(script)
       -        address = '(pubkey)'
       +        address = None
       +    else:
       +        raise Exception(f'unexpected txin_type to sweep: {txin_type}')
        
            u = network.run_from_another_thread(network.listunspent_for_scripthash(scripthash))
            for item in u:
                if len(inputs) >= imax:
                    break
       -        item['address'] = address
       -        item['type'] = txin_type
       -        item['prevout_hash'] = item['tx_hash']
       -        item['prevout_n'] = int(item['tx_pos'])
       -        item['pubkeys'] = [pubkey]
       -        item['x_pubkeys'] = [pubkey]
       -        item['signatures'] = [None]
       -        item['num_sig'] = 1
       -        inputs.append(item)
       +        prevout_str = item['tx_hash'] + ':%d' % item['tx_pos']
       +        prevout = TxOutpoint.from_str(prevout_str)
       +        utxo = PartialTxInput(prevout=prevout)
       +        utxo._trusted_value_sats = int(item['value'])
       +        utxo._trusted_address = address
       +        utxo.block_height = int(item['height'])
       +        utxo.script_type = txin_type
       +        utxo.pubkeys = [bfh(pubkey)]
       +        utxo.num_sig = 1
       +        if txin_type == 'p2wpkh-p2sh':
       +            utxo.redeem_script = bfh(bitcoin.p2wpkh_nested_script(pubkey))
       +        inputs.append(utxo)
        
        def sweep_preparations(privkeys, network: 'Network', imax=100):
        
            def find_utxos_for_privkey(txin_type, privkey, compressed):
                pubkey = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed)
       -        append_utxos_to_inputs(inputs, network, pubkey, txin_type, imax)
       +        _append_utxos_to_inputs(inputs, network, pubkey, txin_type, imax)
                keypairs[pubkey] = privkey, compressed
       -    inputs = []
       +    inputs = []  # type: List[PartialTxInput]
            keypairs = {}
            for sec in privkeys:
                txin_type, privkey, compressed = bitcoin.deserialize_privkey(sec)
       t@@ -134,24 +140,27 @@ def sweep_preparations(privkeys, network: 'Network', imax=100):
            return inputs, keypairs
        
        
       -def sweep(privkeys, network: 'Network', config: 'SimpleConfig', recipient, fee=None, imax=100,
       -          *, locktime=None, tx_version=None):
       +def sweep(privkeys, *, network: 'Network', config: 'SimpleConfig',
       +          to_address: str, fee: int = None, imax=100,
       +          locktime=None, tx_version=None) -> PartialTransaction:
            inputs, keypairs = sweep_preparations(privkeys, network, imax)
       -    total = sum(i.get('value') for i in inputs)
       +    total = sum(txin.value_sats() for txin in inputs)
            if fee is None:
       -        outputs = [TxOutput(TYPE_ADDRESS, recipient, total)]
       -        tx = Transaction.from_io(inputs, outputs)
       +        outputs = [PartialTxOutput(scriptpubkey=bfh(bitcoin.address_to_script(to_address)),
       +                                   value=total)]
       +        tx = PartialTransaction.from_io(inputs, outputs)
                fee = config.estimate_fee(tx.estimated_size())
            if total - fee < 0:
                raise Exception(_('Not enough funds on address.') + '\nTotal: %d satoshis\nFee: %d'%(total, fee))
            if total - fee < dust_threshold(network):
                raise Exception(_('Not enough funds on address.') + '\nTotal: %d satoshis\nFee: %d\nDust Threshold: %d'%(total, fee, dust_threshold(network)))
        
       -    outputs = [TxOutput(TYPE_ADDRESS, recipient, total - fee)]
       +    outputs = [PartialTxOutput(scriptpubkey=bfh(bitcoin.address_to_script(to_address)),
       +                               value=total - fee)]
            if locktime is None:
                locktime = get_locktime_for_new_transaction(network)
        
       -    tx = Transaction.from_io(inputs, outputs, locktime=locktime, version=tx_version)
       +    tx = PartialTransaction.from_io(inputs, outputs, locktime=locktime, version=tx_version)
            tx.set_rbf(True)
            tx.sign(keypairs)
            return tx
       t@@ -231,9 +240,13 @@ class Abstract_Wallet(AddressSynchronizer):
                self.receive_requests      = storage.get('payment_requests', {})
                self.invoices              = storage.get('invoices', {})
                # convert invoices
       +        # TODO invoices being these contextual dicts even internally,
       +        #      where certain keys are only present depending on values of other keys...
       +        #      it's horrible. we need to change this, at least for the internal representation,
       +        #      to something that can be typed.
                for invoice_key, invoice in self.invoices.items():
                    if invoice.get('type') == PR_TYPE_ONCHAIN:
       -                outputs = [TxOutput(*output) for output in invoice.get('outputs')]
       +                outputs = [PartialTxOutput.from_legacy_tuple(*output) for output in invoice.get('outputs')]
                        invoice['outputs'] = outputs
                self.calc_unused_change_addresses()
                # save wallet type the first time
       t@@ -272,6 +285,8 @@ class Abstract_Wallet(AddressSynchronizer):
        
            def stop_threads(self):
                super().stop_threads()
       +        if any([ks.is_requesting_to_be_rewritten_to_wallet_file for ks in self.get_keystores()]):
       +            self.save_keystore()
                self.storage.write()
        
            def set_up_to_date(self, b):
       t@@ -305,7 +320,10 @@ class Abstract_Wallet(AddressSynchronizer):
            def get_master_public_key(self):
                return None
        
       -    def basename(self):
       +    def get_master_public_keys(self):
       +        return []
       +
       +    def basename(self) -> str:
                return os.path.basename(self.storage.path)
        
            def test_addresses_sanity(self):
       t@@ -392,15 +410,28 @@ class Abstract_Wallet(AddressSynchronizer):
            def is_change(self, address) -> bool:
                if not self.is_mine(address):
                    return False
       -        return self.get_address_index(address)[0]
       +        return self.get_address_index(address)[0] == 1
        
            def get_address_index(self, address):
                raise NotImplementedError()
        
       -    def get_redeem_script(self, address):
       +    def get_redeem_script(self, address: str) -> Optional[str]:
       +        txin_type = self.get_txin_type(address)
       +        if txin_type in ('p2pkh', 'p2wpkh', 'p2pk'):
       +            return None
       +        if txin_type == 'p2wpkh-p2sh':
       +            pubkey = self.get_public_key(address)
       +            return bitcoin.p2wpkh_nested_script(pubkey)
       +        raise UnknownTxinType(f'unexpected txin_type {txin_type}')
       +
       +    def get_witness_script(self, address: str) -> Optional[str]:
                return None
        
       -    def export_private_key(self, address, password):
       +    def get_txin_type(self, address: str) -> str:
       +        """Return script type of wallet address."""
       +        raise NotImplementedError()
       +
       +    def export_private_key(self, address, password) -> str:
                if self.is_watching_only():
                    raise Exception(_("This is a watching-only wallet"))
                if not is_address(address):
       t@@ -410,19 +441,24 @@ class Abstract_Wallet(AddressSynchronizer):
                index = self.get_address_index(address)
                pk, compressed = self.keystore.get_private_key(index, password)
                txin_type = self.get_txin_type(address)
       -        redeem_script = self.get_redeem_script(address)
                serialized_privkey = bitcoin.serialize_privkey(pk, compressed, txin_type)
       -        return serialized_privkey, redeem_script
       +        return serialized_privkey
        
            def get_public_keys(self, address):
                return [self.get_public_key(address)]
        
       +    def get_public_keys_with_deriv_info(self, address: str) -> Dict[str, Tuple[KeyStore, Sequence[int]]]:
       +        """Returns a map: pubkey_hex -> (keystore, derivation_suffix)"""
       +        return {}
       +
            def is_found(self):
                return True
                #return self.history.values() != [[]] * len(self.history)
        
            def get_tx_info(self, tx) -> TxWalletDetails:
                is_relevant, is_mine, v, fee = self.get_wallet_delta(tx)
       +        if fee is None and isinstance(tx, PartialTransaction):
       +            fee = tx.get_fee()
                exp_n = None
                can_broadcast = False
                can_bump = False
       t@@ -480,7 +516,7 @@ class Abstract_Wallet(AddressSynchronizer):
                    mempool_depth_bytes=exp_n,
                )
        
       -    def get_spendable_coins(self, domain, *, nonlocal_only=False):
       +    def get_spendable_coins(self, domain, *, nonlocal_only=False) -> Sequence[PartialTxInput]:
                confirmed_only = self.config.get('confirmed_only', False)
                utxos = self.get_utxos(domain,
                                       excluded_addresses=self.frozen_addresses,
       t@@ -490,10 +526,10 @@ class Abstract_Wallet(AddressSynchronizer):
                utxos = [utxo for utxo in utxos if not self.is_frozen_coin(utxo)]
                return utxos
        
       -    def get_receiving_addresses(self, *, slice_start=None, slice_stop=None) -> Sequence:
       +    def get_receiving_addresses(self, *, slice_start=None, slice_stop=None) -> Sequence[str]:
                raise NotImplementedError()  # implemented by subclasses
        
       -    def get_change_addresses(self, *, slice_start=None, slice_stop=None) -> Sequence:
       +    def get_change_addresses(self, *, slice_start=None, slice_stop=None) -> Sequence[str]:
                raise NotImplementedError()  # implemented by subclasses
        
            def dummy_address(self):
       t@@ -536,7 +572,7 @@ class Abstract_Wallet(AddressSynchronizer):
                        'txpos_in_block': hist_item.tx_mined_status.txpos,
                    }
        
       -    def create_invoice(self, outputs: List[TxOutput], message, pr, URI):
       +    def create_invoice(self, outputs: List[PartialTxOutput], message, pr, URI):
                if '!' in (x.value for x in outputs):
                    amount = '!'
                else:
       t@@ -676,9 +712,9 @@ class Abstract_Wallet(AddressSynchronizer):
                    tx_fee = item['fee_sat']
                    item['fee'] = Satoshis(tx_fee) if tx_fee is not None else None
                    if show_addresses:
       -                item['inputs'] = list(map(lambda x: dict((k, x[k]) for k in ('prevout_hash', 'prevout_n')), tx.inputs()))
       -                item['outputs'] = list(map(lambda x:{'address':x.address, 'value':Satoshis(x.value)},
       -                                           tx.get_outputs_for_UI()))
       +                item['inputs'] = list(map(lambda x: x.to_json(), tx.inputs()))
       +                item['outputs'] = list(map(lambda x: {'address': x.get_ui_address_str(), 'value': Satoshis(x.value)},
       +                                           tx.outputs()))
                    # fixme: use in and out values
                    value = item['bc_value'].value
                    if value < 0:
       t@@ -756,10 +792,10 @@ class Abstract_Wallet(AddressSynchronizer):
                    item['capital_gain'] = Fiat(cg, fx.ccy)
                return item
        
       -    def get_label(self, tx_hash):
       +    def get_label(self, tx_hash: str) -> str:
                return self.labels.get(tx_hash, '') or self.get_default_label(tx_hash)
        
       -    def get_default_label(self, tx_hash):
       +    def get_default_label(self, tx_hash) -> str:
                if not self.db.get_txi_addresses(tx_hash):
                    labels = []
                    for addr in self.db.get_txo_addresses(tx_hash):
       t@@ -876,34 +912,32 @@ class Abstract_Wallet(AddressSynchronizer):
                max_change = self.max_change_outputs if self.multiple_change else 1
                return change_addrs[:max_change]
        
       -    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:
                # check outputs
                i_max = None
                for i, o in enumerate(outputs):
       -            if o.type == TYPE_ADDRESS:
       -                if not is_address(o.address):
       -                    raise Exception("Invalid bitcoin address: {}".format(o.address))
                    if o.value == '!':
                        if i_max is not None:
                            raise Exception("More than one output set to spend max")
                        i_max = i
        
       -        if fixed_fee is None and self.config.fee_per_kb() is None:
       +        if fee is None and self.config.fee_per_kb() is None:
                    raise NoDynamicFeeEstimates()
        
                for item in coins:
                    self.add_input_info(item)
        
                # Fee estimator
       -        if fixed_fee is None:
       +        if fee is None:
                    fee_estimator = self.config.estimate_fee
       -        elif isinstance(fixed_fee, Number):
       -            fee_estimator = lambda size: fixed_fee
       -        elif callable(fixed_fee):
       -            fee_estimator = fixed_fee
       +        elif isinstance(fee, Number):
       +            fee_estimator = lambda size: fee
       +        elif callable(fee):
       +            fee_estimator = fee
                else:
       -            raise Exception('Invalid argument fixed_fee: %s' % fixed_fee)
       +            raise Exception(f'Invalid argument fee: {fee}')
        
                if i_max is None:
                    # Let the coin chooser select the coins to spend
       t@@ -912,12 +946,10 @@ class Abstract_Wallet(AddressSynchronizer):
                    base_tx = self.get_unconfirmed_base_tx_for_batching()
                    if self.config.get('batch_rbf', False) and base_tx:
                        # make sure we don't try to spend change from the tx-to-be-replaced:
       -                coins = [c for c in coins if c['prevout_hash'] != base_tx.txid()]
       +                coins = [c for c in coins if c.prevout.txid.hex() != base_tx.txid()]
                        is_local = self.get_tx_height(base_tx.txid()).height == TX_HEIGHT_LOCAL
       -                base_tx = Transaction(base_tx.serialize())
       -                base_tx.deserialize(force_full_parse=True)
       -                base_tx.remove_signatures()
       -                base_tx.add_inputs_info(self)
       +                base_tx = PartialTransaction.from_tx(base_tx)
       +                base_tx.add_info_from_wallet(self)
                        base_tx_fee = base_tx.get_fee()
                        relayfeerate = Decimal(self.relayfee()) / 1000
                        original_fee_estimator = fee_estimator
       t@@ -935,8 +967,12 @@ class Abstract_Wallet(AddressSynchronizer):
                        old_change_addrs = []
                    # change address. if empty, coin_chooser will set it
                    change_addrs = self.get_change_addresses_for_new_transaction(change_addr or old_change_addrs)
       -            tx = coin_chooser.make_tx(coins, txi, outputs[:] + txo, change_addrs,
       -                                      fee_estimator, self.dust_threshold())
       +            tx = coin_chooser.make_tx(coins=coins,
       +                                      inputs=txi,
       +                                      outputs=list(outputs) + txo,
       +                                      change_addrs=change_addrs,
       +                                      fee_estimator_vb=fee_estimator,
       +                                      dust_threshold=self.dust_threshold())
                else:
                    # "spend max" branch
                    # note: This *will* spend inputs with negative effective value (if there are any).
       t@@ -945,38 +981,43 @@ class Abstract_Wallet(AddressSynchronizer):
                    #       forever. see #5433
                    # note: Actually it might be the case that not all UTXOs from the wallet are
                    #       being spent if the user manually selected UTXOs.
       -            sendable = sum(map(lambda x:x['value'], coins))
       -            outputs[i_max] = outputs[i_max]._replace(value=0)
       -            tx = Transaction.from_io(coins, outputs[:])
       +            sendable = sum(map(lambda c: c.value_sats(), coins))
       +            outputs[i_max].value = 0
       +            tx = PartialTransaction.from_io(list(coins), list(outputs))
                    fee = fee_estimator(tx.estimated_size())
                    amount = sendable - tx.output_value() - fee
                    if amount < 0:
                        raise NotEnoughFunds()
       -            outputs[i_max] = outputs[i_max]._replace(value=amount)
       -            tx = Transaction.from_io(coins, outputs[:])
       +            outputs[i_max].value = amount
       +            tx = PartialTransaction.from_io(list(coins), list(outputs))
        
                # Timelock tx to current height.
                tx.locktime = get_locktime_for_new_transaction(self.network)
       +
       +        tx.add_info_from_wallet(self)
                run_hook('make_unsigned_transaction', self, tx)
                return tx
        
       -    def mktx(self, outputs, password, fee=None, change_addr=None,
       -             domain=None, rbf=False, nonlocal_only=False, *, tx_version=None):
       +    def mktx(self, *, outputs: List[PartialTxOutput], password=None, fee=None, change_addr=None,
       +             domain=None, rbf=False, nonlocal_only=False, tx_version=None, sign=True) -> PartialTransaction:
                coins = self.get_spendable_coins(domain, nonlocal_only=nonlocal_only)
       -        tx = self.make_unsigned_transaction(coins, outputs, fee, change_addr)
       +        tx = self.make_unsigned_transaction(coins=coins,
       +                                            outputs=outputs,
       +                                            fee=fee,
       +                                            change_addr=change_addr)
                tx.set_rbf(rbf)
                if tx_version is not None:
                    tx.version = tx_version
       -        self.sign_transaction(tx, password)
       +        if sign:
       +            self.sign_transaction(tx, password)
                return tx
        
            def is_frozen_address(self, addr: str) -> bool:
                return addr in self.frozen_addresses
        
       -    def is_frozen_coin(self, utxo) -> bool:
       -        # utxo is either a txid:vout str, or a dict
       -        utxo = self._utxo_str_from_utxo(utxo)
       -        return utxo in self.frozen_coins
       +    def is_frozen_coin(self, utxo: PartialTxInput) -> bool:
       +        prevout_str = utxo.prevout.to_str()
       +        return prevout_str in self.frozen_coins
        
            def set_frozen_state_of_addresses(self, addrs, freeze: bool):
                """Set frozen state of the addresses to FREEZE, True or False"""
       t@@ -990,9 +1031,9 @@ class Abstract_Wallet(AddressSynchronizer):
                    return True
                return False
        
       -    def set_frozen_state_of_coins(self, utxos, freeze: bool):
       +    def set_frozen_state_of_coins(self, utxos: Sequence[PartialTxInput], freeze: bool):
                """Set frozen state of the utxos to FREEZE, True or False"""
       -        utxos = {self._utxo_str_from_utxo(utxo) for utxo in utxos}
       +        utxos = {utxo.prevout.to_str() for utxo in utxos}
                # FIXME take lock?
                if freeze:
                    self.frozen_coins |= set(utxos)
       t@@ -1000,15 +1041,6 @@ class Abstract_Wallet(AddressSynchronizer):
                    self.frozen_coins -= set(utxos)
                self.storage.put('frozen_coins', list(self.frozen_coins))
        
       -    @staticmethod
       -    def _utxo_str_from_utxo(utxo: Union[dict, str]) -> str:
       -        """Return a txid:vout str"""
       -        if isinstance(utxo, dict):
       -            return "{}:{}".format(utxo['prevout_hash'], utxo['prevout_n'])
       -        assert isinstance(utxo, str), f"utxo should be a str, not {type(utxo)}"
       -        # just assume it is already of the correct format
       -        return utxo
       -
            def wait_until_synchronized(self, callback=None):
                def wait_for_wallet():
                    self.set_up_to_date(False)
       t@@ -1055,7 +1087,7 @@ class Abstract_Wallet(AddressSynchronizer):
                    max_conf = max(max_conf, tx_age)
                return max_conf >= req_conf
        
       -    def bump_fee(self, *, tx: Transaction, new_fee_rate) -> Transaction:
       +    def bump_fee(self, *, tx: Transaction, new_fee_rate) -> PartialTransaction:
                """Increase the miner fee of 'tx'.
                'new_fee_rate' is the target min rate in sat/vbyte
                """
       t@@ -1097,13 +1129,11 @@ class Abstract_Wallet(AddressSynchronizer):
                tx_new.locktime = get_locktime_for_new_transaction(self.network)
                return tx_new
        
       -    def _bump_fee_through_coinchooser(self, *, tx: Transaction, new_fee_rate) -> Transaction:
       -        tx = Transaction(tx.serialize())
       -        tx.deserialize(force_full_parse=True)  # need to parse inputs
       -        tx.remove_signatures()
       -        tx.add_inputs_info(self)
       -        old_inputs = tx.inputs()[:]
       -        old_outputs = tx.outputs()[:]
       +    def _bump_fee_through_coinchooser(self, *, tx: Transaction, new_fee_rate) -> PartialTransaction:
       +        tx = PartialTransaction.from_tx(tx)
       +        tx.add_info_from_wallet(self)
       +        old_inputs = list(tx.inputs())
       +        old_outputs = list(tx.outputs())
                # change address
                old_change_addrs = [o.address for o in old_outputs if self.is_change(o.address)]
                change_addrs = self.get_change_addresses_for_new_transaction(old_change_addrs)
       t@@ -1131,18 +1161,20 @@ class Abstract_Wallet(AddressSynchronizer):
                    return self.config.estimate_fee_for_feerate(fee_per_kb=new_fee_rate*1000, size=size)
                coin_chooser = coinchooser.get_coin_chooser(self.config)
                try:
       -            return coin_chooser.make_tx(coins, old_inputs, fixed_outputs, change_addrs,
       -                                        fee_estimator, self.dust_threshold())
       +            return coin_chooser.make_tx(coins=coins,
       +                                        inputs=old_inputs,
       +                                        outputs=fixed_outputs,
       +                                        change_addrs=change_addrs,
       +                                        fee_estimator_vb=fee_estimator,
       +                                        dust_threshold=self.dust_threshold())
                except NotEnoughFunds as e:
                    raise CannotBumpFee(e)
        
       -    def _bump_fee_through_decreasing_outputs(self, *, tx: Transaction, new_fee_rate) -> Transaction:
       -        tx = Transaction(tx.serialize())
       -        tx.deserialize(force_full_parse=True)  # need to parse inputs
       -        tx.remove_signatures()
       -        tx.add_inputs_info(self)
       +    def _bump_fee_through_decreasing_outputs(self, *, tx: Transaction, new_fee_rate) -> PartialTransaction:
       +        tx = PartialTransaction.from_tx(tx)
       +        tx.add_info_from_wallet(self)
                inputs = tx.inputs()
       -        outputs = tx.outputs()
       +        outputs = list(tx.outputs())
        
                # use own outputs
                s = list(filter(lambda o: self.is_mine(o.address), outputs))
       t@@ -1165,7 +1197,7 @@ class Abstract_Wallet(AddressSynchronizer):
                    if o.value - delta >= self.dust_threshold():
                        new_output_value = o.value - delta
                        assert isinstance(new_output_value, int)
       -                outputs[i] = o._replace(value=new_output_value)
       +                outputs[i].value = new_output_value
                        delta = 0
                        break
                    else:
       t@@ -1176,48 +1208,92 @@ class Abstract_Wallet(AddressSynchronizer):
                if delta > 0:
                    raise CannotBumpFee(_('Cannot bump fee') + ': ' + _('could not find suitable outputs'))
        
       -        return Transaction.from_io(inputs, outputs)
       +        return PartialTransaction.from_io(inputs, outputs)
        
       -    def cpfp(self, tx: Transaction, fee: int) -> Optional[Transaction]:
       +    def cpfp(self, tx: Transaction, fee: int) -> Optional[PartialTransaction]:
                txid = tx.txid()
                for i, o in enumerate(tx.outputs()):
                    address, value = o.address, o.value
       -            if o.type == TYPE_ADDRESS and self.is_mine(address):
       +            if self.is_mine(address):
                        break
                else:
                    return
                coins = self.get_addr_utxo(address)
       -        item = coins.get(txid+':%d'%i)
       +        item = coins.get(TxOutpoint.from_str(txid+':%d'%i))
                if not item:
                    return
                self.add_input_info(item)
                inputs = [item]
                out_address = self.get_unused_address() or address
       -        outputs = [TxOutput(TYPE_ADDRESS, out_address, value - fee)]
       +        outputs = [PartialTxOutput.from_address_and_value(out_address, value - fee)]
                locktime = get_locktime_for_new_transaction(self.network)
       -        return Transaction.from_io(inputs, outputs, locktime=locktime)
       +        return PartialTransaction.from_io(inputs, outputs, locktime=locktime)
        
       -    def add_input_sig_info(self, txin, address):
       +    def _add_input_sig_info(self, txin: PartialTxInput, address: str, *, only_der_suffix: bool = True) -> None:
                raise NotImplementedError()  # implemented by subclasses
        
       -    def add_input_info(self, txin):
       -        address = self.get_txin_address(txin)
       -        if self.is_mine(address):
       -            txin['address'] = address
       -            txin['type'] = self.get_txin_type(address)
       -            # segwit needs value to sign
       -            if txin.get('value') is None:
       +    def _add_txinout_derivation_info(self, txinout: Union[PartialTxInput, PartialTxOutput],
       +                                     address: str, *, only_der_suffix: bool = True) -> None:
       +        pass  # implemented by subclasses
       +
       +    def _add_input_utxo_info(self, txin: PartialTxInput, address: str) -> None:
       +        if Transaction.is_segwit_input(txin):
       +            if txin.witness_utxo is None:
                        received, spent = self.get_addr_io(address)
       -                item = received.get(txin['prevout_hash']+':%d'%txin['prevout_n'])
       +                item = received.get(txin.prevout.to_str())
                        if item:
       -                    txin['value'] = item[1]
       -            self.add_input_sig_info(txin, address)
       +                    txin_value = item[1]
       +                    txin.witness_utxo = TxOutput.from_address_and_value(address, txin_value)
       +        else:  # legacy input
       +            if txin.utxo is None:
       +                # note: for hw wallets, for legacy inputs, ignore_network_issues used to be False
       +                txin.utxo = self.get_input_tx(txin.prevout.txid.hex(), ignore_network_issues=True)
       +        # If there is a NON-WITNESS UTXO, but we know input is segwit, add a WITNESS UTXO, based on it.
       +        # This could have happened if previously another wallet had put a NON-WITNESS UTXO for txin,
       +        # as they did not know if it was segwit. This switch is needed to interop with bitcoin core.
       +        if txin.utxo and Transaction.is_segwit_input(txin):
       +            txin.convert_utxo_to_witness_utxo()
       +        txin.ensure_there_is_only_one_utxo()
       +
       +    def _learn_derivation_path_for_address_from_txinout(self, txinout: Union[PartialTxInput, PartialTxOutput],
       +                                                        address: str) -> bool:
       +        """Tries to learn the derivation path for an address (potentially beyond gap limit)
       +        using data available in given txin/txout.
       +        Returns whether the address was found to be is_mine.
       +        """
       +        return False  # implemented by subclasses
       +
       +    def add_input_info(self, txin: PartialTxInput, *, only_der_suffix: bool = True) -> None:
       +        address = self.get_txin_address(txin)
       +        if not self.is_mine(address):
       +            is_mine = self._learn_derivation_path_for_address_from_txinout(txin, address)
       +            if not is_mine:
       +                return
       +        # set script_type first, as later checks might rely on it:
       +        txin.script_type = self.get_txin_type(address)
       +        self._add_input_utxo_info(txin, address)
       +        txin.num_sig = self.m if isinstance(self, Multisig_Wallet) else 1
       +        if txin.redeem_script is None:
       +            try:
       +                redeem_script_hex = self.get_redeem_script(address)
       +                txin.redeem_script = bfh(redeem_script_hex) if redeem_script_hex else None
       +            except UnknownTxinType:
       +                pass
       +        if txin.witness_script is None:
       +            try:
       +                witness_script_hex = self.get_witness_script(address)
       +                txin.witness_script = bfh(witness_script_hex) if witness_script_hex else None
       +            except UnknownTxinType:
       +                pass
       +        self._add_input_sig_info(txin, address, only_der_suffix=only_der_suffix)
        
            def can_sign(self, tx: Transaction) -> bool:
       +        if not isinstance(tx, PartialTransaction):
       +            return False
                if tx.is_complete():
                    return False
                # add info to inputs if we can; otherwise we might return a false negative:
       -        tx.add_inputs_info(self)
       +        tx.add_info_from_wallet(self)
                for k in self.get_keystores():
                    if k.can_sign(tx):
                        return True
       t@@ -1241,45 +1317,51 @@ class Abstract_Wallet(AddressSynchronizer):
                        tx = Transaction(raw_tx)
                return tx
        
       -    def add_hw_info(self, tx: Transaction) -> None:
       -        # add previous tx for hw wallets
       -        for txin in tx.inputs():
       -            tx_hash = txin['prevout_hash']
       -            # segwit inputs might not be needed for some hw wallets
       -            ignore_network_issues = Transaction.is_segwit_input(txin)
       -            txin['prev_tx'] = self.get_input_tx(tx_hash, ignore_network_issues=ignore_network_issues)
       -        # add output info for hw wallets
       -        info = {}
       -        xpubs = self.get_master_public_keys()
       -        for o in tx.outputs():
       -            if self.is_mine(o.address):
       -                index = self.get_address_index(o.address)
       -                pubkeys = self.get_public_keys(o.address)
       -                # sort xpubs using the order of pubkeys
       -                sorted_pubkeys, sorted_xpubs = zip(*sorted(zip(pubkeys, xpubs)))
       -                num_sig = self.m if isinstance(self, Multisig_Wallet) else None
       -                is_change = self.is_change(o.address)
       -                info[o.address] = TxOutputHwInfo(address_index=index,
       -                                                 sorted_xpubs=sorted_xpubs,
       -                                                 num_sig=num_sig,
       -                                                 script_type=self.txin_type,
       -                                                 is_change=is_change)
       -        tx.output_info = info
       -
       -    def sign_transaction(self, tx, password):
       +    def add_output_info(self, txout: PartialTxOutput, *, only_der_suffix: bool = True) -> None:
       +        address = txout.address
       +        if not self.is_mine(address):
       +            is_mine = self._learn_derivation_path_for_address_from_txinout(txout, address)
       +            if not is_mine:
       +                return
       +        txout.script_type = self.get_txin_type(address)
       +        txout.is_mine = True
       +        txout.is_change = self.is_change(address)
       +        if isinstance(self, Multisig_Wallet):
       +            txout.num_sig = self.m
       +        self._add_txinout_derivation_info(txout, address, only_der_suffix=only_der_suffix)
       +        if txout.redeem_script is None:
       +            try:
       +                redeem_script_hex = self.get_redeem_script(address)
       +                txout.redeem_script = bfh(redeem_script_hex) if redeem_script_hex else None
       +            except UnknownTxinType:
       +                pass
       +        if txout.witness_script is None:
       +            try:
       +                witness_script_hex = self.get_witness_script(address)
       +                txout.witness_script = bfh(witness_script_hex) if witness_script_hex else None
       +            except UnknownTxinType:
       +                pass
       +
       +    def sign_transaction(self, tx: Transaction, password) -> Optional[PartialTransaction]:
                if self.is_watching_only():
                    return
       -        tx.add_inputs_info(self)
       -        # hardware wallets require extra info
       -        if any([(isinstance(k, Hardware_KeyStore) and k.can_sign(tx)) for k in self.get_keystores()]):
       -            self.add_hw_info(tx)
       +        if not isinstance(tx, PartialTransaction):
       +            return
       +        # add info to a temporary tx copy; including xpubs
       +        # and full derivation paths as hw keystores might want them
       +        tmp_tx = copy.deepcopy(tx)
       +        tmp_tx.add_info_from_wallet(self, include_xpubs_and_full_paths=True)
                # sign. start with ready keystores.
                for k in sorted(self.get_keystores(), key=lambda ks: ks.ready_to_sign(), reverse=True):
                    try:
       -                if k.can_sign(tx):
       -                    k.sign_transaction(tx, password)
       +                if k.can_sign(tmp_tx):
       +                    k.sign_transaction(tmp_tx, password)
                    except UserCancelled:
                        continue
       +        # remove sensitive info; then copy back details from temporary tx
       +        tmp_tx.remove_xpubs_and_bip32_paths()
       +        tx.combine_with_other_psbt(tmp_tx)
       +        tx.add_info_from_wallet(self, include_xpubs_and_full_paths=False)
                return tx
        
            def try_detecting_internal_addresses_corruption(self):
       t@@ -1423,7 +1505,6 @@ class Abstract_Wallet(AddressSynchronizer):
                        self.network.trigger_callback('payment_received', self, addr, status)
        
            def make_payment_request(self, addr, amount, message, expiration):
       -        from .bitcoin import TYPE_ADDRESS
                timestamp = int(time.time())
                _id = bh2u(sha256d(addr + "%d"%timestamp))[0:10]
                return {
       t@@ -1434,12 +1515,12 @@ class Abstract_Wallet(AddressSynchronizer):
                    'address':addr,
                    'memo':message,
                    'id':_id,
       -            'outputs': [(TYPE_ADDRESS, addr, amount)]
       +            'outputs': [PartialTxOutput.from_address_and_value(addr, amount)],
                }
        
            def sign_payment_request(self, key, alias, alias_addr, password):
                req = self.receive_requests.get(key)
       -        alias_privkey = self.export_private_key(alias_addr, password)[0]
       +        alias_privkey = self.export_private_key(alias_addr, password)
                pr = paymentrequest.make_unsigned_request(req)
                paymentrequest.sign_request_with_alias(pr, alias, alias_privkey)
                req['name'] = pr.pki_data
       t@@ -1577,9 +1658,12 @@ class Abstract_Wallet(AddressSynchronizer):
                index = self.get_address_index(addr)
                return self.keystore.decrypt_message(index, message, password)
        
       -    def txin_value(self, txin):
       -        txid = txin['prevout_hash']
       -        prev_n = txin['prevout_n']
       +    def txin_value(self, txin: TxInput) -> Optional[int]:
       +        if isinstance(txin, PartialTxInput):
       +            v = txin.value_sats()
       +            if v: return v
       +        txid = txin.prevout.txid.hex()
       +        prev_n = txin.prevout.out_idx
                for addr in self.db.get_txo_addresses(txid):
                    d = self.db.get_txo_addr(txid, addr)
                    for n, v, cb in d:
       t@@ -1597,8 +1681,8 @@ class Abstract_Wallet(AddressSynchronizer):
                coins = self.get_utxos(domain)
                now = time.time()
                p = price_func(now)
       -        ap = sum(self.coin_price(coin['prevout_hash'], price_func, ccy, self.txin_value(coin)) for coin in coins)
       -        lp = sum([coin['value'] for coin in coins]) * p / Decimal(COIN)
       +        ap = sum(self.coin_price(coin.prevout.txid.hex(), price_func, ccy, self.txin_value(coin)) for coin in coins)
       +        lp = sum([coin.value_sats() for coin in coins]) * p / Decimal(COIN)
                return lp - ap
        
            def average_price(self, txid, price_func, ccy):
       t@@ -1651,6 +1735,9 @@ class Abstract_Wallet(AddressSynchronizer):
            def get_keystores(self) -> Sequence[KeyStore]:
                return [self.keystore] if self.keystore else []
        
       +    def save_keystore(self):
       +        raise NotImplementedError()
       +
        
        class Simple_Wallet(Abstract_Wallet):
            # wallet with a single keystore
       t@@ -1684,9 +1771,6 @@ class Imported_Wallet(Simple_Wallet):
        
            def load_keystore(self):
                self.keystore = load_keystore(self.storage, 'keystore') if self.storage.get('keystore') else None
       -        # fixme: a reference to addresses is needed
       -        if self.keystore:
       -            self.keystore.addresses = self.db.imported_addresses
        
            def save_keystore(self):
                self.storage.put('keystore', self.keystore.dump())
       t@@ -1706,9 +1790,6 @@ class Imported_Wallet(Simple_Wallet):
            def is_change(self, address):
                return False
        
       -    def get_master_public_keys(self):
       -        return []
       -
            def is_beyond_limit(self, address):
                return False
        
       t@@ -1795,11 +1876,11 @@ class Imported_Wallet(Simple_Wallet):
            def is_mine(self, address) -> bool:
                return self.db.has_imported_address(address)
        
       -    def get_address_index(self, address):
       +    def get_address_index(self, address) -> Optional[str]:
                # returns None if address is not mine
                return self.get_public_key(address)
        
       -    def get_public_key(self, address):
       +    def get_public_key(self, address) -> Optional[str]:
                x = self.db.get_imported_address(address)
                return x.get('pubkey') if x else None
        
       t@@ -1818,7 +1899,7 @@ class Imported_Wallet(Simple_Wallet):
                        continue
                    addr = bitcoin.pubkey_to_address(txin_type, pubkey)
                    good_addr.append(addr)
       -            self.db.add_imported_address(addr, {'type':txin_type, 'pubkey':pubkey, 'redeem_script':None})
       +            self.db.add_imported_address(addr, {'type':txin_type, 'pubkey':pubkey})
                    self.add_address(addr)
                self.save_keystore()
                if write_to_disk:
       t@@ -1832,27 +1913,22 @@ class Imported_Wallet(Simple_Wallet):
                else:
                    raise BitcoinException(str(bad_keys[0][1]))
        
       -    def get_redeem_script(self, address):
       -        d = self.db.get_imported_address(address)
       -        redeem_script = d['redeem_script']
       -        return redeem_script
       -
            def get_txin_type(self, address):
                return self.db.get_imported_address(address).get('type', 'address')
        
       -    def add_input_sig_info(self, txin, address):
       -        if self.is_watching_only():
       -            x_pubkey = 'fd' + address_to_script(address)
       -            txin['x_pubkeys'] = [x_pubkey]
       -            txin['signatures'] = [None]
       +    def _add_input_sig_info(self, txin, address, *, only_der_suffix=True):
       +        if not self.is_mine(address):
                    return
       -        if txin['type'] in ['p2pkh', 'p2wpkh', 'p2wpkh-p2sh']:
       -            pubkey = self.db.get_imported_address(address)['pubkey']
       -            txin['num_sig'] = 1
       -            txin['x_pubkeys'] = [pubkey]
       -            txin['signatures'] = [None]
       +        if txin.script_type in ('unknown', 'address'):
       +            return
       +        elif txin.script_type in ('p2pkh', 'p2wpkh', 'p2wpkh-p2sh'):
       +            pubkey = self.get_public_key(address)
       +            if not pubkey:
       +                return
       +            txin.pubkeys = [bfh(pubkey)]
                else:
       -            raise NotImplementedError('imported wallets for p2sh are not implemented')
       +            raise Exception(f'Unexpected script type: {txin.script_type}. '
       +                            f'Imported wallets are not implemented to handle this.')
        
            def pubkeys_to_address(self, pubkey):
                for addr in self.db.get_imported_addresses():
       t@@ -1862,6 +1938,7 @@ class Imported_Wallet(Simple_Wallet):
        class Deterministic_Wallet(Abstract_Wallet):
        
            def __init__(self, storage, *, config):
       +        self._ephemeral_addr_to_addr_index = {}  # type: Dict[str, Sequence[int]]
                Abstract_Wallet.__init__(self, storage, config=config)
                self.gap_limit = storage.get('gap_limit', 20)
                # generate addresses now. note that without libsecp this might block
       t@@ -1945,6 +2022,26 @@ class Deterministic_Wallet(Abstract_Wallet):
                x = self.derive_pubkeys(for_change, n)
                return self.pubkeys_to_address(x)
        
       +    def get_public_keys_with_deriv_info(self, address: str):
       +        der_suffix = self.get_address_index(address)
       +        der_suffix = [int(x) for x in der_suffix]
       +        return {k.derive_pubkey(*der_suffix): (k, der_suffix)
       +                for k in self.get_keystores()}
       +
       +    def _add_input_sig_info(self, txin, address, *, only_der_suffix=True):
       +        self._add_txinout_derivation_info(txin, address, only_der_suffix=only_der_suffix)
       +
       +    def _add_txinout_derivation_info(self, txinout, address, *, only_der_suffix=True):
       +        if not self.is_mine(address):
       +            return
       +        pubkey_deriv_info = self.get_public_keys_with_deriv_info(address)
       +        txinout.pubkeys = sorted([bfh(pk) for pk in list(pubkey_deriv_info)])
       +        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=only_der_suffix)
       +            txinout.bip32_paths[bfh(pubkey_hex)] = (fp_bytes, der_full)
       +
            def create_new_address(self, for_change=False):
                assert type(for_change) is bool
                with self.lock:
       t@@ -1995,8 +2092,16 @@ class Deterministic_Wallet(Abstract_Wallet):
                        return False
                return True
        
       -    def get_address_index(self, address):
       -        return self.db.get_address_index(address)
       +    def get_address_index(self, address) -> Optional[Sequence[int]]:
       +        return self.db.get_address_index(address) or self._ephemeral_addr_to_addr_index.get(address)
       +
       +    def _learn_derivation_path_for_address_from_txinout(self, txinout, address):
       +        for ks in self.get_keystores():
       +            pubkey, der_suffix = ks.find_my_pubkey_in_txinout(txinout, only_der_suffix=True)
       +            if der_suffix is not None:
       +                self._ephemeral_addr_to_addr_index[address] = list(der_suffix)
       +                return True
       +        return False
        
            def get_master_public_keys(self):
                return [self.get_master_public_key()]
       t@@ -2017,7 +2122,7 @@ class Simple_Deterministic_Wallet(Simple_Wallet, Deterministic_Wallet):
        
            def get_public_key(self, address):
                sequence = self.get_address_index(address)
       -        pubkey = self.get_pubkey(*sequence)
       +        pubkey = self.derive_pubkeys(*sequence)
                return pubkey
        
            def load_keystore(self):
       t@@ -2028,16 +2133,6 @@ class Simple_Deterministic_Wallet(Simple_Wallet, Deterministic_Wallet):
                    xtype = 'standard'
                self.txin_type = 'p2pkh' if xtype == 'standard' else xtype
        
       -    def get_pubkey(self, c, i):
       -        return self.derive_pubkeys(c, i)
       -
       -    def add_input_sig_info(self, txin, address):
       -        derivation = self.get_address_index(address)
       -        x_pubkey = self.keystore.get_xpubkey(*derivation)
       -        txin['x_pubkeys'] = [x_pubkey]
       -        txin['signatures'] = [None]
       -        txin['num_sig'] = 1
       -
            def get_master_public_key(self):
                return self.keystore.get_master_public_key()
        
       t@@ -2065,24 +2160,37 @@ class Multisig_Wallet(Deterministic_Wallet):
                self.m, self.n = multisig_type(self.wallet_type)
                Deterministic_Wallet.__init__(self, storage, config=config)
        
       -    def get_pubkeys(self, c, i):
       -        return self.derive_pubkeys(c, i)
       -
            def get_public_keys(self, address):
       -        sequence = self.get_address_index(address)
       -        return self.get_pubkeys(*sequence)
       +        return list(self.get_public_keys_with_deriv_info(address))
        
            def pubkeys_to_address(self, pubkeys):
       -        redeem_script = self.pubkeys_to_redeem_script(pubkeys)
       +        redeem_script = self.pubkeys_to_scriptcode(pubkeys)
                return bitcoin.redeem_script_to_address(self.txin_type, redeem_script)
        
       -    def pubkeys_to_redeem_script(self, pubkeys):
       +    def pubkeys_to_scriptcode(self, pubkeys: Sequence[str]) -> str:
                return transaction.multisig_script(sorted(pubkeys), self.m)
        
            def get_redeem_script(self, address):
       +        txin_type = self.get_txin_type(address)
                pubkeys = self.get_public_keys(address)
       -        redeem_script = self.pubkeys_to_redeem_script(pubkeys)
       -        return redeem_script
       +        scriptcode = self.pubkeys_to_scriptcode(pubkeys)
       +        if txin_type == 'p2sh':
       +            return scriptcode
       +        elif txin_type == 'p2wsh-p2sh':
       +            return bitcoin.p2wsh_nested_script(scriptcode)
       +        elif txin_type == 'p2wsh':
       +            return None
       +        raise UnknownTxinType(f'unexpected txin_type {txin_type}')
       +
       +    def get_witness_script(self, address):
       +        txin_type = self.get_txin_type(address)
       +        pubkeys = self.get_public_keys(address)
       +        scriptcode = self.pubkeys_to_scriptcode(pubkeys)
       +        if txin_type == 'p2sh':
       +            return None
       +        elif txin_type in ('p2wsh-p2sh', 'p2wsh'):
       +            return scriptcode
       +        raise UnknownTxinType(f'unexpected txin_type {txin_type}')
        
            def derive_pubkeys(self, c, i):
                return [k.derive_pubkey(c, i) for k in self.get_keystores()]
       t@@ -2140,23 +2248,6 @@ class Multisig_Wallet(Deterministic_Wallet):
            def get_fingerprint(self):
                return ''.join(sorted(self.get_master_public_keys()))
        
       -    def add_input_sig_info(self, txin, address):
       -        # x_pubkeys are not sorted here because it would be too slow
       -        # they are sorted in transaction.get_sorted_pubkeys
       -        # pubkeys is set to None to signal that x_pubkeys are unsorted
       -        derivation = self.get_address_index(address)
       -        x_pubkeys_expected = [k.get_xpubkey(*derivation) for k in self.get_keystores()]
       -        x_pubkeys_actual = txin.get('x_pubkeys')
       -        # if 'x_pubkeys' is already set correctly (ignoring order, as above), leave it.
       -        # otherwise we might delete signatures
       -        if x_pubkeys_actual and set(x_pubkeys_actual) == set(x_pubkeys_expected):
       -            return
       -        txin['x_pubkeys'] = x_pubkeys_expected
       -        txin['pubkeys'] = None
       -        # we need n place holders
       -        txin['signatures'] = [None] * self.n
       -        txin['num_sig'] = self.m
       -
        
        wallet_types = ['standard', 'multisig', 'imported']