From 98544cb5967cf768653688c3bd98272ac6286d2b Mon Sep 17 00:00:00 2001 From: amitx13 Date: Sun, 9 Jun 2024 18:47:57 +0530 Subject: [PATCH 01/14] Fixed direct-send RPC-API to accept list of UTXOs --- src/jmclient/taker_utils.py | 104 ++++++++++++++++++------------------ src/jmclient/wallet_rpc.py | 23 +++++--- 2 files changed, 68 insertions(+), 59 deletions(-) diff --git a/src/jmclient/taker_utils.py b/src/jmclient/taker_utils.py index f43b76389..e85bd62dd 100644 --- a/src/jmclient/taker_utils.py +++ b/src/jmclient/taker_utils.py @@ -36,6 +36,7 @@ def get_utxo_scripts(wallet: BaseWallet, utxos: dict) -> list: def direct_send(wallet_service: WalletService, mixdepth: int, + selected_utxos: List[str], dest_and_amounts: List[Tuple[str, int]], answeryes: bool = False, accept_callback: Optional[Callable[[str, str, int, int, Optional[str]], bool]] = None, @@ -46,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 destination address; + """Send coins directly from one mixdepth to one or more destination addresses using specific UTXOs; 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. @@ -56,13 +57,13 @@ def direct_send(wallet_service: WalletService, ==== args: deserialized tx, destination address, amount in satoshis, - fee in satoshis, custom change address + fee in satoshis, custom change address, selected UTXOs returns: True if accepted, False if not ==== - info_callback and error_callback takes one parameter, the information - message (when tx is pushed or error occured), and returns nothing. + info_callback and error_callback take one parameter, the information + message (when tx is pushed or error occurred), and return nothing. This function returns: 1. False if there is any failure. @@ -77,7 +78,7 @@ def direct_send(wallet_service: WalletService, outtypes = [] total_outputs_val = 0 - #Sanity checks + # Sanity checks assert isinstance(dest_and_amounts, list) assert len(dest_and_amounts) > 0 assert custom_change_addr is None or validate_address(custom_change_addr)[0] @@ -128,67 +129,72 @@ def direct_send(wallet_service: WalletService, #doing a sweep destination = dest_and_amounts[0][0] amount = dest_and_amounts[0][1] - utxos = wallet_service.get_utxos_by_mixdepth()[mixdepth] - if utxos == {}: + selected_utxo_dict = wallet_service.get_utxos_by_mixdepth()[mixdepth] + if selected_utxo_dict == {}: 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, + total_inputs_val = sum([va['value'] for u, va in selected_utxo_dict.items()]) + script_types = get_utxo_scripts(wallet_service.wallet, selected_utxo_dict) + fee_est = estimate_tx_fee(len(selected_utxo_dict), 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 + + # 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 + + 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) - # 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()]) + + 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]}) - change_addr = wallet_service.get_internal_addr(mixdepth) \ - if custom_change_addr is None else custom_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}) - + #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 utxos.items(): + if mixdepth == FidelityBondMixin.FIDELITY_BOND_MIXDEPTH and isinstance(wallet_service.wallet, FidelityBondMixin): + for outpoint, utxo in selected_utxo_dict.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 @@ -198,8 +204,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(utxos.keys()), outs, - version=2, locktime=tx_locktime) + + tx = make_shuffled_tx(list(selected_utxo_dict.keys()), outs, version=2, locktime=tx_locktime) if optin_rbf: for inp in tx.vin: @@ -209,9 +215,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] = (utxos[u]["script"], utxos[u]["value"]) - spent_outs.append(CMutableTxOut(utxos[u]["value"], - utxos[u]["script"])) + 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"])) + if with_final_psbt: # here we have the PSBTWalletMixin do the signing stage # for us: @@ -228,12 +234,11 @@ 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 + return False log.info("Got signed transaction:\n") log.info(human_readable_transaction(tx)) - actual_amount = amount if amount != 0 else total_inputs_val - fee_est - sending_info = "Sends: " + amount_to_str(actual_amount) + \ - " to destination: " + destination + 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]) if custom_change_addr: sending_info += ", custom change to: " + custom_change_addr log.info(sending_info) @@ -243,16 +248,13 @@ 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), - destination, actual_amount, fee_est, - custom_change_addr) + accepted = accept_callback(human_readable_transaction(tx), dest_and_amounts[0][0], 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 a2fdc7d6b..00db7b03a 100644 --- a/src/jmclient/wallet_rpc.py +++ b/src/jmclient/wallet_rpc.py @@ -770,7 +770,7 @@ def directsend(self, request, walletname): """ self.check_cookie(request) assert isinstance(request.content, BytesIO) - payment_info_json = self.get_POST_body(request, ["mixdepth", "amount_sats", + payment_info_json = self.get_POST_body(request, ["mixdepth","utxos","amount_sats", "destination"], ["txfee"]) if not payment_info_json: @@ -795,13 +795,20 @@ def directsend(self, request, walletname): raise InvalidRequestFormat() try: - tx = direct_send(self.services["wallet"], - int(payment_info_json["mixdepth"]), - [( - payment_info_json["destination"], - int(payment_info_json["amount_sats"]) - )], - return_transaction=True, answeryes=True) + mixdepth = int(payment_info_json["mixdepth"]) + destination = payment_info_json["destination"] + amount_sats = int(payment_info_json["amount_sats"]) + dest_and_amounts = [(destination, amount_sats)] + utxos = payment_info_json.get("utxos") + + tx = direct_send( + self.services["wallet"], + mixdepth, + utxos, + dest_and_amounts, + return_transaction=True, + answeryes=True + ) jm_single().config.set("POLICY", "tx_fees", self.default_policy_tx_fees) except AssertionError: From 36ba946aa2560f8dd60535160e2dc675c1d72554 Mon Sep 17 00:00:00 2001 From: amitx13 Date: Mon, 10 Jun 2024 00:07:54 +0530 Subject: [PATCH 02/14] making sure directSend is backward compatible --- src/jmclient/taker_utils.py | 51 ++++++++++++++++++------------------- src/jmclient/wallet_rpc.py | 7 +++-- 2 files changed, 28 insertions(+), 30 deletions(-) diff --git a/src/jmclient/taker_utils.py b/src/jmclient/taker_utils.py index e85bd62dd..bf6b8b0e0 100644 --- a/src/jmclient/taker_utils.py +++ b/src/jmclient/taker_utils.py @@ -36,8 +36,8 @@ def get_utxo_scripts(wallet: BaseWallet, utxos: dict) -> list: def direct_send(wallet_service: WalletService, mixdepth: int, - selected_utxos: List[str], dest_and_amounts: List[Tuple[str, int]], + selected_utxos: Optional[List[str]] = None, answeryes: bool = False, accept_callback: Optional[Callable[[str, str, int, int, Optional[str]], bool]] = None, info_callback: Optional[Callable[[str], None]] = None, @@ -129,37 +129,36 @@ def direct_send(wallet_service: WalletService, #doing a sweep destination = dest_and_amounts[0][0] amount = dest_and_amounts[0][1] - selected_utxo_dict = wallet_service.get_utxos_by_mixdepth()[mixdepth] - if selected_utxo_dict == {}: - log.error( - f"There are no available utxos in mixdepth {mixdepth}, " - "quitting.") - return - total_inputs_val = sum([va['value'] for u, va in selected_utxo_dict.items()]) - script_types = get_utxo_scripts(wallet_service.wallet, selected_utxo_dict) - fee_est = estimate_tx_fee(len(selected_utxo_dict), 1, txtype=script_types, - outtype=outtypes[0]) - outs = [{"address": destination, - "value": total_inputs_val - fee_est}] + utxos = wallet_service.get_utxos_by_mixdepth()[mixdepth] + if utxos == {}: + log.error(f"There are no available utxos in mixdepth {mixdepth}, quitting.") + return False + 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}] 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 - # 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 - + 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.") diff --git a/src/jmclient/wallet_rpc.py b/src/jmclient/wallet_rpc.py index 00db7b03a..7a442dea0 100644 --- a/src/jmclient/wallet_rpc.py +++ b/src/jmclient/wallet_rpc.py @@ -770,9 +770,8 @@ def directsend(self, request, walletname): """ self.check_cookie(request) assert isinstance(request.content, BytesIO) - payment_info_json = self.get_POST_body(request, ["mixdepth","utxos","amount_sats", - "destination"], - ["txfee"]) + payment_info_json = self.get_POST_body(request, ["mixdepth", "amount_sats", "destination"], ["txfee", "utxos"]) + if not payment_info_json: raise InvalidRequestFormat() if not self.services["wallet"]: @@ -799,7 +798,7 @@ def directsend(self, request, walletname): destination = payment_info_json["destination"] amount_sats = int(payment_info_json["amount_sats"]) dest_and_amounts = [(destination, amount_sats)] - utxos = payment_info_json.get("utxos") + utxos = payment_info_json.get("utxos", None) tx = direct_send( self.services["wallet"], From 1ebcd96959114b2830336335374140b0922c562f Mon Sep 17 00:00:00 2001 From: amitx13 Date: Tue, 11 Jun 2024 09:09:50 +0530 Subject: [PATCH 03/14] 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: From f2fbdd63519e718b8e139c88ee02e53c3b8d5036 Mon Sep 17 00:00:00 2001 From: amitx13 Date: Wed, 12 Jun 2024 16:35:01 +0530 Subject: [PATCH 04/14] Updated wallet-rpc.yaml --- docs/api/wallet-rpc.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/api/wallet-rpc.yaml b/docs/api/wallet-rpc.yaml index fa8a2e682..c04ad983c 100644 --- a/docs/api/wallet-rpc.yaml +++ b/docs/api/wallet-rpc.yaml @@ -1124,8 +1124,9 @@ components: 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. + 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: From 716cb468442f66a57d6d5a738b27066d85bf9700 Mon Sep 17 00:00:00 2001 From: amitx13 Date: Thu, 13 Jun 2024 00:03:55 +0530 Subject: [PATCH 05/14] Updated optional parameter utxos in wallet-rpc.yaml --- docs/api/wallet-rpc.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api/wallet-rpc.yaml b/docs/api/wallet-rpc.yaml index c04ad983c..4064c6a25 100644 --- a/docs/api/wallet-rpc.yaml +++ b/docs/api/wallet-rpc.yaml @@ -1124,9 +1124,9 @@ components: 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." + 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: From 75097a8e6ddc2326f569eb0af580b0c0fb8ca034 Mon Sep 17 00:00:00 2001 From: amitx13 Date: Sun, 16 Jun 2024 17:25:34 +0530 Subject: [PATCH 06/14] added checks in direct-send for selected_utxos and updated wallet-rpc.yaml --- docs/api/wallet-rpc.yaml | 7 +++---- src/jmclient/taker_utils.py | 9 ++++++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/api/wallet-rpc.yaml b/docs/api/wallet-rpc.yaml index 4064c6a25..fccc70db3 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) 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. + description: create and broadcast a transaction (without coinjoin) parameters: - name: walletname in: path @@ -1124,9 +1124,8 @@ components: 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." + 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 61ef6192a..a799fb5b6 100644 --- a/src/jmclient/taker_utils.py +++ b/src/jmclient/taker_utils.py @@ -173,7 +173,14 @@ def direct_send(wallet_service: WalletService, utxo_str = f"{txid}:{index}" if utxo_str in selected_utxos: utxos[(u[0], u[1])] = va - + + # Check if all selected_utxos are present in utxos + for utxo_str in selected_utxos: + txid, index = utxo_str.split(':') + if not any(u[0].hex() == txid and str(u[1]) == index for u in utxos.keys()): + log.error(f"Selected UTXO {utxo_str} is not available in the specified mixdepth.") + return False + if not utxos: log.error("None of the selected UTXOs are available in the specified mixdepth.") return False From 5b305b3ce2a2e5df6f7eed182b23cd815ac5c2a5 Mon Sep 17 00:00:00 2001 From: amitx13 Date: Sun, 16 Jun 2024 17:30:23 +0530 Subject: [PATCH 07/14] direct-send function comment updated --- src/jmclient/taker_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/jmclient/taker_utils.py b/src/jmclient/taker_utils.py index a799fb5b6..16f304cd5 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 either using specific UTXOs or by mixdepth; + """Send coins directly either by mixdepth or selected UTXOs from a certain mixdepth to one or more destination addresses; 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. @@ -173,7 +173,7 @@ def direct_send(wallet_service: WalletService, utxo_str = f"{txid}:{index}" if utxo_str in selected_utxos: utxos[(u[0], u[1])] = va - + # Check if all selected_utxos are present in utxos for utxo_str in selected_utxos: txid, index = utxo_str.split(':') From 81881235829582b14e0669373d9442cedae23986 Mon Sep 17 00:00:00 2001 From: amitx13 Date: Wed, 19 Jun 2024 13:18:44 +0530 Subject: [PATCH 08/14] Updated wallet-rpc.yaml for optional parameter utxos --- docs/api/wallet-rpc.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/api/wallet-rpc.yaml b/docs/api/wallet-rpc.yaml index fccc70db3..300d84da9 100644 --- a/docs/api/wallet-rpc.yaml +++ b/docs/api/wallet-rpc.yaml @@ -1124,8 +1124,11 @@ components: 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. + 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: From 88f1917349b5a65d962e0fe2d10d6aebb46ef859 Mon Sep 17 00:00:00 2001 From: amitx13 Date: Wed, 19 Jun 2024 23:05:37 +0530 Subject: [PATCH 09/14] Updated wallet-rpc.yaml --- docs/api/wallet-rpc.yaml | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/docs/api/wallet-rpc.yaml b/docs/api/wallet-rpc.yaml index 300d84da9..c66bd6c2f 100644 --- a/docs/api/wallet-rpc.yaml +++ b/docs/api/wallet-rpc.yaml @@ -1121,14 +1121,9 @@ components: 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. + 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: From a0c1777bc09fd921ef5f0e83349dea2c0dd83516 Mon Sep 17 00:00:00 2001 From: amitx13 Date: Thu, 20 Jun 2024 17:48:33 +0530 Subject: [PATCH 10/14] Removed utxos field from wallet-rpc.yaml --- docs/api/wallet-rpc.yaml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/docs/api/wallet-rpc.yaml b/docs/api/wallet-rpc.yaml index c66bd6c2f..df39ed3d2 100644 --- a/docs/api/wallet-rpc.yaml +++ b/docs/api/wallet-rpc.yaml @@ -1120,10 +1120,6 @@ 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: 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: @@ -1305,4 +1301,4 @@ components: content: application/json: schema: - $ref: '#/components/schemas/ErrorMessage' + $ref: '#/components/schemas/ErrorMessage' \ No newline at end of file From 2fcd7e7cd8242bb9a9b1ce4debb4449cad37a4ef Mon Sep 17 00:00:00 2001 From: amitx13 Date: Sat, 13 Jul 2024 18:11:10 +0530 Subject: [PATCH 11/14] Handled utxos as txfee , Updated wallet-rpc.yaml added optional parameter utxos --- docs/api/wallet-rpc.yaml | 6 ++++++ src/jmclient/taker_utils.py | 6 ++++-- src/jmclient/wallet_rpc.py | 11 +++++++++-- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/docs/api/wallet-rpc.yaml b/docs/api/wallet-rpc.yaml index df39ed3d2..55946e2b4 100644 --- a/docs/api/wallet-rpc.yaml +++ b/docs/api/wallet-rpc.yaml @@ -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: "85cf4c880876eead0a6674cbc341b21b86058530c2eacf18a16007f8f9cb1b1a: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 16f304cd5..fe4ddbfcd 100644 --- a/src/jmclient/taker_utils.py +++ b/src/jmclient/taker_utils.py @@ -4,6 +4,7 @@ import sys import time import numbers +import ast from typing import Callable, List, Optional, Tuple, Union from jmbase import get_log, jmprint, bintohex, hextobin, \ @@ -37,7 +38,6 @@ def get_utxo_scripts(wallet: BaseWallet, utxos: dict) -> list: def direct_send(wallet_service: WalletService, mixdepth: int, dest_and_amounts: List[Tuple[str, int]], - selected_utxos: Optional[List[str]] = None, answeryes: bool = False, accept_callback: Optional[Callable[[str, str, int, int, Optional[str]], bool]] = None, info_callback: Optional[Callable[[str], None]] = None, @@ -161,7 +161,9 @@ def direct_send(wallet_service: WalletService, outs = [] utxos = {} - if selected_utxos: + selected_utxos = jm_single().config.get("POLICY","utxos") + selected_utxos = ast.literal_eval(selected_utxos) + if selected_utxos is not None: # Filter UTXOs based on selected_utxos all_utxos = wallet_service.get_utxos_by_mixdepth().get(mixdepth, {}) if not all_utxos: diff --git a/src/jmclient/wallet_rpc.py b/src/jmclient/wallet_rpc.py index 86b2109fa..f1f78b333 100644 --- a/src/jmclient/wallet_rpc.py +++ b/src/jmclient/wallet_rpc.py @@ -792,36 +792,43 @@ def directsend(self, request, walletname): str(payment_info_json["txfee"])) else: raise InvalidRequestFormat() + + if "utxos" in payment_info_json: + jm_single().config.set("POLICY", "utxos", str(payment_info_json["utxos"])) + else: + jm_single().config.set("POLICY", "utxos", str(None)) try: mixdepth = int(payment_info_json["mixdepth"]) destination = payment_info_json["destination"] amount_sats = int(payment_info_json["amount_sats"]) dest_and_amounts = [(destination, amount_sats)] - utxos = payment_info_json.get("utxos", None) tx = direct_send( self.services["wallet"], mixdepth, dest_and_amounts, - utxos, return_transaction=True, answeryes=True ) jm_single().config.set("POLICY", "tx_fees", self.default_policy_tx_fees) + jm_single().config.set("POLICY", "utxos", str(None)) except AssertionError: jm_single().config.set("POLICY", "tx_fees", self.default_policy_tx_fees) + jm_single().config.set("POLICY", "utxos", str(None)) raise InvalidRequestFormat() except NotEnoughFundsException as e: jm_single().config.set("POLICY", "tx_fees", self.default_policy_tx_fees) + jm_single().config.set("POLICY", "utxos", str(None)) raise TransactionFailed(repr(e)) except Exception: jm_single().config.set("POLICY", "tx_fees", self.default_policy_tx_fees) + jm_single().config.set("POLICY", "utxos", str(None)) raise if not tx: # this should not really happen; not a coinjoin From 48928ff155cc1ceaee9e08ade5206a3f406922bb Mon Sep 17 00:00:00 2001 From: amitx13 Date: Sat, 13 Jul 2024 18:17:26 +0530 Subject: [PATCH 12/14] Updated some comments --- src/jmclient/taker_utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/jmclient/taker_utils.py b/src/jmclient/taker_utils.py index fe4ddbfcd..891242193 100644 --- a/src/jmclient/taker_utils.py +++ b/src/jmclient/taker_utils.py @@ -57,13 +57,13 @@ def direct_send(wallet_service: WalletService, ==== args: deserialized tx, destination address, amount in satoshis, - fee in satoshis, custom change address, selected UTXOs + fee in satoshis, custom change address returns: True if accepted, False if not ==== - info_callback and error_callback take one parameter, the information - message (when tx is pushed or error occurred), and return nothing. + info_callback and error_callback takes one parameter, the information + message (when tx is pushed or error occured), and returns nothing. This function returns: 1. False if there is any failure. @@ -78,7 +78,7 @@ def direct_send(wallet_service: WalletService, outtypes = [] total_outputs_val = 0 - # Sanity checks + #Sanity checks assert isinstance(dest_and_amounts, list) assert len(dest_and_amounts) > 0 assert custom_change_addr is None or validate_address(custom_change_addr)[0] From e92dd4ecac2864dc06ef1a54d9c92a3eca646167 Mon Sep 17 00:00:00 2001 From: amitx13 Date: Tue, 16 Jul 2024 12:03:37 +0530 Subject: [PATCH 13/14] reverted to previous commits --- src/jmclient/taker_utils.py | 6 ++---- src/jmclient/wallet_rpc.py | 11 ++--------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/jmclient/taker_utils.py b/src/jmclient/taker_utils.py index 891242193..1929fa02e 100644 --- a/src/jmclient/taker_utils.py +++ b/src/jmclient/taker_utils.py @@ -4,7 +4,6 @@ import sys import time import numbers -import ast from typing import Callable, List, Optional, Tuple, Union from jmbase import get_log, jmprint, bintohex, hextobin, \ @@ -38,6 +37,7 @@ def get_utxo_scripts(wallet: BaseWallet, utxos: dict) -> list: def direct_send(wallet_service: WalletService, mixdepth: int, dest_and_amounts: List[Tuple[str, int]], + selected_utxos: Optional[List[str]] = None, answeryes: bool = False, accept_callback: Optional[Callable[[str, str, int, int, Optional[str]], bool]] = None, info_callback: Optional[Callable[[str], None]] = None, @@ -161,9 +161,7 @@ def direct_send(wallet_service: WalletService, outs = [] utxos = {} - selected_utxos = jm_single().config.get("POLICY","utxos") - selected_utxos = ast.literal_eval(selected_utxos) - if selected_utxos is not None: + if selected_utxos: # Filter UTXOs based on selected_utxos all_utxos = wallet_service.get_utxos_by_mixdepth().get(mixdepth, {}) if not all_utxos: diff --git a/src/jmclient/wallet_rpc.py b/src/jmclient/wallet_rpc.py index f1f78b333..86b2109fa 100644 --- a/src/jmclient/wallet_rpc.py +++ b/src/jmclient/wallet_rpc.py @@ -792,43 +792,36 @@ def directsend(self, request, walletname): str(payment_info_json["txfee"])) else: raise InvalidRequestFormat() - - if "utxos" in payment_info_json: - jm_single().config.set("POLICY", "utxos", str(payment_info_json["utxos"])) - else: - jm_single().config.set("POLICY", "utxos", str(None)) try: mixdepth = int(payment_info_json["mixdepth"]) destination = payment_info_json["destination"] amount_sats = int(payment_info_json["amount_sats"]) dest_and_amounts = [(destination, amount_sats)] + utxos = payment_info_json.get("utxos", None) tx = direct_send( self.services["wallet"], mixdepth, dest_and_amounts, + utxos, return_transaction=True, answeryes=True ) jm_single().config.set("POLICY", "tx_fees", self.default_policy_tx_fees) - jm_single().config.set("POLICY", "utxos", str(None)) except AssertionError: jm_single().config.set("POLICY", "tx_fees", self.default_policy_tx_fees) - jm_single().config.set("POLICY", "utxos", str(None)) raise InvalidRequestFormat() except NotEnoughFundsException as e: jm_single().config.set("POLICY", "tx_fees", self.default_policy_tx_fees) - jm_single().config.set("POLICY", "utxos", str(None)) raise TransactionFailed(repr(e)) except Exception: jm_single().config.set("POLICY", "tx_fees", self.default_policy_tx_fees) - jm_single().config.set("POLICY", "utxos", str(None)) raise if not tx: # this should not really happen; not a coinjoin From 871948442710ba25e256df54b6d32fc8198d4422 Mon Sep 17 00:00:00 2001 From: amitx13 Date: Thu, 18 Jul 2024 09:50:27 +0530 Subject: [PATCH 14/14] Fixed selected_utxo parameter acc to wallet-rpc.yaml, Added Type Checking, Added nullable:true in wallet-rpc.yaml --- docs/api/wallet-rpc.yaml | 6 +++--- src/jmclient/taker_utils.py | 2 +- src/jmclient/wallet_rpc.py | 13 ++++++++++--- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/api/wallet-rpc.yaml b/docs/api/wallet-rpc.yaml index 55946e2b4..e3c3e2fa1 100644 --- a/docs/api/wallet-rpc.yaml +++ b/docs/api/wallet-rpc.yaml @@ -1120,12 +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: + selected_utxos: type: array items: type: string - example: "85cf4c880876eead0a6674cbc341b21b86058530c2eacf18a16007f8f9cb1b1a: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. + example: 85cf4c880876eead0a6674cbc341b21b86058530c2eacf18a16007f8f9cb1b1a:0 + nullable: true ErrorMessage: type: object properties: diff --git a/src/jmclient/taker_utils.py b/src/jmclient/taker_utils.py index 1929fa02e..61ba5b6f8 100644 --- a/src/jmclient/taker_utils.py +++ b/src/jmclient/taker_utils.py @@ -57,7 +57,7 @@ def direct_send(wallet_service: WalletService, ==== args: deserialized tx, destination address, amount in satoshis, - fee in satoshis, custom change address + fee in satoshis, custom change address, selected utxos returns: True if accepted, False if not diff --git a/src/jmclient/wallet_rpc.py b/src/jmclient/wallet_rpc.py index 86b2109fa..d89fcf3d6 100644 --- a/src/jmclient/wallet_rpc.py +++ b/src/jmclient/wallet_rpc.py @@ -770,7 +770,7 @@ def directsend(self, request, walletname): """ self.check_cookie(request) assert isinstance(request.content, BytesIO) - payment_info_json = self.get_POST_body(request, ["mixdepth", "amount_sats", "destination"], ["txfee", "utxos"]) + payment_info_json = self.get_POST_body(request, ["mixdepth", "amount_sats", "destination"], ["txfee", "selected_utxos"]) if not payment_info_json: raise InvalidRequestFormat() @@ -793,18 +793,25 @@ def directsend(self, request, walletname): else: raise InvalidRequestFormat() + selected_utxos = payment_info_json.get("selected_utxos") + if selected_utxos: + if not isinstance(selected_utxos, list): + raise InvalidRequestFormat() + for utxo in selected_utxos: + if not isinstance(utxo, str) or ":" not in utxo: + raise InvalidRequestFormat() + try: mixdepth = int(payment_info_json["mixdepth"]) destination = payment_info_json["destination"] amount_sats = int(payment_info_json["amount_sats"]) dest_and_amounts = [(destination, amount_sats)] - utxos = payment_info_json.get("utxos", None) tx = direct_send( self.services["wallet"], mixdepth, dest_and_amounts, - utxos, + selected_utxos, return_transaction=True, answeryes=True )