Skip to content

Commit

Permalink
Updated Direct-Send RPC-Api to accept list of UTXOs
Browse files Browse the repository at this point in the history
  • Loading branch information
amitx13 committed Jul 19, 2024
1 parent d17689a commit d84b532
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 32 deletions.
6 changes: 6 additions & 0 deletions docs/api/wallet-rpc.yaml
Original file line number Diff line number Diff line change
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.
selected_utxos:
type: array
items:
type: string
example: 85cf4c880876eead0a6674cbc341b21b86058530c2eacf18a16007f8f9cb1b1a:0
nullable: true
ErrorMessage:
type: object
properties:
Expand Down
83 changes: 61 additions & 22 deletions src/jmclient/taker_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,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,
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 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.
Expand All @@ -56,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
Expand Down Expand Up @@ -157,28 +158,66 @@ def direct_send(wallet_service: WalletService,
# 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()])
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
outs.append({"value": changeval, "address": change_addr})
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

# 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
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})

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 \
Expand Down
34 changes: 24 additions & 10 deletions src/jmclient/wallet_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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", "amount_sats",
"destination"],
["txfee"])
payment_info_json = self.get_POST_body(request, ["mixdepth", "amount_sats", "destination"], ["txfee", "selected_utxos"])

if not payment_info_json:
raise InvalidRequestFormat()
if not self.services["wallet"]:
Expand All @@ -794,14 +793,29 @@ 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:
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)]

tx = direct_send(
self.services["wallet"],
mixdepth,
dest_and_amounts,
selected_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 d84b532

Please sign in to comment.