Merge pull request #339 from arcbtc/FastAPI

Wallet and offlineshop working
This commit is contained in:
Arc 2021-09-13 12:53:02 +01:00 committed by GitHub
commit ad3e8daddd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 270 additions and 140 deletions

View file

@ -0,0 +1,108 @@
## Check if a user exists and access user object
**old:**
```python
# decorators
@check_user_exists()
async def do_routing_stuff():
pass
```
**new:**
If user doesn't exist, `Depends(check_user_exists)` will raise an exception.
If user exists, `user` will be the user object
```python
# depends calls
@core_html_routes.get("/my_route")
async def extensions(user: User = Depends(check_user_exists)):
pass
```
## Returning data from API calls
**old:**
```python
return (
{
"id": wallet.wallet.id,
"name": wallet.wallet.name,
"balance": wallet.wallet.balance_msat
},
HTTPStatus.OK,
)
```
FastAPI returns `HTTPStatus.OK` by default id no Exception is raised
**new:**
```python
return {
"id": wallet.wallet.id,
"name": wallet.wallet.name,
"balance": wallet.wallet.balance_msat
}
```
To change the default HTTPStatus, add it to the path decorator
```python
@core_app.post("/api/v1/payments", status_code=HTTPStatus.CREATED)
async def payments():
pass
```
## Raise exceptions
**old:**
```python
return (
{"message": f"Failed to connect to {domain}."},
HTTPStatus.BAD_REQUEST,
)
# or the Quart way via abort function
abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.")
```
**new:**
Raise an exception to return a status code other than the default status code.
```python
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"Failed to connect to {domain}."
)
```
## Extensions
**old:**
```python
from quart import Blueprint
amilk_ext: Blueprint = Blueprint(
"amilk", __name__, static_folder="static", template_folder="templates"
)
```
**new:**
```python
from fastapi import APIRouter
from lnbits.jinja2_templating import Jinja2Templates
from lnbits.helpers import template_renderer
from fastapi.staticfiles import StaticFiles
offlineshop_ext: APIRouter = APIRouter(
prefix="/Extension",
tags=["Offlineshop"]
)
offlineshop_ext.mount(
"lnbits/extensions/offlineshop/static",
StaticFiles("lnbits/extensions/offlineshop/static")
)
offlineshop_rndr = template_renderer([
"lnbits/extensions/offlineshop/templates",
])
```
## Possible optimizations
### Use Redis as a cache server
Instead of hitting the database over and over again, we can store a short lived object in [Redis](https://redis.io) for an arbitrary key.
Example:
* Get transactions for a wallet ID
* User data for a user id
* Wallet data for a Admin / Invoice key

View file

@ -31,23 +31,18 @@ from ..tasks import api_invoice_listeners
@core_app.get("/api/v1/wallet") @core_app.get("/api/v1/wallet")
async def api_wallet(wallet: WalletTypeInfo = Depends(get_key_type)): async def api_wallet(wallet: WalletTypeInfo = Depends(get_key_type)):
return ( return {"id": wallet.wallet.id, "name": wallet.wallet.name, "balance": wallet.wallet.balance_msat}
{"id": wallet.wallet.id, "name": wallet.wallet.name, "balance": wallet.wallet.balance_msat},
HTTPStatus.OK,
)
@core_app.put("/api/v1/wallet/{new_name}") @core_app.put("/api/v1/wallet/{new_name}")
async def api_update_wallet(new_name: str, wallet: WalletTypeInfo = Depends(get_key_type)): async def api_update_wallet(new_name: str, wallet: WalletTypeInfo = Depends(get_key_type)):
await update_wallet(wallet.wallet.id, new_name) await update_wallet(wallet.wallet.id, new_name)
return ( return {
{ "id": wallet.wallet.id,
"id": wallet.wallet.id, "name": wallet.wallet.name,
"name": wallet.wallet.name, "balance": wallet.wallet.balance_msat,
"balance": wallet.wallet.balance_msat, }
},
HTTPStatus.OK,
)
@core_app.get("/api/v1/payments") @core_app.get("/api/v1/payments")
@ -92,7 +87,7 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
conn=conn, conn=conn,
) )
except InvoiceFailure as e: except InvoiceFailure as e:
return {"message": str(e)}, 520 raise HTTPException(status_code=520, detail=str(e))
except Exception as exc: except Exception as exc:
raise exc raise exc
@ -128,16 +123,15 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
except (httpx.ConnectError, httpx.RequestError): except (httpx.ConnectError, httpx.RequestError):
lnurl_response = False lnurl_response = False
return ( return {
{ "payment_hash": invoice.payment_hash,
"payment_hash": invoice.payment_hash, "payment_request": payment_request,
"payment_request": payment_request, # maintain backwards compatibility with API clients:
# maintain backwards compatibility with API clients: "checking_id": invoice.payment_hash,
"checking_id": invoice.payment_hash, "lnurl_response": lnurl_response,
"lnurl_response": lnurl_response, }
},
HTTPStatus.CREATED,
)
async def api_payments_pay_invoice(bolt11: str, wallet: Wallet): async def api_payments_pay_invoice(bolt11: str, wallet: Wallet):
@ -147,29 +141,37 @@ async def api_payments_pay_invoice(bolt11: str, wallet: Wallet):
payment_request=bolt11, payment_request=bolt11,
) )
except ValueError as e: except ValueError as e:
return {"message": str(e)}, HTTPStatus.BAD_REQUEST raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=str(e)
)
except PermissionError as e: except PermissionError as e:
return {"message": str(e)}, HTTPStatus.FORBIDDEN raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
detail=str(e)
)
except PaymentFailure as e: except PaymentFailure as e:
return {"message": str(e)}, 520 raise HTTPException(
status_code=520,
detail=str(e)
)
except Exception as exc: except Exception as exc:
raise exc raise exc
return ( return {
{ "payment_hash": payment_hash,
"payment_hash": payment_hash, # maintain backwards compatibility with API clients:
# maintain backwards compatibility with API clients: "checking_id": payment_hash,
"checking_id": payment_hash, }
},
HTTPStatus.CREATED,
)
@core_app.post("/api/v1/payments", deprecated=True, @core_app.post("/api/v1/payments", deprecated=True,
description="DEPRECATED. Use /api/v2/TBD and /api/v2/TBD instead") description="DEPRECATED. Use /api/v2/TBD and /api/v2/TBD instead",
status_code=HTTPStatus.CREATED)
async def api_payments_create(wallet: WalletTypeInfo = Depends(get_key_type), out: bool = True, async def api_payments_create(wallet: WalletTypeInfo = Depends(get_key_type), out: bool = True,
invoiceData: Optional[CreateInvoiceData] = Body(None), invoiceData: Optional[CreateInvoiceData] = Body(None),
bolt11: Optional[str] = Query(None)): bolt11: Optional[str] = Body(None)):
if wallet.wallet_type < 0 or wallet.wallet_type > 2: if wallet.wallet_type < 0 or wallet.wallet_type > 2:
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="Key is invalid") raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="Key is invalid")
@ -201,32 +203,32 @@ async def api_payments_pay_lnurl(data: CreateLNURLData):
if r.is_error: if r.is_error:
raise httpx.ConnectError raise httpx.ConnectError
except (httpx.ConnectError, httpx.RequestError): except (httpx.ConnectError, httpx.RequestError):
return ( raise HTTPException(
{"message": f"Failed to connect to {domain}."}, status_code=HTTPStatus.BAD_REQUEST,
HTTPStatus.BAD_REQUEST, detail=f"Failed to connect to {domain}."
) )
params = json.loads(r.text) params = json.loads(r.text)
if params.get("status") == "ERROR": if params.get("status") == "ERROR":
return ({"message": f"{domain} said: '{params.get('reason', '')}'"}, raise HTTPException(
HTTPStatus.BAD_REQUEST, status_code=HTTPStatus.BAD_REQUEST,
detail=f"{domain} said: '{params.get('reason', '')}'"
) )
invoice = bolt11.decode(params["pr"]) invoice = bolt11.decode(params["pr"])
if invoice.amount_msat != data.amount: if invoice.amount_msat != data.amount:
return ( raise HTTPException(
{ status_code=HTTPStatus.BAD_REQUEST,
"message": f"{domain} returned an invalid invoice. Expected {g().data['amount']} msat, got {invoice.amount_msat}." detail=f"{domain} returned an invalid invoice. Expected {g().data['amount']} msat, got {invoice.amount_msat}."
},
HTTPStatus.BAD_REQUEST,
) )
if invoice.description_hash != g().data["description_hash"]: if invoice.description_hash != g().data["description_hash"]:
return ( raise HTTPException(
{ status_code=HTTPStatus.BAD_REQUEST,
"message": f"{domain} returned an invalid invoice. Expected description_hash == {g().data['description_hash']}, got {invoice.description_hash}." detail=f"{domain} returned an invalid invoice. Expected description_hash == {g().data['description_hash']}, got {invoice.description_hash}."
},
HTTPStatus.BAD_REQUEST,
) )
extra = {} extra = {}
@ -242,15 +244,13 @@ async def api_payments_pay_lnurl(data: CreateLNURLData):
extra=extra, extra=extra,
) )
return ( return {
{ "success_action": params.get("successAction"),
"success_action": params.get("successAction"), "payment_hash": payment_hash,
"payment_hash": payment_hash, # maintain backwards compatibility with API clients:
# maintain backwards compatibility with API clients: "checking_id": payment_hash,
"checking_id": payment_hash, }
},
HTTPStatus.CREATED,
)
async def subscribe(request: Request, wallet: Wallet): async def subscribe(request: Request, wallet: Wallet):
this_wallet_id = wallet.wallet.id this_wallet_id = wallet.wallet.id
@ -273,20 +273,21 @@ async def subscribe(request: Request, wallet: Wallet):
try: try:
while True: while True:
typ, data = await send_queue.get() typ, data = await send_queue.get()
message = [f"event: {typ}".encode("utf-8")]
if data: if data:
jdata = json.dumps(dict(data.dict(), pending=False)) jdata = json.dumps(dict(data.dict(), pending=False))
message.append(f"data: {jdata}".encode("utf-8"))
# yield dict(id=1, event="this", data="1234")
yield dict(data=jdata.encode("utf-8"), event=typ.encode("utf-8")) # await asyncio.sleep(2)
yield dict(data=jdata, event=typ)
# yield dict(data=jdata.encode("utf-8"), event=typ.encode("utf-8"))
except asyncio.CancelledError: except asyncio.CancelledError:
return return
@core_app.get("/api/v1/payments/sse") @core_app.get("/api/v1/payments/sse")
async def api_payments_sse(request: Request, wallet: WalletTypeInfo = Depends(get_key_type)): async def api_payments_sse(request: Request, wallet: WalletTypeInfo = Depends(get_key_type)):
return EventSourceResponse(subscribe(request, wallet)) return EventSourceResponse(subscribe(request, wallet), ping=20, media_type="text/event-stream")
@core_app.get("/api/v1/payments/{payment_hash}") @core_app.get("/api/v1/payments/{payment_hash}")
@ -294,19 +295,17 @@ async def api_payment(payment_hash, wallet: WalletTypeInfo = Depends(get_key_typ
payment = await wallet.wallet.get_payment(payment_hash) payment = await wallet.wallet.get_payment(payment_hash)
if not payment: if not payment:
return {"message": "Payment does not exist."}, HTTPStatus.NOT_FOUND return {"message": "Payment does not exist."}
elif not payment.pending: elif not payment.pending:
return {"paid": True, "preimage": payment.preimage}, HTTPStatus.OK return {"paid": True, "preimage": payment.preimage}
try: try:
await payment.check_pending() await payment.check_pending()
except Exception: except Exception:
return {"paid": False}, HTTPStatus.OK return {"paid": False}
return ( return {"paid": not payment.pending, "preimage": payment.preimage}
{"paid": not payment.pending, "preimage": payment.preimage},
HTTPStatus.OK,
)
@core_app.get("/api/v1/lnurlscan/{code}", dependencies=[Depends(WalletInvoiceKeyChecker())]) @core_app.get("/api/v1/lnurlscan/{code}", dependencies=[Depends(WalletInvoiceKeyChecker())])
async def api_lnurlscan(code: str): async def api_lnurlscan(code: str):
@ -326,7 +325,7 @@ async def api_lnurlscan(code: str):
) )
# will proceed with these values # will proceed with these values
else: else:
return {"message": "invalid lnurl"}, HTTPStatus.BAD_REQUEST raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="invalid lnurl")
# params is what will be returned to the client # params is what will be returned to the client
params: Dict = {"domain": domain} params: Dict = {"domain": domain}
@ -341,28 +340,25 @@ async def api_lnurlscan(code: str):
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
r = await client.get(url, timeout=5) r = await client.get(url, timeout=5)
if r.is_error: if r.is_error:
return ( raise HTTPException(
{"domain": domain, "message": "failed to get parameters"}, status_code=HTTPStatus.SERVICE_UNAVAILABLE,
HTTPStatus.SERVICE_UNAVAILABLE, detail={"domain": domain, "message": "failed to get parameters"}
) )
try: try:
data = json.loads(r.text) data = json.loads(r.text)
except json.decoder.JSONDecodeError: except json.decoder.JSONDecodeError:
return ( raise HTTPException(
{ status_code=HTTPStatus.SERVICE_UNAVAILABLE,
"domain": domain, detail={"domain": domain, "message": f"got invalid response '{r.text[:200]}'"}
"message": f"got invalid response '{r.text[:200]}'",
},
HTTPStatus.SERVICE_UNAVAILABLE,
) )
try: try:
tag = data["tag"] tag = data["tag"]
if tag == "channelRequest": if tag == "channelRequest":
return ( raise HTTPException(
{"domain": domain, "kind": "channel", "message": "unsupported"}, status_code=HTTPStatus.BAD_REQUEST,
HTTPStatus.BAD_REQUEST, detail={"domain": domain, "kind": "channel", "message": "unsupported"}
) )
params.update(**data) params.update(**data)
@ -407,13 +403,13 @@ async def api_lnurlscan(code: str):
params.update(commentAllowed=data.get("commentAllowed", 0)) params.update(commentAllowed=data.get("commentAllowed", 0))
except KeyError as exc: except KeyError as exc:
return ( raise HTTPException(
{ status_code=HTTPStatus.SERVICE_UNAVAILABLE,
"domain": domain, detail={
"message": f"lnurl JSON response invalid: {exc}", "domain": domain,
}, "message": f"lnurl JSON response invalid: {exc}",
HTTPStatus.SERVICE_UNAVAILABLE, })
)
return params return params
@ -421,8 +417,9 @@ async def api_lnurlscan(code: str):
async def api_perform_lnurlauth(callback: str): async def api_perform_lnurlauth(callback: str):
err = await perform_lnurlauth(callback) err = await perform_lnurlauth(callback)
if err: if err:
return {"reason": err.reason}, HTTPStatus.SERVICE_UNAVAILABLE raise HTTPException(status_code=HTTPStatus.SERVICE_UNAVAILABLE, detail=err.reason)
return "", HTTPStatus.OK
return ""
@core_app.get("/api/v1/currencies") @core_app.get("/api/v1/currencies")

View file

@ -15,6 +15,8 @@ from starlette.responses import HTMLResponse
from lnbits.core import db from lnbits.core import db
from lnbits.helpers import template_renderer, url_for from lnbits.helpers import template_renderer, url_for
from lnbits.requestvars import g from lnbits.requestvars import g
from lnbits.core.models import User
from lnbits.decorators import check_user_exists
from lnbits.settings import (LNBITS_ALLOWED_USERS, LNBITS_SITE_TITLE, from lnbits.settings import (LNBITS_ALLOWED_USERS, LNBITS_SITE_TITLE,
SERVICE_FEE) SERVICE_FEE)
@ -35,10 +37,13 @@ async def home(request: Request, lightning: str = None):
return template_renderer().TemplateResponse("core/index.html", {"request": request, "lnurl": lightning}) return template_renderer().TemplateResponse("core/index.html", {"request": request, "lnurl": lightning})
@core_html_routes.get("/extensions") @core_html_routes.get("/extensions", name="core.extensions")
# @validate_uuids(["usr"], required=True) async def extensions(
# @check_user_exists() request: Request,
async def extensions(request: Request, enable: str, disable: str): user: User = Depends(check_user_exists),
enable: str= Query(None),
disable: str = Query(None)
):
extension_to_enable = enable extension_to_enable = enable
extension_to_disable = disable extension_to_disable = disable
@ -47,13 +52,18 @@ async def extensions(request: Request, enable: str, disable: str):
if extension_to_enable: if extension_to_enable:
await update_user_extension( await update_user_extension(
user_id=g.user.id, extension=extension_to_enable, active=True user_id=user.id, extension=extension_to_enable, active=True
) )
elif extension_to_disable: elif extension_to_disable:
await update_user_extension( await update_user_extension(
user_id=g.user.id, extension=extension_to_disable, active=False user_id=user.id, extension=extension_to_disable, active=False
) )
return template_renderer().TemplateResponse("core/extensions.html", {"request": request, "user": get_user(g.user.id)})
# Update user as his extensions have been updated
if extension_to_enable or extension_to_disable:
user = await get_user(user.id)
return template_renderer().TemplateResponse("core/extensions.html", {"request": request, "user": user.dict()})
@core_html_routes.get("/wallet", response_class=HTMLResponse) @core_html_routes.get("/wallet", response_class=HTMLResponse)
@ -204,7 +214,7 @@ async def lnurlwallet(request: Request):
async def manifest(usr: str): async def manifest(usr: str):
user = await get_user(usr) user = await get_user(usr)
if not user: if not user:
return "", HTTPStatus.NOT_FOUND raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
return { return {
"short_name": "LNbits", "short_name": "LNbits",

View file

@ -1,7 +1,7 @@
import asyncio import asyncio
import datetime import datetime
from http import HTTPStatus from http import HTTPStatus
from fastapi import HTTPException
from lnbits import bolt11 from lnbits import bolt11
from .. import core_app from .. import core_app
@ -14,17 +14,23 @@ async def api_public_payment_longpolling(payment_hash):
payment = await get_standalone_payment(payment_hash) payment = await get_standalone_payment(payment_hash)
if not payment: if not payment:
return {"message": "Payment does not exist."}, HTTPStatus.NOT_FOUND raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="Payment does not exist."
)
elif not payment.pending: elif not payment.pending:
return {"status": "paid"}, HTTPStatus.OK return {"status": "paid"}
try: try:
invoice = bolt11.decode(payment.bolt11) invoice = bolt11.decode(payment.bolt11)
expiration = datetime.datetime.fromtimestamp(invoice.date + invoice.expiry) expiration = datetime.datetime.fromtimestamp(invoice.date + invoice.expiry)
if expiration < datetime.datetime.now(): if expiration < datetime.datetime.now():
return {"status": "expired"}, HTTPStatus.OK return {"status": "expired"}
except: except:
return {"message": "Invalid bolt11 invoice."}, HTTPStatus.BAD_REQUEST raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="Invalid bolt11 invoice."
)
payment_queue = asyncio.Queue(0) payment_queue = asyncio.Queue(0)
@ -37,7 +43,7 @@ async def api_public_payment_longpolling(payment_hash):
async for payment in payment_queue.get(): async for payment in payment_queue.get():
if payment.payment_hash == payment_hash: if payment.payment_hash == payment_hash:
nonlocal response nonlocal response
response = ({"status": "paid"}, HTTPStatus.OK) response = {"status": "paid"}
cancel_scope.cancel() cancel_scope.cancel()
async def timeouter(cancel_scope): async def timeouter(cancel_scope):
@ -51,4 +57,7 @@ async def api_public_payment_longpolling(payment_hash):
if response: if response:
return response return response
else: else:
return {"message": "timeout"}, HTTPStatus.REQUEST_TIMEOUT raise HTTPException(
status_code=HTTPStatus.REQUEST_TIMEOUT,
detail="timeout"
)

View file

@ -2,7 +2,8 @@ from functools import wraps
from http import HTTPStatus from http import HTTPStatus
from fastapi.security import api_key from fastapi.security import api_key
from lnbits.core.models import Wallet from pydantic.types import UUID4
from lnbits.core.models import User, Wallet
from typing import List, Union from typing import List, Union
from uuid import UUID from uuid import UUID
@ -115,9 +116,9 @@ def api_validate_post_request(*, schema: dict):
@wraps(view) @wraps(view)
async def wrapped_view(**kwargs): async def wrapped_view(**kwargs):
if "application/json" not in request.headers["Content-Type"]: if "application/json" not in request.headers["Content-Type"]:
return ( raise HTTPException(
jsonify({"message": "Content-Type must be `application/json`."}), status_code=HTTPStatus.BAD_REQUEST,
HTTPStatus.BAD_REQUEST, detail=jsonify({"message": "Content-Type must be `application/json`."})
) )
v = Validator(schema) v = Validator(schema)
@ -125,10 +126,11 @@ def api_validate_post_request(*, schema: dict):
g().data = {key: data[key] for key in schema.keys() if key in data} g().data = {key: data[key] for key in schema.keys() if key in data}
if not v.validate(g().data): if not v.validate(g().data):
return ( raise HTTPException(
jsonify({"message": f"Errors in request data: {v.errors}"}), status_code=HTTPStatus.BAD_REQUEST,
HTTPStatus.BAD_REQUEST, detail=jsonify({"message": f"Errors in request data: {v.errors}"})
) )
return await view(**kwargs) return await view(**kwargs)
@ -137,22 +139,18 @@ def api_validate_post_request(*, schema: dict):
return wrap return wrap
def check_user_exists(param: str = "usr"): async def check_user_exists(usr: UUID4) -> User:
def wrap(view): g().user = await get_user(usr.hex)
@wraps(view) if not g().user:
async def wrapped_view(**kwargs): raise HTTPException(
g().user = await get_user(request.args.get(param, type=str)) or abort( status_code=HTTPStatus.NOT_FOUND,
HTTPStatus.NOT_FOUND, "User does not exist." detail="User does not exist."
) )
if LNBITS_ALLOWED_USERS and g().user.id not in LNBITS_ALLOWED_USERS:
abort(HTTPStatus.UNAUTHORIZED, "User not authorized.")
return await view(**kwargs)
return wrapped_view
return wrap
if LNBITS_ALLOWED_USERS and g().user.id not in LNBITS_ALLOWED_USERS:
raise HTTPException(
status_code=HTTPStatus.UNAUTHORIZED,
detail="User not authorized."
)
return g().user

View file

@ -4,6 +4,8 @@ import traceback
from http import HTTPStatus from http import HTTPStatus
from typing import List, Callable from typing import List, Callable
from fastapi.exceptions import HTTPException
from lnbits.settings import WALLET from lnbits.settings import WALLET
from lnbits.core.crud import ( from lnbits.core.crud import (
get_payments, get_payments,
@ -61,7 +63,7 @@ async def webhook_handler():
handler = getattr(WALLET, "webhook_listener", None) handler = getattr(WALLET, "webhook_listener", None)
if handler: if handler:
return await handler() return await handler()
return "", HTTPStatus.NO_CONTENT raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
internal_invoice_queue = asyncio.Queue(0) internal_invoice_queue = asyncio.Queue(0)

View file

@ -1,5 +1,6 @@
import json import json
import asyncio import asyncio
from fastapi.exceptions import HTTPException
import httpx import httpx
from os import getenv from os import getenv
from http import HTTPStatus from http import HTTPStatus
@ -133,7 +134,7 @@ class LNPayWallet(Wallet):
or "event" not in data or "event" not in data
or data["event"].get("name") != "wallet_receive" or data["event"].get("name") != "wallet_receive"
): ):
return "", HTTPStatus.NO_CONTENT raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
lntx_id = data["data"]["wtx"]["lnTx"]["id"] lntx_id = data["data"]["wtx"]["lnTx"]["id"]
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
@ -145,4 +146,5 @@ class LNPayWallet(Wallet):
if data["settled"]: if data["settled"]:
await self.queue.put(lntx_id) await self.queue.put(lntx_id)
return "", HTTPStatus.NO_CONTENT raise HTTPException(status_code=HTTPStatus.NO_CONTENT)

View file

@ -1,4 +1,6 @@
import asyncio import asyncio
from fastapi.exceptions import HTTPException
from lnbits.helpers import url_for from lnbits.helpers import url_for
import hmac import hmac
import httpx import httpx
@ -133,14 +135,16 @@ class OpenNodeWallet(Wallet):
async def webhook_listener(self): async def webhook_listener(self):
data = await request.form data = await request.form
if "status" not in data or data["status"] != "paid": if "status" not in data or data["status"] != "paid":
return "", HTTPStatus.NO_CONTENT raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
charge_id = data["id"] charge_id = data["id"]
x = hmac.new(self.auth["Authorization"].encode("ascii"), digestmod="sha256") x = hmac.new(self.auth["Authorization"].encode("ascii"), digestmod="sha256")
x.update(charge_id.encode("ascii")) x.update(charge_id.encode("ascii"))
if x.hexdigest() != data["hashed_order"]: if x.hexdigest() != data["hashed_order"]:
print("invalid webhook, not from opennode") print("invalid webhook, not from opennode")
return "", HTTPStatus.NO_CONTENT raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
await self.queue.put(charge_id) await self.queue.put(charge_id)
return "", HTTPStatus.NO_CONTENT raise HTTPException(status_code=HTTPStatus.NO_CONTENT)