Skip to content

Commit

Permalink
Added optional utxos parameter to allow specifying UTXOs for the tran…
Browse files Browse the repository at this point in the history
…saction and Updated wallet-rpc.yaml
  • Loading branch information
amitx13 committed Jun 11, 2024
1 parent 36ba946 commit 1ebcd96
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 55 deletions.
8 changes: 7 additions & 1 deletion docs/api/wallet-rpc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
137 changes: 84 additions & 53 deletions src/jmclient/taker_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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)
Expand All @@ -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])
Expand Down
3 changes: 2 additions & 1 deletion src/jmclient/wallet_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit 1ebcd96

Please sign in to comment.