mirror of
https://github.com/lnbits/lnbits.git
synced 2025-11-25 21:39:13 +01:00
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:
@@ -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))
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user