From 449cbfed89d612ae7e944c2f8039caec7b02d7fb Mon Sep 17 00:00:00 2001 From: Ben Arc Date: Mon, 23 Aug 2021 19:19:43 +0100 Subject: [PATCH 01/10] Error page --- lnbits/app.py | 16 ++++------------ lnbits/templates/error.html | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 12 deletions(-) create mode 100644 lnbits/templates/error.html diff --git a/lnbits/app.py b/lnbits/app.py index b61880b6..81570b11 100644 --- a/lnbits/app.py +++ b/lnbits/app.py @@ -6,7 +6,7 @@ import importlib import traceback import trio -from fastapi import FastAPI +from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.gzip import GZipMiddleware from fastapi.staticfiles import StaticFiles @@ -162,21 +162,13 @@ def register_async_tasks(app): async def stop_listeners(): pass +templates = Jinja2Templates(directory="templates") def register_exception_handlers(app): @app.errorhandler(Exception) - async def basic_error(err): + async def basic_error(request: Request, err): print("handled error", traceback.format_exc()) etype, value, tb = sys.exc_info() traceback.print_exception(etype, err, tb) exc = traceback.format_exc() - return ( - "\n\n".join( - [ - "LNbits internal error!", - exc, - "If you believe this shouldn't be an error please bring it up on https://t.me/lnbits", - ] - ), - 500, - ) + return await templates.TemplateResponse("templates/error.html", {"request": request, "err": err}) diff --git a/lnbits/templates/error.html b/lnbits/templates/error.html new file mode 100644 index 00000000..43a9ad8a --- /dev/null +++ b/lnbits/templates/error.html @@ -0,0 +1,36 @@ +{% extends "public.html" %} {% block page %} +
+
+ + +
+

Error

+
+ + +
{{ err }}
+

If you believe this shouldn't be an error please bring it up on https://t.me/lnbits

+
+
+
+
+
+ + {% endblock %} {% block scripts %} + + + + {% endblock %} +
\ No newline at end of file From 6f38fdc39b2c3566c84d326dc08f02af72d9b8ab Mon Sep 17 00:00:00 2001 From: Stefan Stammberger Date: Mon, 23 Aug 2021 21:17:46 +0200 Subject: [PATCH 02/10] feat: move all non-api routes to their own router This unclutters the docs page a bit by making html routes collapsible. --- lnbits/app.py | 4 +++- lnbits/core/views/generic.py | 27 +++++++++++++++------------ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/lnbits/app.py b/lnbits/app.py index b61880b6..afa5f5b2 100644 --- a/lnbits/app.py +++ b/lnbits/app.py @@ -30,6 +30,7 @@ from .tasks import ( ) from .settings import WALLET from .requestvars import g, request_global +from .core.views.generic import core_html_routes import lnbits.settings async def create_app(config_object="lnbits.settings") -> FastAPI: @@ -106,8 +107,9 @@ def check_funding_source(app: FastAPI) -> None: def register_routes(app: FastAPI) -> None: - """Register Flask blueprints / LNbits extensions.""" + """Register FastAPI routes / LNbits extensions.""" app.include_router(core_app) + app.include_router(core_html_routes) for ext in get_valid_extensions(): try: diff --git a/lnbits/core/views/generic.py b/lnbits/core/views/generic.py index fae60e31..fa77030a 100644 --- a/lnbits/core/views/generic.py +++ b/lnbits/core/views/generic.py @@ -1,3 +1,5 @@ +from fastapi.params import Query +from fastapi.routing import APIRouter from lnbits.requestvars import g from os import path from http import HTTPStatus @@ -22,20 +24,20 @@ from ..crud import ( from ..services import redeem_lnurl_withdraw, pay_invoice from fastapi import FastAPI, Request from fastapi.responses import FileResponse -from lnbits.jinja2_templating import Jinja2Templates +core_html_routes: APIRouter = APIRouter(tags=["Core NON-API Website Routes"]) -@core_app.get("/favicon.ico") +@core_html_routes.get("/favicon.ico") async def favicon(): return FileResponse("lnbits/core/static/favicon.ico") -@core_app.get("/", response_class=HTMLResponse) +@core_html_routes.get("/", response_class=HTMLResponse) async def home(request: Request, lightning: str = None): return g().templates.TemplateResponse("core/index.html", {"request": request, "lnurl": lightning}) -@core_app.get("/extensions") +@core_html_routes.get("/extensions") @validate_uuids(["usr"], required=True) @check_user_exists() async def extensions(enable: str, disable: str): @@ -58,10 +60,11 @@ async def extensions(enable: str, disable: str): return await templates.TemplateResponse("core/extensions.html", {"request": request, "user": get_user(g.user.id)}) -@core_app.get("/wallet{usr}{wal}{nme}") +@core_html_routes.get("/wallet") #Not sure how to validate @validate_uuids(["usr", "wal"]) -async def wallet(request: Request, usr: Optional[str], wal: Optional[str], nme: Optional[str]): +async def wallet(request: Request, usr: Optional[str] = Query(None), + wal: Optional[str]=Query(None, description=""), nme: Optional[str]=Query(None)): user_id = usr wallet_id = wal wallet_name = nme @@ -101,7 +104,7 @@ async def wallet(request: Request, usr: Optional[str], wal: Optional[str], nme: ) -@core_app.get("/withdraw") +@core_html_routes.get("/withdraw") @validate_uuids(["usr", "wal"], required=True) async def lnurl_full_withdraw(): user = await get_user(request.args.get("usr")) @@ -130,7 +133,7 @@ async def lnurl_full_withdraw(): } -@core_app.get("/withdraw/cb") +@core_html_routes.get("/withdraw/cb") @validate_uuids(["usr", "wal"], required=True) async def lnurl_full_withdraw_callback(): user = await get_user(request.args.get("usr")) @@ -158,7 +161,7 @@ async def lnurl_full_withdraw_callback(): return {"status": "OK"} -@core_app.get("/deletewallet") +@core_html_routes.get("/deletewallet") @validate_uuids(["usr", "wal"], required=True) @check_user_exists() async def deletewallet(): @@ -177,7 +180,7 @@ async def deletewallet(): return redirect(url_for("core.home")) -@core_app.get("/withdraw/notify/{service}") +@core_html_routes.get("/withdraw/notify/{service}") @validate_uuids(["wal"], required=True) async def lnurl_balance_notify(service: str): bc = await get_balance_check(request.args.get("wal"), service) @@ -185,7 +188,7 @@ async def lnurl_balance_notify(service: str): redeem_lnurl_withdraw(bc.wallet, bc.url) -@core_app.get("/lnurlwallet") +@core_html_routes.get("/lnurlwallet") async def lnurlwallet(): async with db.connect() as conn: account = await create_account(conn=conn) @@ -204,7 +207,7 @@ async def lnurlwallet(): return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id)) -@core_app.get("/manifest/{usr}.webmanifest") +@core_html_routes.get("/manifest/{usr}.webmanifest") async def manifest(usr: str): user = await get_user(usr) if not user: From de4d3b012c7f25d8c9ab09689bb49f9aa66ba89e Mon Sep 17 00:00:00 2001 From: Stefan Stammberger Date: Tue, 24 Aug 2021 19:14:04 +0200 Subject: [PATCH 03/10] fix: invoke error page correctly --- lnbits/app.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lnbits/app.py b/lnbits/app.py index 7fd0fada..716979fd 100644 --- a/lnbits/app.py +++ b/lnbits/app.py @@ -164,8 +164,6 @@ def register_async_tasks(app): async def stop_listeners(): pass -templates = Jinja2Templates(directory="templates") - def register_exception_handlers(app): @app.errorhandler(Exception) async def basic_error(request: Request, err): @@ -173,4 +171,5 @@ def register_exception_handlers(app): etype, value, tb = sys.exc_info() traceback.print_exception(etype, err, tb) exc = traceback.format_exc() - return await templates.TemplateResponse("templates/error.html", {"request": request, "err": err}) + return g().templates.TemplateResponse("error.html", {"request": request, "err": err}) + From f11905395395e29762438daa431d479a3953f5ff Mon Sep 17 00:00:00 2001 From: Stefan Stammberger Date: Tue, 24 Aug 2021 21:23:18 +0200 Subject: [PATCH 04/10] fix: main page and creating a user and a wallet The wallet page will still not renders correctly, but the backend does create the user his first wallet. --- lnbits/core/crud.py | 20 ++++++---------- lnbits/core/models.py | 32 +++++++++++++------------- lnbits/core/views/generic.py | 44 ++++++++++++++++++++---------------- 3 files changed, 47 insertions(+), 49 deletions(-) diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index 5378d593..e981f22c 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -54,20 +54,14 @@ async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[ """, (user_id,), ) + else: + return None - return ( - User( - **{ - **user, - **{ - "extensions": [e[0] for e in extensions], - "wallets": [Wallet(**w) for w in wallets], - }, - } - ) - if user - else None - ) + return User( + id = user['id'], + email = user['email'], + extensions = [e[0] for e in extensions], + wallets = [Wallet(**w) for w in wallets]) async def update_user_extension( diff --git a/lnbits/core/models.py b/lnbits/core/models.py index 8b6ec6ad..c1dc99f9 100644 --- a/lnbits/core/models.py +++ b/lnbits/core/models.py @@ -10,22 +10,6 @@ from pydantic import BaseModel from lnbits.settings import WALLET -class User(BaseModel): - id: str - email: str - extensions: List[str] = [] - wallets: List["Wallet"] = [] - password: Optional[str] = None - - @property - def wallet_ids(self) -> List[str]: - return [wallet.id for wallet in self.wallets] - - def get_wallet(self, wallet_id: str) -> Optional["Wallet"]: - w = [wallet for wallet in self.wallets if wallet.id == wallet_id] - return w[0] if w else None - - class Wallet(BaseModel): id: str name: str @@ -73,6 +57,22 @@ class Wallet(BaseModel): return await get_wallet_payment(self.id, payment_hash) +class User(BaseModel): + id: str + email: Optional[str] = None + extensions: List[str] = [] + wallets: List[Wallet] = [] + password: Optional[str] = None + + @property + def wallet_ids(self) -> List[str]: + return [wallet.id for wallet in self.wallets] + + def get_wallet(self, wallet_id: str) -> Optional["Wallet"]: + w = [wallet for wallet in self.wallets if wallet.id == wallet_id] + return w[0] if w else None + + class Payment(BaseModel): checking_id: str pending: bool diff --git a/lnbits/core/views/generic.py b/lnbits/core/views/generic.py index fa77030a..516bcd5c 100644 --- a/lnbits/core/views/generic.py +++ b/lnbits/core/views/generic.py @@ -1,5 +1,8 @@ +from lnbits.core.models import Wallet from fastapi.params import Query from fastapi.routing import APIRouter +from fastapi.responses import RedirectResponse +from fastapi import status from lnbits.requestvars import g from os import path from http import HTTPStatus @@ -60,11 +63,15 @@ async def extensions(enable: str, disable: str): return await templates.TemplateResponse("core/extensions.html", {"request": request, "user": get_user(g.user.id)}) -@core_html_routes.get("/wallet") +@core_html_routes.get("/wallet", response_class=HTMLResponse) #Not sure how to validate @validate_uuids(["usr", "wal"]) -async def wallet(request: Request, usr: Optional[str] = Query(None), - wal: Optional[str]=Query(None, description=""), nme: Optional[str]=Query(None)): +async def wallet(request: Request, + usr: Optional[str] = Query(None), + wal: Optional[str] = Query(None), + nme: Optional[str] = Query(None), + ): + user_id = usr wallet_id = wal wallet_name = nme @@ -77,30 +84,27 @@ async def wallet(request: Request, usr: Optional[str] = Query(None), # nothing: create everything if not user_id: - user = await get_user((await create_account()).id) + usr = await get_user((await create_account()).id) else: - user = await get_user(user_id) - if not user: - abort(HTTPStatus.NOT_FOUND, "User does not exist.") - return - + usr = await get_user(user_id) + if not usr: + return g().templates.TemplateResponse("error.html", {"request": request, "err": "User does not exist."}) if LNBITS_ALLOWED_USERS and user_id not in LNBITS_ALLOWED_USERS: - abort(HTTPStatus.UNAUTHORIZED, "User not authorized.") - + return g().templates.TemplateResponse("error.html", {"request": request, "err": "User not authorized."}) if not wallet_id: - if user.wallets and not wallet_name: - wallet = user.wallets[0] + if usr.wallets and not wallet_name: + wal = usr.wallets[0] else: - wallet = await create_wallet(user_id=user.id, wallet_name=wallet_name) + wal = await create_wallet(user_id=usr.id, wallet_name=wallet_name) - return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id)) + return RedirectResponse(f"/wallet?usr={usr.id}&wal={wal.id}", status_code=status.HTTP_307_TEMPORARY_REDIRECT) - wallet = user.get_wallet(wallet_id) - if not wallet: - abort(HTTPStatus.FORBIDDEN, "Not your wallet.") + wal = usr.get_wallet(wallet_id) + if not wal: + return g().templates.TemplateResponse("error.html", {"request": request, ...}) - return await templates.TemplateResponse( - "core/wallet.html", {"request":request,"user":user, "wallet":wallet, "service_fee":service_fee} + return g().templates.TemplateResponse( + "core/wallet.html", {"request":request,"user":usr, "wallet":wal, "service_fee":service_fee} ) From ede038976f92bfe57e1abeb6826ddd36fb0628fb Mon Sep 17 00:00:00 2001 From: Tiago vasconcelos Date: Wed, 25 Aug 2021 15:44:18 +0100 Subject: [PATCH 05/10] refactor http status responses --- lnbits/extensions/withdraw/views.py | 45 ++++++++------ lnbits/extensions/withdraw/views_api.py | 82 +++++++++++-------------- 2 files changed, 61 insertions(+), 66 deletions(-) diff --git a/lnbits/extensions/withdraw/views.py b/lnbits/extensions/withdraw/views.py index 0f23d733..dc1b6e31 100644 --- a/lnbits/extensions/withdraw/views.py +++ b/lnbits/extensions/withdraw/views.py @@ -6,31 +6,34 @@ from lnbits.decorators import check_user_exists, validate_uuids from . import withdraw_ext from .crud import get_withdraw_link, chunks -from fastapi import FastAPI, Request +from fastapi import FastAPI, Request, Response + from fastapi.templating import Jinja2Templates templates = Jinja2Templates(directory="templates") -@withdraw_ext.get("/") +@withdraw_ext.get("/", status_code=HTTPStatus.OK) @validate_uuids(["usr"], required=True) @check_user_exists() async def index(request: Request): return await templates.TemplateResponse("withdraw/index.html", {"request":request,"user":g.user}) -@withdraw_ext.get("/") -async def display(request: Request, link_id): - link = await get_withdraw_link(link_id, 0) or abort( - HTTPStatus.NOT_FOUND, "Withdraw link does not exist." - ) +@withdraw_ext.get("/{link_id}", status_code=HTTPStatus.OK) +async def display(request: Request, link_id, response: Response): + link = await get_withdraw_link(link_id, 0) + if not link: + response.status_code = HTTPStatus.NOT_FOUND + return "Withdraw link does not exist." #probably here is where we should return the 404?? return await templates.TemplateResponse("withdraw/display.html", {"request":request,"link":link, "unique":True}) -@withdraw_ext.get("/img/") -async def img(request: Request, link_id): - link = await get_withdraw_link(link_id, 0) or abort( - HTTPStatus.NOT_FOUND, "Withdraw link does not exist." - ) +@withdraw_ext.get("/img/{link_id}", status_code=HTTPStatus.OK) +async def img(request: Request, link_id, response: Response): + link = await get_withdraw_link(link_id, 0) + if not link: + response.status_code = HTTPStatus.NOT_FOUND + return "Withdraw link does not exist." qr = pyqrcode.create(link.lnurl) stream = BytesIO() qr.svg(stream, scale=3) @@ -46,19 +49,21 @@ async def img(request: Request, link_id): ) -@withdraw_ext.get("/print/") -async def print_qr(request: Request, link_id): - link = await get_withdraw_link(link_id) or abort( - HTTPStatus.NOT_FOUND, "Withdraw link does not exist." - ) +@withdraw_ext.get("/print/{link_id}", status_code=HTTPStatus.OK) +async def print_qr(request: Request, link_id, response: Response): + link = await get_withdraw_link(link_id) + if not link: + response.status_code = HTTPStatus.NOT_FOUND + return "Withdraw link does not exist." if link.uses == 0: return await templates.TemplateResponse("withdraw/print_qr.html", {"request":request,link:link, unique:False}) links = [] count = 0 for x in link.usescsv.split(","): - linkk = await get_withdraw_link(link_id, count) or abort( - HTTPStatus.NOT_FOUND, "Withdraw link does not exist." - ) + linkk = await get_withdraw_link(link_id, count) + if not linkk: + response.status_code = HTTPStatus.NOT_FOUND + return "Withdraw link does not exist." links.append(str(linkk.lnurl)) count = count + 1 page_link = list(chunks(links, 2)) diff --git a/lnbits/extensions/withdraw/views_api.py b/lnbits/extensions/withdraw/views_api.py index b34b9fbb..d0ad0656 100644 --- a/lnbits/extensions/withdraw/views_api.py +++ b/lnbits/extensions/withdraw/views_api.py @@ -5,7 +5,7 @@ from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore from lnbits.core.crud import get_user from lnbits.decorators import api_check_wallet_key, api_validate_post_request from pydantic import BaseModel -from fastapi import FastAPI, Query +from fastapi import FastAPI, Query, Response from . import withdraw_ext from .crud import ( @@ -19,47 +19,41 @@ from .crud import ( ) -@withdraw_ext.get("/api/v1/links") +@withdraw_ext.get("/api/v1/links", status_code=200) @api_check_wallet_key("invoice") -async def api_links(): +async def api_links(response: Response): wallet_ids = [g.wallet.id] if "all_wallets" in request.args: wallet_ids = (await get_user(g.wallet.user)).wallet_ids try: - return ( - [ + return [ { **link._asdict(), **{"lnurl": link.lnurl}, } for link in await get_withdraw_links(wallet_ids) - ], - HTTPStatus.OK, - ) + ] + except LnurlInvalidUrl: - return ( - { - "message": "LNURLs need to be delivered over a publically accessible `https` domain or Tor." - }, - HTTPStatus.UPGRADE_REQUIRED, - ) + response.status_code = HTTPStatus.UPGRADE_REQUIRED + return { "message": "LNURLs need to be delivered over a publically accessible `https` domain or Tor." } -@withdraw_ext.get("/api/v1/links/{link_id}") +@withdraw_ext.get("/api/v1/links/{link_id}", status_code=200) @api_check_wallet_key("invoice") -async def api_link_retrieve(link_id): +async def api_link_retrieve(link_id, response: Response): link = await get_withdraw_link(link_id, 0) if not link: - return ({"message": "Withdraw link does not exist."}, - HTTPStatus.NOT_FOUND, - ) + response.status_code = HTTPStatus.NOT_FOUND + return {"message": "Withdraw link does not exist."} if link.wallet != g.wallet.id: - return {"message": "Not your withdraw link."}, HTTPStatus.FORBIDDEN + response.status_code = HTTPStatus.FORBIDDEN + return {"message": "Not your withdraw link."} - return {**link, **{"lnurl": link.lnurl}}, HTTPStatus.OK + return {**link, **{"lnurl": link.lnurl}} class CreateData(BaseModel): title: str = Query(...) @@ -69,17 +63,15 @@ class CreateData(BaseModel): wait_time: int = Query(..., ge=1) is_unique: bool -@withdraw_ext.post("/api/v1/links") +@withdraw_ext.post("/api/v1/links", status_code=HTTPStatus.CREATED) @withdraw_ext.put("/api/v1/links/{link_id}") @api_check_wallet_key("admin") -async def api_link_create_or_update(data: CreateData, link_id: str = None): +async def api_link_create_or_update(data: CreateData, link_id: str = None, response: Response): if data.max_withdrawable < data.min_withdrawable: - return ( - { + response.status_code = HTTPStatus.BAD_REQUEST + return { "message": "`max_withdrawable` needs to be at least `min_withdrawable`." - }, - HTTPStatus.BAD_REQUEST, - ) + } usescsv = "" for i in range(data.uses): @@ -92,43 +84,41 @@ async def api_link_create_or_update(data: CreateData, link_id: str = None): if link_id: link = await get_withdraw_link(link_id, 0) if not link: - return ( - jsonify({"message": "Withdraw link does not exist."}), - HTTPStatus.NOT_FOUND, - ) + response.status_code = HTTPStatus.NOT_FOUND + return {"message": "Withdraw link does not exist."} if link.wallet != g.wallet.id: - return {"message": "Not your withdraw link."}, HTTPStatus.FORBIDDEN + response.status_code = HTTPStatus.FORBIDDEN + return {"message": "Not your withdraw link."} link = await update_withdraw_link(link_id, **data, usescsv=usescsv, used=0) else: link = await create_withdraw_link( wallet_id=g.wallet.id, **data, usescsv=usescsv ) - - return ({**link, **{"lnurl": link.lnurl}}, - HTTPStatus.OK if link_id else HTTPStatus.CREATED, - ) + if link_id: + response.status_code = HTTPStatus.OK + return {**link, **{"lnurl": link.lnurl}} -@withdraw_ext.delete("/api/v1/links/{link_id}") +@withdraw_ext.delete("/api/v1/links/{link_id}", status_code=HTTPStatus.NO_CONTENT) @api_check_wallet_key("admin") -async def api_link_delete(link_id): +async def api_link_delete(link_id, response: Response): link = await get_withdraw_link(link_id) if not link: - return ({"message": "Withdraw link does not exist."}, - HTTPStatus.NOT_FOUND, - ) + response.status_code = HTTPStatus.NOT_FOUND + return {"message": "Withdraw link does not exist."} if link.wallet != g.wallet.id: - return {"message": "Not your withdraw link."}, HTTPStatus.FORBIDDEN + response.status_code = HTTPStatus.FORBIDDEN + return {"message": "Not your withdraw link."} await delete_withdraw_link(link_id) - return "", HTTPStatus.NO_CONTENT + return "" -@withdraw_ext.get("/api/v1/links/{the_hash}/{lnurl_id}") +@withdraw_ext.get("/api/v1/links/{the_hash}/{lnurl_id}", status_code=HTTPStatus.OK) @api_check_wallet_key("invoice") async def api_hash_retrieve(the_hash, lnurl_id): hashCheck = await get_hash_check(the_hash, lnurl_id) - return hashCheck, HTTPStatus.OK + return hashCheck From 3e5af8c1d175159b339d4fc7285a1b1aaa4b8886 Mon Sep 17 00:00:00 2001 From: Stefan Stammberger Date: Fri, 27 Aug 2021 20:54:42 +0200 Subject: [PATCH 06/10] refactor: purge Quart from the codebase Most functionality is still broken --- Pipfile | 12 +- Pipfile.lock | 810 +++++++++++++++++++++ lnbits/app.py | 69 +- lnbits/core/models.py | 7 +- lnbits/core/services.py | 11 +- lnbits/core/views/api.py | 7 +- lnbits/core/views/generic.py | 129 ++-- lnbits/core/views/public_api.py | 1 - lnbits/decorators.py | 25 - lnbits/extensions/offlineshop/lnurl.py | 1 - lnbits/extensions/offlineshop/models.py | 2 +- lnbits/extensions/offlineshop/views.py | 7 +- lnbits/extensions/offlineshop/views_api.py | 11 +- lnbits/helpers.py | 45 +- lnbits/tasks.py | 1 - lnbits/wallets/lnpay.py | 1 - lnbits/wallets/opennode.py | 4 +- 17 files changed, 962 insertions(+), 181 deletions(-) create mode 100644 Pipfile.lock diff --git a/Pipfile b/Pipfile index 1b816e1a..24e580c8 100644 --- a/Pipfile +++ b/Pipfile @@ -14,12 +14,8 @@ environs = "*" lnurl = "==0.3.6" pyscss = "*" shortuuid = "*" -quart = "*" -quart-cors = "*" -quart-compress = "*" typing-extensions = "*" httpx = "*" -quart-trio = "*" trio = "==0.16.0" sqlalchemy-aio = "*" embit = "*" @@ -27,6 +23,10 @@ pyqrcode = "*" pypng = "*" sqlalchemy = "==1.3.23" psycopg2-binary = "*" +fastapi = {ref = "anyio", git = "https://github.com/graingert/fastapi"} +trio-asyncio = "*" +hypercorn = {extras = ["trio"], version = "*"} +aiofiles = "*" [dev-packages] black = "==20.8b1" @@ -35,7 +35,3 @@ pytest-cov = "*" mypy = "latest" pytest-trio = "*" trio-typing = "*" - -[packages.hypercorn] -extras = [ "trio",] -version = "*" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 00000000..470cb2bb --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,810 @@ +{ + "_meta": { + "hash": { + "sha256": "ff9251889371e0cec0eda7eb792e8618ea84f6c7eb85e9b472eacf3d3552c7c4" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "aiofiles": { + "hashes": [ + "sha256:a1c4fc9b2ff81568c83e21392a82f344ea9d23da906e4f6a52662764545e19d4", + "sha256:c67a6823b5f23fcab0a2595a289cec7d8c863ffcb4322fb8cd6b90400aedfdbc" + ], + "index": "pypi", + "version": "==0.7.0" + }, + "anyio": { + "hashes": [ + "sha256:929a6852074397afe1d989002aa96d457e3e1e5441357c60d03e7eea0e65e1b0", + "sha256:ae57a67583e5ff8b4af47666ff5651c3732d45fd26c929253748e796af860374" + ], + "markers": "python_full_version >= '3.6.2'", + "version": "==3.3.0" + }, + "async-generator": { + "hashes": [ + "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b", + "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144" + ], + "markers": "python_version >= '3.5'", + "version": "==1.10" + }, + "attrs": { + "hashes": [ + "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", + "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==21.2.0" + }, + "bech32": { + "hashes": [ + "sha256:7d6db8214603bd7871fcfa6c0826ef68b85b0abd90fa21c285a9c5e21d2bd899", + "sha256:990dc8e5a5e4feabbdf55207b5315fdd9b73db40be294a19b3752cde9e79d981" + ], + "markers": "python_version >= '3.5'", + "version": "==1.2.0" + }, + "bitstring": { + "hashes": [ + "sha256:0de167daa6a00c9386255a7cac931b45e6e24e0ad7ea64f1f92a64ac23ad4578", + "sha256:a5848a3f63111785224dca8bb4c0a75b62ecdef56a042c8d6be74b16f7e860e7", + "sha256:e3e340e58900a948787a05e8c08772f1ccbe133f6f41fe3f0fa19a18a22bbf4f" + ], + "index": "pypi", + "version": "==3.1.9" + }, + "cerberus": { + "hashes": [ + "sha256:d1b21b3954b2498d9a79edf16b3170a3ac1021df88d197dc2ce5928ba519237c" + ], + "index": "pypi", + "version": "==1.3.4" + }, + "certifi": { + "hashes": [ + "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", + "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" + ], + "version": "==2021.5.30" + }, + "charset-normalizer": { + "hashes": [ + "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b", + "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3" + ], + "markers": "python_version >= '3.5'", + "version": "==2.0.4" + }, + "ecdsa": { + "hashes": [ + "sha256:5cf31d5b33743abe0dfc28999036c849a69d548f994b535e527ee3cb7f3ef676", + "sha256:b9f500bb439e4153d0330610f5d26baaf18d17b8ced1bc54410d189385ea68aa" + ], + "index": "pypi", + "version": "==0.17.0" + }, + "embit": { + "hashes": [ + "sha256:19f69929caf0d2fcfd4b708dd873384dfc36267944d02d5e6dfebc835f294e1b" + ], + "index": "pypi", + "version": "==0.4.6" + }, + "environs": { + "hashes": [ + "sha256:72b867ff7b553076cdd90f3ee01ecc1cf854987639c9c459f0ed0d3d44ae490c", + "sha256:ee5466156b50fe03aa9fec6e720feea577b5bf515d7f21b2c46608272557ba26" + ], + "index": "pypi", + "version": "==9.3.3" + }, + "fastapi": { + "git": "https://github.com/graingert/fastapi", + "ref": "ada7c747c05c88d37e012d32e97bcc9579f3f006" + }, + "h11": { + "hashes": [ + "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6", + "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042" + ], + "markers": "python_version >= '3.6'", + "version": "==0.12.0" + }, + "h2": { + "hashes": [ + "sha256:ac9e293a1990b339d5d71b19c5fe630e3dd4d768c620d1730d355485323f1b25", + "sha256:bb7ac7099dd67a857ed52c815a6192b6b1f5ba6b516237fc24a085341340593d" + ], + "markers": "python_full_version >= '3.6.1'", + "version": "==4.0.0" + }, + "hpack": { + "hashes": [ + "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c", + "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095" + ], + "markers": "python_full_version >= '3.6.1'", + "version": "==4.0.0" + }, + "httpcore": { + "hashes": [ + "sha256:b0d16f0012ec88d8cc848f5a55f8a03158405f4bca02ee49bc4ca2c1fda49f3e", + "sha256:db4c0dcb8323494d01b8c6d812d80091a31e520033e7b0120883d6f52da649ff" + ], + "markers": "python_version >= '3.6'", + "version": "==0.13.6" + }, + "httpx": { + "hashes": [ + "sha256:92ecd2c00c688b529eda11cedb15161eaf02dee9116712f621c70d9a40b2cdd0", + "sha256:9bd728a6c5ec0a9e243932a9983d57d3cc4a87bb4f554e1360fce407f78f9435" + ], + "index": "pypi", + "version": "==0.19.0" + }, + "hypercorn": { + "extras": [ + "trio" + ], + "hashes": [ + "sha256:5ba1e719c521080abd698ff5781a2331e34ef50fc1c89a50960538115a896a9a", + "sha256:8007c10f81566920f8ae12c0e26e146f94ca70506da964b5a727ad610aa1d821" + ], + "index": "pypi", + "version": "==0.11.2" + }, + "hyperframe": { + "hashes": [ + "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15", + "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914" + ], + "markers": "python_full_version >= '3.6.1'", + "version": "==6.0.1" + }, + "idna": { + "hashes": [ + "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", + "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" + ], + "version": "==3.2" + }, + "lnurl": { + "hashes": [ + "sha256:579982fd8c4d25bc84c61c74ec45cb7999fa1fa2426f5d5aeb0160ba333b9c92", + "sha256:8af07460115a48f3122a5a9c9a6062bee3897d5f6ab4c9a60f6561a83a8234f6" + ], + "index": "pypi", + "version": "==0.3.6" + }, + "marshmallow": { + "hashes": [ + "sha256:c67929438fd73a2be92128caa0325b1b5ed8b626d91a094d2f7f2771bf1f1c0e", + "sha256:dd4724335d3c2b870b641ffe4a2f8728a1380cd2e7e2312756715ffeaa82b842" + ], + "markers": "python_version >= '3.5'", + "version": "==3.13.0" + }, + "outcome": { + "hashes": [ + "sha256:c7dd9375cfd3c12db9801d080a3b63d4b0a261aa996c4c13152380587288d958", + "sha256:e862f01d4e626e63e8f92c38d1f8d5546d3f9cce989263c521b2e7990d186967" + ], + "markers": "python_version >= '3.6'", + "version": "==1.1.0" + }, + "priority": { + "hashes": [ + "sha256:6f8eefce5f3ad59baf2c080a664037bb4725cd0a790d53d59ab4059288faf6aa", + "sha256:c965d54f1b8d0d0b19479db3924c7c36cf672dbf2aec92d43fbdaf4492ba18c0" + ], + "markers": "python_full_version >= '3.6.1'", + "version": "==2.0.0" + }, + "psycopg2-binary": { + "hashes": [ + "sha256:0b7dae87f0b729922e06f85f667de7bf16455d411971b2043bbd9577af9d1975", + "sha256:0f2e04bd2a2ab54fa44ee67fe2d002bb90cee1c0f1cc0ebc3148af7b02034cbd", + "sha256:123c3fb684e9abfc47218d3784c7b4c47c8587951ea4dd5bc38b6636ac57f616", + "sha256:1473c0215b0613dd938db54a653f68251a45a78b05f6fc21af4326f40e8360a2", + "sha256:14db1752acdd2187d99cb2ca0a1a6dfe57fc65c3281e0f20e597aac8d2a5bd90", + "sha256:1e3a362790edc0a365385b1ac4cc0acc429a0c0d662d829a50b6ce743ae61b5a", + "sha256:1e85b74cbbb3056e3656f1cc4781294df03383127a8114cbc6531e8b8367bf1e", + "sha256:20f1ab44d8c352074e2d7ca67dc00843067788791be373e67a0911998787ce7d", + "sha256:2f62c207d1740b0bde5c4e949f857b044818f734a3d57f1d0d0edc65050532ed", + "sha256:3242b9619de955ab44581a03a64bdd7d5e470cc4183e8fcadd85ab9d3756ce7a", + "sha256:35c4310f8febe41f442d3c65066ca93cccefd75013df3d8c736c5b93ec288140", + "sha256:4235f9d5ddcab0b8dbd723dca56ea2922b485ea00e1dafacf33b0c7e840b3d32", + "sha256:5ced67f1e34e1a450cdb48eb53ca73b60aa0af21c46b9b35ac3e581cf9f00e31", + "sha256:7360647ea04db2e7dff1648d1da825c8cf68dc5fbd80b8fb5b3ee9f068dcd21a", + "sha256:8c13d72ed6af7fd2c8acbd95661cf9477f94e381fce0792c04981a8283b52917", + "sha256:988b47ac70d204aed01589ed342303da7c4d84b56c2f4c4b8b00deda123372bf", + "sha256:995fc41ebda5a7a663a254a1dcac52638c3e847f48307b5416ee373da15075d7", + "sha256:a36c7eb6152ba5467fb264d73844877be8b0847874d4822b7cf2d3c0cb8cdcb0", + "sha256:aed4a9a7e3221b3e252c39d0bf794c438dc5453bc2963e8befe9d4cd324dff72", + "sha256:aef9aee84ec78af51107181d02fe8773b100b01c5dfde351184ad9223eab3698", + "sha256:b0221ca5a9837e040ebf61f48899926b5783668b7807419e4adae8175a31f773", + "sha256:b4d7679a08fea64573c969f6994a2631908bb2c0e69a7235648642f3d2e39a68", + "sha256:c250a7ec489b652c892e4f0a5d122cc14c3780f9f643e1a326754aedf82d9a76", + "sha256:ca86db5b561b894f9e5f115d6a159fff2a2570a652e07889d8a383b5fae66eb4", + "sha256:cfc523edecddaef56f6740d7de1ce24a2fdf94fd5e704091856a201872e37f9f", + "sha256:da113b70f6ec40e7d81b43d1b139b9db6a05727ab8be1ee559f3a69854a69d34", + "sha256:f6fac64a38f6768e7bc7b035b9e10d8a538a9fadce06b983fb3e6fa55ac5f5ce", + "sha256:f8559617b1fcf59a9aedba2c9838b5b6aa211ffedecabca412b92a1ff75aac1a", + "sha256:fbb42a541b1093385a2d8c7eec94d26d30437d0e77c1d25dae1dcc46741a385e" + ], + "index": "pypi", + "version": "==2.9.1" + }, + "pydantic": { + "hashes": [ + "sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd", + "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739", + "sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f", + "sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840", + "sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23", + "sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287", + "sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62", + "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b", + "sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb", + "sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820", + "sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3", + "sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b", + "sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e", + "sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3", + "sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316", + "sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b", + "sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4", + "sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20", + "sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e", + "sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505", + "sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1", + "sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833" + ], + "markers": "python_full_version >= '3.6.1'", + "version": "==1.8.2" + }, + "pypng": { + "hashes": [ + "sha256:76f8a1539ec56451da7ab7121f12a361969fe0f2d48d703d198ce2a99d6c5afd" + ], + "index": "pypi", + "version": "==0.0.21" + }, + "pyqrcode": { + "hashes": [ + "sha256:1b2812775fa6ff5c527977c4cd2ccb07051ca7d0bc0aecf937a43864abe5eff6", + "sha256:fdbf7634733e56b72e27f9bce46e4550b75a3a2c420414035cae9d9d26b234d5" + ], + "index": "pypi", + "version": "==1.2.1" + }, + "pyscss": { + "hashes": [ + "sha256:f1df571569021a23941a538eb154405dde80bed35dc1ea7c5f3e18e0144746bf" + ], + "index": "pypi", + "version": "==1.3.7" + }, + "python-dotenv": { + "hashes": [ + "sha256:aae25dc1ebe97c420f50b81fb0e5c949659af713f31fdb63c749ca68748f34b1", + "sha256:f521bc2ac9a8e03c736f62911605c5d83970021e3fa95b37d769e2bbbe9b6172" + ], + "markers": "python_version >= '3.5'", + "version": "==0.19.0" + }, + "represent": { + "hashes": [ + "sha256:026c0de2ee8385d1255b9c2426cd4f03fe9177ac94c09979bc601946c8493aa0", + "sha256:99142650756ef1998ce0661568f54a47dac8c638fb27e3816c02536575dbba8c" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.6.0.post0" + }, + "rfc3986": { + "extras": [ + "idna2008" + ], + "hashes": [ + "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835", + "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97" + ], + "version": "==1.5.0" + }, + "shortuuid": { + "hashes": [ + "sha256:3c11d2007b915c43bee3e10625f068d8a349e04f0d81f08f5fa08507427ebf1f", + "sha256:492c7402ff91beb1342a5898bd61ea953985bf24a41cd9f247409aa2e03c8f77" + ], + "index": "pypi", + "version": "==1.0.1" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" + }, + "sniffio": { + "hashes": [ + "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663", + "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de" + ], + "markers": "python_version >= '3.5'", + "version": "==1.2.0" + }, + "sortedcontainers": { + "hashes": [ + "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", + "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0" + ], + "version": "==2.4.0" + }, + "sqlalchemy": { + "hashes": [ + "sha256:040bdfc1d76a9074717a3f43455685f781c581f94472b010cd6c4754754e1862", + "sha256:1fe5d8d39118c2b018c215c37b73fd6893c3e1d4895be745ca8ff6eb83333ed3", + "sha256:23927c3981d1ec6b4ea71eb99d28424b874d9c696a21e5fbd9fa322718be3708", + "sha256:24f9569e82a009a09ce2d263559acb3466eba2617203170e4a0af91e75b4f075", + "sha256:2578dbdbe4dbb0e5126fb37ffcd9793a25dcad769a95f171a2161030bea850ff", + "sha256:269990b3ab53cb035d662dcde51df0943c1417bdab707dc4a7e4114a710504b4", + "sha256:29cccc9606750fe10c5d0e8bd847f17a97f3850b8682aef1f56f5d5e1a5a64b1", + "sha256:37b83bf81b4b85dda273aaaed5f35ea20ad80606f672d94d2218afc565fb0173", + "sha256:63677d0c08524af4c5893c18dbe42141de7178001360b3de0b86217502ed3601", + "sha256:639940bbe1108ac667dcffc79925db2966826c270112e9159439ab6bb14f8d80", + "sha256:6a939a868fdaa4b504e8b9d4a61f21aac11e3fecc8a8214455e144939e3d2aea", + "sha256:6b8b8c80c7f384f06825612dd078e4a31f0185e8f1f6b8c19e188ff246334205", + "sha256:6c9e6cc9237de5660bcddea63f332428bb83c8e2015c26777281f7ffbd2efb84", + "sha256:6ec1044908414013ebfe363450c22f14698803ce97fbb47e53284d55c5165848", + "sha256:6fca33672578666f657c131552c4ef8979c1606e494f78cd5199742dfb26918b", + "sha256:751934967f5336a3e26fc5993ccad1e4fee982029f9317eb6153bc0bc3d2d2da", + "sha256:8be835aac18ec85351385e17b8665bd4d63083a7160a017bef3d640e8e65cadb", + "sha256:927ce09e49bff3104459e1451ce82983b0a3062437a07d883a4c66f0b344c9b5", + "sha256:94208867f34e60f54a33a37f1c117251be91a47e3bfdb9ab8a7847f20886ad06", + "sha256:94f667d86be82dd4cb17d08de0c3622e77ca865320e0b95eae6153faa7b4ecaf", + "sha256:9e9c25522933e569e8b53ccc644dc993cab87e922fb7e142894653880fdd419d", + "sha256:a0e306e9bb76fd93b29ae3a5155298e4c1b504c7cbc620c09c20858d32d16234", + "sha256:a8bfc1e1afe523e94974132d7230b82ca7fa2511aedde1f537ec54db0399541a", + "sha256:ac2244e64485c3778f012951fdc869969a736cd61375fde6096d08850d8be729", + "sha256:b4b0e44d586cd64b65b507fa116a3814a1a53d55dce4836d7c1a6eb2823ff8d1", + "sha256:baeb451ee23e264de3f577fee5283c73d9bbaa8cb921d0305c0bbf700094b65b", + "sha256:c7dc052432cd5d060d7437e217dd33c97025287f99a69a50e2dc1478dd610d64", + "sha256:d1a85dfc5dee741bf49cb9b6b6b8d2725a268e4992507cf151cba26b17d97c37", + "sha256:d90010304abb4102123d10cbad2cdf2c25a9f2e66a50974199b24b468509bad5", + "sha256:ddfb511e76d016c3a160910642d57f4587dc542ce5ee823b0d415134790eeeb9", + "sha256:e273367f4076bd7b9a8dc2e771978ef2bfd6b82526e80775a7db52bff8ca01dd", + "sha256:e5bb3463df697279e5459a7316ad5a60b04b0107f9392e88674d0ece70e9cf70", + "sha256:e8a1750b44ad6422ace82bf3466638f1aa0862dbb9689690d5f2f48cce3476c8", + "sha256:eab063a70cca4a587c28824e18be41d8ecc4457f8f15b2933584c6c6cccd30f0", + "sha256:ecce8c021894a77d89808222b1ff9687ad84db54d18e4bd0500ca766737faaf6", + "sha256:f4d972139d5000105fcda9539a76452039434013570d6059993120dc2a65e447", + "sha256:fd3b96f8c705af8e938eaa99cbd8fd1450f632d38cad55e7367c33b263bf98ec", + "sha256:fdd2ed7395df8ac2dbb10cefc44737b66c6a5cd7755c92524733d7a443e5b7e2" + ], + "index": "pypi", + "version": "==1.3.23" + }, + "sqlalchemy-aio": { + "hashes": [ + "sha256:7f77366f55d34891c87386dd0962a28b948b684e8ea5edb7daae4187c0b291bf", + "sha256:f767320427c22c66fa5840a1f17f3261110a8ddc8560558f4fbf12d31a66b17b" + ], + "index": "pypi", + "version": "==0.16.0" + }, + "toml": { + "hashes": [ + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.10.2" + }, + "trio": { + "hashes": [ + "sha256:451ddb27b4e5215e00646fcbb8028d341fccf284e053dc376506a14bb133dbcf", + "sha256:df067dd0560c321af39d412cd81fc3a7d13f55af9150527daab980683e9fcf3c" + ], + "index": "pypi", + "version": "==0.16.0" + }, + "trio-asyncio": { + "hashes": [ + "sha256:824be23b0c678c0df942816cdb57b92a8b94f264fffa89f04626b0ba2d009768", + "sha256:9bf678f83204ba33c395783681c69af563a84145fad2110a152a81a4a18ae7e4" + ], + "index": "pypi", + "version": "==0.12.0" + }, + "typing-extensions": { + "hashes": [ + "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", + "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", + "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" + ], + "index": "pypi", + "version": "==3.10.0.0" + }, + "wsproto": { + "hashes": [ + "sha256:868776f8456997ad0d9720f7322b746bbe9193751b5b290b7f924659377c8c38", + "sha256:d8345d1808dd599b5ffb352c25a367adb6157e664e140dbecba3f9bc007edb9f" + ], + "markers": "python_full_version >= '3.6.1'", + "version": "==1.0.0" + } + }, + "develop": { + "appdirs": { + "hashes": [ + "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", + "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" + ], + "version": "==1.4.4" + }, + "async-generator": { + "hashes": [ + "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b", + "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144" + ], + "markers": "python_version >= '3.5'", + "version": "==1.10" + }, + "attrs": { + "hashes": [ + "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", + "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==21.2.0" + }, + "black": { + "hashes": [ + "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea" + ], + "index": "pypi", + "version": "==20.8b1" + }, + "click": { + "hashes": [ + "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a", + "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6" + ], + "markers": "python_version >= '3.6'", + "version": "==8.0.1" + }, + "coverage": { + "hashes": [ + "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c", + "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6", + "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45", + "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a", + "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03", + "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529", + "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a", + "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a", + "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2", + "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6", + "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759", + "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53", + "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a", + "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4", + "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff", + "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502", + "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793", + "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb", + "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905", + "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821", + "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b", + "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81", + "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0", + "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b", + "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3", + "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184", + "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701", + "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a", + "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82", + "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638", + "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5", + "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083", + "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6", + "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90", + "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465", + "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a", + "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3", + "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e", + "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066", + "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf", + "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b", + "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae", + "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669", + "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873", + "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b", + "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6", + "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb", + "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160", + "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c", + "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079", + "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d", + "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==5.5" + }, + "idna": { + "hashes": [ + "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", + "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" + ], + "version": "==3.2" + }, + "importlib-metadata": { + "hashes": [ + "sha256:9e04bf59076a15a9b6dd9c27806e8fcdf15280ba529c6a8cc3f4d5b4875bdd61", + "sha256:c4eb3dec5f697682e383a39701a7de11cd5c02daf8dd93534b69e3e6473f6b1b" + ], + "markers": "python_version < '3.8'", + "version": "==4.7.1" + }, + "iniconfig": { + "hashes": [ + "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", + "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" + ], + "version": "==1.1.1" + }, + "mypy": { + "hashes": [ + "sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9", + "sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a", + "sha256:119bed3832d961f3a880787bf621634ba042cb8dc850a7429f643508eeac97b9", + "sha256:1a85e280d4d217150ce8cb1a6dddffd14e753a4e0c3cf90baabb32cefa41b59e", + "sha256:3c4b8ca36877fc75339253721f69603a9c7fdb5d4d5a95a1a1b899d8b86a4de2", + "sha256:3e382b29f8e0ccf19a2df2b29a167591245df90c0b5a2542249873b5c1d78212", + "sha256:42c266ced41b65ed40a282c575705325fa7991af370036d3f134518336636f5b", + "sha256:53fd2eb27a8ee2892614370896956af2ff61254c275aaee4c230ae771cadd885", + "sha256:704098302473cb31a218f1775a873b376b30b4c18229421e9e9dc8916fd16150", + "sha256:7df1ead20c81371ccd6091fa3e2878559b5c4d4caadaf1a484cf88d93ca06703", + "sha256:866c41f28cee548475f146aa4d39a51cf3b6a84246969f3759cb3e9c742fc072", + "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457", + "sha256:adaeee09bfde366d2c13fe6093a7df5df83c9a2ba98638c7d76b010694db760e", + "sha256:b6fb13123aeef4a3abbcfd7e71773ff3ff1526a7d3dc538f3929a49b42be03f0", + "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb", + "sha256:c0df2d30ed496a08de5daed2a9ea807d07c21ae0ab23acf541ab88c24b26ab97", + "sha256:c6c2602dffb74867498f86e6129fd52a2770c48b7cd3ece77ada4fa38f94eba8", + "sha256:ceb6e0a6e27fb364fb3853389607cf7eb3a126ad335790fa1e14ed02fba50811", + "sha256:d9dd839eb0dc1bbe866a288ba3c1afc33a202015d2ad83b31e875b5905a079b6", + "sha256:e4dab234478e3bd3ce83bac4193b2ecd9cf94e720ddd95ce69840273bf44f6de", + "sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504", + "sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921", + "sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d" + ], + "index": "pypi", + "version": "==0.910" + }, + "mypy-extensions": { + "hashes": [ + "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", + "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" + ], + "version": "==0.4.3" + }, + "outcome": { + "hashes": [ + "sha256:c7dd9375cfd3c12db9801d080a3b63d4b0a261aa996c4c13152380587288d958", + "sha256:e862f01d4e626e63e8f92c38d1f8d5546d3f9cce989263c521b2e7990d186967" + ], + "markers": "python_version >= '3.6'", + "version": "==1.1.0" + }, + "packaging": { + "hashes": [ + "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7", + "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14" + ], + "markers": "python_version >= '3.6'", + "version": "==21.0" + }, + "pathspec": { + "hashes": [ + "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a", + "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1" + ], + "version": "==0.9.0" + }, + "pluggy": { + "hashes": [ + "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", + "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.13.1" + }, + "py": { + "hashes": [ + "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", + "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.10.0" + }, + "pyparsing": { + "hashes": [ + "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", + "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.4.7" + }, + "pytest": { + "hashes": [ + "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b", + "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890" + ], + "index": "pypi", + "version": "==6.2.4" + }, + "pytest-cov": { + "hashes": [ + "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a", + "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7" + ], + "index": "pypi", + "version": "==2.12.1" + }, + "pytest-trio": { + "hashes": [ + "sha256:c01b741819aec2c419555f28944e132d3c711dae1e673d63260809bf92c30c31" + ], + "index": "pypi", + "version": "==0.7.0" + }, + "regex": { + "hashes": [ + "sha256:08d74bfaa4c7731b8dac0a992c63673a2782758f7cfad34cf9c1b9184f911354", + "sha256:0fc1f8f06977c2d4f5e3d3f0d4a08089be783973fc6b6e278bde01f0544ff308", + "sha256:121f4b3185feaade3f85f70294aef3f777199e9b5c0c0245c774ae884b110a2d", + "sha256:1413b5022ed6ac0d504ba425ef02549a57d0f4276de58e3ab7e82437892704fc", + "sha256:1743345e30917e8c574f273f51679c294effba6ad372db1967852f12c76759d8", + "sha256:28fc475f560d8f67cc8767b94db4c9440210f6958495aeae70fac8faec631797", + "sha256:31a99a4796bf5aefc8351e98507b09e1b09115574f7c9dbb9cf2111f7220d2e2", + "sha256:328a1fad67445550b982caa2a2a850da5989fd6595e858f02d04636e7f8b0b13", + "sha256:473858730ef6d6ff7f7d5f19452184cd0caa062a20047f6d6f3e135a4648865d", + "sha256:4cde065ab33bcaab774d84096fae266d9301d1a2f5519d7bd58fc55274afbf7a", + "sha256:5f6a808044faae658f546dd5f525e921de9fa409de7a5570865467f03a626fc0", + "sha256:610b690b406653c84b7cb6091facb3033500ee81089867ee7d59e675f9ca2b73", + "sha256:66256b6391c057305e5ae9209941ef63c33a476b73772ca967d4a2df70520ec1", + "sha256:6eebf512aa90751d5ef6a7c2ac9d60113f32e86e5687326a50d7686e309f66ed", + "sha256:79aef6b5cd41feff359acaf98e040844613ff5298d0d19c455b3d9ae0bc8c35a", + "sha256:808ee5834e06f57978da3e003ad9d6292de69d2bf6263662a1a8ae30788e080b", + "sha256:8e44769068d33e0ea6ccdf4b84d80c5afffe5207aa4d1881a629cf0ef3ec398f", + "sha256:999ad08220467b6ad4bd3dd34e65329dd5d0df9b31e47106105e407954965256", + "sha256:9b006628fe43aa69259ec04ca258d88ed19b64791693df59c422b607b6ece8bb", + "sha256:a577a21de2ef8059b58f79ff76a4da81c45a75fe0bfb09bc8b7bb4293fa18983", + "sha256:a617593aeacc7a691cc4af4a4410031654f2909053bd8c8e7db837f179a630eb", + "sha256:abb48494d88e8a82601af905143e0de838c776c1241d92021e9256d5515b3645", + "sha256:ac88856a8cbccfc14f1b2d0b829af354cc1743cb375e7f04251ae73b2af6adf8", + "sha256:b844fb09bd9936ed158ff9df0ab601e2045b316b17aa8b931857365ea8586906", + "sha256:bdc178caebd0f338d57ae445ef8e9b737ddf8fbc3ea187603f65aec5b041248f", + "sha256:c61dcc1cf9fd165127a2853e2c31eb4fb961a4f26b394ac9fe5669c7a6592892", + "sha256:c7cb4c512d2d3b0870e00fbbac2f291d4b4bf2634d59a31176a87afe2777c6f0", + "sha256:d4a332404baa6665b54e5d283b4262f41f2103c255897084ec8f5487ce7b9e8e", + "sha256:d5111d4c843d80202e62b4fdbb4920db1dcee4f9366d6b03294f45ed7b18b42e", + "sha256:e1e8406b895aba6caa63d9fd1b6b1700d7e4825f78ccb1e5260551d168db38ed", + "sha256:e8690ed94481f219a7a967c118abaf71ccc440f69acd583cab721b90eeedb77c", + "sha256:ed283ab3a01d8b53de3a05bfdf4473ae24e43caee7dcb5584e86f3f3e5ab4374", + "sha256:ee329d0387b5b41a5dddbb6243a21cb7896587a651bebb957e2d2bb8b63c0791", + "sha256:f3bf1bc02bc421047bfec3343729c4bbbea42605bcfd6d6bfe2c07ade8b12d2a", + "sha256:f585cbbeecb35f35609edccb95efd95a3e35824cd7752b586503f7e6087303f1", + "sha256:f60667673ff9c249709160529ab39667d1ae9fd38634e006bec95611f632e759" + ], + "version": "==2021.8.28" + }, + "sniffio": { + "hashes": [ + "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663", + "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de" + ], + "markers": "python_version >= '3.5'", + "version": "==1.2.0" + }, + "sortedcontainers": { + "hashes": [ + "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", + "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0" + ], + "version": "==2.4.0" + }, + "toml": { + "hashes": [ + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.10.2" + }, + "trio": { + "hashes": [ + "sha256:451ddb27b4e5215e00646fcbb8028d341fccf284e053dc376506a14bb133dbcf", + "sha256:df067dd0560c321af39d412cd81fc3a7d13f55af9150527daab980683e9fcf3c" + ], + "index": "pypi", + "version": "==0.16.0" + }, + "trio-typing": { + "hashes": [ + "sha256:3eae317514ca18af158bd14ec55ccf20e8b1461efc3a431b87c337a9ca97180b", + "sha256:c3717f097eab29f8deb58a6976da366bd98adb81d90f38002b564932839eaa84" + ], + "index": "pypi", + "version": "==0.5.1" + }, + "typed-ast": { + "hashes": [ + "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace", + "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff", + "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266", + "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528", + "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6", + "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808", + "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4", + "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363", + "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341", + "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04", + "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41", + "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e", + "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3", + "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899", + "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805", + "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c", + "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c", + "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39", + "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a", + "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3", + "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7", + "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f", + "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075", + "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0", + "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40", + "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428", + "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927", + "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3", + "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f", + "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65" + ], + "markers": "python_version < '3.8'", + "version": "==1.4.3" + }, + "typing-extensions": { + "hashes": [ + "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", + "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", + "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" + ], + "index": "pypi", + "version": "==3.10.0.0" + }, + "zipp": { + "hashes": [ + "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3", + "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4" + ], + "markers": "python_version >= '3.6'", + "version": "==3.5.0" + } + } +} diff --git a/lnbits/app.py b/lnbits/app.py index 716979fd..177785bf 100644 --- a/lnbits/app.py +++ b/lnbits/app.py @@ -1,37 +1,27 @@ -import jinja2 -from lnbits.jinja2_templating import Jinja2Templates -import sys -import warnings import importlib +import sys import traceback -import trio +import warnings +import trio from fastapi import FastAPI, Request +from fastapi.exceptions import RequestValidationError from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.gzip import GZipMiddleware from fastapi.staticfiles import StaticFiles +import lnbits.settings + from .commands import db_migrate, handle_assets from .core import core_app -from .helpers import ( - get_valid_extensions, - get_js_vendored, - get_css_vendored, - url_for_vendored, -) -from .proxy_fix import ASGIProxyFix -from .tasks import ( - webhook_handler, - invoice_listener, - run_deferred_async, - check_pending_payments, - internal_invoice_listener, - catch_everything_and_restart, -) -from .settings import WALLET -from .requestvars import g, request_global from .core.views.generic import core_html_routes -import lnbits.settings +from .helpers import (get_css_vendored, get_js_vendored, get_valid_extensions, + template_renderer, url_for_vendored) +from .requestvars import g +from .settings import WALLET +from .tasks import (check_pending_payments, internal_invoice_listener, + invoice_listener, run_deferred_async, webhook_handler) + async def create_app(config_object="lnbits.settings") -> FastAPI: """Create application factory. @@ -55,7 +45,16 @@ async def create_app(config_object="lnbits.settings") -> FastAPI: ) g().config = lnbits.settings - g().templates = build_standard_jinja_templates() + g().base_url = f"http://{lnbits.settings.HOST}:{lnbits.settings.PORT}" + + @app.exception_handler(RequestValidationError) + async def validation_exception_handler(request: Request, exc: RequestValidationError): + return template_renderer().TemplateResponse("error.html", {"request": request, "err": f"`{exc.errors()}` is not a valid UUID."}) + + # return HTMLResponse( + # status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + # content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}), + # ) app.add_middleware(GZipMiddleware, minimum_size=1000) # app.add_middleware(ASGIProxyFix) @@ -69,26 +68,6 @@ async def create_app(config_object="lnbits.settings") -> FastAPI: return app -def build_standard_jinja_templates(): - t = Jinja2Templates( - loader=jinja2.FileSystemLoader(["lnbits/templates", "lnbits/core/templates"]), - ) - t.env.globals["SITE_TITLE"] = lnbits.settings.LNBITS_SITE_TITLE - t.env.globals["SITE_TAGLINE"] = lnbits.settings.LNBITS_SITE_TAGLINE - t.env.globals["SITE_DESCRIPTION"] = lnbits.settings.LNBITS_SITE_DESCRIPTION - t.env.globals["LNBITS_THEME_OPTIONS"] = lnbits.settings.LNBITS_THEME_OPTIONS - t.env.globals["LNBITS_VERSION"] = lnbits.settings.LNBITS_COMMIT - t.env.globals["EXTENSIONS"] = get_valid_extensions() - - if g().config.DEBUG: - t.env.globals["VENDORED_JS"] = map(url_for_vendored, get_js_vendored()) - t.env.globals["VENDORED_CSS"] = map(url_for_vendored, get_css_vendored()) - else: - t.env.globals["VENDORED_JS"] = ["/static/bundle.js"] - t.env.globals["VENDORED_CSS"] = ["/static/bundle.css"] - - return t - def check_funding_source(app: FastAPI) -> None: @app.on_event("startup") async def check_wallet_status(): @@ -171,5 +150,5 @@ def register_exception_handlers(app): etype, value, tb = sys.exc_info() traceback.print_exception(etype, err, tb) exc = traceback.format_exc() - return g().templates.TemplateResponse("error.html", {"request": request, "err": err}) + return template_renderer().TemplateResponse("error.html", {"request": request, "err": err}) diff --git a/lnbits/core/models.py b/lnbits/core/models.py index c1dc99f9..d7d211bf 100644 --- a/lnbits/core/models.py +++ b/lnbits/core/models.py @@ -1,7 +1,7 @@ import json import hmac import hashlib -from quart import url_for +from lnbits.helpers import url_for from ecdsa import SECP256k1, SigningKey # type: ignore from lnurl import encode as lnurl_encode # type: ignore from typing import List, NamedTuple, Optional, Dict @@ -30,11 +30,12 @@ class Wallet(BaseModel): @property def lnurlwithdraw_full(self) -> str: + url = url_for( - "core.lnurl_full_withdraw", + "/withdraw", + external=True, usr=self.user, wal=self.id, - _external=True, ) try: return lnurl_encode(url) diff --git a/lnbits/core/services.py b/lnbits/core/services.py index 09b9f4f7..4fa75f9d 100644 --- a/lnbits/core/services.py +++ b/lnbits/core/services.py @@ -5,7 +5,6 @@ from io import BytesIO from binascii import unhexlify from typing import Optional, Tuple, Dict from urllib.parse import urlparse, parse_qs -from quart import g, url_for from lnurl import LnurlErrorResponse, decode as decode_lnurl # type: ignore try: @@ -15,9 +14,10 @@ except ImportError: # pragma: nocover from lnbits import bolt11 from lnbits.db import Connection -from lnbits.helpers import urlsafe_short_hash +from lnbits.helpers import url_for, urlsafe_short_hash from lnbits.settings import WALLET from lnbits.wallets.base import PaymentStatus, PaymentResponse +from lnbits.requestvars import g from . import db from .crud import ( @@ -220,10 +220,9 @@ async def redeem_lnurl_withdraw( try: params["balanceNotify"] = url_for( - "core.lnurl_balance_notify", - service=urlparse(lnurl_request).netloc, + f"/withdraw/notify/{urlparse(lnurl_request).netloc}", + external=True, wal=wallet_id, - _external=True, ) except Exception: pass @@ -242,7 +241,7 @@ async def perform_lnurlauth( cb = urlparse(callback) k1 = unhexlify(parse_qs(cb.query)["k1"][0]) - key = g.wallet.lnurlauth_key(cb.netloc) + key = g().wallet.lnurlauth_key(cb.netloc) def int_to_bytes_suitable_der(x: int) -> bytes: """for strict DER we need to encode the integer with some quirks""" diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index 44f16802..7f660478 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -1,3 +1,4 @@ +from lnbits.helpers import url_for from fastapi.param_functions import Depends from lnbits.auth_bearer import AuthBearer from pydantic import BaseModel @@ -6,7 +7,6 @@ import json import httpx import hashlib from urllib.parse import urlparse, urlunparse, urlencode, parse_qs, ParseResult -from quart import current_app, make_response, url_for from fastapi import Query @@ -121,10 +121,9 @@ async def api_payments_create_invoice(data: CreateInvoiceData): params={ "pr": payment_request, "balanceNotify": url_for( - "core.lnurl_balance_notify", - service=urlparse(data.lnurl_callback).netloc, + f"/withdraw/notify/{urlparse(data.lnurl_callback).netloc}", + external=True, wal=g().wallet.id, - _external=True, ), }, timeout=10, diff --git a/lnbits/core/views/generic.py b/lnbits/core/views/generic.py index 516bcd5c..c5d6b3f2 100644 --- a/lnbits/core/views/generic.py +++ b/lnbits/core/views/generic.py @@ -1,32 +1,24 @@ -from lnbits.core.models import Wallet -from fastapi.params import Query -from fastapi.routing import APIRouter -from fastapi.responses import RedirectResponse -from fastapi import status -from lnbits.requestvars import g -from os import path from http import HTTPStatus from typing import Optional -import jinja2 +from fastapi import Request, status +from fastapi.param_functions import Body +from fastapi.params import Depends, Query +from fastapi.responses import FileResponse, RedirectResponse +from fastapi.routing import APIRouter +from pydantic.types import UUID4 from starlette.responses import HTMLResponse -from lnbits.core import core_app, db -from lnbits.decorators import check_user_exists, validate_uuids -from lnbits.settings import LNBITS_ALLOWED_USERS, SERVICE_FEE, LNBITS_SITE_TITLE +from lnbits.core import db +from lnbits.helpers import template_renderer, url_for +from lnbits.requestvars import g +from lnbits.settings import (LNBITS_ALLOWED_USERS, LNBITS_SITE_TITLE, + SERVICE_FEE) -from ..crud import ( - create_account, - get_user, - update_user_extension, - create_wallet, - delete_wallet, - get_balance_check, - save_balance_notify, -) -from ..services import redeem_lnurl_withdraw, pay_invoice -from fastapi import FastAPI, Request -from fastapi.responses import FileResponse +from ..crud import (create_account, create_wallet, delete_wallet, + get_balance_check, get_user, save_balance_notify, + update_user_extension) +from ..services import pay_invoice, redeem_lnurl_withdraw core_html_routes: APIRouter = APIRouter(tags=["Core NON-API Website Routes"]) @@ -37,13 +29,13 @@ async def favicon(): @core_html_routes.get("/", response_class=HTMLResponse) async def home(request: Request, lightning: str = None): - return g().templates.TemplateResponse("core/index.html", {"request": request, "lnurl": lightning}) + return template_renderer().TemplateResponse("core/index.html", {"request": request, "lnurl": lightning}) @core_html_routes.get("/extensions") -@validate_uuids(["usr"], required=True) -@check_user_exists() -async def extensions(enable: str, disable: str): +# @validate_uuids(["usr"], required=True) +# @check_user_exists() +async def extensions(request: Request, enable: str, disable: str): extension_to_enable = enable extension_to_disable = disable @@ -60,20 +52,16 @@ async def extensions(enable: str, disable: str): await update_user_extension( user_id=g.user.id, extension=extension_to_disable, active=False ) - return await templates.TemplateResponse("core/extensions.html", {"request": request, "user": get_user(g.user.id)}) + return template_renderer().TemplateResponse("core/extensions.html", {"request": request, "user": get_user(g.user.id)}) @core_html_routes.get("/wallet", response_class=HTMLResponse) #Not sure how to validate -@validate_uuids(["usr", "wal"]) -async def wallet(request: Request, - usr: Optional[str] = Query(None), - wal: Optional[str] = Query(None), - nme: Optional[str] = Query(None), - ): - - user_id = usr - wallet_id = wal +# @validate_uuids(["usr", "nme"]) +async def wallet(request: Request = Query(None), nme: Optional[str] = Query(None), + usr: Optional[UUID4] = Query(None), wal: Optional[UUID4] = Query(None)): + user_id = usr.hex if usr else None + wallet_id = wal.hex if wal else None wallet_name = nme service_fee = int(SERVICE_FEE) if int(SERVICE_FEE) == SERVICE_FEE else SERVICE_FEE @@ -84,33 +72,33 @@ async def wallet(request: Request, # nothing: create everything if not user_id: - usr = await get_user((await create_account()).id) + user = await get_user((await create_account()).id) else: - usr = await get_user(user_id) - if not usr: - return g().templates.TemplateResponse("error.html", {"request": request, "err": "User does not exist."}) + user = await get_user(user_id) + if not user: + return template_renderer().TemplateResponse("error.html", {"request": request, "err": "User does not exist."}) if LNBITS_ALLOWED_USERS and user_id not in LNBITS_ALLOWED_USERS: - return g().templates.TemplateResponse("error.html", {"request": request, "err": "User not authorized."}) + return template_renderer().TemplateResponse("error.html", {"request": request, "err": "User not authorized."}) if not wallet_id: - if usr.wallets and not wallet_name: - wal = usr.wallets[0] + if user.wallets and not wallet_name: + wallet = user.wallets[0] else: - wal = await create_wallet(user_id=usr.id, wallet_name=wallet_name) + wallet = await create_wallet(user_id=user.id, wallet_name=wallet_name) - return RedirectResponse(f"/wallet?usr={usr.id}&wal={wal.id}", status_code=status.HTTP_307_TEMPORARY_REDIRECT) + return RedirectResponse(f"/wallet?usr={user.id}&wal={wallet.id}", status_code=status.HTTP_307_TEMPORARY_REDIRECT) - wal = usr.get_wallet(wallet_id) - if not wal: - return g().templates.TemplateResponse("error.html", {"request": request, ...}) + wallet = user.get_wallet(wallet_id) + if not wallet: + return template_renderer().TemplateResponse("error.html", {"request": request, "err": "Wallet not found"}) - return g().templates.TemplateResponse( - "core/wallet.html", {"request":request,"user":usr, "wallet":wal, "service_fee":service_fee} + return template_renderer().TemplateResponse( + "core/wallet.html", {"request":request,"user":user.dict(), "wallet":wallet.dict(), "service_fee":service_fee} ) @core_html_routes.get("/withdraw") -@validate_uuids(["usr", "wal"], required=True) -async def lnurl_full_withdraw(): +# @validate_uuids(["usr", "wal"], required=True) +async def lnurl_full_withdraw(request: Request): user = await get_user(request.args.get("usr")) if not user: return {"status": "ERROR", "reason": "User does not exist."} @@ -122,24 +110,22 @@ async def lnurl_full_withdraw(): return { "tag": "withdrawRequest", "callback": url_for( - "core.lnurl_full_withdraw_callback", + "/withdraw/cb", + external=True, usr=user.id, wal=wallet.id, - _external=True, ), "k1": "0", "minWithdrawable": 1000 if wallet.withdrawable_balance else 0, "maxWithdrawable": wallet.withdrawable_balance, "defaultDescription": f"{LNBITS_SITE_TITLE} balance withdraw from {wallet.id[0:5]}", - "balanceCheck": url_for( - "core.lnurl_full_withdraw", usr=user.id, wal=wallet.id, _external=True - ), + "balanceCheck": url_for("/withdraw", external=True, usr=user.id, wal=wallet.id), } @core_html_routes.get("/withdraw/cb") -@validate_uuids(["usr", "wal"], required=True) -async def lnurl_full_withdraw_callback(): +# @validate_uuids(["usr", "wal"], required=True) +async def lnurl_full_withdraw_callback(request: Request): user = await get_user(request.args.get("usr")) if not user: return {"status": "ERROR", "reason": "User does not exist."} @@ -166,34 +152,35 @@ async def lnurl_full_withdraw_callback(): @core_html_routes.get("/deletewallet") -@validate_uuids(["usr", "wal"], required=True) -@check_user_exists() -async def deletewallet(): - wallet_id = request.args.get("wal", type=str) - user_wallet_ids = g.user.wallet_ids +# @validate_uuids(["usr", "wal"], required=True) +# @check_user_exists() +async def deletewallet(request: Request): + wallet_id = request.path_params.get("wal", type=str) + user_wallet_ids = g().user.wallet_ids if wallet_id not in user_wallet_ids: abort(HTTPStatus.FORBIDDEN, "Not your wallet.") else: - await delete_wallet(user_id=g.user.id, wallet_id=wallet_id) + await delete_wallet(user_id=g().user.id, wallet_id=wallet_id) user_wallet_ids.remove(wallet_id) if user_wallet_ids: - return redirect(url_for("core.wallet", usr=g.user.id, wal=user_wallet_ids[0])) + return RedirectResponse(url_for("/wallet", usr=g().user.id, wal=user_wallet_ids[0]), + status_code=status.HTTP_307_TEMPORARY_REDIRECT) - return redirect(url_for("core.home")) + return RedirectResponse(url_for("/"), status_code=status.HTTP_307_TEMPORARY_REDIRECT) @core_html_routes.get("/withdraw/notify/{service}") -@validate_uuids(["wal"], required=True) -async def lnurl_balance_notify(service: str): +# @validate_uuids(["wal"], required=True) +async def lnurl_balance_notify(request: Request, service: str): bc = await get_balance_check(request.args.get("wal"), service) if bc: redeem_lnurl_withdraw(bc.wallet, bc.url) @core_html_routes.get("/lnurlwallet") -async def lnurlwallet(): +async def lnurlwallet(request: Request): async with db.connect() as conn: account = await create_account(conn=conn) user = await get_user(account.id, conn=conn) diff --git a/lnbits/core/views/public_api.py b/lnbits/core/views/public_api.py index 9402d573..9cf7b6cf 100644 --- a/lnbits/core/views/public_api.py +++ b/lnbits/core/views/public_api.py @@ -1,7 +1,6 @@ import trio import datetime from http import HTTPStatus -from quart import jsonify from lnbits import bolt11 diff --git a/lnbits/decorators.py b/lnbits/decorators.py index a5a270e1..880ddc5f 100644 --- a/lnbits/decorators.py +++ b/lnbits/decorators.py @@ -1,5 +1,4 @@ from cerberus import Validator # type: ignore -from quart import g, abort, jsonify, request from functools import wraps from http import HTTPStatus from typing import List, Union @@ -77,28 +76,4 @@ def check_user_exists(param: str = "usr"): return wrap -def validate_uuids( - params: List[str], *, required: Union[bool, List[str]] = False, version: int = 4 -): - def wrap(view): - @wraps(view) - async def wrapped_view(**kwargs): - query_params = { - param: request.args.get(param, type=str) for param in params - } - for param, value in query_params.items(): - if not value and (required is True or (required and param in required)): - abort(HTTPStatus.BAD_REQUEST, f"`{param}` is required.") - - if value: - try: - UUID(value, version=version) - except ValueError: - abort(HTTPStatus.BAD_REQUEST, f"`{param}` is not a valid UUID.") - - return await view(**kwargs) - - return wrapped_view - - return wrap diff --git a/lnbits/extensions/offlineshop/lnurl.py b/lnbits/extensions/offlineshop/lnurl.py index ff8b71dc..8ebf7fa1 100644 --- a/lnbits/extensions/offlineshop/lnurl.py +++ b/lnbits/extensions/offlineshop/lnurl.py @@ -1,5 +1,4 @@ import hashlib -from quart import jsonify, url_for, request from lnurl import LnurlPayResponse, LnurlPayActionResponse, LnurlErrorResponse # type: ignore from lnbits.core.services import create_invoice diff --git a/lnbits/extensions/offlineshop/models.py b/lnbits/extensions/offlineshop/models.py index 8839f0dd..fa798e5c 100644 --- a/lnbits/extensions/offlineshop/models.py +++ b/lnbits/extensions/offlineshop/models.py @@ -2,7 +2,7 @@ import json import base64 import hashlib from collections import OrderedDict -from quart import url_for + from typing import Optional, List, Dict from lnurl import encode as lnurl_encode # type: ignore from lnurl.types import LnurlPayMetadata # type: ignore diff --git a/lnbits/extensions/offlineshop/views.py b/lnbits/extensions/offlineshop/views.py index 78ebb799..9715c76c 100644 --- a/lnbits/extensions/offlineshop/views.py +++ b/lnbits/extensions/offlineshop/views.py @@ -1,9 +1,8 @@ import time from datetime import datetime -from quart import g, render_template, request from http import HTTPStatus -from lnbits.decorators import check_user_exists, validate_uuids +from lnbits.decorators import check_user_exists from lnbits.core.models import Payment from lnbits.core.crud import get_standalone_payment @@ -15,8 +14,8 @@ from fastapi.templating import Jinja2Templates templates = Jinja2Templates(directory="templates") @offlineshop_ext.get("/") -@validate_uuids(["usr"], required=True) -@check_user_exists() +# @validate_uuids(["usr"], required=True) +# @check_user_exists() async def index(request: Request): return await templates.TemplateResponse("offlineshop/index.html", {"request": request,"user":g.user}) diff --git a/lnbits/extensions/offlineshop/views_api.py b/lnbits/extensions/offlineshop/views_api.py index be860bc0..284170ef 100644 --- a/lnbits/extensions/offlineshop/views_api.py +++ b/lnbits/extensions/offlineshop/views_api.py @@ -1,11 +1,12 @@ from typing import Optional from pydantic.main import BaseModel -from quart import g, jsonify + from http import HTTPStatus from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore from lnbits.decorators import api_check_wallet_key, api_validate_post_request from lnbits.utils.exchange_rates import currencies +from lnbits.requestvars import g from . import offlineshop_ext from .crud import ( @@ -27,7 +28,7 @@ async def api_list_currencies_available(): @offlineshop_ext.get("/api/v1/offlineshop") @api_check_wallet_key("invoice") async def api_shop_from_wallet(): - shop = await get_or_create_shop_by_wallet(g.wallet.id) + shop = await get_or_create_shop_by_wallet(g().wallet.id) items = await get_items(shop.id) try: @@ -60,7 +61,7 @@ class CreateItemsData(BaseModel): @offlineshop_ext.put("/api/v1/offlineshop/items/{item_id}") @api_check_wallet_key("invoice") async def api_add_or_update_item(data: CreateItemsData, item_id=None): - shop = await get_or_create_shop_by_wallet(g.wallet.id) + shop = await get_or_create_shop_by_wallet(g().wallet.id) if item_id == None: await add_item( shop.id, @@ -87,7 +88,7 @@ async def api_add_or_update_item(data: CreateItemsData, item_id=None): @offlineshop_ext.delete("/api/v1/offlineshop/items/{item_id}") @api_check_wallet_key("invoice") async def api_delete_item(item_id): - shop = await get_or_create_shop_by_wallet(g.wallet.id) + shop = await get_or_create_shop_by_wallet(g().wallet.id) await delete_item_from_shop(shop.id, item_id) return "", HTTPStatus.NO_CONTENT @@ -104,7 +105,7 @@ async def api_set_method(data: CreateMethodData): wordlist = data.wordlist.split("\n") if data.wordlist else None wordlist = [word.strip() for word in wordlist if word.strip()] - shop = await get_or_create_shop_by_wallet(g.wallet.id) + shop = await get_or_create_shop_by_wallet(g().wallet.id) if not shop: return "", HTTPStatus.NOT_FOUND diff --git a/lnbits/helpers.py b/lnbits/helpers.py index 8b57fe2b..43623527 100644 --- a/lnbits/helpers.py +++ b/lnbits/helpers.py @@ -1,11 +1,18 @@ +import glob import json import os -import glob +from typing import Any, List, NamedTuple, Optional + +import jinja2 import shortuuid # type: ignore -from typing import List, NamedTuple, Optional +from lnbits.jinja2_templating import Jinja2Templates +from lnbits.requestvars import g -from .settings import LNBITS_DISABLED_EXTENSIONS, LNBITS_PATH +from .settings import (DEBUG, LNBITS_COMMIT, LNBITS_DISABLED_EXTENSIONS, + LNBITS_PATH, LNBITS_SITE_DESCRIPTION, + LNBITS_SITE_TAGLINE, LNBITS_SITE_TITLE, + LNBITS_THEME_OPTIONS) class Extension(NamedTuple): @@ -132,3 +139,35 @@ def get_vendored(ext: str, prefer_minified: bool = False) -> List[str]: def url_for_vendored(abspath: str) -> str: return "/" + os.path.relpath(abspath, LNBITS_PATH) + +def url_for( + endpoint: str, + external: Optional[bool] = False, + **params: Any, +) -> str: + base = g().base_url if external else "" + url_params = "?" + for key in params: + url_params += f"{key}={params[key]}&" + url = f"{base}{endpoint}{url_params}" + return url + +def template_renderer() -> Jinja2Templates: + t = Jinja2Templates( + loader=jinja2.FileSystemLoader(["lnbits/templates", "lnbits/core/templates"]), + ) + t.env.globals["SITE_TITLE"] = LNBITS_SITE_TITLE + t.env.globals["SITE_TAGLINE"] = LNBITS_SITE_TAGLINE + t.env.globals["SITE_DESCRIPTION"] = LNBITS_SITE_DESCRIPTION + t.env.globals["LNBITS_THEME_OPTIONS"] = LNBITS_THEME_OPTIONS + t.env.globals["LNBITS_VERSION"] = LNBITS_COMMIT + t.env.globals["EXTENSIONS"] = get_valid_extensions() + + if DEBUG: + t.env.globals["VENDORED_JS"] = map(url_for_vendored, get_js_vendored()) + t.env.globals["VENDORED_CSS"] = map(url_for_vendored, get_css_vendored()) + else: + t.env.globals["VENDORED_JS"] = ["/static/bundle.js"] + t.env.globals["VENDORED_CSS"] = ["/static/bundle.css"] + + return t diff --git a/lnbits/tasks.py b/lnbits/tasks.py index d7b297a0..b6facd60 100644 --- a/lnbits/tasks.py +++ b/lnbits/tasks.py @@ -2,7 +2,6 @@ import time import trio import traceback from http import HTTPStatus -from quart import current_app from typing import List, Callable from lnbits.settings import WALLET diff --git a/lnbits/wallets/lnpay.py b/lnbits/wallets/lnpay.py index bb08d35e..034d1529 100644 --- a/lnbits/wallets/lnpay.py +++ b/lnbits/wallets/lnpay.py @@ -4,7 +4,6 @@ import httpx from os import getenv from http import HTTPStatus from typing import Optional, Dict, AsyncGenerator -from quart import request from .base import ( StatusResponse, diff --git a/lnbits/wallets/opennode.py b/lnbits/wallets/opennode.py index 5cc046a5..ba315503 100644 --- a/lnbits/wallets/opennode.py +++ b/lnbits/wallets/opennode.py @@ -1,10 +1,10 @@ +from lnbits.helpers import url_for import trio import hmac import httpx from http import HTTPStatus from os import getenv from typing import Optional, AsyncGenerator -from quart import request, url_for from .base import ( StatusResponse, @@ -63,7 +63,7 @@ class OpenNodeWallet(Wallet): json={ "amount": amount, "description": memo or "", - "callback_url": url_for("webhook_listener", _external=True), + "callback_url": url_for("/webhook_listener", _external=True), }, timeout=40, ) From ee775b66ef06fb70ff0e797787b72df4307ed9e6 Mon Sep 17 00:00:00 2001 From: Stefan Stammberger Date: Sat, 28 Aug 2021 11:16:59 +0200 Subject: [PATCH 07/10] fix: settings import --- lnbits/helpers.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/lnbits/helpers.py b/lnbits/helpers.py index 43623527..b1a8c1d8 100644 --- a/lnbits/helpers.py +++ b/lnbits/helpers.py @@ -9,10 +9,7 @@ import shortuuid # type: ignore from lnbits.jinja2_templating import Jinja2Templates from lnbits.requestvars import g -from .settings import (DEBUG, LNBITS_COMMIT, LNBITS_DISABLED_EXTENSIONS, - LNBITS_PATH, LNBITS_SITE_DESCRIPTION, - LNBITS_SITE_TAGLINE, LNBITS_SITE_TITLE, - LNBITS_THEME_OPTIONS) +import lnbits.settings as settings class Extension(NamedTuple): @@ -27,9 +24,9 @@ class Extension(NamedTuple): class ExtensionManager: def __init__(self): - self._disabled: List[str] = LNBITS_DISABLED_EXTENSIONS + self._disabled: List[str] = settings.LNBITS_DISABLED_EXTENSIONS self._extension_folders: List[str] = [ - x[1] for x in os.walk(os.path.join(LNBITS_PATH, "extensions")) + x[1] for x in os.walk(os.path.join(settings.LNBITS_PATH, "extensions")) ][0] @property @@ -44,7 +41,7 @@ class ExtensionManager: ]: try: with open( - os.path.join(LNBITS_PATH, "extensions", extension, "config.json") + os.path.join(settings.LNBITS_PATH, "extensions", extension, "config.json") ) as json_file: config = json.load(json_file) is_valid = True @@ -112,7 +109,7 @@ def get_css_vendored(prefer_minified: bool = False) -> List[str]: def get_vendored(ext: str, prefer_minified: bool = False) -> List[str]: paths: List[str] = [] for path in glob.glob( - os.path.join(LNBITS_PATH, "static/vendor/**"), recursive=True + os.path.join(settings.LNBITS_PATH, "static/vendor/**"), recursive=True ): if path.endswith(".min" + ext): # path is minified @@ -138,7 +135,7 @@ def get_vendored(ext: str, prefer_minified: bool = False) -> List[str]: def url_for_vendored(abspath: str) -> str: - return "/" + os.path.relpath(abspath, LNBITS_PATH) + return "/" + os.path.relpath(abspath, settings.LNBITS_PATH) def url_for( endpoint: str, @@ -156,14 +153,14 @@ def template_renderer() -> Jinja2Templates: t = Jinja2Templates( loader=jinja2.FileSystemLoader(["lnbits/templates", "lnbits/core/templates"]), ) - t.env.globals["SITE_TITLE"] = LNBITS_SITE_TITLE - t.env.globals["SITE_TAGLINE"] = LNBITS_SITE_TAGLINE - t.env.globals["SITE_DESCRIPTION"] = LNBITS_SITE_DESCRIPTION - t.env.globals["LNBITS_THEME_OPTIONS"] = LNBITS_THEME_OPTIONS - t.env.globals["LNBITS_VERSION"] = LNBITS_COMMIT + t.env.globals["SITE_TITLE"] = settings.LNBITS_SITE_TITLE + t.env.globals["SITE_TAGLINE"] = settings.LNBITS_SITE_TAGLINE + t.env.globals["SITE_DESCRIPTION"] = settings.LNBITS_SITE_DESCRIPTION + t.env.globals["LNBITS_THEME_OPTIONS"] = settings.LNBITS_THEME_OPTIONS + t.env.globals["LNBITS_VERSION"] = settings.LNBITS_COMMIT t.env.globals["EXTENSIONS"] = get_valid_extensions() - if DEBUG: + if settings.DEBUG: t.env.globals["VENDORED_JS"] = map(url_for_vendored, get_js_vendored()) t.env.globals["VENDORED_CSS"] = map(url_for_vendored, get_css_vendored()) else: From 5ae124408e5428b3593025d71250f8f3fd72f279 Mon Sep 17 00:00:00 2001 From: Stefan Stammberger Date: Sat, 28 Aug 2021 13:55:31 +0200 Subject: [PATCH 08/10] fix: wallet page Javascript was incompatible because of the switch to FastAPI. --- lnbits/static/js/base.js | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/lnbits/static/js/base.js b/lnbits/static/js/base.js index 729e0938..bb65a824 100644 --- a/lnbits/static/js/base.js +++ b/lnbits/static/js/base.js @@ -135,7 +135,7 @@ window.LNbits = { return obj }, user: function (data) { - var obj = _.object(['id', 'email', 'extensions', 'wallets'], data) + var obj = {id: data.id, email: data.email, extensions: data.extensions, wallets: data.wallets} var mapWallet = this.wallet obj.wallets = obj.wallets .map(function (obj) { @@ -153,15 +153,12 @@ window.LNbits = { return obj }, wallet: function (data) { - var obj = _.object( - ['id', 'name', 'user', 'adminkey', 'inkey', 'balance'], - data - ) - obj.msat = obj.balance - obj.sat = Math.round(obj.balance / 1000) - obj.fsat = new Intl.NumberFormat(window.LOCALE).format(obj.sat) - obj.url = ['/wallet?usr=', obj.user, '&wal=', obj.id].join('') - return obj + newWallet = {id: data.id, name: data.name, adminkey: data.adminkey, inkey: data.inkey} + newWallet.msat = data.balance_msat + newWallet.sat = Math.round(data.balance_msat / 1000) + newWallet.fsat = new Intl.NumberFormat(window.LOCALE).format(newWallet.sat) + newWallet.url = ['/wallet?usr=', data.user, '&wal=', data.id].join('') + return newWallet }, payment: function (data) { var obj = _.object( From fe79709698cbcdcd17536c5e46a3d7e6a5c4bee9 Mon Sep 17 00:00:00 2001 From: Stefan Stammberger Date: Sun, 29 Aug 2021 19:38:42 +0200 Subject: [PATCH 09/10] fix: several more API calls restored --- lnbits/core/models.py | 6 +- lnbits/core/views/api.py | 165 ++++++++++++++++------------------- lnbits/core/views/generic.py | 28 +++--- lnbits/decorators.py | 117 +++++++++++++++++++++---- lnbits/static/js/base.js | 34 ++++---- 5 files changed, 208 insertions(+), 142 deletions(-) diff --git a/lnbits/core/models.py b/lnbits/core/models.py index d7d211bf..672a252c 100644 --- a/lnbits/core/models.py +++ b/lnbits/core/models.py @@ -84,10 +84,10 @@ class Payment(BaseModel): bolt11: str preimage: str payment_hash: str - extra: Dict + extra: Optional[Dict] = {} wallet_id: str - webhook: str - webhook_status: int + webhook: Optional[str] + webhook_status: Optional[int] @classmethod def from_row(cls, row: Row): diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index 7f660478..f277e6cc 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -1,69 +1,59 @@ -from lnbits.helpers import url_for -from fastapi.param_functions import Depends -from lnbits.auth_bearer import AuthBearer -from pydantic import BaseModel -import trio -import json -import httpx import hashlib -from urllib.parse import urlparse, urlunparse, urlencode, parse_qs, ParseResult - -from fastapi import Query - -from http import HTTPStatus +import json from binascii import unhexlify -from typing import Dict, List, Optional, Union +from http import HTTPStatus +from typing import Dict, Optional, Union +from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse + +import httpx +import trio +from fastapi import Query, security +from fastapi.exceptions import HTTPException +from fastapi.param_functions import Depends +from fastapi.params import Body +from pydantic import BaseModel from lnbits import bolt11, lnurl -from lnbits.decorators import api_check_wallet_key, api_validate_post_request -from lnbits.utils.exchange_rates import currencies, fiat_amount_as_satoshis +from lnbits.core.models import Wallet +from lnbits.decorators import (WalletAdminKeyChecker, WalletInvoiceKeyChecker, + WalletTypeInfo, get_key_type) +from lnbits.helpers import url_for from lnbits.requestvars import g +from lnbits.utils.exchange_rates import currencies, fiat_amount_as_satoshis from .. import core_app, db from ..crud import get_payments, save_balance_check, update_wallet -from ..services import ( - PaymentFailure, - InvoiceFailure, - create_invoice, - pay_invoice, - perform_lnurlauth, -) +from ..services import (InvoiceFailure, PaymentFailure, create_invoice, + pay_invoice, perform_lnurlauth) from ..tasks import api_invoice_listeners -@core_app.get( - "/api/v1/wallet", - # dependencies=[Depends(AuthBearer())] -) -# @api_check_wallet_key("invoice") -async def api_wallet(): +@core_app.get("/api/v1/wallet") +async def api_wallet(wallet: WalletTypeInfo = Depends(get_key_type)): return ( - {"id": g().wallet.id, "name": g().wallet.name, "balance": g().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}") -@api_check_wallet_key("invoice") -async def api_update_wallet(new_name: str): - await update_wallet(g().wallet.id, new_name) +async def api_update_wallet(new_name: str, wallet: WalletTypeInfo = Depends(get_key_type)): + await update_wallet(wallet.wallet.id, new_name) return ( { - "id": g().wallet.id, - "name": g().wallet.name, - "balance": g().wallet.balance_msat, + "id": wallet.wallet.id, + "name": wallet.wallet.name, + "balance": wallet.wallet.balance_msat, }, HTTPStatus.OK, ) @core_app.get("/api/v1/payments") -@api_check_wallet_key("invoice") -async def api_payments(): - return ( - await get_payments(wallet_id=g().wallet.id, pending=True, complete=True), - HTTPStatus.OK, - ) +async def api_payments(wallet: WalletTypeInfo = Depends(get_key_type)): + return await get_payments(wallet_id=wallet.wallet.id, pending=True, complete=True) + + class CreateInvoiceData(BaseModel): amount: int = Query(None, ge=1) @@ -75,9 +65,7 @@ class CreateInvoiceData(BaseModel): extra: Optional[dict] = None webhook: Optional[str] = None -@api_check_wallet_key("invoice") -# async def api_payments_create_invoice(amount: List[str] = Query([type: str = Query(None)])): -async def api_payments_create_invoice(data: CreateInvoiceData): +async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet): if "description_hash" in data: description_hash = unhexlify(data.description_hash) memo = "" @@ -94,7 +82,7 @@ async def api_payments_create_invoice(data: CreateInvoiceData): async with db.connect() as conn: try: payment_hash, payment_request = await create_invoice( - wallet_id=g().wallet.id, + wallet_id=wallet.id, amount=amount, memo=memo, description_hash=description_hash, @@ -151,10 +139,7 @@ async def api_payments_create_invoice(data: CreateInvoiceData): ) -@api_check_wallet_key("admin") -async def api_payments_pay_invoice( - bolt11: str = Query(...), wallet: Optional[List[str]] = Query(None) -): +async def api_payments_pay_invoice(bolt11: str, wallet: Wallet): try: payment_hash = await pay_invoice( wallet_id=wallet.id, @@ -179,11 +164,20 @@ async def api_payments_pay_invoice( ) -@core_app.post("/api/v1/payments") -async def api_payments_create(out: bool = True): - if out is True: - return await api_payments_pay_invoice() - return await api_payments_create_invoice() +@core_app.post("/api/v1/payments", deprecated=True, + description="DEPRECATED. Use /api/v2/TBD and /api/v2/TBD instead") +async def api_payments_create(wallet: WalletTypeInfo = Depends(get_key_type), out: bool = True, + invoiceData: Optional[CreateInvoiceData] = Body(None), + bolt11: Optional[str] = Query(None)): + + if wallet.wallet_type < 0 or wallet.wallet_type > 2: + raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="Key is invalid") + + if out is True and wallet.wallet_type == 0: + if not bolt11: + raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="BOLT11 string is invalid or not given") + return await api_payments_pay_invoice(bolt11, wallet.wallet) # admin key + return await api_payments_create_invoice(invoiceData, wallet.wallet) # invoice key class CreateLNURLData(BaseModel): description_hash: str @@ -192,8 +186,7 @@ class CreateLNURLData(BaseModel): comment: Optional[str] = None description: Optional[str] = None -@core_app.post("/api/v1/payments/lnurl") -@api_check_wallet_key("admin") +@core_app.post("/api/v1/payments/lnurl", dependencies=[Depends(WalletAdminKeyChecker())]) async def api_payments_pay_lnurl(data: CreateLNURLData): domain = urlparse(data.callback).netloc @@ -258,32 +251,9 @@ async def api_payments_pay_lnurl(data: CreateLNURLData): HTTPStatus.CREATED, ) - -@core_app.get("/api/v1/payments/{payment_hash}") -@api_check_wallet_key("invoice") -async def api_payment(payment_hash): - payment = await g().wallet.get_payment(payment_hash) - - if not payment: - return {"message": "Payment does not exist."}, HTTPStatus.NOT_FOUND - elif not payment.pending: - return {"paid": True, "preimage": payment.preimage}, HTTPStatus.OK - - try: - await payment.check_pending() - except Exception: - return {"paid": False}, HTTPStatus.OK - - return ( - {"paid": not payment.pending, "preimage": payment.preimage}, - HTTPStatus.OK, - ) - - @core_app.get("/api/v1/payments/sse") -@api_check_wallet_key("invoice", accept_querystring=True) -async def api_payments_sse(): - this_wallet_id = g().wallet.id +async def api_payments_sse(wallet: WalletTypeInfo = Depends(get_key_type)): + this_wallet_id = wallet.wallet.id send_payment, receive_payment = trio.open_memory_channel(0) @@ -303,9 +273,10 @@ async def api_payments_sse(): await send_event.send(("keepalive", "")) await trio.sleep(25) - current_app.nursery.start_soon(payment_received) - current_app.nursery.start_soon(repeat_keepalive) - + async with trio.open_nursery() as nursery: + nursery.start_soon(payment_received) + nursery.start_soon(repeat_keepalive) + async def send_events(): try: async for typ, data in event_to_send: @@ -332,9 +303,26 @@ async def api_payments_sse(): response.timeout = None return response +@core_app.get("/api/v1/payments/{payment_hash}") +async def api_payment(payment_hash, wallet: WalletTypeInfo = Depends(get_key_type)): + payment = await wallet.wallet.get_payment(payment_hash) -@core_app.get("/api/v1/lnurlscan/{code}") -@api_check_wallet_key("invoice") + if not payment: + return {"message": "Payment does not exist."}, HTTPStatus.NOT_FOUND + elif not payment.pending: + return {"paid": True, "preimage": payment.preimage}, HTTPStatus.OK + + try: + await payment.check_pending() + except Exception: + return {"paid": False}, HTTPStatus.OK + + return ( + {"paid": not payment.pending, "preimage": payment.preimage}, + HTTPStatus.OK, + ) + +@core_app.get("/api/v1/lnurlscan/{code}", dependencies=[Depends(WalletInvoiceKeyChecker())]) async def api_lnurlscan(code: str): try: url = lnurl.decode(code) @@ -443,8 +431,7 @@ async def api_lnurlscan(code: str): return params -@core_app.post("/api/v1/lnurlauth") -@api_check_wallet_key("admin") +@core_app.post("/api/v1/lnurlauth", dependencies=[Depends(WalletAdminKeyChecker())]) async def api_perform_lnurlauth(callback: str): err = await perform_lnurlauth(callback) if err: @@ -452,6 +439,6 @@ async def api_perform_lnurlauth(callback: str): return "", HTTPStatus.OK -@core_app.route("/api/v1/currencies", methods=["GET"]) +@core_app.get("/api/v1/currencies") async def api_list_currencies_available(): return list(currencies.keys()) diff --git a/lnbits/core/views/generic.py b/lnbits/core/views/generic.py index c5d6b3f2..d628e699 100644 --- a/lnbits/core/views/generic.py +++ b/lnbits/core/views/generic.py @@ -2,12 +2,14 @@ from http import HTTPStatus from typing import Optional from fastapi import Request, status +from fastapi.exceptions import HTTPException from fastapi.param_functions import Body from fastapi.params import Depends, Query from fastapi.responses import FileResponse, RedirectResponse from fastapi.routing import APIRouter from pydantic.types import UUID4 from starlette.responses import HTMLResponse +import trio from lnbits.core import db from lnbits.helpers import template_renderer, url_for @@ -40,9 +42,7 @@ async def extensions(request: Request, enable: str, disable: str): extension_to_disable = disable if extension_to_enable and extension_to_disable: - abort( - HTTPStatus.BAD_REQUEST, "You can either `enable` or `disable` an extension." - ) + raise HTTPException(HTTPStatus.BAD_REQUEST, "You can either `enable` or `disable` an extension.") if extension_to_enable: await update_user_extension( @@ -142,7 +142,8 @@ async def lnurl_full_withdraw_callback(request: Request): except: pass - current_app.nursery.start_soon(pay) + async with trio.open_nursery() as n: + n.start_soon(pay) balance_notify = request.args.get("balanceNotify") if balance_notify: @@ -159,7 +160,7 @@ async def deletewallet(request: Request): user_wallet_ids = g().user.wallet_ids if wallet_id not in user_wallet_ids: - abort(HTTPStatus.FORBIDDEN, "Not your wallet.") + raise HTTPException(HTTPStatus.FORBIDDEN, "Not your wallet.") else: await delete_wallet(user_id=g().user.id, wallet_id=wallet_id) user_wallet_ids.remove(wallet_id) @@ -186,16 +187,17 @@ async def lnurlwallet(request: Request): user = await get_user(account.id, conn=conn) wallet = await create_wallet(user_id=user.id, conn=conn) - current_app.nursery.start_soon( - redeem_lnurl_withdraw, - wallet.id, - request.args.get("lightning"), - "LNbits initial funding: voucher redeem.", - {"tag": "lnurlwallet"}, - 5, # wait 5 seconds before sending the invoice to the service + async with trio.open_nursery() as n: + n.start_soon( + redeem_lnurl_withdraw, + wallet.id, + request.args.get("lightning"), + "LNbits initial funding: voucher redeem.", + {"tag": "lnurlwallet"}, + 5, # wait 5 seconds before sending the invoice to the service ) - return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id)) + return RedirectResponse(f"/wallet?usr={user.id}&wal={wallet.id}", status_code=status.HTTP_307_TEMPORARY_REDIRECT) @core_html_routes.get("/manifest/{usr}.webmanifest") diff --git a/lnbits/decorators.py b/lnbits/decorators.py index 880ddc5f..372d3955 100644 --- a/lnbits/decorators.py +++ b/lnbits/decorators.py @@ -1,35 +1,114 @@ -from cerberus import Validator # type: ignore from functools import wraps from http import HTTPStatus + +from fastapi.security import api_key +from lnbits.core.models import Wallet from typing import List, Union from uuid import UUID +from cerberus import Validator # type: ignore +from fastapi.exceptions import HTTPException +from fastapi.openapi.models import APIKey, APIKeyIn +from fastapi.params import Security +from fastapi.security.api_key import APIKeyHeader, APIKeyQuery +from fastapi.security.base import SecurityBase +from starlette.requests import Request + from lnbits.core.crud import get_user, get_wallet_for_key -from lnbits.settings import LNBITS_ALLOWED_USERS from lnbits.requestvars import g +from lnbits.settings import LNBITS_ALLOWED_USERS -def api_check_wallet_key(key_type: str = "invoice", accept_querystring=False): - def wrap(view): - @wraps(view) - async def wrapped_view(**kwargs): - try: - key_value = request.headers.get("X-Api-Key") or request.args["api-key"] - g().wallet = await get_wallet_for_key(key_value, key_type) - except KeyError: - return ( - jsonify({"message": "`X-Api-Key` header missing."}), - HTTPStatus.BAD_REQUEST, - ) - if not g().wallet: - return jsonify({"message": "Wrong keys."}), HTTPStatus.UNAUTHORIZED +class KeyChecker(SecurityBase): + def __init__(self, scheme_name: str = None, auto_error: bool = True, api_key: str = None): + self.scheme_name = scheme_name or self.__class__.__name__ + self.auto_error = auto_error + self._key_type = "invoice" + self._api_key = api_key + if api_key: + self.model: APIKey= APIKey( + **{"in": APIKeyIn.query}, name="X-API-KEY", description="Wallet API Key - QUERY" + ) + else: + self.model: APIKey= APIKey( + **{"in": APIKeyIn.header}, name="X-API-KEY", description="Wallet API Key - HEADER" + ) + self.wallet = None - return await view(**kwargs) + async def __call__(self, request: Request) -> Wallet: + try: + key_value = self._api_key if self._api_key else request.headers.get("X-API-KEY") or request.query_params["api-key"] + # FIXME: Find another way to validate the key. A fetch from DB should be avoided here. + # Also, we should not return the wallet here - thats silly. + # Possibly store it in a Redis DB + self.wallet = await get_wallet_for_key(key_value, self._key_type) + if not self.wallet: + raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="Invalid key or expired key.") - return wrapped_view + except KeyError: + raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, + detail="`X-API-KEY` header missing.") - return wrap +class WalletInvoiceKeyChecker(KeyChecker): + """ + WalletInvoiceKeyChecker will ensure that the provided invoice + wallet key is correct and populate g().wallet with the wallet + for the key in `X-API-key`. + The checker will raise an HTTPException when the key is wrong in some ways. + """ + def __init__(self, scheme_name: str = None, auto_error: bool = True, api_key: str = None): + super().__init__(scheme_name, auto_error, api_key) + self._key_type = "invoice" + +class WalletAdminKeyChecker(KeyChecker): + """ + WalletAdminKeyChecker will ensure that the provided admin + wallet key is correct and populate g().wallet with the wallet + for the key in `X-API-key`. + + The checker will raise an HTTPException when the key is wrong in some ways. + """ + def __init__(self, scheme_name: str = None, auto_error: bool = True, api_key: str = None): + super().__init__(scheme_name, auto_error, api_key) + self._key_type = "admin" + +class WalletTypeInfo(): + wallet_type: int + wallet: Wallet + + def __init__(self, wallet_type: int, wallet: Wallet) -> None: + self.wallet_type = wallet_type + self.wallet = wallet + + +api_key_header = APIKeyHeader(name="X-API-KEY", auto_error=False, description="Admin or Invoice key for wallet API's") +api_key_query = APIKeyQuery(name="api-key", auto_error=False, description="Admin or Invoice key for wallet API's") +async def get_key_type(r: Request, + api_key_header: str = Security(api_key_header), + api_key_query: str = Security(api_key_query)) -> WalletTypeInfo: + # 0: admin + # 1: invoice + # 2: invalid + try: + checker = WalletAdminKeyChecker(api_key=api_key_query) + await checker.__call__(r) + return WalletTypeInfo(0, checker.wallet) + except HTTPException as e: + if e.status_code == HTTPStatus.UNAUTHORIZED: + pass + except: + raise + + try: + checker = WalletInvoiceKeyChecker() + await checker.__call__(r) + return WalletTypeInfo(1, checker.wallet) + except HTTPException as e: + if e.status_code == HTTPStatus.UNAUTHORIZED: + return WalletTypeInfo(2, None) + except: + raise def api_validate_post_request(*, schema: dict): def wrap(view): diff --git a/lnbits/static/js/base.js b/lnbits/static/js/base.js index bb65a824..fec75796 100644 --- a/lnbits/static/js/base.js +++ b/lnbits/static/js/base.js @@ -161,24 +161,22 @@ window.LNbits = { return newWallet }, payment: function (data) { - var obj = _.object( - [ - 'checking_id', - 'pending', - 'amount', - 'fee', - 'memo', - 'time', - 'bolt11', - 'preimage', - 'payment_hash', - 'extra', - 'wallet_id', - 'webhook', - 'webhook_status' - ], - data - ) + obj = { + checking_id:data.id, + pending: data.pending, + amount: data.amount, + fee: data.fee, + memo: data.memo, + time: data.time, + bolt11: data.bolt11, + preimage: data.preimage, + payment_hash: data.payment_hash, + extra: data.extra, + wallet_id: data.wallet_id, + webhook: data.webhook, + webhook_status: data.webhook_status, + } + obj.date = Quasar.utils.date.formatDate( new Date(obj.time * 1000), 'YYYY-MM-DD HH:mm' From d9849d43d24277bbaaa6c6b4c865d3313f423c91 Mon Sep 17 00:00:00 2001 From: Stefan Stammberger Date: Mon, 30 Aug 2021 19:55:02 +0200 Subject: [PATCH 10/10] refactor: replace Trio with asyncio/uvloop --- Pipfile | 10 +- Pipfile.lock | 352 ++++++++++++++++++-------------- lnbits/__main__.py | 32 +-- lnbits/app.py | 23 +-- lnbits/commands.py | 4 +- lnbits/core/__init__.py | 6 - lnbits/core/services.py | 4 +- lnbits/core/tasks.py | 22 +- lnbits/core/views/api.py | 72 +++---- lnbits/core/views/generic.py | 14 +- lnbits/core/views/public_api.py | 18 +- lnbits/db.py | 12 +- lnbits/tasks.py | 29 +-- lnbits/utils/exchange_rates.py | 13 +- lnbits/wallets/clightning.py | 4 +- lnbits/wallets/lnbits.py | 4 +- lnbits/wallets/lndrest.py | 4 +- lnbits/wallets/lnpay.py | 9 +- lnbits/wallets/lntxbot.py | 4 +- lnbits/wallets/opennode.py | 9 +- lnbits/wallets/spark.py | 4 +- 21 files changed, 332 insertions(+), 317 deletions(-) diff --git a/Pipfile b/Pipfile index 24e580c8..2b8a67f7 100644 --- a/Pipfile +++ b/Pipfile @@ -16,22 +16,20 @@ pyscss = "*" shortuuid = "*" typing-extensions = "*" httpx = "*" -trio = "==0.16.0" sqlalchemy-aio = "*" embit = "*" pyqrcode = "*" pypng = "*" sqlalchemy = "==1.3.23" psycopg2-binary = "*" -fastapi = {ref = "anyio", git = "https://github.com/graingert/fastapi"} -trio-asyncio = "*" -hypercorn = {extras = ["trio"], version = "*"} aiofiles = "*" +asyncio = "*" +fastapi = "*" +uvicorn = {extras = ["standard"], version = "*"} +sse-starlette = "*" [dev-packages] black = "==20.8b1" pytest = "*" pytest-cov = "*" mypy = "latest" -pytest-trio = "*" -trio-typing = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 470cb2bb..98d92adb 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "ff9251889371e0cec0eda7eb792e8618ea84f6c7eb85e9b472eacf3d3552c7c4" + "sha256": "e26f678c4b89a86400e0a62396d06e360bfdf1e0f922d474ded200ee1ffde5c4" }, "pipfile-spec": 6, "requires": { @@ -32,13 +32,23 @@ "markers": "python_full_version >= '3.6.2'", "version": "==3.3.0" }, - "async-generator": { + "asgiref": { "hashes": [ - "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b", - "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144" + "sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9", + "sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214" ], - "markers": "python_version >= '3.5'", - "version": "==1.10" + "markers": "python_version >= '3.6'", + "version": "==3.4.1" + }, + "asyncio": { + "hashes": [ + "sha256:83360ff8bc97980e4ff25c964c7bd3923d333d177aa4f7fb736b019f26c7cb41", + "sha256:b62c9157d36187eca799c378e572c969f0da87cd5fc42ca372d92cdb06e7e1de", + "sha256:c46a87b48213d7464f22d9a497b9eef8c1928b68320a2fa94240f969f6fec08c", + "sha256:c4d18b22701821de07bd6aea8b53d21449ec0ec5680645e5317062ea21817d2d" + ], + "index": "pypi", + "version": "==3.4.3" }, "attrs": { "hashes": [ @@ -87,6 +97,14 @@ "markers": "python_version >= '3.5'", "version": "==2.0.4" }, + "click": { + "hashes": [ + "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a", + "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6" + ], + "markers": "python_version >= '3.6'", + "version": "==8.0.1" + }, "ecdsa": { "hashes": [ "sha256:5cf31d5b33743abe0dfc28999036c849a69d548f994b535e527ee3cb7f3ef676", @@ -111,8 +129,12 @@ "version": "==9.3.3" }, "fastapi": { - "git": "https://github.com/graingert/fastapi", - "ref": "ada7c747c05c88d37e012d32e97bcc9579f3f006" + "hashes": [ + "sha256:644bb815bae326575c4b2842469fb83053a4b974b82fa792ff9283d17fbbd99d", + "sha256:94d2820906c36b9b8303796fb7271337ec89c74223229e3cfcf056b5a7d59e23" + ], + "index": "pypi", + "version": "==0.68.1" }, "h11": { "hashes": [ @@ -122,22 +144,6 @@ "markers": "python_version >= '3.6'", "version": "==0.12.0" }, - "h2": { - "hashes": [ - "sha256:ac9e293a1990b339d5d71b19c5fe630e3dd4d768c620d1730d355485323f1b25", - "sha256:bb7ac7099dd67a857ed52c815a6192b6b1f5ba6b516237fc24a085341340593d" - ], - "markers": "python_full_version >= '3.6.1'", - "version": "==4.0.0" - }, - "hpack": { - "hashes": [ - "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c", - "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095" - ], - "markers": "python_full_version >= '3.6.1'", - "version": "==4.0.0" - }, "httpcore": { "hashes": [ "sha256:b0d16f0012ec88d8cc848f5a55f8a03158405f4bca02ee49bc4ca2c1fda49f3e", @@ -146,6 +152,26 @@ "markers": "python_version >= '3.6'", "version": "==0.13.6" }, + "httptools": { + "hashes": [ + "sha256:01b392a166adcc8bc2f526a939a8aabf89fe079243e1543fd0e7dc1b58d737cb", + "sha256:200fc1cdf733a9ff554c0bb97a4047785cfaad9875307d6087001db3eb2b417f", + "sha256:3ab1f390d8867f74b3b5ee2a7ecc9b8d7f53750bd45714bf1cb72a953d7dfa77", + "sha256:78d03dd39b09c99ec917d50189e6743adbfd18c15d5944392d2eabda688bf149", + "sha256:79dbc21f3612a78b28384e989b21872e2e3cf3968532601544696e4ed0007ce5", + "sha256:80ffa04fe8c8dfacf6e4cef8277347d35b0442c581f5814f3b0cf41b65c43c6e", + "sha256:813871f961edea6cb2fe312f2d9b27d12a51ba92545380126f80d0de1917ea15", + "sha256:94505026be56652d7a530ab03d89474dc6021019d6b8682281977163b3471ea0", + "sha256:a23166e5ae2775709cf4f7ad4c2048755ebfb272767d244e1a96d55ac775cca7", + "sha256:a289c27ccae399a70eacf32df9a44059ca2ba4ac444604b00a19a6c1f0809943", + "sha256:a7594f9a010cdf1e16a58b3bf26c9da39bbf663e3b8d46d39176999d71816658", + "sha256:b08d00d889a118f68f37f3c43e359aab24ee29eb2e3fe96d64c6a2ba8b9d6557", + "sha256:cc9be041e428c10f8b6ab358c6b393648f9457094e1dcc11b4906026d43cd380", + "sha256:d5682eeb10cca0606c4a8286a3391d4c3c5a36f0c448e71b8bd05be4e1694bfb", + "sha256:fd3b8905e21431ad306eeaf56644a68fdd621bf8f3097eff54d0f6bdf7262065" + ], + "version": "==0.2.0" + }, "httpx": { "hashes": [ "sha256:92ecd2c00c688b529eda11cedb15161eaf02dee9116712f621c70d9a40b2cdd0", @@ -154,25 +180,6 @@ "index": "pypi", "version": "==0.19.0" }, - "hypercorn": { - "extras": [ - "trio" - ], - "hashes": [ - "sha256:5ba1e719c521080abd698ff5781a2331e34ef50fc1c89a50960538115a896a9a", - "sha256:8007c10f81566920f8ae12c0e26e146f94ca70506da964b5a727ad610aa1d821" - ], - "index": "pypi", - "version": "==0.11.2" - }, - "hyperframe": { - "hashes": [ - "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15", - "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914" - ], - "markers": "python_full_version >= '3.6.1'", - "version": "==6.0.1" - }, "idna": { "hashes": [ "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", @@ -180,6 +187,14 @@ ], "version": "==3.2" }, + "importlib-metadata": { + "hashes": [ + "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15", + "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1" + ], + "markers": "python_version < '3.8'", + "version": "==4.8.1" + }, "lnurl": { "hashes": [ "sha256:579982fd8c4d25bc84c61c74ec45cb7999fa1fa2426f5d5aeb0160ba333b9c92", @@ -204,14 +219,6 @@ "markers": "python_version >= '3.6'", "version": "==1.1.0" }, - "priority": { - "hashes": [ - "sha256:6f8eefce5f3ad59baf2c080a664037bb4725cd0a790d53d59ab4059288faf6aa", - "sha256:c965d54f1b8d0d0b19479db3924c7c36cf672dbf2aec92d43fbdaf4492ba18c0" - ], - "markers": "python_full_version >= '3.6.1'", - "version": "==2.0.0" - }, "psycopg2-binary": { "hashes": [ "sha256:0b7dae87f0b729922e06f85f667de7bf16455d411971b2043bbd9577af9d1975", @@ -305,6 +312,40 @@ "markers": "python_version >= '3.5'", "version": "==0.19.0" }, + "pyyaml": { + "hashes": [ + "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf", + "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696", + "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393", + "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77", + "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922", + "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5", + "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8", + "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10", + "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc", + "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018", + "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e", + "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253", + "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347", + "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183", + "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541", + "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb", + "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185", + "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc", + "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db", + "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa", + "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46", + "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122", + "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b", + "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63", + "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df", + "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc", + "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247", + "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6", + "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0" + ], + "version": "==5.4.1" + }, "represent": { "hashes": [ "sha256:026c0de2ee8385d1255b9c2426cd4f03fe9177ac94c09979bc601946c8493aa0", @@ -347,13 +388,6 @@ "markers": "python_version >= '3.5'", "version": "==1.2.0" }, - "sortedcontainers": { - "hashes": [ - "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", - "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0" - ], - "version": "==2.4.0" - }, "sqlalchemy": { "hashes": [ "sha256:040bdfc1d76a9074717a3f43455685f781c581f94472b010cd6c4754754e1862", @@ -406,46 +440,114 @@ "index": "pypi", "version": "==0.16.0" }, - "toml": { + "sse-starlette": { "hashes": [ - "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", - "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" - ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.10.2" - }, - "trio": { - "hashes": [ - "sha256:451ddb27b4e5215e00646fcbb8028d341fccf284e053dc376506a14bb133dbcf", - "sha256:df067dd0560c321af39d412cd81fc3a7d13f55af9150527daab980683e9fcf3c" + "sha256:1c0cc62cc7d021a386dc06a16a9ddc3e2861d19da6bc2e654e65cc111e820456" ], "index": "pypi", - "version": "==0.16.0" + "version": "==0.6.2" }, - "trio-asyncio": { + "starlette": { "hashes": [ - "sha256:824be23b0c678c0df942816cdb57b92a8b94f264fffa89f04626b0ba2d009768", - "sha256:9bf678f83204ba33c395783681c69af563a84145fad2110a152a81a4a18ae7e4" + "sha256:3c8e48e52736b3161e34c9f0e8153b4f32ec5d8995a3ee1d59410d92f75162ed", + "sha256:7d49f4a27f8742262ef1470608c59ddbc66baf37c148e938c7038e6bc7a998aa" ], - "index": "pypi", - "version": "==0.12.0" + "markers": "python_version >= '3.6'", + "version": "==0.14.2" }, "typing-extensions": { "hashes": [ - "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", - "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", - "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" + "sha256:045dd532231acfa03628df5e0c66dba64e2cc8fc8b844538d4ad6d5dd6cb82dc", + "sha256:83af6730a045fda60f46510f7f1f094776d90321caa4d97d20ef38871bef4bd3", + "sha256:8bbffbd37fbeb9747a0241fdfde5ae99d4531ad1d1a41ccaea62100e15a5814c" ], "index": "pypi", - "version": "==3.10.0.0" + "version": "==3.10.0.1" }, - "wsproto": { - "hashes": [ - "sha256:868776f8456997ad0d9720f7322b746bbe9193751b5b290b7f924659377c8c38", - "sha256:d8345d1808dd599b5ffb352c25a367adb6157e664e140dbecba3f9bc007edb9f" + "uvicorn": { + "extras": [ + "standard" ], - "markers": "python_full_version >= '3.6.1'", - "version": "==1.0.0" + "hashes": [ + "sha256:17f898c64c71a2640514d4089da2689e5db1ce5d4086c2d53699bf99513421c1", + "sha256:d9a3c0dd1ca86728d3e235182683b4cf94cd53a867c288eaeca80ee781b2caff" + ], + "index": "pypi", + "version": "==0.15.0" + }, + "uvloop": { + "hashes": [ + "sha256:04ff57aa137230d8cc968f03481176041ae789308b4d5079118331ab01112450", + "sha256:089b4834fd299d82d83a25e3335372f12117a7d38525217c2258e9b9f4578897", + "sha256:1e5f2e2ff51aefe6c19ee98af12b4ae61f5be456cd24396953244a30880ad861", + "sha256:30ba9dcbd0965f5c812b7c2112a1ddf60cf904c1c160f398e7eed3a6b82dcd9c", + "sha256:3a19828c4f15687675ea912cc28bbcb48e9bb907c801873bd1519b96b04fb805", + "sha256:6224f1401025b748ffecb7a6e2652b17768f30b1a6a3f7b44660e5b5b690b12d", + "sha256:647e481940379eebd314c00440314c81ea547aa636056f554d491e40503c8464", + "sha256:6ccd57ae8db17d677e9e06192e9c9ec4bd2066b77790f9aa7dede2cc4008ee8f", + "sha256:772206116b9b57cd625c8a88f2413df2fcfd0b496eb188b82a43bed7af2c2ec9", + "sha256:8e0d26fa5875d43ddbb0d9d79a447d2ace4180d9e3239788208527c4784f7cab", + "sha256:98d117332cc9e5ea8dfdc2b28b0a23f60370d02e1395f88f40d1effd2cb86c4f", + "sha256:b572256409f194521a9895aef274cea88731d14732343da3ecdb175228881638", + "sha256:bd53f7f5db562f37cd64a3af5012df8cac2c464c97e732ed556800129505bd64", + "sha256:bd8f42ea1ea8f4e84d265769089964ddda95eb2bb38b5cbe26712b0616c3edee", + "sha256:e814ac2c6f9daf4c36eb8e85266859f42174a4ff0d71b99405ed559257750382", + "sha256:f74bc20c7b67d1c27c72601c78cf95be99d5c2cdd4514502b4f3eb0933ff1228" + ], + "version": "==0.16.0" + }, + "watchgod": { + "hashes": [ + "sha256:48140d62b0ebe9dd9cf8381337f06351e1f2e70b2203fa9c6eff4e572ca84f29", + "sha256:d6c1ea21df37847ac0537ca0d6c2f4cdf513562e95f77bb93abbcf05573407b7" + ], + "version": "==0.7" + }, + "websockets": { + "hashes": [ + "sha256:0dd4eb8e0bbf365d6f652711ce21b8fd2b596f873d32aabb0fbb53ec604418cc", + "sha256:1d0971cc7251aeff955aa742ec541ee8aaea4bb2ebf0245748fbec62f744a37e", + "sha256:1d6b4fddb12ab9adf87b843cd4316c4bd602db8d5efd2fb83147f0458fe85135", + "sha256:230a3506df6b5f446fed2398e58dcaafdff12d67fe1397dff196411a9e820d02", + "sha256:276d2339ebf0df4f45df453923ebd2270b87900eda5dfd4a6b0cfa15f82111c3", + "sha256:2cf04601633a4ec176b9cc3d3e73789c037641001dbfaf7c411f89cd3e04fcaf", + "sha256:3ddff38894c7857c476feb3538dd847514379d6dc844961dc99f04b0384b1b1b", + "sha256:48c222feb3ced18f3dc61168ca18952a22fb88e5eb8902d2bf1b50faefdc34a2", + "sha256:51d04df04ed9d08077d10ccbe21e6805791b78eac49d16d30a1f1fe2e44ba0af", + "sha256:597c28f3aa7a09e8c070a86b03107094ee5cdafcc0d55f2f2eac92faac8dc67d", + "sha256:5c8f0d82ea2468282e08b0cf5307f3ad022290ed50c45d5cb7767957ca782880", + "sha256:7189e51955f9268b2bdd6cc537e0faa06f8fffda7fb386e5922c6391de51b077", + "sha256:7df3596838b2a0c07c6f6d67752c53859a54993d4f062689fdf547cb56d0f84f", + "sha256:826ccf85d4514609219725ba4a7abd569228c2c9f1968e8be05be366f68291ec", + "sha256:836d14eb53b500fd92bd5db2fc5894f7c72b634f9c2a28f546f75967503d8e25", + "sha256:85db8090ba94e22d964498a47fdd933b8875a1add6ebc514c7ac8703eb97bbf0", + "sha256:85e701a6c316b7067f1e8675c638036a796fe5116783a4c932e7eb8e305a3ffe", + "sha256:900589e19200be76dd7cbaa95e9771605b5ce3f62512d039fb3bc5da9014912a", + "sha256:9147868bb0cc01e6846606cd65cbf9c58598f187b96d14dd1ca17338b08793bb", + "sha256:9e7fdc775fe7403dbd8bc883ba59576a6232eac96dacb56512daacf7af5d618d", + "sha256:ab5ee15d3462198c794c49ccd31773d8a2b8c17d622aa184f669d2b98c2f0857", + "sha256:ad893d889bc700a5835e0a95a3e4f2c39e91577ab232a3dc03c262a0f8fc4b5c", + "sha256:b2e71c4670ebe1067fa8632f0d081e47254ee2d3d409de54168b43b0ba9147e0", + "sha256:b43b13e5622c5a53ab12f3272e6f42f1ce37cd5b6684b2676cb365403295cd40", + "sha256:b4ad84b156cf50529b8ac5cc1638c2cf8680490e3fccb6121316c8c02620a2e4", + "sha256:be5fd35e99970518547edc906efab29afd392319f020c3c58b0e1a158e16ed20", + "sha256:caa68c95bc1776d3521f81eeb4d5b9438be92514ec2a79fececda814099c8314", + "sha256:d144b350045c53c8ff09aa1cfa955012dd32f00c7e0862c199edcabb1a8b32da", + "sha256:d2c2d9b24d3c65b5a02cac12cbb4e4194e590314519ed49db2f67ef561c3cf58", + "sha256:e9e5fd6dbdf95d99bc03732ded1fc8ef22ebbc05999ac7e0c7bf57fe6e4e5ae2", + "sha256:ebf459a1c069f9866d8569439c06193c586e72c9330db1390af7c6a0a32c4afd", + "sha256:f31722f1c033c198aa4a39a01905951c00bd1c74f922e8afc1b1c62adbcdd56a", + "sha256:f68c352a68e5fdf1e97288d5cec9296664c590c25932a8476224124aaf90dbcd" + ], + "version": "==9.1" + }, + "zipp": { + "hashes": [ + "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3", + "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4" + ], + "markers": "python_version >= '3.6'", + "version": "==3.5.0" } }, "develop": { @@ -456,14 +558,6 @@ ], "version": "==1.4.4" }, - "async-generator": { - "hashes": [ - "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b", - "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144" - ], - "markers": "python_version >= '3.5'", - "version": "==1.10" - }, "attrs": { "hashes": [ "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", @@ -545,20 +639,13 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "version": "==5.5" }, - "idna": { - "hashes": [ - "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", - "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" - ], - "version": "==3.2" - }, "importlib-metadata": { "hashes": [ - "sha256:9e04bf59076a15a9b6dd9c27806e8fcdf15280ba529c6a8cc3f4d5b4875bdd61", - "sha256:c4eb3dec5f697682e383a39701a7de11cd5c02daf8dd93534b69e3e6473f6b1b" + "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15", + "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1" ], "markers": "python_version < '3.8'", - "version": "==4.7.1" + "version": "==4.8.1" }, "iniconfig": { "hashes": [ @@ -603,14 +690,6 @@ ], "version": "==0.4.3" }, - "outcome": { - "hashes": [ - "sha256:c7dd9375cfd3c12db9801d080a3b63d4b0a261aa996c4c13152380587288d958", - "sha256:e862f01d4e626e63e8f92c38d1f8d5546d3f9cce989263c521b2e7990d186967" - ], - "markers": "python_version >= '3.6'", - "version": "==1.1.0" - }, "packaging": { "hashes": [ "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7", @@ -666,15 +745,9 @@ "index": "pypi", "version": "==2.12.1" }, - "pytest-trio": { - "hashes": [ - "sha256:c01b741819aec2c419555f28944e132d3c711dae1e673d63260809bf92c30c31" - ], - "index": "pypi", - "version": "==0.7.0" - }, "regex": { "hashes": [ + "sha256:04f6b9749e335bb0d2f68c707f23bb1773c3fb6ecd10edf0f04df12a8920d468", "sha256:08d74bfaa4c7731b8dac0a992c63673a2782758f7cfad34cf9c1b9184f911354", "sha256:0fc1f8f06977c2d4f5e3d3f0d4a08089be783973fc6b6e278bde01f0544ff308", "sha256:121f4b3185feaade3f85f70294aef3f777199e9b5c0c0245c774ae884b110a2d", @@ -694,12 +767,15 @@ "sha256:8e44769068d33e0ea6ccdf4b84d80c5afffe5207aa4d1881a629cf0ef3ec398f", "sha256:999ad08220467b6ad4bd3dd34e65329dd5d0df9b31e47106105e407954965256", "sha256:9b006628fe43aa69259ec04ca258d88ed19b64791693df59c422b607b6ece8bb", + "sha256:9d05ad5367c90814099000442b2125535e9d77581855b9bee8780f1b41f2b1a2", "sha256:a577a21de2ef8059b58f79ff76a4da81c45a75fe0bfb09bc8b7bb4293fa18983", "sha256:a617593aeacc7a691cc4af4a4410031654f2909053bd8c8e7db837f179a630eb", "sha256:abb48494d88e8a82601af905143e0de838c776c1241d92021e9256d5515b3645", "sha256:ac88856a8cbccfc14f1b2d0b829af354cc1743cb375e7f04251ae73b2af6adf8", + "sha256:b4c220a1fe0d2c622493b0a1fd48f8f991998fb447d3cd368033a4b86cf1127a", "sha256:b844fb09bd9936ed158ff9df0ab601e2045b316b17aa8b931857365ea8586906", "sha256:bdc178caebd0f338d57ae445ef8e9b737ddf8fbc3ea187603f65aec5b041248f", + "sha256:c206587c83e795d417ed3adc8453a791f6d36b67c81416676cad053b4104152c", "sha256:c61dcc1cf9fd165127a2853e2c31eb4fb961a4f26b394ac9fe5669c7a6592892", "sha256:c7cb4c512d2d3b0870e00fbbac2f291d4b4bf2634d59a31176a87afe2777c6f0", "sha256:d4a332404baa6665b54e5d283b4262f41f2103c255897084ec8f5487ce7b9e8e", @@ -707,6 +783,7 @@ "sha256:e1e8406b895aba6caa63d9fd1b6b1700d7e4825f78ccb1e5260551d168db38ed", "sha256:e8690ed94481f219a7a967c118abaf71ccc440f69acd583cab721b90eeedb77c", "sha256:ed283ab3a01d8b53de3a05bfdf4473ae24e43caee7dcb5584e86f3f3e5ab4374", + "sha256:ed4b50355b066796dacdd1cf538f2ce57275d001838f9b132fab80b75e8c84dd", "sha256:ee329d0387b5b41a5dddbb6243a21cb7896587a651bebb957e2d2bb8b63c0791", "sha256:f3bf1bc02bc421047bfec3343729c4bbbea42605bcfd6d6bfe2c07ade8b12d2a", "sha256:f585cbbeecb35f35609edccb95efd95a3e35824cd7752b586503f7e6087303f1", @@ -714,21 +791,6 @@ ], "version": "==2021.8.28" }, - "sniffio": { - "hashes": [ - "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663", - "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de" - ], - "markers": "python_version >= '3.5'", - "version": "==1.2.0" - }, - "sortedcontainers": { - "hashes": [ - "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", - "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0" - ], - "version": "==2.4.0" - }, "toml": { "hashes": [ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", @@ -737,22 +799,6 @@ "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.10.2" }, - "trio": { - "hashes": [ - "sha256:451ddb27b4e5215e00646fcbb8028d341fccf284e053dc376506a14bb133dbcf", - "sha256:df067dd0560c321af39d412cd81fc3a7d13f55af9150527daab980683e9fcf3c" - ], - "index": "pypi", - "version": "==0.16.0" - }, - "trio-typing": { - "hashes": [ - "sha256:3eae317514ca18af158bd14ec55ccf20e8b1461efc3a431b87c337a9ca97180b", - "sha256:c3717f097eab29f8deb58a6976da366bd98adb81d90f38002b564932839eaa84" - ], - "index": "pypi", - "version": "==0.5.1" - }, "typed-ast": { "hashes": [ "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace", @@ -791,12 +837,12 @@ }, "typing-extensions": { "hashes": [ - "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", - "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", - "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" + "sha256:045dd532231acfa03628df5e0c66dba64e2cc8fc8b844538d4ad6d5dd6cb82dc", + "sha256:83af6730a045fda60f46510f7f1f094776d90321caa4d97d20ef38871bef4bd3", + "sha256:8bbffbd37fbeb9747a0241fdfde5ae99d4531ad1d1a41ccaea62100e15a5814c" ], "index": "pypi", - "version": "==3.10.0.0" + "version": "==3.10.0.1" }, "zipp": { "hashes": [ diff --git a/lnbits/__main__.py b/lnbits/__main__.py index bd564787..186b2901 100644 --- a/lnbits/__main__.py +++ b/lnbits/__main__.py @@ -1,28 +1,21 @@ -from hypercorn.trio import serve -import trio -import trio_asyncio -from hypercorn.config import Config +import asyncio -from .commands import migrate_databases, transpile_scss, bundle_vendored +import uvloop +from starlette.requests import Request -trio.run(migrate_databases) +from .commands import bundle_vendored, migrate_databases, transpile_scss +from .settings import (DEBUG, LNBITS_COMMIT, LNBITS_DATA_FOLDER, + LNBITS_SITE_TITLE, PORT, SERVICE_FEE, WALLET) + +uvloop.install() + +asyncio.create_task(migrate_databases()) transpile_scss() bundle_vendored() from .app import create_app -app = trio.run(create_app) - -from .settings import ( - LNBITS_SITE_TITLE, - SERVICE_FEE, - DEBUG, - LNBITS_DATA_FOLDER, - WALLET, - LNBITS_COMMIT, - HOST, - PORT -) +app = create_app() print( f"""Starting LNbits with @@ -35,6 +28,3 @@ print( """ ) -config = Config() -config.bind = [f"{HOST}:{PORT}"] -trio_asyncio.run(serve, app, config) diff --git a/lnbits/app.py b/lnbits/app.py index 177785bf..7e53c0e1 100644 --- a/lnbits/app.py +++ b/lnbits/app.py @@ -1,9 +1,10 @@ +import asyncio import importlib +from lnbits.core.tasks import register_task_listeners import sys import traceback import warnings -import trio from fastapi import FastAPI, Request from fastapi.exceptions import RequestValidationError from fastapi.middleware.cors import CORSMiddleware @@ -19,11 +20,11 @@ from .helpers import (get_css_vendored, get_js_vendored, get_valid_extensions, template_renderer, url_for_vendored) from .requestvars import g from .settings import WALLET -from .tasks import (check_pending_payments, internal_invoice_listener, +from .tasks import (catch_everything_and_restart, check_pending_payments, internal_invoice_listener, invoice_listener, run_deferred_async, webhook_handler) -async def create_app(config_object="lnbits.settings") -> FastAPI: +def create_app(config_object="lnbits.settings") -> FastAPI: """Create application factory. :param config_object: The configuration object to use. """ @@ -128,16 +129,12 @@ def register_async_tasks(app): @app.on_event("startup") async def listeners(): - run_deferred_async() - trio.open_process(check_pending_payments) - trio.open_process(invoice_listener) - trio.open_process(internal_invoice_listener) - - async with trio.open_nursery() as n: - pass - # n.start_soon(catch_everything_and_restart, check_pending_payments) - # n.start_soon(catch_everything_and_restart, invoice_listener) - # n.start_soon(catch_everything_and_restart, internal_invoice_listener) + loop = asyncio.get_event_loop() + loop.create_task(catch_everything_and_restart(check_pending_payments)) + loop.create_task(catch_everything_and_restart(invoice_listener)) + loop.create_task(catch_everything_and_restart(internal_invoice_listener)) + await register_task_listeners() + await run_deferred_async() @app.on_event("shutdown") async def stop_listeners(): diff --git a/lnbits/commands.py b/lnbits/commands.py index 021d26dc..0a48e05f 100644 --- a/lnbits/commands.py +++ b/lnbits/commands.py @@ -1,4 +1,4 @@ -import trio +import asyncio import warnings import click import importlib @@ -18,7 +18,7 @@ from .settings import LNBITS_PATH @click.command("migrate") def db_migrate(): - trio.run(migrate_databases) + asyncio.create_task(migrate_databases()) @click.command("assets") diff --git a/lnbits/core/__init__.py b/lnbits/core/__init__.py index d988d573..e632fba8 100644 --- a/lnbits/core/__init__.py +++ b/lnbits/core/__init__.py @@ -6,14 +6,8 @@ db = Database("database") core_app: APIRouter = APIRouter() -from lnbits.tasks import record_async - -from .tasks import register_listeners from .views.api import * # noqa from .views.generic import * # noqa from .views.public_api import * # noqa -@core_app.on_event("startup") -def do_startup(): - record_async(register_listeners) diff --git a/lnbits/core/services.py b/lnbits/core/services.py index 4fa75f9d..ee83b338 100644 --- a/lnbits/core/services.py +++ b/lnbits/core/services.py @@ -1,4 +1,4 @@ -import trio +import asyncio import json import httpx from io import BytesIO @@ -211,7 +211,7 @@ async def redeem_lnurl_withdraw( return None if wait_seconds: - await trio.sleep(wait_seconds) + await asyncio.sleep(wait_seconds) params = { "k1": res["k1"], diff --git a/lnbits/core/tasks.py b/lnbits/core/tasks.py index fa2df964..c75fbac8 100644 --- a/lnbits/core/tasks.py +++ b/lnbits/core/tasks.py @@ -1,4 +1,4 @@ -import trio +import asyncio import httpx from typing import List @@ -8,17 +8,19 @@ from . import db from .crud import get_balance_notify from .models import Payment -api_invoice_listeners: List[trio.MemorySendChannel] = [] +api_invoice_listeners: List[asyncio.Queue] = [] -async def register_listeners(): - invoice_paid_chan_send, invoice_paid_chan_recv = trio.open_memory_channel(5) - register_invoice_listener(invoice_paid_chan_send) - await wait_for_paid_invoices(invoice_paid_chan_recv) +async def register_task_listeners(): + invoice_paid_queue = asyncio.Queue(5) + register_invoice_listener(invoice_paid_queue) + asyncio.create_task(wait_for_paid_invoices(invoice_paid_queue)) -async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel): - async for payment in invoice_paid_chan: +async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue): + while True: + payment = await invoice_paid_queue.get() + # send information to sse channel await dispatch_invoice_listener(payment) @@ -43,8 +45,8 @@ async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel): async def dispatch_invoice_listener(payment: Payment): for send_channel in api_invoice_listeners: try: - send_channel.send_nowait(payment) - except trio.WouldBlock: + send_channel.put_nowait(payment) + except asyncio.QueueFull: print("removing sse listener", send_channel) api_invoice_listeners.remove(send_channel) diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index f277e6cc..12c3e0a7 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -1,3 +1,4 @@ +import asyncio import hashlib import json from binascii import unhexlify @@ -6,15 +7,15 @@ from typing import Dict, Optional, Union from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse import httpx -import trio -from fastapi import Query, security +from fastapi import Query, Request from fastapi.exceptions import HTTPException from fastapi.param_functions import Depends from fastapi.params import Body +from sse_starlette.sse import EventSourceResponse from pydantic import BaseModel from lnbits import bolt11, lnurl -from lnbits.core.models import Wallet +from lnbits.core.models import Payment, Wallet from lnbits.decorators import (WalletAdminKeyChecker, WalletInvoiceKeyChecker, WalletTypeInfo, get_key_type) from lnbits.helpers import url_for @@ -251,57 +252,42 @@ async def api_payments_pay_lnurl(data: CreateLNURLData): HTTPStatus.CREATED, ) -@core_app.get("/api/v1/payments/sse") -async def api_payments_sse(wallet: WalletTypeInfo = Depends(get_key_type)): +async def subscribe(request: Request, wallet: Wallet): this_wallet_id = wallet.wallet.id - send_payment, receive_payment = trio.open_memory_channel(0) + payment_queue = asyncio.Queue(0) - print("adding sse listener", send_payment) - api_invoice_listeners.append(send_payment) + print("adding sse listener", payment_queue) + api_invoice_listeners.append(payment_queue) - send_event, event_to_send = trio.open_memory_channel(0) + send_queue = asyncio.Queue(0) async def payment_received() -> None: - async for payment in receive_payment: - if payment.wallet_id == this_wallet_id: - await send_event.send(("payment-received", payment)) - - async def repeat_keepalive(): - await trio.sleep(1) while True: - await send_event.send(("keepalive", "")) - await trio.sleep(25) + payment: Payment = await payment_queue.get() + if payment.wallet_id == this_wallet_id: + await send_queue.put(("payment-received", payment)) - async with trio.open_nursery() as nursery: - nursery.start_soon(payment_received) - nursery.start_soon(repeat_keepalive) - - async def send_events(): - try: - async for typ, data in event_to_send: - message = [f"event: {typ}".encode("utf-8")] + asyncio.create_task(payment_received()) - if data: - jdata = json.dumps(dict(data._asdict(), pending=False)) - message.append(f"data: {jdata}".encode("utf-8")) + try: + while True: + typ, data = await send_queue.get() + message = [f"event: {typ}".encode("utf-8")] - yield b"\n".join(message) + b"\r\n\r\n" - except trio.Cancelled: - return + if data: + jdata = json.dumps(dict(data.dict(), pending=False)) + message.append(f"data: {jdata}".encode("utf-8")) + + yield dict(data=jdata.encode("utf-8"), event=typ.encode("utf-8")) + except asyncio.CancelledError: + return + + +@core_app.get("/api/v1/payments/sse") +async def api_payments_sse(request: Request, wallet: WalletTypeInfo = Depends(get_key_type)): + return EventSourceResponse(subscribe(request, wallet)) - response = await make_response( - send_events(), - { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - "X-Accel-Buffering": "no", - "Connection": "keep-alive", - "Transfer-Encoding": "chunked", - }, - ) - response.timeout = None - return response @core_app.get("/api/v1/payments/{payment_hash}") async def api_payment(payment_hash, wallet: WalletTypeInfo = Depends(get_key_type)): diff --git a/lnbits/core/views/generic.py b/lnbits/core/views/generic.py index d628e699..2e041802 100644 --- a/lnbits/core/views/generic.py +++ b/lnbits/core/views/generic.py @@ -1,3 +1,5 @@ +import asyncio + from http import HTTPStatus from typing import Optional @@ -9,7 +11,6 @@ from fastapi.responses import FileResponse, RedirectResponse from fastapi.routing import APIRouter from pydantic.types import UUID4 from starlette.responses import HTMLResponse -import trio from lnbits.core import db from lnbits.helpers import template_renderer, url_for @@ -142,8 +143,7 @@ async def lnurl_full_withdraw_callback(request: Request): except: pass - async with trio.open_nursery() as n: - n.start_soon(pay) + asyncio.create_task(pay()) balance_notify = request.args.get("balanceNotify") if balance_notify: @@ -187,14 +187,14 @@ async def lnurlwallet(request: Request): user = await get_user(account.id, conn=conn) wallet = await create_wallet(user_id=user.id, conn=conn) - async with trio.open_nursery() as n: - n.start_soon( - redeem_lnurl_withdraw, + asyncio.create_task( + redeem_lnurl_withdraw( wallet.id, request.args.get("lightning"), "LNbits initial funding: voucher redeem.", {"tag": "lnurlwallet"}, - 5, # wait 5 seconds before sending the invoice to the service + 5 # wait 5 seconds before sending the invoice to the service + ) ) return RedirectResponse(f"/wallet?usr={user.id}&wal={wallet.id}", status_code=status.HTTP_307_TEMPORARY_REDIRECT) diff --git a/lnbits/core/views/public_api.py b/lnbits/core/views/public_api.py index 9cf7b6cf..02758521 100644 --- a/lnbits/core/views/public_api.py +++ b/lnbits/core/views/public_api.py @@ -1,4 +1,4 @@ -import trio +import asyncio import datetime from http import HTTPStatus @@ -26,27 +26,27 @@ async def api_public_payment_longpolling(payment_hash): except: return {"message": "Invalid bolt11 invoice."}, HTTPStatus.BAD_REQUEST - send_payment, receive_payment = trio.open_memory_channel(0) + payment_queue = asyncio.Queue(0) - print("adding standalone invoice listener", payment_hash, send_payment) - api_invoice_listeners.append(send_payment) + print("adding standalone invoice listener", payment_hash, payment_queue) + api_invoice_listeners.append(payment_queue) response = None async def payment_info_receiver(cancel_scope): - async for payment in receive_payment: + async for payment in payment_queue.get(): if payment.payment_hash == payment_hash: nonlocal response response = ({"status": "paid"}, HTTPStatus.OK) cancel_scope.cancel() async def timeouter(cancel_scope): - await trio.sleep(45) + await asyncio.sleep(45) cancel_scope.cancel() - async with trio.open_nursery() as nursery: - nursery.start_soon(payment_info_receiver, nursery.cancel_scope) - nursery.start_soon(timeouter, nursery.cancel_scope) + + asyncio.create_task(payment_info_receiver()) + asyncio.create_task(timeouter()) if response: return response diff --git a/lnbits/db.py b/lnbits/db.py index f5009463..6142952d 100644 --- a/lnbits/db.py +++ b/lnbits/db.py @@ -1,12 +1,12 @@ import os -import trio +import asyncio import time import datetime from typing import Optional from contextlib import asynccontextmanager -from sqlalchemy import create_engine # type: ignore -from sqlalchemy_aio import TRIO_STRATEGY # type: ignore -from sqlalchemy_aio.base import AsyncConnection # type: ignore +from sqlalchemy import create_engine +from sqlalchemy_aio.base import AsyncConnection +from sqlalchemy_aio.strategy import ASYNCIO_STRATEGY # type: ignore from .settings import LNBITS_DATA_FOLDER, LNBITS_DATABASE_URL @@ -132,8 +132,8 @@ class Database(Compat): else: self.schema = None - self.engine = create_engine(database_uri, strategy=TRIO_STRATEGY) - self.lock = trio.StrictFIFOLock() + self.engine = create_engine(database_uri, strategy=ASYNCIO_STRATEGY) + self.lock = asyncio.Lock() @asynccontextmanager async def connect(self): diff --git a/lnbits/tasks.py b/lnbits/tasks.py index b6facd60..ab1ebc46 100644 --- a/lnbits/tasks.py +++ b/lnbits/tasks.py @@ -1,5 +1,5 @@ import time -import trio +import asyncio import traceback from http import HTTPStatus from typing import List, Callable @@ -24,21 +24,21 @@ def record_async(func: Callable) -> Callable: return recorder -def run_deferred_async(): +async def run_deferred_async(): for func in deferred_async: - current_app.nursery.start_soon(catch_everything_and_restart, func) + asyncio.create_task(catch_everything_and_restart(func)) async def catch_everything_and_restart(func): try: await func() - except trio.Cancelled: + except asyncio.CancelledError: raise # because we must pass this up except Exception as exc: print("caught exception in background task:", exc) print(traceback.format_exc()) print("will restart the task in 5 seconds.") - await trio.sleep(5) + await asyncio.sleep(5) await catch_everything_and_restart(func) @@ -46,10 +46,10 @@ async def send_push_promise(a, b) -> None: pass -invoice_listeners: List[trio.MemorySendChannel] = [] +invoice_listeners: List[asyncio.Queue] = [] -def register_invoice_listener(send_chan: trio.MemorySendChannel): +def register_invoice_listener(send_chan: asyncio.Queue): """ A method intended for extensions to call when they want to be notified about new invoice payments incoming. @@ -64,18 +64,19 @@ async def webhook_handler(): return "", HTTPStatus.NO_CONTENT -internal_invoice_paid, internal_invoice_received = trio.open_memory_channel(0) +internal_invoice_queue = asyncio.Queue(0) async def internal_invoice_listener(): - async for checking_id in internal_invoice_received: - current_app.nursery.start_soon(invoice_callback_dispatcher, checking_id) + while True: + checking_id = await internal_invoice_queue.get() + asyncio.create_task(invoice_callback_dispatcher(checking_id)) async def invoice_listener(): async for checking_id in WALLET.paid_invoices_stream(): print("> got a payment notification", checking_id) - current_app.nursery.start_soon(invoice_callback_dispatcher, checking_id) + asyncio.create_task(invoice_callback_dispatcher(checking_id)) async def check_pending_payments(): @@ -99,7 +100,7 @@ async def check_pending_payments(): # that will be handled by the global invoice listeners, hopefully incoming = False - await trio.sleep(60 * 30) # every 30 minutes + await asyncio.sleep(60 * 30) # every 30 minutes async def perform_balance_checks(): @@ -107,7 +108,7 @@ async def perform_balance_checks(): for bc in await get_balance_checks(): redeem_lnurl_withdraw(bc.wallet, bc.url) - await trio.sleep(60 * 60 * 6) # every 6 hours + await asyncio.sleep(60 * 60 * 6) # every 6 hours async def invoice_callback_dispatcher(checking_id: str): @@ -115,4 +116,4 @@ async def invoice_callback_dispatcher(checking_id: str): if payment and payment.is_in: await payment.set_pending(False) for send_chan in invoice_listeners: - await send_chan.send(payment) + await send_chan.put(payment) diff --git a/lnbits/utils/exchange_rates.py b/lnbits/utils/exchange_rates.py index 801994be..14e912fc 100644 --- a/lnbits/utils/exchange_rates.py +++ b/lnbits/utils/exchange_rates.py @@ -1,4 +1,4 @@ -import trio +import asyncio import httpx from typing import Callable, NamedTuple @@ -219,12 +219,12 @@ async def btc_price(currency: str) -> float: "to": currency.lower(), } rates = [] - send_channel, receive_channel = trio.open_memory_channel(0) + send_channel = asyncio.Queue(0) async def controller(nursery): failures = 0 while True: - rate = await receive_channel.receive() + rate = await send_channel.get() if rate: rates.append(rate) else: @@ -248,10 +248,9 @@ async def btc_price(currency: str) -> float: except Exception: await send_channel.send(None) - async with trio.open_nursery() as nursery: - nursery.start_soon(controller, nursery) - for key, provider in exchange_rate_providers.items(): - nursery.start_soon(fetch_price, key, provider) + # asyncio.create_task(controller, nursery) + for key, provider in exchange_rate_providers.items(): + asyncio.create_task(fetch_price(key, provider)) if not rates: return 9999999999 diff --git a/lnbits/wallets/clightning.py b/lnbits/wallets/clightning.py index bd692193..0780776f 100644 --- a/lnbits/wallets/clightning.py +++ b/lnbits/wallets/clightning.py @@ -3,7 +3,7 @@ try: except ImportError: # pragma: nocover LightningRpc = None -import trio +import asyncio import random import json @@ -116,7 +116,7 @@ class CLightningWallet(Wallet): raise KeyError("supplied an invalid checking_id") async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: - stream = await trio.open_unix_socket(self.rpc) + stream = await asyncio.open_unix_socket(self.rpc) i = 0 while True: diff --git a/lnbits/wallets/lnbits.py b/lnbits/wallets/lnbits.py index e6ded79d..b262cb1e 100644 --- a/lnbits/wallets/lnbits.py +++ b/lnbits/wallets/lnbits.py @@ -1,4 +1,4 @@ -import trio +import asyncio import json import httpx from os import getenv @@ -146,4 +146,4 @@ class LNbitsWallet(Wallet): pass print("lost connection to lnbits /payments/sse, retrying in 5 seconds") - await trio.sleep(5) + await asyncio.sleep(5) diff --git a/lnbits/wallets/lndrest.py b/lnbits/wallets/lndrest.py index c0310e77..4b31c726 100644 --- a/lnbits/wallets/lndrest.py +++ b/lnbits/wallets/lndrest.py @@ -1,4 +1,4 @@ -import trio +import asyncio import httpx import json import base64 @@ -183,4 +183,4 @@ class LndRestWallet(Wallet): pass print("lost connection to lnd invoices stream, retrying in 5 seconds") - await trio.sleep(5) + await asyncio.sleep(5) diff --git a/lnbits/wallets/lnpay.py b/lnbits/wallets/lnpay.py index 034d1529..305400df 100644 --- a/lnbits/wallets/lnpay.py +++ b/lnbits/wallets/lnpay.py @@ -1,5 +1,5 @@ import json -import trio +import asyncio import httpx from os import getenv from http import HTTPStatus @@ -116,8 +116,9 @@ class LNPayWallet(Wallet): return PaymentStatus(statuses[r.json()["settled"]]) async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: - self.send, receive = trio.open_memory_channel(0) - async for value in receive: + self.queue = asyncio.Queue(0) + while True: + value = await self.queue.get() yield value async def webhook_listener(self): @@ -142,6 +143,6 @@ class LNPayWallet(Wallet): ) data = r.json() if data["settled"]: - await self.send.send(lntx_id) + await self.queue.put(lntx_id) return "", HTTPStatus.NO_CONTENT diff --git a/lnbits/wallets/lntxbot.py b/lnbits/wallets/lntxbot.py index e5e1cf15..c804542a 100644 --- a/lnbits/wallets/lntxbot.py +++ b/lnbits/wallets/lntxbot.py @@ -1,4 +1,4 @@ -import trio +import asyncio import json import httpx from os import getenv @@ -150,4 +150,4 @@ class LntxbotWallet(Wallet): pass print("lost connection to lntxbot /payments/stream, retrying in 5 seconds") - await trio.sleep(5) + await asyncio.sleep(5) diff --git a/lnbits/wallets/opennode.py b/lnbits/wallets/opennode.py index ba315503..d955cc0b 100644 --- a/lnbits/wallets/opennode.py +++ b/lnbits/wallets/opennode.py @@ -1,5 +1,5 @@ +import asyncio from lnbits.helpers import url_for -import trio import hmac import httpx from http import HTTPStatus @@ -125,8 +125,9 @@ class OpenNodeWallet(Wallet): return PaymentStatus(statuses[r.json()["data"]["status"]]) async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: - self.send, receive = trio.open_memory_channel(0) - async for value in receive: + self.queue = asyncio.Queue(0) + while True: + value = await self.queue.get() yield value async def webhook_listener(self): @@ -141,5 +142,5 @@ class OpenNodeWallet(Wallet): print("invalid webhook, not from opennode") return "", HTTPStatus.NO_CONTENT - await self.send.send(charge_id) + await self.queue.put(charge_id) return "", HTTPStatus.NO_CONTENT diff --git a/lnbits/wallets/spark.py b/lnbits/wallets/spark.py index 770d5777..ca8f6efe 100644 --- a/lnbits/wallets/spark.py +++ b/lnbits/wallets/spark.py @@ -1,4 +1,4 @@ -import trio +import asyncio import json import httpx import random @@ -199,4 +199,4 @@ class SparkWallet(Wallet): pass print("lost connection to spark /stream, retrying in 5 seconds") - await trio.sleep(5) + await asyncio.sleep(5)