From 1ebcd96959114b2830336335374140b0922c562f Mon Sep 17 00:00:00 2001 From: amitx13 Date: Tue, 11 Jun 2024 09:09:50 +0530 Subject: [PATCH] Added optional utxos parameter to allow specifying UTXOs for the transaction and Updated wallet-rpc.yaml --- docs/api/wallet-rpc.yaml | 8 ++- src/jmclient/taker_utils.py | 137 ++++++++++++++++++++++-------------- src/jmclient/wallet_rpc.py | 3 +- 3 files changed, 93 insertions(+), 55 deletions(-) diff --git a/docs/api/wallet-rpc.yaml b/docs/api/wallet-rpc.yaml index 5bc34bfc0..fa8a2e682 100644 --- a/docs/api/wallet-rpc.yaml +++ b/docs/api/wallet-rpc.yaml @@ -297,7 +297,7 @@ paths: - bearerAuth: [] summary: create and broadcast a transaction (without coinjoin) operationId: directsend - description: create and broadcast a transaction (without coinjoin) + description: create and broadcast a transaction (without coinjoin) You can specify the UTXOs to be used in the transaction by providing them in the `utxos` parameter. If `utxos` is not specified, the default UTXO selection logic will be used. parameters: - name: walletname in: path @@ -1120,6 +1120,12 @@ components: type: integer example: 6 description: Bitcoin miner fee to use for transaction. A number higher than 1000 is used as satoshi per kvB tx fee. The number lower than that uses the dynamic fee estimation of blockchain provider as confirmation target. + utxos: + type: array + items: + type: string + example: 96a1195792bfc851fbbd8ff51d07294cf9daa81af720bd020e6397311fc3b220:0 + description: Optional parameter. If specified, these UTXOs will be used in the transaction. If not specified, the default UTXO selection logic will be used. ErrorMessage: type: object properties: diff --git a/src/jmclient/taker_utils.py b/src/jmclient/taker_utils.py index bf6b8b0e0..61ef6192a 100644 --- a/src/jmclient/taker_utils.py +++ b/src/jmclient/taker_utils.py @@ -47,7 +47,7 @@ def direct_send(wallet_service: WalletService, optin_rbf: bool = True, custom_change_addr: Optional[str] = None, change_label: Optional[str] = None) -> Union[bool, str]: - """Send coins directly from one mixdepth to one or more destination addresses using specific UTXOs; + """Send coins directly from one mixdepth to one or more destination addresses either using specific UTXOs or by mixdepth; does not need IRC. Sweep as for normal sendpayment (set amount=0). If answeryes is True, callback/command line query is not performed. If optin_rbf is True, the nSequence values are changed as appropriate. @@ -131,69 +131,96 @@ def direct_send(wallet_service: WalletService, amount = dest_and_amounts[0][1] utxos = wallet_service.get_utxos_by_mixdepth()[mixdepth] if utxos == {}: - log.error(f"There are no available utxos in mixdepth {mixdepth}, quitting.") - return False + log.error( + f"There are no available utxos in mixdepth {mixdepth}, " + "quitting.") + return total_inputs_val = sum([va['value'] for u, va in utxos.items()]) script_types = get_utxo_scripts(wallet_service.wallet, utxos) - fee_est = estimate_tx_fee(len(utxos), 1, txtype=script_types, outtype=outtypes[0]) - outs = [{"address": destination, "value": total_inputs_val - fee_est}] + fee_est = estimate_tx_fee(len(utxos), 1, txtype=script_types, + outtype=outtypes[0]) + outs = [{"address": destination, + "value": total_inputs_val - fee_est}] else: - utxos = wallet_service.get_utxos_by_mixdepth().get(mixdepth, {}) - if not utxos: - log.error(f"There are no available utxos in mixdepth {mixdepth}.") - return False - - if selected_utxos: - # Filter UTXOs based on selected_utxos - selected_utxo_dict = {} - for u, va in utxos.items(): - txid = u[0].hex() - index = u[1] - utxo_str = f"{txid}:{index}" - if utxo_str in selected_utxos: - selected_utxo_dict[(u[0], u[1])] = va - - if not selected_utxo_dict: - log.error("None of the selected UTXOs are available in the specified mixdepth.") - return False - else: - selected_utxo_dict = utxos - - total_inputs_val = sum([va['value'] for u, va in selected_utxo_dict.items()]) - if total_inputs_val < total_outputs_val: - log.error("Selected UTXOs do not cover the total output value.") - return False - if custom_change_addr: change_type = wallet_service.get_outtype(custom_change_addr) if change_type is None: + # we don't recognize this type; best we can do is revert to + # default, even though it may be inaccurate: change_type = txtype else: change_type = txtype - if outtypes[0] is None: + # we don't recognize the destination script type, + # so set it as the same as the change (which will usually + # be the same as the spending wallet, but see above for custom) + # Notice that this is handled differently to the sweep case above, + # because we must use a list - there is more than one output outtypes[0] = change_type outtypes.append(change_type) - - fee_est = estimate_tx_fee(len(selected_utxo_dict), len(dest_and_amounts) + 1, txtype=txtype, outtype=outtypes) - changeval = total_inputs_val - fee_est - total_outputs_val - + outs = [] - for out in dest_and_amounts: - outs.append({"value": out[1], "address": out[0]}) + utxos = {} + if selected_utxos: + # Filter UTXOs based on selected_utxos + all_utxos = wallet_service.get_utxos_by_mixdepth().get(mixdepth, {}) + if not all_utxos: + log.error(f"There are no available utxos in mixdepth {mixdepth}.") + return False + for u, va in all_utxos.items(): + txid = u[0].hex() + index = u[1] + utxo_str = f"{txid}:{index}" + if utxo_str in selected_utxos: + utxos[(u[0], u[1])] = va + + if not utxos: + log.error("None of the selected UTXOs are available in the specified mixdepth.") + return False + script_types = get_utxo_scripts(wallet_service.wallet, utxos) + fee_est = estimate_tx_fee(len(utxos), len(dest_and_amounts) + 1, txtype=script_types, outtype=outtypes) + total_inputs_val = sum([va['value'] for u, va in utxos.items()]) + changeval = total_inputs_val - fee_est - total_outputs_val + + for out in dest_and_amounts: + outs.append({"value": out[1], "address": out[0]}) + + change_addr = wallet_service.get_internal_addr(mixdepth) if custom_change_addr is None else custom_change_addr + outs.append({"value": changeval, "address": change_addr}) - change_addr = wallet_service.get_internal_addr(mixdepth) if custom_change_addr is None else custom_change_addr - outs.append({"value": changeval, "address": change_addr}) - + else: + # not doing a sweep; we will have change. + # 8 inputs to be conservative; note we cannot account for the possibility + # of non-standard input types at this point. + initial_fee_est = estimate_tx_fee(8, len(dest_and_amounts) + 1, + txtype=txtype, outtype=outtypes) + utxos = wallet_service.select_utxos(mixdepth, amount + initial_fee_est, + includeaddr=True) + script_types = get_utxo_scripts(wallet_service.wallet, utxos) + if len(utxos) < 8: + fee_est = estimate_tx_fee(len(utxos), len(dest_and_amounts) + 1, + txtype=script_types, outtype=outtypes) + else: + fee_est = initial_fee_est + total_inputs_val = sum([va['value'] for u, va in utxos.items()]) + changeval = total_inputs_val - fee_est - total_outputs_val + + for out in dest_and_amounts: + outs.append({"value": out[1], "address": out[0]}) + change_addr = wallet_service.get_internal_addr(mixdepth) \ + if custom_change_addr is None else custom_change_addr + outs.append({"value": changeval, "address": change_addr}) + #compute transaction locktime, has special case for spending timelocked coins tx_locktime = compute_tx_locktime() - if mixdepth == FidelityBondMixin.FIDELITY_BOND_MIXDEPTH and isinstance(wallet_service.wallet, FidelityBondMixin): - for outpoint, utxo in selected_utxo_dict.items(): + if mixdepth == FidelityBondMixin.FIDELITY_BOND_MIXDEPTH and \ + isinstance(wallet_service.wallet, FidelityBondMixin): + for outpoint, utxo in utxos.items(): path = wallet_service.script_to_path(utxo["script"]) if not FidelityBondMixin.is_timelocked_path(path): continue path_locktime = path[-1] - tx_locktime = max(tx_locktime, path_locktime + 1) + tx_locktime = max(tx_locktime, path_locktime+1) #compute_tx_locktime() gives a locktime in terms of block height #timelocked addresses use unix time instead #OP_CHECKLOCKTIMEVERIFY can only compare like with like, so we @@ -203,8 +230,8 @@ def direct_send(wallet_service: WalletService, log.info("Using a fee of: " + amount_to_str(fee_est) + ".") if not is_sweep: log.info("Using a change value of: " + amount_to_str(changeval) + ".") - - tx = make_shuffled_tx(list(selected_utxo_dict.keys()), outs, version=2, locktime=tx_locktime) + tx = make_shuffled_tx(list(utxos.keys()), outs, + version=2, locktime=tx_locktime) if optin_rbf: for inp in tx.vin: @@ -214,9 +241,9 @@ def direct_send(wallet_service: WalletService, spent_outs = [] for i, txinp in enumerate(tx.vin): u = (txinp.prevout.hash[::-1], txinp.prevout.n) - inscripts[i] = (selected_utxo_dict[u]["script"], selected_utxo_dict[u]["value"]) - spent_outs.append(CMutableTxOut(selected_utxo_dict[u]["value"], selected_utxo_dict[u]["script"])) - + inscripts[i] = (utxos[u]["script"], utxos[u]["value"]) + 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: @@ -233,11 +260,12 @@ def direct_send(wallet_service: WalletService, success, msg = wallet_service.sign_tx(tx, inscripts) if not success: log.error("Failed to sign transaction, quitting. Error msg: " + msg) - return False + return log.info("Got signed transaction:\n") log.info(human_readable_transaction(tx)) - actual_amount = sum([out[1] for out in dest_and_amounts]) - sending_info = "Sends: " + amount_to_str(actual_amount) + " to destination: " + ", ".join([out[0] for out in dest_and_amounts]) + actual_amount = amount if amount != 0 else total_inputs_val - fee_est + sending_info = "Sends: " + amount_to_str(actual_amount) + \ + " to destination: " + destination if custom_change_addr: sending_info += ", custom change to: " + custom_change_addr log.info(sending_info) @@ -247,13 +275,16 @@ def direct_send(wallet_service: WalletService, log.info("You chose not to broadcast the transaction, quitting.") return False else: - accepted = accept_callback(human_readable_transaction(tx), dest_and_amounts[0][0], actual_amount, fee_est, custom_change_addr) + accepted = accept_callback(human_readable_transaction(tx), + destination, actual_amount, fee_est, + custom_change_addr) if not accepted: return False if change_label: try: wallet_service.set_address_label(change_addr, change_label) except UnknownAddressForLabel: + # ignore, will happen with custom change not part of a wallet pass if jm_single().bc_interface.pushtx(tx.serialize()): txid = bintohex(tx.GetTxid()[::-1]) diff --git a/src/jmclient/wallet_rpc.py b/src/jmclient/wallet_rpc.py index 7a442dea0..86b2109fa 100644 --- a/src/jmclient/wallet_rpc.py +++ b/src/jmclient/wallet_rpc.py @@ -803,11 +803,12 @@ def directsend(self, request, walletname): tx = direct_send( self.services["wallet"], mixdepth, - utxos, dest_and_amounts, + utxos, return_transaction=True, answeryes=True ) + jm_single().config.set("POLICY", "tx_fees", self.default_policy_tx_fees) except AssertionError: