From e038ceb9be501b7f252d73c55c84d1409025307c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Mon, 10 Nov 2025 16:26:14 +0100 Subject: [PATCH] refactor: lndrest get_payment_status and pay_invoice use api v2 (#3470) Co-authored-by: Vlad Stan --- lnbits/wallets/lndrest.py | 145 +++++++++--------- .../wallets/fixtures/json/fixtures_rest.json | 23 +-- 2 files changed, 81 insertions(+), 87 deletions(-) diff --git a/lnbits/wallets/lndrest.py b/lnbits/wallets/lndrest.py index b6b7d8db0..861448d6d 100644 --- a/lnbits/wallets/lndrest.py +++ b/lnbits/wallets/lndrest.py @@ -3,7 +3,6 @@ import base64 import hashlib import json from collections.abc import AsyncGenerator -from typing import Any import httpx from loguru import logger @@ -172,41 +171,23 @@ class LndRestWallet(Wallet): ) async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: - # set the fee limit for the payment - lnrpc_fee_limit = {} - lnrpc_fee_limit["fixed_msat"] = f"{fee_limit_msat}" + req = { + "payment_request": bolt11, + "fee_limit_msat": fee_limit_msat, + "timeout_seconds": 30, + "no_inflight_updates": True, + } + if settings.lnd_rest_allow_self_payment: + req["allow_self_payment"] = 1 try: - json_: dict[str, Any] = { - "payment_request": bolt11, - "fee_limit": lnrpc_fee_limit, - } - if settings.lnd_rest_allow_self_payment: - json_["allow_self_payment"] = 1 r = await self.client.post( - url="/v1/channels/transactions", - json=json_, + url="/v2/router/send", + json=req, timeout=None, ) r.raise_for_status() data = r.json() - - payment_error = data.get("payment_error") - if payment_error: - logger.warning(f"LndRestWallet payment_error: {payment_error}.") - return PaymentResponse(ok=False, error_message=payment_error) - - checking_id = base64.b64decode(data["payment_hash"]).hex() - fee_msat = int(data["payment_route"]["total_fees_msat"]) - preimage = base64.b64decode(data["payment_preimage"]).hex() - return PaymentResponse( - ok=True, checking_id=checking_id, fee_msat=fee_msat, preimage=preimage - ) - except KeyError as exc: - logger.warning(exc) - return PaymentResponse( - error_message="Server error: 'missing required fields'" - ) except json.JSONDecodeError: return PaymentResponse( error_message="Server error: 'invalid json response'" @@ -217,6 +198,40 @@ class LndRestWallet(Wallet): error_message=f"Unable to connect to {self.endpoint}." ) + payment_error = data.get("payment_error") + if payment_error: + logger.warning(f"LndRestWallet payment_error: {payment_error}.") + return PaymentResponse(ok=False, error_message=payment_error) + + try: + payment = data["result"] + status = payment["status"] + checking_id = payment["payment_hash"] + preimage = payment["payment_preimage"] + fee_msat = abs(int(payment["fee_msat"])) + except KeyError as exc: + logger.warning(exc) + return PaymentResponse( + error_message="Server error: 'missing required fields'" + ) + + if status == "SUCCEEDED": + return PaymentResponse( + ok=True, checking_id=checking_id, fee_msat=fee_msat, preimage=preimage + ) + elif status == "FAILED": + reason = payment.get("failure_reason", "unknown reason") + return PaymentResponse( + ok=False, checking_id=checking_id, error_message=reason + ) + elif status == "IN_FLIGHT": + return PaymentResponse(ok=None, checking_id=checking_id) + return PaymentResponse( + ok=False, + checking_id=checking_id, + error_message="Server error: 'unknown payment status returned'", + ) + async def get_invoice_status(self, checking_id: str) -> PaymentStatus: r = await self.client.get(url=f"/v1/invoice/{checking_id}") @@ -224,7 +239,7 @@ class LndRestWallet(Wallet): r.raise_for_status() data = r.json() except Exception as e: - logger.error(f"Error getting invoice status: {e}") + logger.warning(f"Error getting invoice status: {e}") return PaymentPendingStatus() if r.is_error or data.get("settled") is None: @@ -244,7 +259,6 @@ class LndRestWallet(Wallet): """ This routine checks the payment status using routerpc.TrackPaymentV2. """ - # convert checking_id from hex to base64 and some LND magic try: checking_id = base64.urlsafe_b64encode(bytes.fromhex(checking_id)).decode( "ascii" @@ -253,51 +267,40 @@ class LndRestWallet(Wallet): return PaymentPendingStatus() url = f"/v2/router/track/{checking_id}" - - # check payment.status: - # https://api.lightning.community/?python=#paymentpaymentstatus - statuses = { - "UNKNOWN": None, - "IN_FLIGHT": None, - "SUCCEEDED": True, - "FAILED": False, - } - async with self.client.stream("GET", url, timeout=None) as r: async for json_line in r.aiter_lines(): try: line = json.loads(json_line) - if line.get("error"): - logger.error( - line["error"]["message"] - if "message" in line["error"] - else line["error"] + error = line.get("error") + if error: + logger.warning( + error["message"] if "message" in error else error ) - if ( - line["error"].get("code") == 5 - and line["error"].get("message") - == "payment isn't initiated" - ): - return PaymentFailedStatus() - return PaymentPendingStatus() - payment = line.get("result") - if payment is not None and payment.get("status"): - return PaymentStatus( - paid=statuses[payment["status"]], - # API returns fee_msat as string, explicitly convert to int - fee_msat=( - int(payment["fee_msat"]) - if payment.get("fee_msat") - else None - ), - preimage=payment.get("payment_preimage"), - ) - else: return PaymentPendingStatus() except Exception as exc: - logger.debug(exc) + logger.warning("Invalid JSON line in payment status stream:", exc) + return PaymentPendingStatus() + + payment = line.get("result") + if not payment: + logger.warning(f"No payment info found for: {checking_id}") continue + status = payment.get("status") + if status == "SUCCEEDED": + return PaymentSuccessStatus( + fee_msat=abs(int(payment.get("fee_msat", 0))), + preimage=payment.get("payment_preimage"), + ) + elif status == "FAILED": + reason = payment.get("failure_reason", "unknown reason") + logger.info(f"LNDRest Payment failed: {reason}") + return PaymentFailedStatus() + elif status == "IN_FLIGHT": + logger.info(f"LNDRest Payment in flight: {checking_id}") + return PaymentPendingStatus() + + logger.info(f"LNDRest Payment non-existent: {checking_id}") return PaymentPendingStatus() async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: @@ -317,7 +320,7 @@ class LndRestWallet(Wallet): payment_hash = base64.b64decode(inv["r_hash"]).hex() yield payment_hash except Exception as exc: - logger.error( + logger.warning( f"lost connection to lnd invoices stream: '{exc}', retrying in 5" " seconds" ) @@ -356,7 +359,7 @@ class LndRestWallet(Wallet): logger.warning(exc) return InvoiceResponse(ok=False, error_message=exc.response.text) except Exception as exc: - logger.error(exc) + logger.warning(exc) return InvoiceResponse(ok=False, error_message=str(exc)) payment_request = data["payment_request"] @@ -378,7 +381,7 @@ class LndRestWallet(Wallet): logger.warning(exc) return InvoiceResponse(ok=False, error_message=exc.response.text) except Exception as exc: - logger.error(exc) + logger.warning(exc) return InvoiceResponse(ok=False, error_message=str(exc)) async def cancel_hold_invoice(self, payment_hash: str) -> InvoiceResponse: @@ -394,5 +397,5 @@ class LndRestWallet(Wallet): except httpx.HTTPStatusError as exc: return InvoiceResponse(ok=False, error_message=exc.response.text) except Exception as exc: - logger.error(exc) + logger.warning(exc) return InvoiceResponse(ok=False, error_message=str(exc)) diff --git a/tests/wallets/fixtures/json/fixtures_rest.json b/tests/wallets/fixtures/json/fixtures_rest.json index 9ccce9b6f..3206b0e5b 100644 --- a/tests/wallets/fixtures/json/fixtures_rest.json +++ b/tests/wallets/fixtures/json/fixtures_rest.json @@ -1092,7 +1092,7 @@ }, "lndrest": { "pay_invoice_endpoint": { - "uri": "/v1/channels/transactions", + "uri": "/v2/router/send", "headers": { "Grpc-Metadata-macaroon": "eNcRyPtEdMaCaRoOn", "User-Agent": "LNbits/Tests" @@ -1204,11 +1204,12 @@ }, "response_type": "json", "response": { - "payment_hash": "41UmpD0E6YVZTA36uEiBT1JLHHhlmOyaY77dstcmrJY=", - "payment_route": { - "total_fees_msat": 30000 - }, - "payment_preimage": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + "result": { + "status": "SUCCEEDED", + "payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96", + "fee_msat": 30000, + "payment_preimage": "0000000000000000000000000000000000000000000000000000000000000000" + } } } ] @@ -2625,16 +2626,6 @@ "status": "FAILED" } } - }, - { - "description": "error code 5", - "response_type": "stream", - "response": { - "error": { - "code": 5, - "message": "payment isn't initiated" - } - } } ] },