Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ Feat ] Updated Direct-Send RPC-Api to accept list of UTXOs #1721

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
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
Copy link
Contributor

@roshii roshii Aug 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This (nullable) may be the line that breaks compatibility. Parameter is simply not required, and beside compatibility issue it makes no sense to supply the parameter with a null value.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @roshii, I appreciate your feedback, and thanks for catching that! I've updated the PR and removed nullable: true since it wasn't serving any purpose. However, I don't believe that's the reason behind the OpenAPI Diff failure.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That wasn't the reason for the failure indeed. OpenAPI Diff still catches selected_utxos as required 🤔
I will dig into it soon as as I can.

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
Loading