diff --git a/Pipfile b/Pipfile index 0ae2523b9..0569db9e4 100644 --- a/Pipfile +++ b/Pipfile @@ -20,6 +20,7 @@ quart-cors = "*" quart-compress = "*" secure = "*" typing-extensions = "*" +httpx = "*" [dev-packages] black = "==20.8b1" diff --git a/lnbits/core/__init__.py b/lnbits/core/__init__.py index e35d66d25..e34c68c5d 100644 --- a/lnbits/core/__init__.py +++ b/lnbits/core/__init__.py @@ -8,4 +8,6 @@ core_app: Blueprint = Blueprint( from .views.api import * # noqa from .views.generic import * # noqa -from .views.lnurl import * # noqa +from .tasks import grab_app_for_later + +core_app.record(grab_app_for_later) diff --git a/lnbits/core/services.py b/lnbits/core/services.py index e16b2f21f..d3665d55b 100644 --- a/lnbits/core/services.py +++ b/lnbits/core/services.py @@ -1,5 +1,7 @@ +import httpx from typing import Optional, Tuple, Dict from quart import g +from lnurl import LnurlWithdrawResponse try: from typing import TypedDict # type: ignore @@ -94,7 +96,7 @@ def pay_invoice( # do the balance check wallet = get_wallet(wallet_id) - assert wallet, "invalid wallet id" + assert wallet if wallet.balance_msat < 0: g.db.rollback() raise PermissionError("Insufficient balance.") @@ -119,6 +121,24 @@ def pay_invoice( return invoice.payment_hash +async def redeem_lnurl_withdraw(wallet_id: str, res: LnurlWithdrawResponse, memo: Optional[str] = None) -> None: + if not memo: + memo = res.default_description + + _, payment_request = create_invoice( + wallet_id=wallet_id, + amount=res.max_sats, + memo=memo, + extra={"tag": "lnurlwallet"}, + ) + + async with httpx.AsyncClient() as client: + await client.get( + res.callback.base, + params={**res.callback.query_params, **{"k1": res.k1, "pr": payment_request}}, + ) + + def check_invoice_status(wallet_id: str, payment_hash: str) -> PaymentStatus: payment = get_wallet_payment(wallet_id, payment_hash) if not payment: diff --git a/lnbits/core/tasks.py b/lnbits/core/tasks.py new file mode 100644 index 000000000..be48d8a63 --- /dev/null +++ b/lnbits/core/tasks.py @@ -0,0 +1,33 @@ +import asyncio +from typing import Optional, Awaitable +from quart import Quart, Request, g +from werkzeug.datastructures import Headers + +from lnbits.db import open_db + +main_app: Optional[Quart] = None + + +def grab_app_for_later(state): + global main_app + main_app = state.app + + +def run_on_pseudo_request(awaitable: Awaitable): + async def run(awaitable): + fk = Request( + "GET", + "http", + "/background/pseudo", + b"", + Headers([("host", "lnbits.background")]), + "", + "1.1", + send_push_promise=lambda x, h: None, + ) + async with main_app.request_context(fk): + g.db = open_db() + await awaitable + + loop = asyncio.get_event_loop() + loop.create_task(run(awaitable)) diff --git a/lnbits/core/views/generic.py b/lnbits/core/views/generic.py index 36720d90a..b189e82ff 100644 --- a/lnbits/core/views/generic.py +++ b/lnbits/core/views/generic.py @@ -1,6 +1,8 @@ -from quart import g, abort, redirect, request, render_template, send_from_directory, url_for -from http import HTTPStatus +import httpx from os import path +from http import HTTPStatus +from quart import g, abort, redirect, request, render_template, send_from_directory, url_for +from lnurl import LnurlResponse, LnurlWithdrawResponse, decode as decode_lnurl # type: ignore from lnbits.core import core_app from lnbits.decorators import check_user_exists, validate_uuids @@ -13,6 +15,8 @@ from ..crud import ( create_wallet, delete_wallet, ) +from ..services import redeem_lnurl_withdraw +from ..tasks import run_on_pseudo_request @core_app.route("/favicon.ico") @@ -73,12 +77,11 @@ async def wallet(): return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id)) - if wallet_id not in user.wallet_ids: + wallet = user.get_wallet(wallet_id) + if not wallet: abort(HTTPStatus.FORBIDDEN, "Not your wallet.") - return await render_template( - "core/wallet.html", user=user, wallet=user.get_wallet(wallet_id), service_fee=service_fee - ) + return await render_template("core/wallet.html", user=user, wallet=wallet, service_fee=service_fee) @core_app.route("/deletewallet") @@ -98,3 +101,28 @@ async def deletewallet(): return redirect(url_for("core.wallet", usr=g.user.id, wal=user_wallet_ids[0])) return redirect(url_for("core.home")) + + +@core_app.route("/lnurlwallet") +async def lnurlwallet(): + async with httpx.AsyncClient() as client: + try: + lnurl = decode_lnurl(request.args.get("lightning")) + r = await client.get(str(lnurl)) + withdraw_res = LnurlResponse.from_dict(r.json()) + + if not withdraw_res.ok: + return f"Could not process lnurl-withdraw: {withdraw_res.error_msg}", HTTPStatus.BAD_REQUEST + + if not isinstance(withdraw_res, LnurlWithdrawResponse): + return f"Expected an lnurl-withdraw code, got {withdraw_res.tag}", HTTPStatus.BAD_REQUEST + except Exception as exc: + return f"Could not process lnurl-withdraw: {exc}", HTTPStatus.INTERNAL_SERVER_ERROR + + account = create_account() + user = get_user(account.id) + wallet = create_wallet(user_id=user.id) + + run_on_pseudo_request(redeem_lnurl_withdraw(wallet.id, withdraw_res, "LNbits initial funding: voucher redeem.")) + + return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id)) diff --git a/lnbits/core/views/lnurl.py b/lnbits/core/views/lnurl.py deleted file mode 100644 index 0d0ac12c1..000000000 --- a/lnbits/core/views/lnurl.py +++ /dev/null @@ -1,66 +0,0 @@ -import requests - -from quart import abort, redirect, request, url_for -from http import HTTPStatus -from time import sleep -from lnurl import LnurlWithdrawResponse, handle as handle_lnurl # type: ignore -from lnurl.exceptions import LnurlException # type: ignore - -from lnbits import bolt11 -from lnbits.core import core_app -from lnbits.settings import WALLET - -from ..crud import create_account, get_user, create_wallet, create_payment - - -@core_app.route("/lnurlwallet") -async def lnurlwallet(): - memo = "LNbits LNURL funding" - - try: - withdraw_res = handle_lnurl(request.args.get("lightning")) - if not withdraw_res.ok: - abort(HTTPStatus.BAD_REQUEST, f"Could not process LNURL-withdraw: {withdraw_res.error_msg}") - if not isinstance(withdraw_res, LnurlWithdrawResponse): - abort(HTTPStatus.BAD_REQUEST, "Not a valid LNURL-withdraw.") - except LnurlException: - abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process LNURL-withdraw.") - - try: - ok, checking_id, payment_request, error_message = WALLET.create_invoice(withdraw_res.max_sats, memo) - except Exception as e: - ok, error_message = False, str(e) - - if not ok: - abort(HTTPStatus.INTERNAL_SERVER_ERROR, error_message) - - r = requests.get( - withdraw_res.callback.base, - params={**withdraw_res.callback.query_params, **{"k1": withdraw_res.k1, "pr": payment_request}}, - ) - - if not r.ok: - abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process LNURL-withdraw.") - - inv = bolt11.decode(payment_request) - - for i in range(10): - invoice_status = WALLET.get_invoice_status(checking_id) - sleep(i) - if not invoice_status.paid: - continue - break - - user = get_user(create_account().id) - wallet = create_wallet(user_id=user.id) - create_payment( - wallet_id=wallet.id, - checking_id=checking_id, - amount=withdraw_res.max_sats * 1000, - memo=memo, - pending=invoice_status.pending, - payment_request=payment_request, - payment_hash=inv.payment_hash, - ) - - return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id)) diff --git a/lnbits/extensions/withdraw/views_api.py b/lnbits/extensions/withdraw/views_api.py index db24d50a1..76b1bc46d 100644 --- a/lnbits/extensions/withdraw/views_api.py +++ b/lnbits/extensions/withdraw/views_api.py @@ -152,8 +152,6 @@ async def api_lnurl_multi_response(unique_hash, id_unique_hash): found = True else: usescsv += "," + x - print(x) - print("usescsv: " + usescsv) if not found: return jsonify({"status": "ERROR", "reason": "LNURL-withdraw not found."}), HTTPStatus.OK