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

Fixed direct-send RPC-API to accept list of UTXOs #1713

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
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
8 changes: 7 additions & 1 deletion 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 Expand Up @@ -1301,4 +1307,4 @@ components:
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
$ref: '#/components/schemas/ErrorMessage'
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,27 +158,65 @@ 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()
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