diff --git a/lnbits/app.py b/lnbits/app.py index b27496f8..009f884d 100644 --- a/lnbits/app.py +++ b/lnbits/app.py @@ -7,7 +7,7 @@ import traceback from http import HTTPStatus from fastapi import FastAPI, Request -from fastapi.exceptions import RequestValidationError +from fastapi.exceptions import HTTPException, RequestValidationError from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.gzip import GZipMiddleware from fastapi.responses import JSONResponse @@ -214,12 +214,33 @@ def register_async_tasks(app): def register_exception_handlers(app: FastAPI): @app.exception_handler(Exception) - async def basic_error(request: Request, err): - logger.error("handled error", traceback.format_exc()) - logger.error("ERROR:", err) + async def exception_handler(request: Request, exc: Exception): etype, _, tb = sys.exc_info() - traceback.print_exception(etype, err, tb) - exc = traceback.format_exc() + traceback.print_exception(etype, exc, tb) + logger.error(f"Exception: {str(exc)}") + # Only the browser sends "text/html" request + # not fail proof, but everything else get's a JSON response + if ( + request.headers + and "accept" in request.headers + and "text/html" in request.headers["accept"] + ): + return template_renderer().TemplateResponse( + "error.html", {"request": request, "err": f"Error: {str(exc)}"} + ) + + return JSONResponse( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, + content={"detail": str(exc)}, + ) + + @app.exception_handler(RequestValidationError) + async def validation_exception_handler( + request: Request, exc: RequestValidationError + ): + logger.error(f"RequestValidationError: {str(exc)}") + # Only the browser sends "text/html" request + # not fail proof, but everything else get's a JSON response if ( request.headers @@ -227,12 +248,37 @@ def register_exception_handlers(app: FastAPI): and "text/html" in request.headers["accept"] ): return template_renderer().TemplateResponse( - "error.html", {"request": request, "err": err} + "error.html", + {"request": request, "err": f"Error: {str(exc)}"}, ) return JSONResponse( - status_code=HTTPStatus.NO_CONTENT, - content={"detail": err}, + status_code=HTTPStatus.BAD_REQUEST, + content={"detail": str(exc)}, + ) + + @app.exception_handler(HTTPException) + async def http_exception_handler(request: Request, exc: HTTPException): + logger.error(f"HTTPException {exc.status_code}: {exc.detail}") + # Only the browser sends "text/html" request + # not fail proof, but everything else get's a JSON response + + if ( + request.headers + and "accept" in request.headers + and "text/html" in request.headers["accept"] + ): + return template_renderer().TemplateResponse( + "error.html", + { + "request": request, + "err": f"HTTP Error {exc.status_code}: {exc.detail}", + }, + ) + + return JSONResponse( + status_code=exc.status_code, + content={"detail": exc.detail}, ) @app.exception_handler(RequestValidationError) diff --git a/lnbits/extensions/cashu/views_api.py b/lnbits/extensions/cashu/views_api.py index ad253abf..2d10f3f0 100644 --- a/lnbits/extensions/cashu/views_api.py +++ b/lnbits/extensions/cashu/views_api.py @@ -221,7 +221,7 @@ async def mint_coins( status: PaymentStatus = await check_transaction_status(cashu.wallet, payment_hash) - if status.paid != True: + if LIGHTNING and status.paid != True: raise HTTPException( status_code=HTTPStatus.PAYMENT_REQUIRED, detail="Invoice not paid." ) @@ -265,37 +265,51 @@ async def melt_coins( detail="Error: Tokens are from another mint.", ) - assert all([ledger._verify_proof(p) for p in proofs]), HTTPException( - status_code=HTTPStatus.BAD_REQUEST, - detail="Could not verify proofs.", - ) + # set proofs as pending + await ledger._set_proofs_pending(proofs) - total_provided = sum([p["amount"] for p in proofs]) - invoice_obj = bolt11.decode(invoice) - amount = math.ceil(invoice_obj.amount_msat / 1000) + try: + ledger._verify_proofs(proofs) - internal_checking_id = await check_internal(invoice_obj.payment_hash) + total_provided = sum([p["amount"] for p in proofs]) + invoice_obj = bolt11.decode(invoice) + amount = math.ceil(invoice_obj.amount_msat / 1000) - if not internal_checking_id: - fees_msat = fee_reserve(invoice_obj.amount_msat) - else: - fees_msat = 0 - assert total_provided >= amount + fees_msat / 1000, Exception( - f"Provided proofs ({total_provided} sats) not enough for Lightning payment ({amount + fees_msat} sats)." - ) + internal_checking_id = await check_internal(invoice_obj.payment_hash) - await pay_invoice( - wallet_id=cashu.wallet, - payment_request=invoice, - description=f"pay cashu invoice", - extra={"tag": "cashu", "cahsu_name": cashu.name}, - ) + if not internal_checking_id: + fees_msat = fee_reserve(invoice_obj.amount_msat) + else: + fees_msat = 0 + assert total_provided >= amount + math.ceil(fees_msat / 1000), Exception( + f"Provided proofs ({total_provided} sats) not enough for Lightning payment ({amount + fees_msat} sats)." + ) + logger.debug(f"Cashu: Initiating payment of {total_provided} sats") + await pay_invoice( + wallet_id=cashu.wallet, + payment_request=invoice, + description=f"Pay cashu invoice", + extra={"tag": "cashu", "cashu_name": cashu.name}, + ) + + logger.debug( + f"Cashu: Wallet {cashu.wallet} checking PaymentStatus of {invoice_obj.payment_hash}" + ) + status: PaymentStatus = await check_transaction_status( + cashu.wallet, invoice_obj.payment_hash + ) + if status.paid == True: + logger.debug("Cashu: Payment successful, invalidating proofs") + await ledger._invalidate_proofs(proofs) + except Exception as e: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=f"Cashu: {str(e)}", + ) + finally: + # delete proofs from pending list + await ledger._unset_proofs_pending(proofs) - status: PaymentStatus = await check_transaction_status( - cashu.wallet, invoice_obj.payment_hash - ) - if status.paid == True: - await ledger._invalidate_proofs(proofs) return GetMeltResponse(paid=status.paid, preimage=status.preimage) @@ -333,7 +347,7 @@ async def check_fees( fees_msat = fee_reserve(invoice_obj.amount_msat) else: fees_msat = 0 - return CheckFeesResponse(fee=fees_msat / 1000) + return CheckFeesResponse(fee=math.ceil(fees_msat / 1000)) @cashu_ext.post("/api/v1/{cashu_id}/split") diff --git a/lnbits/extensions/satspay/helpers.py b/lnbits/extensions/satspay/helpers.py index 60c5ba4a..b21a3ae2 100644 --- a/lnbits/extensions/satspay/helpers.py +++ b/lnbits/extensions/satspay/helpers.py @@ -37,7 +37,11 @@ async def call_webhook(charge: Charges): json=public_charge(charge), timeout=40, ) - return {"webhook_success": r.is_success, "webhook_message": r.reason_phrase} + return { + "webhook_success": r.is_success, + "webhook_message": r.reason_phrase, + "webhook_response": r.text, + } except Exception as e: logger.warning(f"Failed to call webhook for charge {charge.id}") logger.warning(e) diff --git a/lnbits/extensions/satspay/static/js/utils.js b/lnbits/extensions/satspay/static/js/utils.js index 2b1be8bd..5317673f 100644 --- a/lnbits/extensions/satspay/static/js/utils.js +++ b/lnbits/extensions/satspay/static/js/utils.js @@ -23,6 +23,7 @@ const mapCharge = (obj, oldObj = {}) => { charge.displayUrl = ['/satspay/', obj.id].join('') charge.expanded = oldObj.expanded || false charge.pendingBalance = oldObj.pendingBalance || 0 + charge.extra = charge.extra ? JSON.parse(charge.extra) : charge.extra return charge } diff --git a/lnbits/extensions/satspay/templates/satspay/index.html b/lnbits/extensions/satspay/templates/satspay/index.html index 602b1a28..2dda8792 100644 --- a/lnbits/extensions/satspay/templates/satspay/index.html +++ b/lnbits/extensions/satspay/templates/satspay/index.html @@ -227,7 +227,12 @@ >