mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-08-23 17:47:31 +00:00
integrate PSBT support natively. WIP
This commit is contained in:
parent
6d12ebabbb
commit
bafe8a2fff
61 changed files with 3405 additions and 3310 deletions
|
@ -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
|
||||
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,9 +643,9 @@ 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:
|
||||
|
@ -736,23 +730,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 +789,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 +800,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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,8 @@ 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"
|
||||
if i<0xfd:
|
||||
return int_to_hex(i)
|
||||
elif i<=0xffff:
|
||||
|
@ -372,24 +375,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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -76,18 +76,18 @@ class Bucket(NamedTuple):
|
|||
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
|
||||
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)
|
||||
|
|
|
@ -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
|
||||
txin.script_type = txin_type
|
||||
txin.pubkeys = [bfh(pubkey)]
|
||||
txin.num_sig = 1
|
||||
inputs.append(txin)
|
||||
|
||||
outputs = [TxOutput(TYPE_ADDRESS, x['address'], int(x['value'])) for x in outputs]
|
||||
tx = Transaction.from_io(inputs, outputs, locktime=locktime)
|
||||
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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
@ -855,7 +854,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 +865,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 +1198,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")
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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
|
||||
|
@ -17,6 +17,10 @@ from electrum.util import InvalidPassword
|
|||
from electrum.address_synchronizer import TX_HEIGHT_LOCAL
|
||||
from electrum.wallet import CannotBumpFee
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ...main_window import ElectrumWindow
|
||||
from electrum.transaction import Transaction
|
||||
|
||||
|
||||
Builder.load_string('''
|
||||
|
||||
|
@ -121,9 +125,9 @@ 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
|
||||
|
||||
def on_open(self):
|
||||
|
@ -166,7 +170,7 @@ class TxDialog(Factory.Popup):
|
|||
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,7 +256,7 @@ class TxDialog(Factory.Popup):
|
|||
|
||||
def show_qr(self):
|
||||
from electrum.bitcoin import base_encode, bfh
|
||||
raw_tx = str(self.tx)
|
||||
raw_tx = self.tx.serialize()
|
||||
text = bfh(raw_tx)
|
||||
text = base_encode(text, base=43)
|
||||
self.app.qr_dialog(_("Raw Transaction"), text, text_for_clipboard=raw_tx)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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, txt: 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(txt)
|
||||
except BaseException as e:
|
||||
self.show_critical(_("Electrum was unable to parse your transaction") + ":\n" + repr(e))
|
||||
return
|
||||
|
@ -2752,14 +2756,15 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
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 +2836,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 +3135,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 +3262,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):
|
||||
|
|
|
@ -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[:]
|
||||
|
||||
|
|
|
@ -26,8 +26,8 @@
|
|||
import sys
|
||||
import copy
|
||||
import datetime
|
||||
import json
|
||||
import traceback
|
||||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from PyQt5.QtCore import QSize, Qt
|
||||
|
@ -42,11 +42,11 @@ 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)
|
||||
MONOSPACE_FONT, ColorScheme, ButtonsLineEdit, text_dialog)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .main_window import ElectrumWindow
|
||||
|
@ -60,9 +60,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 +73,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.
|
||||
'''
|
||||
|
@ -97,7 +97,7 @@ 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)
|
||||
tx.add_info_from_wallet(self.wallet)
|
||||
|
||||
self.setMinimumWidth(950)
|
||||
self.setWindowTitle(_("Transaction"))
|
||||
|
@ -123,6 +123,9 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||
self.broadcast_button = b = QPushButton(_("Broadcast"))
|
||||
b.clicked.connect(self.do_broadcast)
|
||||
|
||||
self.merge_sigs_button = b = QPushButton(_("Merge sigs from"))
|
||||
b.clicked.connect(self.merge_sigs)
|
||||
|
||||
self.save_button = b = QPushButton(_("Save"))
|
||||
save_button_disabled = not tx.is_complete()
|
||||
b.setDisabled(save_button_disabled)
|
||||
|
@ -152,7 +155,10 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||
self.export_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.merge_sigs_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]
|
||||
|
||||
|
@ -190,7 +196,7 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||
self.close()
|
||||
|
||||
def show_qr(self):
|
||||
text = bfh(str(self.tx))
|
||||
text = self.tx.serialize_as_bytes()
|
||||
text = base_encode(text, base=43)
|
||||
try:
|
||||
self.main_window.show_qrcode(text, 'Transaction', parent=self)
|
||||
|
@ -222,16 +228,44 @@ 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:
|
||||
if isinstance(self.tx, PartialTransaction):
|
||||
self.tx.finalize_psbt()
|
||||
if self.tx.is_complete():
|
||||
name = 'signed_%s.txn' % (self.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 self.tx.is_complete(): # network tx hex
|
||||
with open(fileName, "w+") as f:
|
||||
f.write(json.dumps(self.tx.as_dict(), indent=4) + '\n')
|
||||
network_tx_hex = self.tx.serialize_to_network()
|
||||
f.write(network_tx_hex + '\n')
|
||||
else: # if partial: PSBT bytes
|
||||
assert isinstance(self.tx, PartialTransaction)
|
||||
with open(fileName, "wb+") as f:
|
||||
f.write(self.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:"), _("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 update(self):
|
||||
desc = self.desc
|
||||
base_unit = self.main_window.base_unit()
|
||||
|
@ -319,19 +353,19 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||
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)
|
||||
|
@ -340,8 +374,8 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||
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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,34 @@ 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
|
||||
if not self._is_upgrade_method_needed(19, 19):
|
||||
return
|
||||
|
||||
from .bip32 import BIP32Node
|
||||
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
|
||||
# derivation prefix
|
||||
derivation_prefix = ks.get('derivation', 'm')
|
||||
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 we don't have prior data, we set it to the fp of the xpub
|
||||
# EVEN IF there was already a derivation prefix saved different than 'm'
|
||||
node = BIP32Node.from_xkey(xpub)
|
||||
root_fingerprint = node.calc_fingerprint_of_this_node().hex().lower()
|
||||
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 +779,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 +822,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):
|
||||
|
|
|
@ -26,16 +26,15 @@
|
|||
|
||||
from unicodedata import normalize
|
||||
import hashlib
|
||||
from typing import Tuple, TYPE_CHECKING, Union, Sequence
|
||||
from typing import Tuple, TYPE_CHECKING, Union, Sequence, Optional, Dict, List
|
||||
|
||||
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)
|
||||
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,7 +42,7 @@ 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):
|
||||
|
@ -67,25 +66,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 +101,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 +258,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 +274,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 +320,54 @@ 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
|
||||
|
||||
# if these are None now, then it is the responsibility of the caller to
|
||||
# also call self.add_derivation_prefix_and_root_fingerprint:
|
||||
self._derivation_prefix = derivation_prefix # note: subclass should persist this
|
||||
self._root_fingerprint = root_fingerprint # note: subclass should persist this
|
||||
|
||||
def get_master_public_key(self):
|
||||
return self.xpub
|
||||
|
||||
def derive_pubkey(self, for_change, n):
|
||||
def get_derivation_prefix(self) -> str:
|
||||
"""Returns to bip32 path from some root node to self.xpub"""
|
||||
assert self._derivation_prefix is not None, 'derivation_prefix should have been set already'
|
||||
return self._derivation_prefix
|
||||
|
||||
def get_root_fingerprint(self) -> 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
|
||||
"""
|
||||
assert self._root_fingerprint is not None, 'root_fingerprint should have been set already'
|
||||
return self._root_fingerprint
|
||||
|
||||
def add_derivation_prefix_and_root_fingerprint(self, *, derivation_prefix: str, root_node: BIP32Node):
|
||||
assert self.xpub
|
||||
derivation_prefix = normalize_bip32_derivation(derivation_prefix)
|
||||
# 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")
|
||||
# store
|
||||
self._root_fingerprint = root_node.calc_fingerprint_of_this_node().hex().lower()
|
||||
self._derivation_prefix = derivation_prefix
|
||||
|
||||
def reset_derivation_prefix(self):
|
||||
assert self.xpub
|
||||
self._derivation_prefix = 'm'
|
||||
self._root_fingerprint = BIP32Node.from_xkey(self.xpub).calc_fingerprint_of_this_node().hex().lower()
|
||||
|
||||
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 +383,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 +401,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 +431,20 @@ class BIP32_KeyStore(Deterministic_KeyStore, Xpub):
|
|||
def is_watching_only(self):
|
||||
return self.xprv is None
|
||||
|
||||
def add_xprv(self, xprv):
|
||||
def add_xpub(self, xpub, *, default_der_prefix=True):
|
||||
self.xpub = xpub
|
||||
if default_der_prefix:
|
||||
self.reset_derivation_prefix()
|
||||
|
||||
def add_xprv(self, xprv, *, default_der_prefix=True):
|
||||
self.xprv = xprv
|
||||
self.xpub = bip32.xpub_from_xprv(xprv)
|
||||
self.add_xpub(bip32.xpub_from_xprv(xprv), default_der_prefix=default_der_prefix)
|
||||
|
||||
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_xprv(node.to_xprv(), default_der_prefix=False)
|
||||
self.add_derivation_prefix_and_root_fingerprint(derivation_prefix=derivation, root_node=rootnode)
|
||||
|
||||
def get_private_key(self, sequence, password):
|
||||
xprv = self.get_master_private_key(password)
|
||||
|
@ -415,6 +464,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 +527,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 +558,15 @@ 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
|
||||
def get_derivation_prefix(self) -> str:
|
||||
return 'm'
|
||||
|
||||
@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_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
|
||||
|
||||
def update_password(self, old_password, new_password):
|
||||
self.check_password(old_password)
|
||||
|
@ -554,14 +588,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 +615,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,
|
||||
}
|
||||
|
||||
|
@ -704,40 +738,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):
|
||||
|
@ -861,14 +861,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):
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -449,7 +449,7 @@ class DeviceMgr(ThreadJob):
|
|||
handler.update_status(False)
|
||||
devices = self.scan_devices()
|
||||
xpub = keystore.xpub
|
||||
derivation = keystore.get_derivation()
|
||||
derivation = keystore.get_derivation_prefix()
|
||||
client = self.client_by_xpub(plugin, xpub, handler, devices)
|
||||
if client is None and force_pair:
|
||||
info = self.select_device(plugin, handler, keystore, devices)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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,47 +254,26 @@ 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_client(self):
|
||||
# called when user tries to do something like view address, sign somthing.
|
||||
# - not called during probing/setup
|
||||
# - will fail if indicated device can't produce the xpub (at derivation) expected
|
||||
rv = self.plugin.get_client(self)
|
||||
if rv:
|
||||
rv.verify_connection(self.ckcc_xfp, self.ckcc_xpub)
|
||||
xfp_int = xfp_int_for_keystore(self)
|
||||
rv.verify_connection(xfp_int, self.ckcc_xpub)
|
||||
|
||||
return rv
|
||||
|
||||
|
@ -332,7 +315,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 +355,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 == xfp_int_for_keystore(self)
|
||||
|
||||
# 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,16 +398,9 @@ 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:
|
||||
tx2 = PartialTransaction.from_raw_psbt(raw_resp)
|
||||
# apply partial signatures back into txn
|
||||
psbt = BasicPSBT()
|
||||
psbt.parse(raw_resp, client.label())
|
||||
|
||||
merge_sigs_from_psbt(tx, psbt)
|
||||
|
||||
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.
|
||||
|
||||
|
@ -447,7 +418,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 +544,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 +557,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 +569,10 @@ 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()):
|
||||
der_prefix = ks.get_derivation_prefix()
|
||||
xpubs.append( (ks.get_root_fingerprint(), xpub, der_prefix) )
|
||||
derivs.add(der_prefix)
|
||||
|
||||
# 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 +583,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 +603,38 @@ 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)
|
||||
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]
|
||||
xfp_int = xfp_int_for_keystore(ks)
|
||||
der_prefix = bip32.convert_bip32_path_to_list_of_uint32(ks.get_derivation_prefix())
|
||||
der_full = der_prefix + list(der_suffix)
|
||||
xfp_paths.append([xfp_int] + der_full)
|
||||
|
||||
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))
|
||||
script = bfh(wallet.pubkeys_to_scriptcode(pubkeys))
|
||||
|
||||
# need master XFP for each co-signers
|
||||
ks_xfp = xfp_for_keystore(ks)
|
||||
xfps.append(unpacked_xfp_path(ks_xfp, path))
|
||||
|
||||
# put into BIP45 (sorted) order
|
||||
pkx = list(sorted(zip(pubkeys, xfps)))
|
||||
|
||||
script = bfh(multisig_script([pk for pk,xfp in pkx], wallet.m))
|
||||
|
||||
keystore.show_p2sh_address(wallet.m, script, [xfp for pk,xfp in pkx], txin_type)
|
||||
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_for_keystore(keystore: Xpub) -> int:
|
||||
xfp = keystore.get_root_fingerprint()
|
||||
return int.from_bytes(bfh(xfp), 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
|
||||
|
|
|
@ -15,11 +15,6 @@ 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 +68,11 @@ 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
|
||||
|
||||
# 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)
|
||||
|
||||
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
|
||||
|
||||
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 +178,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('')
|
||||
|
|
|
@ -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 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):
|
||||
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)
|
||||
|
|
|
@ -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
|
||||
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,40 +533,35 @@ 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)))
|
||||
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)
|
||||
break
|
||||
else:
|
||||
self.give_error("No matching x_key for sign_transaction") # should never happen
|
||||
|
||||
# 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])
|
||||
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)
|
||||
|
||||
|
@ -581,17 +569,14 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore):
|
|||
# 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':
|
||||
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)
|
||||
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()
|
||||
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):
|
||||
|
@ -788,11 +769,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',
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
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
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from electrum.bip32 import BIP32Node
|
||||
from electrum.wallet import Abstract_Wallet
|
||||
from electrum.keystore import Hardware_KeyStore
|
||||
|
||||
|
||||
class HW_PluginBase(BasePlugin):
|
||||
|
@ -65,7 +72,7 @@ class HW_PluginBase(BasePlugin):
|
|||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def show_address(self, wallet, address, keystore=None):
|
||||
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 +139,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 +153,21 @@ 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]
|
||||
xpubs = [xfp_to_xpub_map[xfp] for xfp in xfps]
|
||||
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
|
||||
|
|
|
@ -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,112 @@ 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)
|
||||
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])
|
||||
|
||||
# prepare multisig, if available:
|
||||
xpubs = wallet.get_master_public_keys()
|
||||
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)
|
||||
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, for_sig=False):
|
||||
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'])
|
||||
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'])
|
||||
assert isinstance(tx, PartialTransaction)
|
||||
assert isinstance(txin, PartialTxInput)
|
||||
assert keystore
|
||||
xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txin)
|
||||
multisig = self._make_multisig(txin.num_sig, xpubs_and_deriv_suffixes)
|
||||
script_type = self.get_keepkey_input_script_type(txin.script_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
|
||||
multisig=multisig)
|
||||
my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txin)
|
||||
if full_path:
|
||||
txinputtype.address_n = full_path
|
||||
|
||||
prev_hash = unhexlify(txin['prevout_hash'])
|
||||
prev_index = txin['prevout_n']
|
||||
prev_hash = txin.prevout.txid
|
||||
prev_index = txin.prevout.out_idx
|
||||
|
||||
if 'value' in txin:
|
||||
txinputtype.amount = txin['value']
|
||||
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 create_output_by_derivation():
|
||||
script_type = self.get_keepkey_output_script_type(info.script_type)
|
||||
def _make_multisig(self, m, xpubs):
|
||||
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,
|
||||
)
|
||||
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(
|
||||
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' = None):
|
||||
|
||||
def create_output_by_derivation():
|
||||
script_type = self.get_keepkey_output_script_type(txout.script_type)
|
||||
xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txout)
|
||||
multisig = self._make_multisig(txout.num_sig, xpubs_and_deriv_suffixes)
|
||||
my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txout)
|
||||
assert full_path
|
||||
txoutputtype = self.types.TxOutputType(
|
||||
multisig=multisig,
|
||||
amount=amount,
|
||||
address_n=self.client_class.expand_path(derivation + "/%d/%d" % index),
|
||||
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 +436,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)
|
||||
|
|
|
@ -5,10 +5,10 @@ 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.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
|
||||
|
@ -217,6 +217,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 +233,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 +259,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 +269,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 +310,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 +325,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)
|
||||
|
||||
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 +384,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)
|
||||
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 +448,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 +472,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 +495,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'
|
||||
|
|
|
@ -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,114 @@ 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)
|
||||
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])
|
||||
|
||||
# prepare multisig, if available:
|
||||
xpubs = wallet.get_master_public_keys()
|
||||
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)
|
||||
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, for_sig=False):
|
||||
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'])
|
||||
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'])
|
||||
assert isinstance(tx, PartialTransaction)
|
||||
assert isinstance(txin, PartialTxInput)
|
||||
assert keystore
|
||||
xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txin)
|
||||
multisig = self._make_multisig(txin.num_sig, xpubs_and_deriv_suffixes)
|
||||
script_type = self.get_safet_input_script_type(txin.script_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
|
||||
multisig=multisig)
|
||||
my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txin)
|
||||
if full_path:
|
||||
txinputtype.address_n = full_path
|
||||
|
||||
prev_hash = unhexlify(txin['prevout_hash'])
|
||||
prev_index = txin['prevout_n']
|
||||
prev_hash = txin.prevout.txid
|
||||
prev_index = txin.prevout.out_idx
|
||||
|
||||
if 'value' in txin:
|
||||
txinputtype.amount = txin['value']
|
||||
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 create_output_by_derivation():
|
||||
script_type = self.get_safet_output_script_type(info.script_type)
|
||||
def _make_multisig(self, m, xpubs):
|
||||
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,
|
||||
)
|
||||
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(
|
||||
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' = None):
|
||||
|
||||
def create_output_by_derivation():
|
||||
script_type = self.get_safet_output_script_type(txout.script_type)
|
||||
xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txout)
|
||||
multisig = self._make_multisig(txout.num_sig, xpubs_and_deriv_suffixes)
|
||||
my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txout)
|
||||
assert full_path
|
||||
txoutputtype = self.types.TxOutputType(
|
||||
multisig=multisig,
|
||||
amount=amount,
|
||||
address_n=self.client_class.expand_path(derivation + "/%d/%d" % index),
|
||||
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 +434,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)
|
||||
|
|
|
@ -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,101 @@ 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
|
||||
xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txin)
|
||||
multisig = self._make_multisig(txin.num_sig, xpubs_and_deriv_suffixes)
|
||||
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
|
||||
my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txin)
|
||||
if full_path:
|
||||
txinputtype.address_n = full_path
|
||||
|
||||
prev_hash = bfh(txin['prevout_hash'])
|
||||
prev_index = txin['prevout_n']
|
||||
prev_hash = txin.prevout.txid
|
||||
prev_index = txin.prevout.out_idx
|
||||
|
||||
if 'value' in txin:
|
||||
txinputtype.amount = txin['value']
|
||||
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' = None):
|
||||
|
||||
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)
|
||||
xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txout)
|
||||
multisig = self._make_multisig(txout.num_sig, xpubs_and_deriv_suffixes)
|
||||
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 +452,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
|
||||
|
|
|
@ -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():
|
||||
|
|
106
electrum/plugins/trustedcoin/legacy_tx_format.py
Normal file
106
electrum/plugins/trustedcoin/legacy_tx_format.py
Normal file
|
@ -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
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,)))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
267
electrum/tests/test_psbt.py
Normal file
267
electrum/tests/test_psbt.py
Normal file
|
@ -0,0 +1,267 @@
|
|||
from pprint import pprint
|
||||
|
||||
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
|
||||
|
||||
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())
|
|
@ -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
|
||||
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,72 +117,49 @@ 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'))
|
||||
|
||||
|
||||
#####
|
||||
|
@ -811,45 +749,54 @@ class TestTransaction(ElectrumTestCase):
|
|||
# txns from Bitcoin Core ends <---
|
||||
|
||||
|
||||
class TestTransactionTestnet(TestCaseForTestnet):
|
||||
class TestLegacyPartialTxFormat(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)
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.config = SimpleConfig({'electrum_path': self.electrum_path})
|
||||
|
||||
# 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_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')
|
||||
|
||||
def test_txid_partial_segwit_p2wpkh(self):
|
||||
raw_tx = '45505446ff000100000000010115a847356cbb44be67f345965bb3f2589e2fec1c9a0ada21fd28225dcc602e8f0100000000fdffffff02f6fd1200000000001600149c756aa33f4f89418b33872a973274b5445c727b80969800000000001600140f9de573bc679d040e763d13f0250bd03e625f6ffeffffffff9095ab000000000000000201ff53ff045f1cf6014af5fa07800000002fa3f450ba41799b9b62642979505817783a9b6c656dc11cd0bb4fa362096808026adc616c25a4d0a877d1741eb1db9cef65c15118bd7d5f31bf65f319edda81840100c8000f391400'
|
||||
txid = '63ff7e99d85d8e33f683e6ec84574bdf8f5111078a5fe900893e019f9a7f95c3'
|
||||
self._run_naive_tests_on_tx(raw_tx, txid)
|
||||
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)
|
||||
|
||||
def test_txid_partial_segwit_p2wpkh_p2sh_simple(self):
|
||||
raw_tx = '45505446ff0001000000000101d0d23a6fbddb21cc664cb81cca96715baa4d6dbe5b7b9bcc6632f1005a7b0b840100000017160014a78a91261e71a681b6312cd184b14503a21f856afdffffff0134410f000000000017a914d6514ca17ecc31952c990daf96e307fbc58529cd87feffffffff40420f000000000000000201ff53ff044a5262033601222e800000001618aa51e49a961f63fd111f64cd4a7e792c1d7168be7a07703de505ebed2cf70286ebbe755767adaa5835f4d78dec1ee30849d69eacfe80b7ee6b1585279536c30000020011391400'
|
||||
txid = '2739f2e7fde9b8ec73fce4aee53722cc7683312d1321ded073284c51fadf44df'
|
||||
self._run_naive_tests_on_tx(raw_tx, txid)
|
||||
wallet = WalletIntegrityHelper.create_multisig_wallet([ks1, ks2, ks3], '2of3', config=self.config)
|
||||
|
||||
def test_txid_partial_segwit_p2wpkh_p2sh_mixed_outputs(self):
|
||||
raw_tx = '45505446ff00010000000001011dcac788f24b84d771b60c44e1f9b6b83429e50f06e1472d47241922164013b00100000017160014801d28ca6e2bde551112031b6cb75de34f10851ffdffffff0440420f00000000001600140f9de573bc679d040e763d13f0250bd03e625f6fc0c62d000000000017a9142899f6484e477233ce60072fc185ef4c1f2c654487809698000000000017a914d40f85ba3c8fa0f3615bcfa5d6603e36dfc613ef87712d19040000000017a914e38c0cffde769cb65e72cda1c234052ae8d2254187feffffffff6ad1ee040000000000000201ff53ff044a5262033601222e800000001618aa51e49a961f63fd111f64cd4a7e792c1d7168be7a07703de505ebed2cf70286ebbe755767adaa5835f4d78dec1ee30849d69eacfe80b7ee6b1585279536c301000c000f391400'
|
||||
txid = 'ba5c88e07a4025a39ad3b85247cbd4f556a70d6312b18e04513c7cec9d45d6ac'
|
||||
self._run_naive_tests_on_tx(raw_tx, txid)
|
||||
tx = tx_from_any('cHNidP8BAJQCAAAAAcqqxrXrkW4wZ9AiT5QvszHOHc+0Axz7R555Qdz5XkCYAQAAAAD9////A6CGAQAAAAAAFgAU+fBLRlKk9v89xVEm2xJ0kG1wcvNMCwMAAAAAABepFPKffLiXEB3Gmv1Y35uy5bTUM59Nh0ANAwAAAAAAGXapFPriyJZefiOenIisUU3nDewLDxYIiKwSKxgATwEENYfPAAAAAAAAAAAAnOMnCVq57ruCJ7c38H6PtmrwS48+kcQJPEh70w/ofCQCDSEN062A0pw2JKkYltX2G3th8zLexPfEVDGu74BeD6cEcH3xxE8BBDWHzwGCB4l2gAAAAJOfYJjOAH6kksFOokIboP3+8Gwhlzlxhl5uY7zokvfcAmGy8e8txy0wkx69/TgZFOMe1aZc2g1HCwrRQ9M9+Ph7BLoE1bNPAQQ1h88BggeJdoAAAAFvoSi9wKWkb8evGv0gXbYgcZHUxAUbbtvQZBPNwLGi3wNqTky+e8Rm3WU3hMhrN3Sb7D6CgCnOo0wVaQlW/WFXbwQqQERnAAEA3wIAAAABnCh8O3p5TIeiImyJBHikJS+aVEdsCr+hgtTU3eimPIIBAAAAakcwRAIgTR7BvR+c9dHhx7CDnuJBXb52St/BOycfgpq7teEnUP4CIFp4DWI/xfKhwIZHZVPgYGOZLQC9jHiFKKiCSl7nXBUTASEDVPOeil/J5isfPp2yEcI6UQL8jFq6CRs/hegA8M2L/xj9////ApydBwAAAAAAGXapFLEFQG/gWw3IvkTHg2ulgS2/Z0zoiKwgoQcAAAAAABepFCwWF9JPk25A0UsN8pTst7uq11M3hxIrGAAiAgP+Qtq1hxjqBBP3yN5pPN7uIs4Zsdw0wLvdekgkVGXFokgwRQIhANA8NcspOwHae+DXcc7a+oke1dcKZ3z8zWlnE1b2vr7cAiALKldsTNHGQkgHVs4f1mCsrwulJhk6MJnh+BzdAa0gkwEBBGlSIQIJHwtNirMAFqXRwIgkngKIP62BYPBvpTWIrYWYZQo+YiEDXy+CY7s2CNbMTuA71MuNZcTXCvcQSfBfv+5JeIMqH9IhA/5C2rWHGOoEE/fI3mk83u4izhmx3DTAu916SCRUZcWiU64iBgP+Qtq1hxjqBBP3yN5pPN7uIs4Zsdw0wLvdekgkVGXFogy6BNWzAAAAAAAAAAAiBgIJHwtNirMAFqXRwIgkngKIP62BYPBvpTWIrYWYZQo+YgwqQERnAAAAAAAAAAAiBgNfL4JjuzYI1sxO4DvUy41lxNcK9xBJ8F+/7kl4gyof0gxwffHEAAAAAAAAAAAAAAEAaVIhAiq2ef2i4zfECrvAggPT1x0qlv2784yQj3uS5TfBxO//IQNM0UcWdnXGYoZFDMVpQBP0N4OTY115ZKuKVzzD0EqNTiEDWOhaggFFh96oM+tf+5PjmFaQ7kumHvMtWyitWqFhc39TriICA0zRRxZ2dcZihkUMxWlAE/Q3g5NjXXlkq4pXPMPQSo1ODLoE1bMBAAAAAAAAACICAiq2ef2i4zfECrvAggPT1x0qlv2784yQj3uS5TfBxO//DCpARGcBAAAAAAAAACICA1joWoIBRYfeqDPrX/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_txid_partial_issue_5366(self):
|
||||
raw_tx = '45505446ff000200000000010127523d70642dabd999fb43191ff6763f5b04150ba4cf38d2cfb53edf6a40ac4f0100000000fdffffff013286010000000000160014e79c7ac0b390a9caf52dc002e1095a5fbc042a18feffffffffa08601000000000000000201ff57ff045f1cf60157e9eb7a8000000038fa0b3a9c155ff3390ca0d639783d97af3b3bf66ebb69a31dfe8317fae0a7fe0324bc048fc0002253dfec9d6299711d708175f950ecee8e09db3518a5685741830000ffffcf01010043281700'
|
||||
txid = 'a0c159616073dc7a4a482092dab4e8516c83dddb769b65919f23f6df63d33eb8'
|
||||
self._run_naive_tests_on_tx(raw_tx, txid)
|
||||
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')
|
||||
|
||||
# end partial txns <---
|
||||
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)
|
||||
|
||||
class NetworkMock(object):
|
||||
|
||||
def __init__(self, unspent):
|
||||
self.unspent = unspent
|
||||
|
||||
def synchronous_send(self, arg):
|
||||
return self.unspent
|
||||
tx = tx_from_any('cHNidP8BAJ8CAAAAAYfEZGymkLOX41eyOyE3AwaRqQoGimaQg000C0voSs1qAQAAAAD9////A6CGAQAAAAAAFgAUi8nZR+TQrdwvTDS4NxA060ez0wUUDAMAAAAAACIAIKYIfE+EpV3DkBRymhjgiVUTnUOEVZ0f0qSKHZXXRqQlQA0DAAAAAAAZdqkUwT/WKU0b57lBClU49LTvEPxZTueIrBMrGABPAQJXVIMAAAAAAAAAAADWRNzdekrLQyNV4BCsSl+VWUDIKpdncxt9idxC6zzaxAJy+qL5i3bMnWVe8oHAes2nXDCpkNw6Unts+SqPWmuKgARL8hIJTwECV1SDAdJM1ZGAAAABmcTWyJP6Gt3sawEhGBE34lw4GUMzuMVyFbPPHm1+evECoj3a5pi7YJW4uANb3R6UR59mwpZ52Bkx4P4HqkNhSe4E5U2yWk8BAldUgwHSTNWRgAAAALYKhQia0IUKb/6FkKPhUGTmPu/QIIE98uSmsyCc499fAsixTykX1VfNSClwwRTD40V+CdJWOXgCJ3ouTRUZq5+MBAHZMVwAAQEqIKEHAAAAAAAAIKlI1/pqu7l+MXea5UODAStBPVOCHH/TlJAPa0Q8Yd7uIgIDB6PEHQftl21l4hPoI9AoQJN0decJtBJT6F6XDjyxZnRHMEQCIC975frTmPGjV2KTM58idNInrxd5hpCqGtAP+D2WclzFAiAAyy6f/1viOoYnGMZlpQNfFyeD6bMVjq6DZz9sWa3DwAEBBWlSIQKp3LVw6CgMdB8JAywVgJW3qjsM5AGtoDDy1HuZnwIGBiEDB6PEHQftl21l4hPoI9AoQJN0decJtBJT6F6XDjyxZnQhA1IbCkXgQvCMzQOvR/2IuyB7VBTg4wu4eZ/KMRoGMjoZU64iBgMHo8QdB+2XbWXiE+gj0ChAk3R15wm0ElPoXpcOPLFmdAwB2TFcAAAAAAEAAAAiBgNSGwpF4ELwjM0Dr0f9iLsge1QU4OMLuHmfyjEaBjI6GQzlTbJaAAAAAAEAAAAiBgKp3LVw6CgMdB8JAywVgJW3qjsM5AGtoDDy1HuZnwIGBgxL8hIJAAAAAAEAAAAAAAEBaVIhAgWCn5UiV3EiypypvrZ/lM8v4K0NF+BSoRBwHPQSjTOcIQIsyigs/dnMHzh9MJhmHWi8nIo5rGvXLDDbV5p4V9CFmyEC2Q48iXOES5zAs4SUxIUVoxnuw6wkiflvb1XmqmkSpghTriICAtkOPIlzhEucwLOElMSFFaMZ7sOsJIn5b29V5qppEqYIDAHZMVwBAAAAAAAAACICAizKKCz92cwfOH0wmGYdaLycijmsa9csMNtXmnhX0IWbDOVNsloBAAAAAAAAACICAgWCn5UiV3EiypypvrZ/lM8v4K0NF+BSoRBwHPQSjTOcDEvyEgkBAAAAAAAAAAAA')
|
||||
tx.add_info_from_wallet(wallet)
|
||||
raw_tx = serialize_tx_in_legacy_format(tx, wallet=wallet)
|
||||
self.assertEqual('45505446ff000200000000010187c4646ca690b397e357b23b2137030691a90a068a6690834d340b4be84acd6a0100000000fdffffff03a0860100000000001600148bc9d947e4d0addc2f4c34b8371034eb47b3d305140c030000000000220020a6087c4f84a55dc39014729a18e08955139d4384559d1fd2a48a1d95d746a425400d0300000000001976a914c13fd6294d1be7b9410a5538f4b4ef10fc594ee788acfeffffffff20a10700000000000000050001ff47304402202f7be5fad398f1a3576293339f2274d227af17798690aa1ad00ff83d96725cc5022000cb2e9fff5be23a862718c665a5035f172783e9b3158eae83673f6c59adc3c00101fffd0201524c53ff02575483000000000000000000d644dcdd7a4acb432355e010ac4a5f955940c82a9767731b7d89dc42eb3cdac40272faa2f98b76cc9d655ef281c07acda75c30a990dc3a527b6cf92a8f5a6b8a80000001004c53ff0257548301d24cd59180000000b60a85089ad0850a6ffe8590a3e15064e63eefd020813df2e4a6b3209ce3df5f02c8b14f2917d557cd482970c114c3e3457e09d256397802277a2e4d1519ab9f8c000001004c53ff0257548301d24cd5918000000199c4d6c893fa1addec6b0121181137e25c38194333b8c57215b3cf1e6d7e7af102a23ddae698bb6095b8b8035bdd1e94479f66c29679d81931e0fe07aa436149ee0000010053ae132b1800',
|
||||
raw_tx)
|
||||
|
|
|
@ -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')
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
|
@ -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):
|
||||
|
|
|
@ -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,
|
||||
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
|
||||
|
@ -305,7 +318,7 @@ class Abstract_Wallet(AddressSynchronizer):
|
|||
def get_master_public_key(self):
|
||||
return None
|
||||
|
||||
def basename(self):
|
||||
def basename(self) -> str:
|
||||
return os.path.basename(self.storage.path)
|
||||
|
||||
def test_addresses_sanity(self):
|
||||
|
@ -392,15 +405,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,13 +436,16 @@ 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)
|
||||
|
@ -480,7 +509,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 +519,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 +565,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 +705,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 +785,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 +905,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 +939,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 +960,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,25 +974,30 @@ 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, fee=None, change_addr=None,
|
||||
domain=None, rbf=False, nonlocal_only=False, tx_version=None) -> 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
|
||||
|
@ -973,10 +1007,9 @@ class Abstract_Wallet(AddressSynchronizer):
|
|||
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 +1023,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 +1033,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 +1079,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 +1121,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 +1153,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 +1189,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 +1200,84 @@ 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) -> 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_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_value_bytes = txin_value.to_bytes(8, byteorder="little", signed=True)
|
||||
txin.witness_utxo = txin_value_bytes + bfh(bitcoin.address_to_script(address))
|
||||
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)
|
||||
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) -> 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)
|
||||
|
||||
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,38 +1301,46 @@ 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 add_output_info(self, txout: PartialTxOutput) -> 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
|
||||
if isinstance(self, Deterministic_Wallet):
|
||||
if not txout.pubkeys or len(txout.pubkeys) != len(txout.bip32_paths):
|
||||
pubkey_deriv_info = self.get_public_keys_with_deriv_info(address)
|
||||
txout.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]
|
||||
xfp_bytes = bfh(ks.get_root_fingerprint())
|
||||
der_prefix = bip32.convert_bip32_path_to_list_of_uint32(ks.get_derivation_prefix())
|
||||
der_full = der_prefix + list(der_suffix)
|
||||
txout.bip32_paths[bfh(pubkey_hex)] = (xfp_bytes, der_full)
|
||||
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, password):
|
||||
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
|
||||
tx.add_info_from_wallet(self)
|
||||
# sign. start with ready keystores.
|
||||
for k in sorted(self.get_keystores(), key=lambda ks: ks.ready_to_sign(), reverse=True):
|
||||
try:
|
||||
|
@ -1423,7 +1491,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 +1501,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 +1644,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 +1667,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):
|
||||
|
@ -1684,9 +1754,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())
|
||||
|
@ -1795,11 +1862,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 +1885,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 +1899,21 @@ 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):
|
||||
assert self.is_mine(address)
|
||||
if txin.script_type in ('unknown', '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]
|
||||
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 +1923,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 +2007,23 @@ 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):
|
||||
assert self.is_mine(address)
|
||||
pubkey_deriv_info = self.get_public_keys_with_deriv_info(address)
|
||||
txin.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]
|
||||
xfp_bytes = bfh(ks.get_root_fingerprint())
|
||||
der_prefix = bip32.convert_bip32_path_to_list_of_uint32(ks.get_derivation_prefix())
|
||||
der_full = der_prefix + list(der_suffix)
|
||||
txin.bip32_paths[bfh(pubkey_hex)] = (xfp_bytes, der_full)
|
||||
|
||||
def create_new_address(self, for_change=False):
|
||||
assert type(for_change) is bool
|
||||
with self.lock:
|
||||
|
@ -1995,8 +2074,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 +2104,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 +2115,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 +2142,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 +2230,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']
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue