commit 707b74d22b28d942c445754311736f158e505990
parent 6d12ebabbb686bcd028b659c6a6ce2cb4782bc14
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:
62 files changed, 4171 insertions(+), 3420 deletions(-)
diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py
@@ -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
@@ -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:
@@ -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
@@ -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.
@@ -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
@@ -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
@@ -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,
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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()
@@ -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
diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py
@@ -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)
@@ -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()
@@ -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')),
]
@@ -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,
}
diff --git a/electrum/bip32.py b/electrum/bip32.py
@@ -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
@@ -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
@@ -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) +
@@ -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 +
@@ -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():
@@ -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
@@ -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
@@ -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):
@@ -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
diff --git a/electrum/bitcoin.py b/electrum/bitcoin.py
@@ -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
@@ -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:
@@ -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
diff --git a/electrum/coinchooser.py b/electrum/coinchooser.py
@@ -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
@@ -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
@@ -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)
@@ -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
@@ -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)
@@ -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])
@@ -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)
@@ -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
@@ -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
@@ -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()
@@ -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:
@@ -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)
diff --git a/electrum/commands.py b/electrum/commands.py
@@ -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
@@ -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):
@@ -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):
@@ -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):
@@ -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):
@@ -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:
@@ -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,
@@ -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):
@@ -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:
@@ -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
@@ -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,
@@ -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,
diff --git a/electrum/ecc.py b/electrum/ecc.py
@@ -25,6 +25,7 @@
import base64
import hashlib
+import functools
from typing import Union, Tuple, Optional
import ecdsa
@@ -181,6 +182,7 @@ class _PubkeyForPointAtInfinity:
point = ecdsa.ellipticcurve.INFINITY
+@functools.total_ordering
class ECPubkey(object):
def __init__(self, b: Optional[bytes]):
@@ -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)
diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py
@@ -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
@@ -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:
@@ -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)
@@ -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 ''
@@ -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")
diff --git a/electrum/gui/kivy/uix/dialogs/__init__.py b/electrum/gui/kivy/uix/dialogs/__init__.py
@@ -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
@@ -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):
@@ -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
diff --git a/electrum/gui/kivy/uix/dialogs/tx_dialog.py b/electrum/gui/kivy/uix/dialogs/tx_dialog.py
@@ -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
@@ -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('''
@@ -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()
@@ -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:
@@ -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()
@@ -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()
diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py
@@ -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
@@ -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
@@ -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):
@@ -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
diff --git a/electrum/gui/qt/address_dialog.py b/electrum/gui/qt/address_dialog.py
@@ -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)
diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
@@ -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
@@ -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,
@@ -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)
@@ -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.
@@ -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
@@ -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
@@ -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()
@@ -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)
@@ -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.
"""
@@ -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
@@ -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))
@@ -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.
'''
@@ -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
@@ -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}")
@@ -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()
@@ -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()
@@ -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))
@@ -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_()
@@ -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
@@ -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)
@@ -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:
@@ -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
@@ -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):
diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py
@@ -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
@@ -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 = ''
@@ -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 = ''
@@ -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]
@@ -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
@@ -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()
@@ -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[:]
diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py
@@ -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
@@ -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
@@ -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))
@@ -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.
'''
@@ -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"))
@@ -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)
@@ -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]
@@ -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)
@@ -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
@@ -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)))
@@ -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()
@@ -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)
diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py
@@ -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
diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py
@@ -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
@@ -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):
@@ -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)
@@ -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:
@@ -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):
@@ -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):
diff --git a/electrum/gui/stdio.py b/electrum/gui/stdio.py
@@ -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
@@ -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
diff --git a/electrum/gui/text.py b/electrum/gui/text.py
@@ -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
@@ -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
diff --git a/electrum/json_db.py b/electrum/json_db.py
@@ -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
@@ -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):
@@ -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()
@@ -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)
@@ -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):
@@ -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
@@ -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):
diff --git a/electrum/keystore.py b/electrum/keystore.py
@@ -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
@@ -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
@@ -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):
@@ -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):
@@ -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)
@@ -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):
@@ -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)
@@ -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')
@@ -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):
@@ -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)
@@ -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')
@@ -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):
@@ -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)
@@ -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)
@@ -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,
}
@@ -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 '')
@@ -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
@@ -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):
@@ -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:
@@ -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:
@@ -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):
@@ -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):
diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py
@@ -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
@@ -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)
@@ -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
@@ -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
@@ -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,
@@ -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
diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py
@@ -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
@@ -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,
@@ -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
@@ -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
@@ -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)
@@ -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
@@ -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')
@@ -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()
diff --git a/electrum/lnsweep.py b/electrum/lnsweep.py
@@ -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,
@@ -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
@@ -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)
@@ -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)
@@ -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():
@@ -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.
@@ -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
diff --git a/electrum/lnutil.py b/electrum/lnutil.py
@@ -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
@@ -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)
@@ -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,
@@ -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,
@@ -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)
@@ -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):
@@ -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))
@@ -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]
@@ -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(
@@ -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
@@ -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)
@@ -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
diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py
@@ -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
diff --git a/electrum/lnworker.py b/electrum/lnworker.py
@@ -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)
diff --git a/electrum/network.py b/electrum/network.py
@@ -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__)
@@ -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
diff --git a/electrum/paymentrequest.py b/electrum/paymentrequest.py
@@ -25,7 +25,7 @@
import hashlib
import sys
import time
-from typing import Optional
+from typing import Optional, List
import asyncio
import urllib.parse
@@ -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
@@ -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])
@@ -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
@@ -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()
@@ -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)
@@ -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:
diff --git a/electrum/plugin.py b/electrum/plugin.py
@@ -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__)
@@ -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
diff --git a/electrum/plugins/audio_modem/qt.py b/electrum/plugins/audio_modem/qt.py
@@ -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)
@@ -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__)
@@ -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)
diff --git a/electrum/plugins/coldcard/basic_psbt.py b/electrum/plugins/coldcard/basic_psbt.py
@@ -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
-
diff --git a/electrum/plugins/coldcard/build_psbt.py b/electrum/plugins/coldcard/build_psbt.py
@@ -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
-
diff --git a/electrum/plugins/coldcard/coldcard.py b/electrum/plugins/coldcard/coldcard.py
@@ -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
@@ -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__)
@@ -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:
@@ -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
@@ -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
@@ -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.
@@ -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
@@ -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:
@@ -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?
@@ -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):
@@ -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:
@@ -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
@@ -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)
@@ -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,
@@ -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):
@@ -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
diff --git a/electrum/plugins/coldcard/qt.py b/electrum/plugins/coldcard/qt.py
@@ -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
@@ -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()
@@ -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('')
diff --git a/electrum/plugins/cosigner_pool/qt.py b/electrum/plugins/cosigner_pool/qt.py
@@ -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)
@@ -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):
@@ -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)
@@ -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():
@@ -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
@@ -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."))
@@ -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
@@ -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)
diff --git a/electrum/plugins/digitalbitbox/digitalbitbox.py b/electrum/plugins/digitalbitbox/digitalbitbox.py
@@ -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
@@ -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
@@ -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 = []
@@ -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
@@ -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
@@ -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):
@@ -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)
@@ -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',
diff --git a/electrum/plugins/digitalbitbox/qt.py b/electrum/plugins/digitalbitbox/qt.py
@@ -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
@@ -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
@@ -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)
diff --git a/electrum/plugins/greenaddress_instant/qt.py b/electrum/plugins/greenaddress_instant/qt.py
@@ -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):
@@ -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():
@@ -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
diff --git a/electrum/plugins/hw_wallet/plugin.py b/electrum/plugins/hw_wallet/plugin.py
@@ -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):
@@ -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):
@@ -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."))
@@ -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
diff --git a/electrum/plugins/keepkey/keepkey.py b/electrum/plugins/keepkey/keepkey.py
@@ -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)
@@ -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)
@@ -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
@@ -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):
@@ -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:
@@ -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]
@@ -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
@@ -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)
diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py
@@ -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
@@ -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()
@@ -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:
@@ -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
@@ -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
@@ -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))
@@ -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)
@@ -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
@@ -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)
@@ -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:
@@ -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):
@@ -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:
@@ -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'
diff --git a/electrum/plugins/safe_t/safe_t.py b/electrum/plugins/safe_t/safe_t.py
@@ -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
@@ -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)
@@ -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)
@@ -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
@@ -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):
@@ -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:
@@ -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]
@@ -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
@@ -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)
diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py
@@ -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
@@ -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__)
@@ -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)
@@ -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
@@ -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):
@@ -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:
@@ -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]
@@ -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)
@@ -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
@@ -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
diff --git a/electrum/plugins/trustedcoin/cmdline.py b/electrum/plugins/trustedcoin/cmdline.py
@@ -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():
diff --git a/electrum/plugins/trustedcoin/legacy_tx_format.py b/electrum/plugins/trustedcoin/legacy_tx_format.py
@@ -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
diff --git a/electrum/plugins/trustedcoin/trustedcoin.py b/electrum/plugins/trustedcoin/trustedcoin.py
@@ -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
@@ -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 _
@@ -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:
@@ -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):
@@ -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:
@@ -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
@@ -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):
diff --git a/electrum/scripts/bip70.py b/electrum/scripts/bip70.py
@@ -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'
@@ -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)
diff --git a/electrum/segwit_addr.py b/electrum/segwit_addr.py
@@ -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)
diff --git a/electrum/synchronizer.py b/electrum/synchronizer.py
@@ -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:
@@ -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:
@@ -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)
diff --git a/electrum/tests/regtest/regtest.sh b/electrum/tests/regtest/regtest.sh
@@ -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
@@ -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
@@ -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"
diff --git a/electrum/tests/test_bitcoin.py b/electrum/tests/test_bitcoin.py
@@ -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
@@ -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'/"))
diff --git a/electrum/tests/test_commands.py b/electrum/tests/test_commands.py
@@ -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,)))
diff --git a/electrum/tests/test_lnchannel.py b/electrum/tests/test_lnchannel.py
@@ -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
diff --git a/electrum/tests/test_lnutil.py b/electrum/tests/test_lnutil.py
@@ -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
@@ -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):
@@ -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
@@ -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',
diff --git a/electrum/tests/test_psbt.py b/electrum/tests/test_psbt.py
@@ -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())
diff --git a/electrum/tests/test_transaction.py b/electrum/tests/test_transaction.py
@@ -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"
@@ -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)
@@ -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())
#####
@@ -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)
diff --git a/electrum/tests/test_wallet.py b/electrum/tests/test_wallet.py
@@ -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')
diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py
@@ -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
@@ -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',
@@ -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',
@@ -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',
@@ -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',
@@ -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)
@@ -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',
@@ -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)
@@ -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',
@@ -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',
@@ -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',
@@ -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)
@@ -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())
@@ -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())
@@ -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())
@@ -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())
@@ -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())
@@ -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())
@@ -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())
@@ -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())
@@ -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())
@@ -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())
@@ -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())
@@ -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())
@@ -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())
@@ -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())
@@ -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())
@@ -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):
@@ -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
@@ -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())
@@ -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
@@ -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())
@@ -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
@@ -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())
@@ -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
@@ -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())
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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))
@@ -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
@@ -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))
@@ -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
@@ -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))
diff --git a/electrum/transaction.py b/electrum/transaction.py
@@ -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:
@@ -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):
@@ -64,7 +65,15 @@ class UnknownTxinType(Exception):
pass
-class NotRecognizedRedeemScript(Exception):
+class BadHeaderMagic(SerializationError):
+ pass
+
+
+class UnexpectedEndOfStream(SerializationError):
+ pass
+
+
+class PSBTInputConsistencyFailure(SerializationError):
pass
@@ -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):
@@ -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):
@@ -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
@@ -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:
@@ -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:
@@ -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
@@ -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
@@ -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'
@@ -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.
@@ -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
@@ -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."""
@@ -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
@@ -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."""
@@ -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
@@ -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:
@@ -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
diff --git a/electrum/util.py b/electrum/util.py
@@ -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):
diff --git a/electrum/wallet.py b/electrum/wallet.py
@@ -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
@@ -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
@@ -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)
@@ -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)
@@ -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
@@ -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
@@ -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):
@@ -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):
@@ -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):
@@ -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
@@ -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,
@@ -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):
@@ -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:
@@ -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:
@@ -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):
@@ -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
@@ -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
@@ -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).
@@ -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"""
@@ -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)
@@ -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)
@@ -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
"""
@@ -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)
@@ -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))
@@ -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:
@@ -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
@@ -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):
@@ -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 {
@@ -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
@@ -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:
@@ -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):
@@ -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
@@ -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())
@@ -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
@@ -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
@@ -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:
@@ -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():
@@ -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
@@ -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:
@@ -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()]
@@ -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):
@@ -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()
@@ -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()]
@@ -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']