From ad459d2fdb7b547116553a8e5ad7bb5c06918a10 Mon Sep 17 00:00:00 2001 From: Adam Gibson Date: Fri, 1 May 2020 19:07:01 +0100 Subject: [PATCH] Add human readable representations of txs and PSBTs Human readable representation for CTransaction objects in jmbitcoin.secp256k1_transaction.py and for PartiallySignedTransaction objects in jmclient.wallet. PSBTWalletMixin, use of these in maker, taker, direct send and in tests. Users should note that PSBT human readable representations can in some cases be really huge. --- jmbitcoin/jmbitcoin/__init__.py | 3 +- jmbitcoin/jmbitcoin/secp256k1_transaction.py | 73 +++++++++++- jmbitcoin/test/test_tx_signing.py | 6 +- jmclient/jmclient/maker.py | 4 +- jmclient/jmclient/taker.py | 4 +- jmclient/jmclient/taker_utils.py | 11 +- jmclient/jmclient/wallet.py | 114 ++++++++++++++++++- jmclient/test/test_psbt_wallet.py | 61 +++++++++- jmclient/test/test_snicker.py | 2 +- 9 files changed, 253 insertions(+), 25 deletions(-) diff --git a/jmbitcoin/jmbitcoin/__init__.py b/jmbitcoin/jmbitcoin/__init__.py index b8efa5a..ae2e0e2 100644 --- a/jmbitcoin/jmbitcoin/__init__.py +++ b/jmbitcoin/jmbitcoin/__init__.py @@ -13,5 +13,6 @@ from bitcointx.core import (x, b2x, b2lx, lx, COutPoint, CTxOut, CTxIn, from bitcointx.core.key import KeyStore from bitcointx.core.script import (CScript, OP_0, SignatureHash, SIGHASH_ALL, SIGVERSION_WITNESS_V0, CScriptWitness) -from bitcointx.core.psbt import PartiallySignedTransaction +from bitcointx.core.psbt import (PartiallySignedTransaction, PSBT_Input, + PSBT_Output) diff --git a/jmbitcoin/jmbitcoin/secp256k1_transaction.py b/jmbitcoin/jmbitcoin/secp256k1_transaction.py index 0966f12..cf988fb 100644 --- a/jmbitcoin/jmbitcoin/secp256k1_transaction.py +++ b/jmbitcoin/jmbitcoin/secp256k1_transaction.py @@ -2,16 +2,81 @@ # note, only used for non-cryptographic randomness: import random +import json from jmbitcoin.secp256k1_main import * - +from jmbase import bintohex, utxo_to_utxostr from bitcointx.core import (CMutableTransaction, Hash160, CTxInWitness, - CMutableOutPoint, CMutableTxIn, - CMutableTxOut, ValidationError) + CMutableOutPoint, CMutableTxIn, CTransaction, + CMutableTxOut, CTxIn, CTxOut, ValidationError) from bitcointx.core.script import * -from bitcointx.wallet import P2WPKHCoinAddress, CCoinAddress, P2PKHCoinAddress +from bitcointx.wallet import (P2WPKHCoinAddress, CCoinAddress, P2PKHCoinAddress, + CCoinAddressError) from bitcointx.core.scripteval import (VerifyScript, SCRIPT_VERIFY_WITNESS, SCRIPT_VERIFY_P2SH, SIGVERSION_WITNESS_V0) +def hrt(tx, jsonified=True): + """ Given a CTransaction object, output a human + readable json-formatted string (suitable for terminal + output or large GUI textbox display) containing + all details of that transaction. + If `jsonified` is False, the dict is returned, instead + of the json string. + """ + assert isinstance(tx, CTransaction) + outdict = {} + outdict["hex"] = bintohex(tx.serialize()) + outdict["inputs"]=[] + outdict["outputs"]=[] + outdict["txid"]= bintohex(tx.GetTxid()[::-1]) + outdict["nLockTime"] = tx.nLockTime + outdict["nVersion"] = tx.nVersion + for i, inp in enumerate(tx.vin): + if not tx.wit.vtxinwit: + # witness section is not initialized/empty + witarg = None + else: + witarg = tx.wit.vtxinwit[i] + outdict["inputs"].append(hrinp(inp, witarg)) + for i, out in enumerate(tx.vout): + outdict["outputs"].append(hrout(out)) + if not jsonified: + return outdict + return json.dumps(outdict, indent=4) + +def hrinp(txinput, txinput_witness): + """ Pass objects of type CTxIn and CTxInWitness (or None) + and a dict of human-readable entries for this input + is returned. + """ + assert isinstance(txinput, CTxIn) + outdict = {} + success, u = utxo_to_utxostr((txinput.prevout.hash[::-1], + txinput.prevout.n)) + assert success + outdict["outpoint"] = u + outdict["scriptSig"] = bintohex(txinput.scriptSig) + outdict["nSequence"] = txinput.nSequence + + if txinput_witness: + outdict["witness"] = bintohex( + txinput_witness.scriptWitness.serialize()) + return outdict + +def hrout(txoutput): + """ Returns a dict of human-readable entries + for this output. + """ + assert isinstance(txoutput, CTxOut) + outdict = {} + outdict["value_sats"] = txoutput.nValue + outdict["scriptPubKey"] = bintohex(txoutput.scriptPubKey) + try: + addr = CCoinAddress.from_scriptPubKey(txoutput.scriptPubKey) + outdict["address"] = str(addr) + except CCoinAddressError: + pass # non standard script + return outdict + def estimate_tx_size(ins, outs, txtype='p2pkh'): '''Estimate transaction size. The txtype field as detailed below is used to distinguish diff --git a/jmbitcoin/test/test_tx_signing.py b/jmbitcoin/test/test_tx_signing.py index 05cbbf5..d7f3d6f 100644 --- a/jmbitcoin/test/test_tx_signing.py +++ b/jmbitcoin/test/test_tx_signing.py @@ -4,6 +4,7 @@ import pytest import binascii import hashlib +from jmbase import bintohex import jmbitcoin as btc @pytest.mark.parametrize( @@ -58,8 +59,9 @@ def test_sign_standard_txs(addrtype): if not sig: print(msg) raise - print("created signature: ", binascii.hexlify(sig)) - print("serialized transaction: {}".format(btc.b2x(tx.serialize()))) + print("created signature: ", bintohex(sig)) + print("serialized transaction: {}".format(bintohex(tx.serialize()))) + print("deserialized transaction: {}\n".format(btc.hrt(tx))) def test_mk_shuffled_tx(): # prepare two addresses for the outputs diff --git a/jmclient/jmclient/maker.py b/jmclient/jmclient/maker.py index b4b9ab6..40ffc3a 100644 --- a/jmclient/jmclient/maker.py +++ b/jmclient/jmclient/maker.py @@ -130,7 +130,9 @@ class Maker(object): tx = btc.CMutableTransaction.deserialize(tx_from_taker) except Exception as e: return (False, 'malformed txhex. ' + repr(e)) - jlog.info('obtained tx\n' + bintohex(tx.serialize())) + # if the above deserialization was successful, the human readable + # parsing will be also: + jlog.info('obtained tx\n' + btc.hrt(tx)) goodtx, errmsg = self.verify_unsigned_tx(tx, offerinfo) if not goodtx: jlog.info('not a good tx, reason=' + errmsg) diff --git a/jmclient/jmclient/taker.py b/jmclient/jmclient/taker.py index a4f13cb..25d5d1a 100644 --- a/jmclient/jmclient/taker.py +++ b/jmclient/jmclient/taker.py @@ -499,7 +499,7 @@ class Taker(object): self.outputs.append({'address': self.coinjoin_address(), 'value': self.cjamount}) self.latest_tx = btc.make_shuffled_tx(self.utxo_tx, self.outputs) - jlog.info('obtained tx\n' + bintohex(self.latest_tx.serialize())) + jlog.info('obtained tx\n' + btc.hrt(self.latest_tx)) for index, ins in enumerate(self.latest_tx.vin): utxo = (ins.prevout.hash[::-1], ins.prevout.n) @@ -1008,7 +1008,7 @@ class P2EPTaker(Taker): # contains only those. tx = btc.make_shuffled_tx(self.input_utxos, self.outputs, version=2, locktime=compute_tx_locktime()) - jlog.info('Created proposed fallback tx\n' + pprint.pformat(str(tx))) + jlog.info('Created proposed fallback tx:\n' + btc.hrt(tx)) # We now sign as a courtesy, because if we disappear the recipient # can still claim his coins with this. # sign our inputs before transfer diff --git a/jmclient/jmclient/taker_utils.py b/jmclient/jmclient/taker_utils.py index b6c4886..b2bed0e 100644 --- a/jmclient/jmclient/taker_utils.py +++ b/jmclient/jmclient/taker_utils.py @@ -11,7 +11,7 @@ from .schedule import human_readable_schedule_entry, tweak_tumble_schedule,\ from .wallet import BaseWallet, estimate_tx_fee, compute_tx_locktime, \ FidelityBondMixin from jmbitcoin import make_shuffled_tx, amount_to_str, mk_burn_script,\ - PartiallySignedTransaction, CMutableTxOut + PartiallySignedTransaction, CMutableTxOut, hrt from jmbase.support import EXIT_SUCCESS log = get_log() @@ -159,8 +159,7 @@ def direct_send(wallet_service, amount, mixdepth, destination, answeryes=False, return False new_psbt_signed = PartiallySignedTransaction.deserialize(serialized_psbt) print("Completed PSBT created: ") - print(pformat(new_psbt_signed)) - # TODO add more readable info here as for case below. + print(wallet_service.hr_psbt(new_psbt_signed)) return new_psbt_signed else: success, msg = wallet_service.sign_tx(tx, inscripts) @@ -168,9 +167,7 @@ def direct_send(wallet_service, amount, mixdepth, destination, answeryes=False, log.error("Failed to sign transaction, quitting. Error msg: " + msg) return log.info("Got signed transaction:\n") - log.info(pformat(str(tx))) - log.info("In serialized form (for copy-paste):") - log.info(bintohex(tx.serialize())) + log.info(hrt(tx)) actual_amount = amount if amount != 0 else total_inputs_val - fee_est log.info("Sends: " + amount_to_str(actual_amount) + " to destination: " + destination) if not answeryes: @@ -179,7 +176,7 @@ def direct_send(wallet_service, amount, mixdepth, destination, answeryes=False, log.info("You chose not to broadcast the transaction, quitting.") return False else: - accepted = accept_callback(pformat(str(tx)), destination, actual_amount, + accepted = accept_callback(hrt(tx), destination, actual_amount, fee_est) if not accepted: return False diff --git a/jmclient/jmclient/wallet.py b/jmclient/jmclient/wallet.py index cd15f55..fb46a99 100644 --- a/jmclient/jmclient/wallet.py +++ b/jmclient/jmclient/wallet.py @@ -6,6 +6,7 @@ import collections import numbers import random import base64 +import json from binascii import hexlify, unhexlify from datetime import datetime from calendar import timegm @@ -28,7 +29,7 @@ from .cryptoengine import TYPE_P2PKH, TYPE_P2SH_P2WPKH,\ from .support import get_random_bytes from . import mn_encode, mn_decode import jmbitcoin as btc -from jmbase import JM_WALLET_NAME_PREFIX +from jmbase import JM_WALLET_NAME_PREFIX, bintohex """ @@ -1006,6 +1007,117 @@ class PSBTWalletMixin(object): def __init__(self, storage, **kwargs): super(PSBTWalletMixin, self).__init__(storage, **kwargs) + @staticmethod + def hr_psbt(in_psbt): + """ Returns a jsonified indented string with all relevant + information, in human readable form, contained in a PSBT. + Warning: the output can be very verbose in certain cases. + """ + assert isinstance(in_psbt, btc.PartiallySignedTransaction) + outdict = {} + outdict["psbt-version"] = in_psbt.version + + # human readable serialization of these three global fields is for + # now on a "best-effort" basis, i.e. just takes the representation + # provided by the underlying classes in bitcointx, though this may + # not be very readable. + # TODO: Improve proprietary/unknown as needed. + if in_psbt.xpubs: + outdict["xpubs"] = {str(k): bintohex( + v.serialize()) for k, v in in_psbt.xpubs.items()} + if in_psbt.proprietary_fields: + outdict["proprietary-fields"] = str(in_psbt.proprietary_fields) + if in_psbt.unknown_fields: + outdict["unknown-fields"] = str(in_psbt.unknown_fields) + + outdict["unsigned-tx"] = btc.hrt(in_psbt.unsigned_tx, jsonified=False) + outdict["psbt-inputs"] = [] + for inp in in_psbt.inputs: + outdict["psbt-inputs"].append(PSBTWalletMixin.hr_psbt_in(inp)) + outdict["psbt-outputs"] = [] + for out in in_psbt.outputs: + outdict["psbt-outputs"].append(PSBTWalletMixin.hr_psbt_out(out)) + return json.dumps(outdict, indent=4) + + @staticmethod + def hr_psbt_in(psbt_input): + """ Returns a dict containing human readable information + about a bitcointx.core.psbt.PSBT_Input object. + """ + assert isinstance(psbt_input, btc.PSBT_Input) + outdict = {} + if psbt_input.index is not None: + outdict["input-index"] = psbt_input.index + if psbt_input.utxo: + if isinstance(psbt_input.utxo, btc.CTxOut): + outdict["utxo"] = btc.hrout(psbt_input.utxo) + elif isinstance(psbt_input.utxo, btc.CTransaction): + # human readable full transaction is *too* verbose: + outdict["utxo"] = bintohex(psbt_input.utxo.serialize()) + else: + assert False, "invalid PSBT Input utxo field." + if psbt_input.sighash_type: + outdict["sighash-type"] = psbt_input.sighash_type + if psbt_input.redeem_script: + outdict["redeem-script"] = bintohex(psbt_input.redeem_script) + if psbt_input.witness_script: + outdict["witness-script"] = bintohex(psbt_input.witness_script) + if psbt_input.partial_sigs: + # convert the dict entries to hex: + outdict["partial-sigs"] = {bintohex(k): bintohex(v) for k,v in \ + psbt_input.partial_sigs.items()} + # Note we do not currently add derivation info to our own inputs, + # but probably will in future ( TODO ), still this is shown for + # externally generated PSBTs: + if psbt_input.derivation_map: + # TODO it would be more useful to print the indexes of the + # derivation path as integers, than 4 byte hex: + outdict["derivation-map"] = {bintohex(k): bintohex(v.serialize( + )) for k, v in psbt_input.derivation_map.items()} + + # we show these fields on a best-effort basis; same comment as for + # globals section as mentioned in hr_psbt() + if psbt_input.proprietary_fields: + outdict["proprietary-fields"] = str(psbt_input.proprietary_fields) + if psbt_input.unknown_fields: + outdict["unknown-fields"] = str(psbt_input.unknown_fields) + if psbt_input.proof_of_reserves_commitment: + outdict["proof-of-reserves-commitment"] = \ + str(psbt_input.proof_of_reserves_commitment) + + outdict["final-scriptSig"] = bintohex(psbt_input.final_script_sig) + outdict["final-scriptWitness"] = bintohex( + psbt_input.final_script_witness.serialize()) + + return outdict + + @staticmethod + def hr_psbt_out(psbt_output): + """ Returns a dict containing human readable information + about a PSBT_Output object. + """ + assert isinstance(psbt_output, btc.PSBT_Output) + outdict = {} + if psbt_output.index is not None: + outdict["output-index"] = psbt_output.index + + if psbt_output.derivation_map: + # See note to derivation map in hr_psbt_in() + outdict["derivation-map"] = {bintohex(k): bintohex(v.serialize( + )) for k, v in psbt_output.derivation_map.items()} + + if psbt_output.redeem_script: + outdict["redeem-script"] = bintohex(psbt_output.redeem_script) + if psbt_output.witness_script: + outdict["witness-script"] = bintohex(psbt_output.witness_script) + + if psbt_output.proprietary_fields: + outdict["proprietary-fields"] = str(psbt_output.proprietary_fields) + if psbt_output.unknown_fields: + outdict["unknown-fields"] = str(psbt_output.unknown_fields) + + return outdict + @staticmethod def witness_utxos_to_psbt_utxos(utxos): """ Given a dict of utxos as returned from select_utxos, diff --git a/jmclient/test/test_psbt_wallet.py b/jmclient/test/test_psbt_wallet.py index f317bb4..18f88d7 100644 --- a/jmclient/test/test_psbt_wallet.py +++ b/jmclient/test/test_psbt_wallet.py @@ -11,10 +11,10 @@ from commontest import make_wallets, dummy_accept_callback, dummy_info_callback import jmbitcoin as bitcoin import pytest -from jmbase import get_log, bintohex +from jmbase import get_log, bintohex, hextobin from jmclient import (load_test_config, jm_single, direct_send, SegwitLegacyWallet, SegwitWallet, LegacyWallet) - +from jmclient.wallet import PSBTWalletMixin log = get_log() def test_create_and_sign_psbt_with_legacy(setup_psbt_wallet): @@ -130,6 +130,8 @@ def test_create_psbt_and_sign(setup_psbt_wallet, unowned_utxo, wallet_cls): if unowned_utxo: newpsbt.inputs[-1].redeem_script = redeem_script print(bintohex(newpsbt.serialize())) + print("human readable: ") + print(wallet_service.hr_psbt(newpsbt)) # we cannot compare with a fixed expected result due to wallet randomization, but we can # check psbt structure: expected_inputs_length = 3 if unowned_utxo else 2 @@ -164,9 +166,9 @@ def test_create_psbt_and_sign(setup_psbt_wallet, unowned_utxo, wallet_cls): @pytest.mark.parametrize('payment_amt, wallet_cls_sender, wallet_cls_receiver', [ (0.05, SegwitLegacyWallet, SegwitLegacyWallet), - (0.95, SegwitLegacyWallet, SegwitWallet), - (0.05, SegwitWallet, SegwitLegacyWallet), - (0.95, SegwitWallet, SegwitWallet), + #(0.95, SegwitLegacyWallet, SegwitWallet), + #(0.05, SegwitWallet, SegwitLegacyWallet), + #(0.95, SegwitWallet, SegwitWallet), ]) def test_payjoin_workflow(setup_psbt_wallet, payment_amt, wallet_cls_sender, wallet_cls_receiver): @@ -211,6 +213,8 @@ def test_payjoin_workflow(setup_psbt_wallet, payment_amt, wallet_cls_sender, info_callback=dummy_info_callback, with_final_psbt=True) + print("Initial payment PSBT created:\n{}".format(wallet_s.hr_psbt( + payment_psbt))) # ensure that the payemnt amount is what was intended: out_amts = [x.nValue for x in payment_psbt.unsigned_tx.vout] # NOTE this would have to change for more than 2 outputs: @@ -274,7 +278,7 @@ def test_payjoin_workflow(setup_psbt_wallet, payment_amt, wallet_cls_sender, version=payment_psbt.unsigned_tx.nVersion, locktime=payment_psbt.unsigned_tx.nLockTime) print("we created this unsigned tx: ") - print(unsigned_payjoin_tx) + print(bitcoin.hrt(unsigned_payjoin_tx)) # to create the PSBT we need the spent_outs for each input, # in the right order: spent_outs = [] @@ -301,6 +305,9 @@ def test_payjoin_workflow(setup_psbt_wallet, payment_amt, wallet_cls_sender, r_payjoin_psbt = wallet_r.create_psbt_from_tx(unsigned_payjoin_tx, spent_outs=spent_outs) + print("Receiver created payjoin PSBT:\n{}".format( + wallet_r.hr_psbt(r_payjoin_psbt))) + signresultandpsbt, err = wallet_r.sign_psbt(r_payjoin_psbt.serialize(), with_sign_result=True) assert not err, err @@ -308,6 +315,9 @@ def test_payjoin_workflow(setup_psbt_wallet, payment_amt, wallet_cls_sender, assert signresult.num_inputs_final == len(receiver_utxos) assert not signresult.is_final + print("Receiver signing successful. Payjoin PSBT is now:\n{}".format( + wallet_r.hr_psbt(receiver_signed_psbt))) + # *** STEP 3 *** # ************** @@ -317,11 +327,50 @@ def test_payjoin_workflow(setup_psbt_wallet, payment_amt, wallet_cls_sender, receiver_signed_psbt.serialize(), with_sign_result=True) assert not err, err signresult, sender_signed_psbt = signresultandpsbt + print("Sender's final signed PSBT is:\n{}".format( + wallet_s.hr_psbt(sender_signed_psbt))) assert signresult.is_final + # broadcast the tx extracted_tx = sender_signed_psbt.extract_transaction().serialize() assert jm_single().bc_interface.pushtx(extracted_tx) +""" test vector data for human readable parsing only, +they are taken from bitcointx/tests/test_psbt.py and in turn +taken from BIP174 test vectors. +TODO add more, but note we are not testing functionality here. +""" +hr_test_vectors = { +# PSBT with one P2PKH input. Outputs are empty +"one-p2pkh": '70736274ff0100750200000001268171371edff285e937adeea4b37b78000c0566cbb3ad64641713ca42171bf60000000000feffffff02d3dff505000000001976a914d0c59903c5bac2868760e90fd521a4665aa7652088ac00e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787b32e1300000100fda5010100000000010289a3c71eab4d20e0371bbba4cc698fa295c9463afa2e397f8533ccb62f9567e50100000017160014be18d152a9b012039daf3da7de4f53349eecb985ffffffff86f8aa43a71dff1448893a530a7237ef6b4608bbb2dd2d0171e63aec6a4890b40100000017160014fe3e9ef1a745e974d902c4355943abcb34bd5353ffffffff0200c2eb0b000000001976a91485cff1097fd9e008bb34af709c62197b38978a4888ac72fef84e2c00000017a914339725ba21efd62ac753a9bcd067d6c7a6a39d05870247304402202712be22e0270f394f568311dc7ca9a68970b8025fdd3b240229f07f8a5f3a240220018b38d7dcd314e734c9276bd6fb40f673325bc4baa144c800d2f2f02db2765c012103d2e15674941bad4a996372cb87e1856d3652606d98562fe39c5e9e7e413f210502483045022100d12b852d85dcd961d2f5f4ab660654df6eedcc794c0c33ce5cc309ffb5fce58d022067338a8e0e1725c197fb1a88af59f51e44e4255b20167c8684031c05d1f2592a01210223b72beef0965d10be0778efecd61fcac6f79a4ea169393380734464f84f2ab300000000000000', +# PSBT with one P2PKH input and one P2SH-P2WPKH input. +# First input is signed and finalized. Outputs are empty +"first-input-signed": '70736274ff0100a00200000002ab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40000000000feffffffab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40100000000feffffff02603bea0b000000001976a914768a40bbd740cbe81d988e71de2a4d5c71396b1d88ac8e240000000000001976a9146f4620b553fa095e721b9ee0efe9fa039cca459788ac000000000001076a47304402204759661797c01b036b25928948686218347d89864b719e1f7fcf57d1e511658702205309eabf56aa4d8891ffd111fdf1336f3a29da866d7f8486d75546ceedaf93190121035cdc61fc7ba971c0b501a646a2a83b102cb43881217ca682dc86e2d73fa882920001012000e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787010416001485d13537f2e265405a34dbafa9e3dda01fb82308000000', +# PSBT with one P2PKH input which has a non-final scriptSig +# and has a sighash type specified. Outputs are empty +"nonfinal-scriptsig": '70736274ff0100750200000001268171371edff285e937adeea4b37b78000c0566cbb3ad64641713ca42171bf60000000000feffffff02d3dff505000000001976a914d0c59903c5bac2868760e90fd521a4665aa7652088ac00e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787b32e1300000100fda5010100000000010289a3c71eab4d20e0371bbba4cc698fa295c9463afa2e397f8533ccb62f9567e50100000017160014be18d152a9b012039daf3da7de4f53349eecb985ffffffff86f8aa43a71dff1448893a530a7237ef6b4608bbb2dd2d0171e63aec6a4890b40100000017160014fe3e9ef1a745e974d902c4355943abcb34bd5353ffffffff0200c2eb0b000000001976a91485cff1097fd9e008bb34af709c62197b38978a4888ac72fef84e2c00000017a914339725ba21efd62ac753a9bcd067d6c7a6a39d05870247304402202712be22e0270f394f568311dc7ca9a68970b8025fdd3b240229f07f8a5f3a240220018b38d7dcd314e734c9276bd6fb40f673325bc4baa144c800d2f2f02db2765c012103d2e15674941bad4a996372cb87e1856d3652606d98562fe39c5e9e7e413f210502483045022100d12b852d85dcd961d2f5f4ab660654df6eedcc794c0c33ce5cc309ffb5fce58d022067338a8e0e1725c197fb1a88af59f51e44e4255b20167c8684031c05d1f2592a01210223b72beef0965d10be0778efecd61fcac6f79a4ea169393380734464f84f2ab30000000001030401000000000000', +# PSBT with one P2PKH input and one P2SH-P2WPKH input both with +# non-final scriptSigs. P2SH-P2WPKH input's redeemScript is available. +# Outputs filled. +"mixed-inputs-nonfinal": '70736274ff0100a00200000002ab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40000000000feffffffab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40100000000feffffff02603bea0b000000001976a914768a40bbd740cbe81d988e71de2a4d5c71396b1d88ac8e240000000000001976a9146f4620b553fa095e721b9ee0efe9fa039cca459788ac00000000000100df0200000001268171371edff285e937adeea4b37b78000c0566cbb3ad64641713ca42171bf6000000006a473044022070b2245123e6bf474d60c5b50c043d4c691a5d2435f09a34a7662a9dc251790a022001329ca9dacf280bdf30740ec0390422422c81cb45839457aeb76fc12edd95b3012102657d118d3357b8e0f4c2cd46db7b39f6d9c38d9a70abcb9b2de5dc8dbfe4ce31feffffff02d3dff505000000001976a914d0c59903c5bac2868760e90fd521a4665aa7652088ac00e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787b32e13000001012000e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787010416001485d13537f2e265405a34dbafa9e3dda01fb8230800220202ead596687ca806043edc3de116cdf29d5e9257c196cd055cf698c8d02bf24e9910b4a6ba670000008000000080020000800022020394f62be9df19952c5587768aeb7698061ad2c4a25c894f47d8c162b4d7213d0510b4a6ba6700000080010000800200008000', +# PSBT with one P2SH-P2WSH input of a 2-of-2 multisig, redeemScript, +# witnessScript, and keypaths are available. Contains one signature. +"2-2-multisig-p2wsh": '70736274ff0100550200000001279a2323a5dfb51fc45f220fa58b0fc13e1e3342792a85d7e36cd6333b5cbc390000000000ffffffff01a05aea0b000000001976a914ffe9c0061097cc3b636f2cb0460fa4fc427d2b4588ac0000000000010120955eea0b0000000017a9146345200f68d189e1adc0df1c4d16ea8f14c0dbeb87220203b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4646304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa94b99bdf86151db9a9a010104220020771fd18ad459666dd49f3d564e3dbc42f4c84774e360ada16816a8ed488d5681010547522103b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd462103de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd52ae220603b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4610b4a6ba67000000800000008004000080220603de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd10b4a6ba670000008000000080050000800000', +# PSBT with unknown types in the inputs +"unknown-input-types": '70736274ff01003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a010000000000000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f0000', +# PSBT with `PSBT_GLOBAL_XPUB` +"global-xpub": '70736274ff01009d0100000002710ea76ab45c5cb6438e607e59cc037626981805ae9e0dfd9089012abb0be5350100000000ffffffff190994d6a8b3c8c82ccbcfb2fba4106aa06639b872a8d447465c0d42588d6d670000000000ffffffff0200e1f505000000001976a914b6bc2c0ee5655a843d79afedd0ccc3f7dd64340988ac605af405000000001600141188ef8e4ce0449eaac8fb141cbf5a1176e6a088000000004f010488b21e039e530cac800000003dbc8a5c9769f031b17e77fea1518603221a18fd18f2b9a54c6c8c1ac75cbc3502f230584b155d1c7f1cd45120a653c48d650b431b67c5b2c13f27d7142037c1691027569c503100008000000080000000800001011f00e1f5050000000016001433b982f91b28f160c920b4ab95e58ce50dda3a4a220203309680f33c7de38ea6a47cd4ecd66f1f5a49747c6ffb8808ed09039243e3ad5c47304402202d704ced830c56a909344bd742b6852dccd103e963bae92d38e75254d2bb424502202d86c437195df46c0ceda084f2a291c3da2d64070f76bf9b90b195e7ef28f77201220603309680f33c7de38ea6a47cd4ecd66f1f5a49747c6ffb8808ed09039243e3ad5c1827569c5031000080000000800000008000000000010000000001011f00e1f50500000000160014388fb944307eb77ef45197d0b0b245e079f011de220202c777161f73d0b7c72b9ee7bde650293d13f095bc7656ad1f525da5fd2e10b11047304402204cb1fb5f869c942e0e26100576125439179ae88dca8a9dc3ba08f7953988faa60220521f49ca791c27d70e273c9b14616985909361e25be274ea200d7e08827e514d01220602c777161f73d0b7c72b9ee7bde650293d13f095bc7656ad1f525da5fd2e10b1101827569c5031000080000000800000008000000000000000000000220202d20ca502ee289686d21815bd43a80637b0698e1fbcdbe4caed445f6c1a0a90ef1827569c50310000800000008000000080000000000400000000', +# PSBT with proprietary values +"proprietary-values": '70736274ff0100550200000001ab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40100000000feffffff018e240000000000001976a9146f4620b553fa095e721b9ee0efe9fa039cca459788ac0000000015fc0a676c6f62616c5f706678016d756c7469706c790563686965660001012000e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787010416001485d13537f2e265405a34dbafa9e3dda01fb823080ffc06696e5f706678fde80377686174056672616d650afc00fe40420f0061736b077361746f7368690012fc076f75745f706678feffffff01636f726e05746967657217fc076f75745f706678ffffffffffffffffff707570707905647269766500' +} + +def test_hr_psbt(setup_psbt_wallet): + bitcoin.select_chain_params("bitcoin") + for k, v in hr_test_vectors.items(): + print(PSBTWalletMixin.hr_psbt( + bitcoin.PartiallySignedTransaction.from_binary(hextobin(v)))) + bitcoin.select_chain_params("bitcoin/regtest") + @pytest.fixture(scope="module") def setup_psbt_wallet(): load_test_config() diff --git a/jmclient/test/test_snicker.py b/jmclient/test/test_snicker.py index 9ed3c88..7d81e33 100644 --- a/jmclient/test/test_snicker.py +++ b/jmclient/test/test_snicker.py @@ -45,7 +45,7 @@ def test_snicker_e2e(setup_snicker, nw, wallet_structures, assert tx, "Failed to spend from receiver wallet" print("Parent transaction OK. It was: ") - print(tx) + print(btc.hrt(tx)) wallet_r.process_new_tx(tx) # we must identify the receiver's output we're going to use; # it can be destination or change, that's up to the proposer