mirror of
https://github.com/lnbits/lnbits.git
synced 2025-11-26 05:48:30 +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 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))
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user