diff --git a/lnbits/extensions/withdraw/__init__.py b/lnbits/extensions/withdraw/__init__.py index 38e8f1b..7afbf23 100644 --- a/lnbits/extensions/withdraw/__init__.py +++ b/lnbits/extensions/withdraw/__init__.py @@ -9,3 +9,4 @@ withdraw_ext: Blueprint = Blueprint("withdraw", __name__, static_folder="static" from .views_api import * # noqa from .views import * # noqa +from .lnurl import * # noqa diff --git a/lnbits/extensions/withdraw/lnurl.py b/lnbits/extensions/withdraw/lnurl.py new file mode 100644 index 0000000..6124550 --- /dev/null +++ b/lnbits/extensions/withdraw/lnurl.py @@ -0,0 +1,103 @@ +import shortuuid # type: ignore +from http import HTTPStatus +from datetime import datetime +from quart import jsonify, request + +from lnbits.core.services import pay_invoice + +from . import withdraw_ext +from .crud import get_withdraw_link_by_hash, update_withdraw_link + + +# FOR LNURLs WHICH ARE NOT UNIQUE + + +@withdraw_ext.route("/api/v1/lnurl/", methods=["GET"]) +async def api_lnurl_response(unique_hash): + link = await get_withdraw_link_by_hash(unique_hash) + + if not link: + return jsonify({"status": "ERROR", "reason": "LNURL-withdraw not found."}), HTTPStatus.OK + + if link.is_unique == 1: + return jsonify({"status": "ERROR", "reason": "LNURL-withdraw not found."}), HTTPStatus.OK + + usescsv = "" + for x in range(1, link.uses - link.used): + usescsv += "," + str(1) + usescsv = usescsv[1:] + link = await update_withdraw_link(link.id, usescsv=usescsv) + + return jsonify(link.lnurl_response.dict()), HTTPStatus.OK + + +# FOR LNURLs WHICH ARE UNIQUE + + +@withdraw_ext.route("/api/v1/lnurl//", methods=["GET"]) +async def api_lnurl_multi_response(unique_hash, id_unique_hash): + link = await get_withdraw_link_by_hash(unique_hash) + + if not link: + return jsonify({"status": "ERROR", "reason": "LNURL-withdraw not found."}), HTTPStatus.OK + + useslist = link.usescsv.split(",") + usescsv = "" + found = False + if link.is_unique == 0: + for x in range(link.uses - link.used): + usescsv += "," + str(1) + else: + for x in useslist: + tohash = link.id + link.unique_hash + str(x) + if id_unique_hash == shortuuid.uuid(name=tohash): + found = True + else: + usescsv += "," + x + if not found: + return jsonify({"status": "ERROR", "reason": "LNURL-withdraw not found."}), HTTPStatus.OK + + usescsv = usescsv[1:] + link = await update_withdraw_link(link.id, usescsv=usescsv) + return jsonify(link.lnurl_response.dict()), HTTPStatus.OK + + +# CALLBACK + + +@withdraw_ext.route("/api/v1/lnurl/cb/", methods=["GET"]) +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) + now = int(datetime.now().timestamp()) + + if not link: + return jsonify({"status": "ERROR", "reason": "LNURL-withdraw not found."}), HTTPStatus.OK + + if link.is_spent: + return jsonify({"status": "ERROR", "reason": "Withdraw is spent."}), HTTPStatus.OK + + if link.k1 != k1: + return jsonify({"status": "ERROR", "reason": "Bad request."}), HTTPStatus.OK + + if now < link.open_time: + return jsonify({"status": "ERROR", "reason": f"Wait {link.open_time - now} seconds."}), HTTPStatus.OK + + try: + await pay_invoice( + wallet_id=link.wallet, + payment_request=payment_request, + max_sat=link.max_withdrawable, + extra={"tag": "withdraw"}, + ) + + changes = {"open_time": link.wait_time + now, "used": link.used + 1} + + await update_withdraw_link(link.id, **changes) + except ValueError as e: + return jsonify({"status": "ERROR", "reason": str(e)}), HTTPStatus.OK + except PermissionError: + return jsonify({"status": "ERROR", "reason": "Withdraw link is empty."}), HTTPStatus.OK + + return jsonify({"status": "OK"}), HTTPStatus.OK diff --git a/lnbits/extensions/withdraw/views_api.py b/lnbits/extensions/withdraw/views_api.py index 8ed9bca..0ec8072 100644 --- a/lnbits/extensions/withdraw/views_api.py +++ b/lnbits/extensions/withdraw/views_api.py @@ -1,18 +1,14 @@ -from datetime import datetime from quart import g, jsonify, request from http import HTTPStatus from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore -import shortuuid # type: ignore from lnbits.core.crud import get_user -from lnbits.core.services import pay_invoice from lnbits.decorators import api_check_wallet_key, api_validate_post_request from . import withdraw_ext from .crud import ( create_withdraw_link, get_withdraw_link, - get_withdraw_link_by_hash, get_withdraw_links, update_withdraw_link, delete_withdraw_link, @@ -28,15 +24,7 @@ async def api_links(): wallet_ids = (await get_user(g.wallet.user)).wallet_ids try: return ( - jsonify( - [ - { - **link._asdict(), - **{"lnurl": link.lnurl}, - } - for link in await get_withdraw_links(wallet_ids) - ] - ), + jsonify([{**link._asdict(), **{"lnurl": link.lnurl},} for link in await get_withdraw_links(wallet_ids)]), HTTPStatus.OK, ) except LnurlInvalidUrl: @@ -79,8 +67,7 @@ async def api_link_create_or_update(link_id=None): jsonify({"message": "`max_withdrawable` needs to be at least `min_withdrawable`."}), HTTPStatus.BAD_REQUEST, ) - if (g.data["max_withdrawable"] * g.data["uses"] * 1000) > g.wallet.balance_msat: - return jsonify({"message": "Insufficient balance."}), HTTPStatus.FORBIDDEN + usescsv = "" for i in range(g.data["uses"]): if g.data["is_unique"]: @@ -116,92 +103,3 @@ async def api_link_delete(link_id): await delete_withdraw_link(link_id) return "", HTTPStatus.NO_CONTENT - - -# FOR LNURLs WHICH ARE NOT UNIQUE - - -@withdraw_ext.route("/api/v1/lnurl/", methods=["GET"]) -async def api_lnurl_response(unique_hash): - link = await get_withdraw_link_by_hash(unique_hash) - - if not link: - return jsonify({"status": "ERROR", "reason": "LNURL-withdraw not found."}), HTTPStatus.OK - - if link.is_unique == 1: - return jsonify({"status": "ERROR", "reason": "LNURL-withdraw not found."}), HTTPStatus.OK - usescsv = "" - for x in range(1, link.uses - link.used): - usescsv += "," + str(1) - usescsv = usescsv[1:] - link = await update_withdraw_link(link.id, used=link.used + 1, usescsv=usescsv) - - return jsonify(link.lnurl_response.dict()), HTTPStatus.OK - - -# FOR LNURLs WHICH ARE UNIQUE - - -@withdraw_ext.route("/api/v1/lnurl//", methods=["GET"]) -async def api_lnurl_multi_response(unique_hash, id_unique_hash): - link = await get_withdraw_link_by_hash(unique_hash) - - if not link: - return jsonify({"status": "ERROR", "reason": "LNURL-withdraw not found."}), HTTPStatus.OK - useslist = link.usescsv.split(",") - usescsv = "" - found = False - if link.is_unique == 0: - for x in range(link.uses - link.used): - usescsv += "," + str(1) - else: - for x in useslist: - tohash = link.id + link.unique_hash + str(x) - if id_unique_hash == shortuuid.uuid(name=tohash): - found = True - else: - usescsv += "," + x - if not found: - return jsonify({"status": "ERROR", "reason": "LNURL-withdraw not found."}), HTTPStatus.OK - - usescsv = usescsv[1:] - link = await update_withdraw_link(link.id, usescsv=usescsv) - return jsonify(link.lnurl_response.dict()), HTTPStatus.OK - - -@withdraw_ext.route("/api/v1/lnurl/cb/", methods=["GET"]) -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) - now = int(datetime.now().timestamp()) - - if not link: - return jsonify({"status": "ERROR", "reason": "LNURL-withdraw not found."}), HTTPStatus.OK - - if link.is_spent: - return jsonify({"status": "ERROR", "reason": "Withdraw is spent."}), HTTPStatus.OK - - if link.k1 != k1: - return jsonify({"status": "ERROR", "reason": "Bad request."}), HTTPStatus.OK - - if now < link.open_time: - return jsonify({"status": "ERROR", "reason": f"Wait {link.open_time - now} seconds."}), HTTPStatus.OK - - try: - await pay_invoice( - wallet_id=link.wallet, - payment_request=payment_request, - max_sat=link.max_withdrawable, - extra={"tag": "withdraw"}, - ) - - changes = {"open_time": link.wait_time + now, "used": link.used + 1} - - await update_withdraw_link(link.id, **changes) - except ValueError as e: - return jsonify({"status": "ERROR", "reason": str(e)}), HTTPStatus.OK - except PermissionError: - return jsonify({"status": "ERROR", "reason": "Withdraw link is empty."}), HTTPStatus.OK - - return jsonify({"status": "OK"}), HTTPStatus.OK