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

View File

@@ -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"
}
}
}
]
},