diff --git a/jmclient/jmclient/cli_options.py b/jmclient/jmclient/cli_options.py index f64e6acfa..0d3762f2d 100644 --- a/jmclient/jmclient/cli_options.py +++ b/jmclient/jmclient/cli_options.py @@ -513,6 +513,14 @@ def get_sendpayment_parser(): help='specify recipient IRC nick for a ' 'p2ep style payment, for example:\n' 'J5Ehn3EieVZFtm4q ') + parser.add_option('--psbt', + action='store_true', + dest='with_psbt', + default=False, + help='output as psbt instead of ' + 'broadcasting the transaction. ' + 'Currently only works with direct ' + 'send (-N 0).') add_common_options(parser) return parser diff --git a/jmclient/jmclient/taker_utils.py b/jmclient/jmclient/taker_utils.py index 5b7839164..5ff6845fc 100644 --- a/jmclient/jmclient/taker_utils.py +++ b/jmclient/jmclient/taker_utils.py @@ -10,7 +10,8 @@ from .schedule import human_readable_schedule_entry, tweak_tumble_schedule,\ schedule_to_text from .wallet import BaseWallet, estimate_tx_fee, compute_tx_locktime -from jmbitcoin import make_shuffled_tx, amount_to_str +from jmbitcoin import (make_shuffled_tx, amount_to_str, + PartiallySignedTransaction, CMutableTxOut) from jmbase.support import EXIT_SUCCESS log = get_log() @@ -21,7 +22,7 @@ def direct_send(wallet_service, amount, mixdepth, destaddr, answeryes=False, accept_callback=None, info_callback=None, - return_transaction=False): + return_transaction=False, with_final_psbt=False): """Send coins directly from one mixdepth to one destination address; does not need IRC. Sweep as for normal sendpayment (set amount=0). If answeryes is True, callback/command line query is not performed. @@ -38,8 +39,13 @@ def direct_send(wallet_service, amount, mixdepth, destaddr, answeryes=False, pushed), and returns nothing. This function returns: - The txid if transaction is pushed, False otherwise, - or the full CMutableTransaction if return_transaction is True. + 1. False if there is any failure. + 2. The txid if transaction is pushed, and return_transaction is False, + and with_final_psbt is False. + 3. The full CMutableTransaction is return_transaction is True and + with_final_psbt is False. + 4. The PSBT object if with_final_psbt is True, and in + this case the transaction is *NOT* broadcast. """ #Sanity checks assert validate_address(destaddr)[0] @@ -81,37 +87,53 @@ def direct_send(wallet_service, amount, mixdepth, destaddr, answeryes=False, tx = make_shuffled_tx(list(utxos.keys()), outs, 2, compute_tx_locktime()) inscripts = {} + spent_outs = [] for i, txinp in enumerate(tx.vin): u = (txinp.prevout.hash[::-1], txinp.prevout.n) inscripts[i] = (utxos[u]["script"], utxos[u]["value"]) - success, msg = wallet_service.sign_tx(tx, inscripts) - if not success: - 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())) - actual_amount = amount if amount != 0 else total_inputs_val - fee_est - log.info("Sends: " + amount_to_str(actual_amount) + " to address: " + destaddr) - if not answeryes: - if not accept_callback: - if input('Would you like to push to the network? (y/n):')[0] != 'y': - log.info("You chose not to broadcast the transaction, quitting.") - return False - else: - accepted = accept_callback(pformat(str(tx)), destaddr, actual_amount, - fee_est) - if not accepted: - return False - jm_single().bc_interface.pushtx(tx.serialize()) - txid = bintohex(tx.GetTxid()[::-1]) - successmsg = "Transaction sent: " + txid - cb = log.info if not info_callback else info_callback - cb(successmsg) - txinfo = txid if not return_transaction else tx - return txinfo - + spent_outs.append(CMutableTxOut(utxos[u]["value"], + utxos[u]["script"])) + if with_final_psbt: + # here we have the PSBTWalletMixin do the signing stage + # for us: + new_psbt = wallet_service.create_psbt_from_tx(tx, spent_outs=spent_outs) + serialized_psbt, err = wallet_service.sign_psbt(new_psbt.serialize()) + if err: + log.error("Failed to sign PSBT, quitting. Error message: " + err) + 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. + return new_psbt_signed + else: + success, msg = wallet_service.sign_tx(tx, inscripts) + if not success: + 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())) + actual_amount = amount if amount != 0 else total_inputs_val - fee_est + log.info("Sends: " + amount_to_str(actual_amount) + " to address: " + destaddr) + if not answeryes: + if not accept_callback: + if input('Would you like to push to the network? (y/n):')[0] != 'y': + log.info("You chose not to broadcast the transaction, quitting.") + return False + else: + accepted = accept_callback(pformat(str(tx)), destaddr, actual_amount, + fee_est) + if not accepted: + return False + jm_single().bc_interface.pushtx(tx.serialize()) + txid = bintohex(tx.GetTxid()[::-1]) + successmsg = "Transaction sent: " + txid + cb = log.info if not info_callback else info_callback + cb(successmsg) + txinfo = txid if not return_transaction else tx + return txinfo def sign_tx(wallet_service, tx, utxos): stx = deserialize(tx) diff --git a/scripts/sendpayment.py b/scripts/sendpayment.py index 1433d9b78..28aa0ab39 100644 --- a/scripts/sendpayment.py +++ b/scripts/sendpayment.py @@ -174,7 +174,12 @@ def main(): .format(exp_tx_fees_ratio)) if options.makercount == 0 and not options.p2ep: - direct_send(wallet_service, amount, mixdepth, destaddr, options.answeryes) + tx = direct_send(wallet_service, amount, mixdepth, destaddr, + options.answeryes, with_final_psbt=options.with_psbt) + if options.with_psbt: + log.info("This PSBT is fully signed and can be sent externally for " + "broadcasting:") + log.info(tx.to_base64()) return if wallet.get_txtype() == 'p2pkh':