diff --git a/lnbits/core/services.py b/lnbits/core/services.py index d24bb443b..6bd989ae0 100644 --- a/lnbits/core/services.py +++ b/lnbits/core/services.py @@ -25,6 +25,7 @@ from lnbits.decorators import ( check_user_extension_access, require_admin_key, ) +from lnbits.exceptions import InvoiceError, PaymentError from lnbits.helpers import url_for from lnbits.lnurl import LnurlErrorResponse from lnbits.lnurl import decode as decode_lnurl @@ -70,18 +71,6 @@ from .helpers import to_valid_user_id from .models import BalanceDelta, Payment, PaymentState, User, UserConfig, Wallet -class PaymentError(Exception): - def __init__(self, message: str, status: str = "pending"): - self.message = message - self.status = status - - -class InvoiceError(Exception): - def __init__(self, message: str, status: str = "pending"): - self.message = message - self.status = status - - async def calculate_fiat_amounts( amount: float, wallet_id: str, diff --git a/lnbits/exceptions.py b/lnbits/exceptions.py index 216d52212..c66582a68 100644 --- a/lnbits/exceptions.py +++ b/lnbits/exceptions.py @@ -8,11 +8,21 @@ from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse, RedirectResponse, Response from loguru import logger -from lnbits.core.services import InvoiceError, PaymentError - from .helpers import template_renderer +class PaymentError(Exception): + def __init__(self, message: str, status: str = "pending"): + self.message = message + self.status = status + + +class InvoiceError(Exception): + def __init__(self, message: str, status: str = "pending"): + self.message = message + self.status = status + + def register_exception_handlers(app: FastAPI): register_exception_handler(app) register_request_validation_exception_handler(app) diff --git a/lnbits/lnurl.py b/lnbits/lnurl.py index d16d87524..18ebdf9ec 100644 --- a/lnbits/lnurl.py +++ b/lnbits/lnurl.py @@ -1 +1,65 @@ -from lnurl import LnurlErrorResponse, decode, encode, handle # noqa: F401 +from typing import Callable + +from fastapi import HTTPException, Request, Response +from fastapi.responses import JSONResponse +from fastapi.routing import APIRoute +from lnurl import LnurlErrorResponse, decode, encode, handle +from loguru import logger + +from lnbits.exceptions import InvoiceError, PaymentError + + +class LnurlErrorResponseHandler(APIRoute): + """ + Custom APIRoute class to handle LNURL errors. + LNURL errors always return with status 200 and + a JSON response with `status="ERROR"` and a `reason` key. + Helps to catch HTTPException and return a valid lnurl error response + + Example: + withdraw_lnurl_router = APIRouter(prefix="/api/v1/lnurl") + withdraw_lnurl_router.route_class = LnurlErrorResponseHandler + """ + + def get_route_handler(self) -> Callable: + original_route_handler = super().get_route_handler() + + async def lnurl_route_handler(request: Request) -> Response: + try: + response = await original_route_handler(request) + return response + except (InvoiceError, PaymentError) as exc: + logger.debug(f"Wallet Error: {exc}") + response = JSONResponse( + status_code=200, + content={"status": "ERROR", "reason": f"{exc.message}"}, + ) + return response + except HTTPException as exc: + logger.debug(f"HTTPException: {exc}") + response = JSONResponse( + status_code=200, + content={"status": "ERROR", "reason": f"{exc.detail}"}, + ) + return response + except Exception as exc: + logger.error("Unknown Error:", exc) + response = JSONResponse( + status_code=200, + content={ + "status": "ERROR", + "reason": f"UNKNOWN ERROR: {exc!s}", + }, + ) + return response + + return lnurl_route_handler + + +__all__ = [ + "decode", + "encode", + "handle", + "LnurlErrorResponse", + "LnurlErrorResponseHandler", +]