From c0280838e7136f001a09b1a42ab43b1b6f2e203d Mon Sep 17 00:00:00 2001 From: Tiago vasconcelos Date: Thu, 30 Sep 2021 14:42:04 +0100 Subject: [PATCH] ext: withdraw ported + lnurlp fixes --- lnbits/extensions/lnurlp/lnurl.py | 57 ++++++---------- lnbits/extensions/lnurlp/views.py | 10 ++- lnbits/extensions/withdraw/__init__.py | 20 +++++- lnbits/extensions/withdraw/lnurl.py | 94 +++++++++++++++++--------- lnbits/extensions/withdraw/views.py | 67 +++++++++++------- 5 files changed, 151 insertions(+), 97 deletions(-) diff --git a/lnbits/extensions/lnurlp/lnurl.py b/lnbits/extensions/lnurlp/lnurl.py index ac0605b3c..bb9eb3332 100644 --- a/lnbits/extensions/lnurlp/lnurl.py +++ b/lnbits/extensions/lnurlp/lnurl.py @@ -1,7 +1,7 @@ import hashlib import math from http import HTTPStatus -from quart import jsonify, url_for, request +from fastapi import FastAPI, Request from lnurl import LnurlPayResponse, LnurlPayActionResponse, LnurlErrorResponse # type: ignore from lnbits.core.services import create_invoice @@ -11,18 +11,18 @@ from . import lnurlp_ext from .crud import increment_pay_link -@lnurlp_ext.route("/api/v1/lnurl/", methods=["GET"]) -async def api_lnurl_response(link_id): +@lnurlp_ext.get("/api/v1/lnurl/{link_id}", status_code=HTTPStatus.OK) +async def api_lnurl_response(request: Request, link_id): link = await increment_pay_link(link_id, served_meta=1) if not link: - return ( - jsonify({"status": "ERROR", "reason": "LNURL-pay not found."}), - HTTPStatus.OK, + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Pay link does not exist." ) rate = await get_fiat_rate_satoshis(link.currency) if link.currency else 1 resp = LnurlPayResponse( - callback=url_for("lnurlp.api_lnurl_callback", link_id=link.id, extra=request.args.get('extra'), _external=True), + callback=url_for("lnurlp.api_lnurl_callback", link_id=link.id, extra=request.path_params['extra'], _external=True), min_sendable=math.ceil(link.min * rate) * 1000, max_sendable=round(link.max * rate) * 1000, metadata=link.lnurlpay_metadata, @@ -32,16 +32,16 @@ async def api_lnurl_response(link_id): if link.comment_chars > 0: params["commentAllowed"] = link.comment_chars - return jsonify(params), HTTPStatus.OK + return params -@lnurlp_ext.route("/api/v1/lnurl/cb/", methods=["GET"]) -async def api_lnurl_callback(link_id): +@lnurlp_ext.get("/api/v1/lnurl/cb/{link_id}", status_code=HTTPStatus.OK) +async def api_lnurl_callback(request: Request, link_id): link = await increment_pay_link(link_id, served_pr=1) if not link: - return ( - jsonify({"status": "ERROR", "reason": "LNURL-pay not found."}), - HTTPStatus.OK, + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Pay link does not exist." ) min, max = link.min, link.max @@ -54,36 +54,23 @@ async def api_lnurl_callback(link_id): min = link.min * 1000 max = link.max * 1000 - amount_received = int(request.args.get("amount") or 0) + amount_received = int(request.path_params['amount'] or 0) if amount_received < min: - return ( - jsonify( - LnurlErrorResponse( + return LnurlErrorResponse( reason=f"Amount {amount_received} is smaller than minimum {min}." ).dict() - ), - HTTPStatus.OK, - ) + elif amount_received > max: - return ( - jsonify( - LnurlErrorResponse( + return LnurlErrorResponse( reason=f"Amount {amount_received} is greater than maximum {max}." ).dict() - ), - HTTPStatus.OK, - ) - comment = request.args.get("comment") + + comment = request.path_params["comment"] if len(comment or "") > link.comment_chars: - return ( - jsonify( - LnurlErrorResponse( + return LnurlErrorResponse( reason=f"Got a comment with {len(comment)} characters, but can only accept {link.comment_chars}" ).dict() - ), - HTTPStatus.OK, - ) payment_hash, payment_request = await create_invoice( wallet_id=link.wallet, @@ -92,7 +79,7 @@ async def api_lnurl_callback(link_id): description_hash=hashlib.sha256( link.lnurlpay_metadata.encode("utf-8") ).digest(), - extra={"tag": "lnurlp", "link": link.id, "comment": comment, 'extra': request.args.get('extra')}, + extra={"tag": "lnurlp", "link": link.id, "comment": comment, 'extra': request.path_params['amount']}, ) success_action = link.success_action(payment_hash) @@ -108,4 +95,4 @@ async def api_lnurl_callback(link_id): routes=[], ) - return jsonify(resp.dict()), HTTPStatus.OK + return resp.dict()) diff --git a/lnbits/extensions/lnurlp/views.py b/lnbits/extensions/lnurlp/views.py index 8b653fab0..c49c28074 100644 --- a/lnbits/extensions/lnurlp/views.py +++ b/lnbits/extensions/lnurlp/views.py @@ -8,13 +8,17 @@ from fastapi import FastAPI, Request from fastapi.params import Depends from fastapi.templating import Jinja2Templates +from starlette.exceptions import HTTPException +from starlette.responses import HTMLResponse +from lnbits.core.models import User + templates = Jinja2Templates(directory="templates") @lnurlp_ext.get("/", response_class=HTMLResponse) @validate_uuids(["usr"], required=True) # @check_user_exists() async def index(request: Request, user: User = Depends(check_user_exists)): - return lnurlp_renderer().TemplateResponse("lnurlp/index.html", {"request": request, "user": user}) + return lnurlp_renderer().TemplateResponse("lnurlp/index.html", {"request": request, "user": user.dict()}) @lnurlp_ext.get("/{link_id}", response_class=HTMLResponse) @@ -27,7 +31,7 @@ async def display(request: Request,link_id): ) # abort(HTTPStatus.NOT_FOUND, "Pay link does not exist.") - return await lnurlp_renderer().TemplateResponse("lnurlp/display.html", {"request": request, "link":link}) + return lnurlp_renderer().TemplateResponse("lnurlp/display.html", {"request": request, "link":link}) @lnurlp_ext.get("/print/{link_id}", response_class=HTMLResponse) @@ -40,4 +44,4 @@ async def print_qr(request: Request,link_id): ) # abort(HTTPStatus.NOT_FOUND, "Pay link does not exist.") - return await lnurlp_renderer().TemplateResponse("lnurlp/print_qr.html", {"request": request, "link":link}) + return lnurlp_renderer().TemplateResponse("lnurlp/print_qr.html", {"request": request, "link":link}) diff --git a/lnbits/extensions/withdraw/__init__.py b/lnbits/extensions/withdraw/__init__.py index 69e45e4d3..5a90179c1 100644 --- a/lnbits/extensions/withdraw/__init__.py +++ b/lnbits/extensions/withdraw/__init__.py @@ -1,14 +1,28 @@ -from quart import Blueprint +from fastapi import APIRouter + from lnbits.db import Database db = Database("ext_withdraw") -withdraw_ext: Blueprint = Blueprint( - "withdraw", __name__, static_folder="static", template_folder="templates" +withdraw_ext: APIRouter = APIRouter( + prefix="/withdraw", + static_folder="static" + # "withdraw", __name__, static_folder="static", template_folder="templates" ) +def withdraw_renderer(): + return template_renderer( + [ + "lnbits/extensions/withdraw/templates", + ] + ) + from .views_api import * # noqa from .views import * # noqa from .lnurl import * # noqa + +@withdraw_ext.on_event("startup") +def _do_it(): + register_listeners() diff --git a/lnbits/extensions/withdraw/lnurl.py b/lnbits/extensions/withdraw/lnurl.py index 1c3cde6c5..32f4a1c3f 100644 --- a/lnbits/extensions/withdraw/lnurl.py +++ b/lnbits/extensions/withdraw/lnurl.py @@ -12,41 +12,57 @@ from .crud import get_withdraw_link_by_hash, update_withdraw_link # FOR LNURLs WHICH ARE NOT UNIQUE -@withdraw_ext.get("/api/v1/lnurl/") +@withdraw_ext.get("/api/v1/lnurl/{unique_hash}", status_code=HTTPStatus.OK) async def api_lnurl_response(unique_hash): link = await get_withdraw_link_by_hash(unique_hash) if not link: - return ({"status": "ERROR", "reason": "LNURL-withdraw not found."}, - HTTPStatus.OK, + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Withdraw link does not exist." ) + # return ({"status": "ERROR", "reason": "LNURL-withdraw not found."}, + # HTTPStatus.OK, + # ) if link.is_spent: - return ({"status": "ERROR", "reason": "Withdraw is spent."}, - HTTPStatus.OK, + raise HTTPException( + # WHAT STATUS_CODE TO USE?? + detail="Withdraw is spent." ) + # return ({"status": "ERROR", "reason": "Withdraw is spent."}, + # HTTPStatus.OK, + # ) - return link.lnurl_response.dict(), HTTPStatus.OK + return link.lnurl_response.dict() # FOR LNURLs WHICH ARE UNIQUE -@withdraw_ext.route("/api/v1/lnurl//") +@withdraw_ext.get("/api/v1/lnurl/{unique_hash}/{id_unique_hash}", status_code=HTTPStatus.OK) async def api_lnurl_multi_response(unique_hash, id_unique_hash): link = await get_withdraw_link_by_hash(unique_hash) if not link: - return ( - {"status": "ERROR", "reason": "LNURL-withdraw not found."}, - HTTPStatus.OK, + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="LNURL-withdraw not found." ) + # return ( + # {"status": "ERROR", "reason": "LNURL-withdraw not found."}, + # HTTPStatus.OK, + # ) if link.is_spent: - return ( - {"status": "ERROR", "reason": "Withdraw is spent."}, - HTTPStatus.OK, + raise HTTPException( + # WHAT STATUS_CODE TO USE?? + detail="Withdraw is spent." ) + # return ( + # {"status": "ERROR", "reason": "Withdraw is spent."}, + # HTTPStatus.OK, + # ) useslist = link.usescsv.split(",") found = False @@ -55,44 +71,57 @@ async def api_lnurl_multi_response(unique_hash, id_unique_hash): if id_unique_hash == shortuuid.uuid(name=tohash): found = True if not found: - return ( - {"status": "ERROR", "reason": "LNURL-withdraw not found."}, - HTTPStatus.OK, + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="LNURL-withdraw not found." ) + # return ( + # {"status": "ERROR", "reason": "LNURL-withdraw not found."}, + # HTTPStatus.OK, + # ) - return link.lnurl_response.dict(), HTTPStatus.OK + return link.lnurl_response.dict() # CALLBACK -@withdraw_ext.get("/api/v1/lnurl/cb/") +@withdraw_ext.get("/api/v1/lnurl/cb/{unique_hash}", status_code=HTTPStatus.OK) async def api_lnurl_callback(unique_hash): link = await get_withdraw_link_by_hash(unique_hash) - k1 = request.args.get("k1", type=str) - payment_request = request.args.get("pr", type=str) + k1 = request.path_params['k1'] + payment_request = request.path_params['pr'] now = int(datetime.now().timestamp()) if not link: - return ( - {"status": "ERROR", "reason": "LNURL-withdraw not found."}, - HTTPStatus.OK, + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="LNURL-withdraw not found." ) + # return ( + # {"status": "ERROR", "reason": "LNURL-withdraw not found."}, + # HTTPStatus.OK, + # ) if link.is_spent: - return ( - {"status": "ERROR", "reason": "Withdraw is spent."}, - HTTPStatus.OK, + raise HTTPException( + # WHAT STATUS_CODE TO USE?? + detail="Withdraw is spent." ) + # return ( + # {"status": "ERROR", "reason": "Withdraw is spent."}, + # HTTPStatus.OK, + # ) if link.k1 != k1: - return {"status": "ERROR", "reason": "Bad request."}, HTTPStatus.OK + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail="Bad request." + ) + # return {"status": "ERROR", "reason": "Bad request."}, HTTPStatus.OK if now < link.open_time: - return ( - {"status": "ERROR", "reason": f"Wait {link.open_time - now} seconds."}, - HTTPStatus.OK, - ) + return {"status": "ERROR", "reason": f"Wait {link.open_time - now} seconds."} try: usescsv = "" @@ -122,6 +151,7 @@ async def api_lnurl_callback(unique_hash): max_sat=link.max_withdrawable, extra={"tag": "withdraw"}, ) + # should these be "raise" instead of the "return" ?? except ValueError as e: await update_withdraw_link(link.id, **changesback) return {"status": "ERROR", "reason": str(e)} @@ -132,4 +162,4 @@ async def api_lnurl_callback(unique_hash): await update_withdraw_link(link.id, **changesback) return {"status": "ERROR", "reason": str(e)} - return {"status": "OK"}, HTTPStatus.OK + return {"status": "OK"} diff --git a/lnbits/extensions/withdraw/views.py b/lnbits/extensions/withdraw/views.py index dc1b6e31d..8caf8f287 100644 --- a/lnbits/extensions/withdraw/views.py +++ b/lnbits/extensions/withdraw/views.py @@ -1,39 +1,50 @@ -from quart import g, abort, render_template from http import HTTPStatus import pyqrcode from io import BytesIO from lnbits.decorators import check_user_exists, validate_uuids -from . import withdraw_ext +from . import withdraw_ext, withdraw_renderer from .crud import get_withdraw_link, chunks -from fastapi import FastAPI, Request, Response - +from fastapi import FastAPI, Request +from fastapi.params import Depends from fastapi.templating import Jinja2Templates +from starlette.exceptions import HTTPException +from starlette.responses import HTMLResponse +from lnbits.core.models import User + templates = Jinja2Templates(directory="templates") -@withdraw_ext.get("/", status_code=HTTPStatus.OK) +@withdraw_ext.get("/", response_class=HTMLResponse) @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}) +# @check_user_exists() +async def index(request: Request, user: User = Depends(check_user_exists)): + return withdraw_renderer().TemplateResponse("withdraw/index.html", {"request":request,"user": user.dict()}) -@withdraw_ext.get("/{link_id}", status_code=HTTPStatus.OK) -async def display(request: Request, link_id, response: Response): +@withdraw_ext.get("/{link_id}", response_class=HTMLResponse) +async def display(request: Request, link_id): 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}) + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Withdraw link does not exist." + ) + # response.status_code = HTTPStatus.NOT_FOUND + # return "Withdraw link does not exist." #probably here is where we should return the 404?? + return withdraw_renderer().TemplateResponse("withdraw/display.html", {"request":request,"link":link, "unique":True}) -@withdraw_ext.get("/img/{link_id}", status_code=HTTPStatus.OK) +@withdraw_ext.get("/img/{link_id}", response_class=HTMLResponse) 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." + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Withdraw link does not exist." + ) + # response.status_code = HTTPStatus.NOT_FOUND + # return "Withdraw link does not exist." qr = pyqrcode.create(link.lnurl) stream = BytesIO() qr.svg(stream, scale=3) @@ -49,23 +60,31 @@ async def img(request: Request, link_id, response: Response): ) -@withdraw_ext.get("/print/{link_id}", status_code=HTTPStatus.OK) -async def print_qr(request: Request, link_id, response: Response): +@withdraw_ext.get("/print/{link_id}", response_class=HTMLResponse) +async def print_qr(request: Request, link_id): link = await get_withdraw_link(link_id) if not link: - response.status_code = HTTPStatus.NOT_FOUND - return "Withdraw link does not exist." + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Withdraw link does not exist." + ) + # 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}) + return withdraw_renderer().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) if not linkk: - response.status_code = HTTPStatus.NOT_FOUND - return "Withdraw link does not exist." + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Withdraw link does not exist." + ) + # 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)) linked = list(chunks(page_link, 5)) - return await templates.TemplateResponse("withdraw/print_qr.html", {"request":request,"link":linked, "unique":True}) + return withdraw_renderer().TemplateResponse("withdraw/print_qr.html", {"request":request,"link":linked, "unique":True})