Skip to content
This repository was archived by the owner on Apr 27, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 53 additions & 51 deletions src/jmclient/taker_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Comment thread
amitx13 marked this conversation as resolved.
Outdated
dest_and_amounts: List[Tuple[str, int]],
answeryes: bool = False,
accept_callback: Optional[Callable[[str, str, int, int, Optional[str]], bool]] = None,
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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]
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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)
Expand All @@ -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])
Expand Down
23 changes: 15 additions & 8 deletions src/jmclient/wallet_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Comment thread
amitx13 marked this conversation as resolved.
Outdated
"destination"],
["txfee"])
if not payment_info_json:
Expand All @@ -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:
Expand Down