refactor: lndrest get_payment_status and pay_invoice use api v2 (#3470)

Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
This commit is contained in:
dni ⚡
2025-11-10 16:26:14 +01:00
committed by GitHub
parent b4eccb9e5d
commit e038ceb9be
2 changed files with 81 additions and 87 deletions

View File

@@ -3,7 +3,6 @@ import base64
import hashlib import hashlib
import json import json
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from typing import Any
import httpx import httpx
from loguru import logger from loguru import logger
@@ -172,41 +171,23 @@ class LndRestWallet(Wallet):
) )
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
# set the fee limit for the payment req = {
lnrpc_fee_limit = {}
lnrpc_fee_limit["fixed_msat"] = f"{fee_limit_msat}"
try:
json_: dict[str, Any] = {
"payment_request": bolt11, "payment_request": bolt11,
"fee_limit": lnrpc_fee_limit, "fee_limit_msat": fee_limit_msat,
"timeout_seconds": 30,
"no_inflight_updates": True,
} }
if settings.lnd_rest_allow_self_payment: if settings.lnd_rest_allow_self_payment:
json_["allow_self_payment"] = 1 req["allow_self_payment"] = 1
try:
r = await self.client.post( r = await self.client.post(
url="/v1/channels/transactions", url="/v2/router/send",
json=json_, json=req,
timeout=None, timeout=None,
) )
r.raise_for_status() r.raise_for_status()
data = r.json() 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: except json.JSONDecodeError:
return PaymentResponse( return PaymentResponse(
error_message="Server error: 'invalid json response'" error_message="Server error: 'invalid json response'"
@@ -217,6 +198,40 @@ class LndRestWallet(Wallet):
error_message=f"Unable to connect to {self.endpoint}." 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: async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
r = await self.client.get(url=f"/v1/invoice/{checking_id}") r = await self.client.get(url=f"/v1/invoice/{checking_id}")
@@ -224,7 +239,7 @@ class LndRestWallet(Wallet):
r.raise_for_status() r.raise_for_status()
data = r.json() data = r.json()
except Exception as e: except Exception as e:
logger.error(f"Error getting invoice status: {e}") logger.warning(f"Error getting invoice status: {e}")
return PaymentPendingStatus() return PaymentPendingStatus()
if r.is_error or data.get("settled") is None: 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. This routine checks the payment status using routerpc.TrackPaymentV2.
""" """
# convert checking_id from hex to base64 and some LND magic
try: try:
checking_id = base64.urlsafe_b64encode(bytes.fromhex(checking_id)).decode( checking_id = base64.urlsafe_b64encode(bytes.fromhex(checking_id)).decode(
"ascii" "ascii"
@@ -253,51 +267,40 @@ class LndRestWallet(Wallet):
return PaymentPendingStatus() return PaymentPendingStatus()
url = f"/v2/router/track/{checking_id}" 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 with self.client.stream("GET", url, timeout=None) as r:
async for json_line in r.aiter_lines(): async for json_line in r.aiter_lines():
try: try:
line = json.loads(json_line) line = json.loads(json_line)
if line.get("error"): error = line.get("error")
logger.error( if error:
line["error"]["message"] logger.warning(
if "message" in line["error"] error["message"] if "message" in error else error
else line["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() return PaymentPendingStatus()
except Exception as exc: 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 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() return PaymentPendingStatus()
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
@@ -317,7 +320,7 @@ class LndRestWallet(Wallet):
payment_hash = base64.b64decode(inv["r_hash"]).hex() payment_hash = base64.b64decode(inv["r_hash"]).hex()
yield payment_hash yield payment_hash
except Exception as exc: except Exception as exc:
logger.error( logger.warning(
f"lost connection to lnd invoices stream: '{exc}', retrying in 5" f"lost connection to lnd invoices stream: '{exc}', retrying in 5"
" seconds" " seconds"
) )
@@ -356,7 +359,7 @@ class LndRestWallet(Wallet):
logger.warning(exc) logger.warning(exc)
return InvoiceResponse(ok=False, error_message=exc.response.text) return InvoiceResponse(ok=False, error_message=exc.response.text)
except Exception as exc: except Exception as exc:
logger.error(exc) logger.warning(exc)
return InvoiceResponse(ok=False, error_message=str(exc)) return InvoiceResponse(ok=False, error_message=str(exc))
payment_request = data["payment_request"] payment_request = data["payment_request"]
@@ -378,7 +381,7 @@ class LndRestWallet(Wallet):
logger.warning(exc) logger.warning(exc)
return InvoiceResponse(ok=False, error_message=exc.response.text) return InvoiceResponse(ok=False, error_message=exc.response.text)
except Exception as exc: except Exception as exc:
logger.error(exc) logger.warning(exc)
return InvoiceResponse(ok=False, error_message=str(exc)) return InvoiceResponse(ok=False, error_message=str(exc))
async def cancel_hold_invoice(self, payment_hash: str) -> InvoiceResponse: async def cancel_hold_invoice(self, payment_hash: str) -> InvoiceResponse:
@@ -394,5 +397,5 @@ class LndRestWallet(Wallet):
except httpx.HTTPStatusError as exc: except httpx.HTTPStatusError as exc:
return InvoiceResponse(ok=False, error_message=exc.response.text) return InvoiceResponse(ok=False, error_message=exc.response.text)
except Exception as exc: except Exception as exc:
logger.error(exc) logger.warning(exc)
return InvoiceResponse(ok=False, error_message=str(exc)) return InvoiceResponse(ok=False, error_message=str(exc))

View File

@@ -1092,7 +1092,7 @@
}, },
"lndrest": { "lndrest": {
"pay_invoice_endpoint": { "pay_invoice_endpoint": {
"uri": "/v1/channels/transactions", "uri": "/v2/router/send",
"headers": { "headers": {
"Grpc-Metadata-macaroon": "eNcRyPtEdMaCaRoOn", "Grpc-Metadata-macaroon": "eNcRyPtEdMaCaRoOn",
"User-Agent": "LNbits/Tests" "User-Agent": "LNbits/Tests"
@@ -1204,11 +1204,12 @@
}, },
"response_type": "json", "response_type": "json",
"response": { "response": {
"payment_hash": "41UmpD0E6YVZTA36uEiBT1JLHHhlmOyaY77dstcmrJY=", "result": {
"payment_route": { "status": "SUCCEEDED",
"total_fees_msat": 30000 "payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
}, "fee_msat": 30000,
"payment_preimage": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" "payment_preimage": "0000000000000000000000000000000000000000000000000000000000000000"
}
} }
} }
] ]
@@ -2625,16 +2626,6 @@
"status": "FAILED" "status": "FAILED"
} }
} }
},
{
"description": "error code 5",
"response_type": "stream",
"response": {
"error": {
"code": 5,
"message": "payment isn't initiated"
}
}
} }
] ]
}, },