From b7fcf461f1f24026ea84fc84960331672fac4008 Mon Sep 17 00:00:00 2001 From: benarc Date: Sun, 28 Nov 2021 18:11:35 +0000 Subject: [PATCH 01/17] init --- lnbits/extensions/lnurlpayout/README.md | 3 + lnbits/extensions/lnurlpayout/__init__.py | 16 ++ lnbits/extensions/lnurlpayout/config.json | 6 + lnbits/extensions/lnurlpayout/crud.py | 42 +++ lnbits/extensions/lnurlpayout/migrations.py | 14 + lnbits/extensions/lnurlpayout/models.py | 14 + .../templates/lnurlpayout/_api_docs.html | 90 ++++++ .../templates/lnurlpayout/index.html | 262 ++++++++++++++++++ lnbits/extensions/lnurlpayout/views.py | 22 ++ lnbits/extensions/lnurlpayout/views_api.py | 52 ++++ 10 files changed, 521 insertions(+) create mode 100644 lnbits/extensions/lnurlpayout/README.md create mode 100644 lnbits/extensions/lnurlpayout/__init__.py create mode 100644 lnbits/extensions/lnurlpayout/config.json create mode 100644 lnbits/extensions/lnurlpayout/crud.py create mode 100644 lnbits/extensions/lnurlpayout/migrations.py create mode 100644 lnbits/extensions/lnurlpayout/models.py create mode 100644 lnbits/extensions/lnurlpayout/templates/lnurlpayout/_api_docs.html create mode 100644 lnbits/extensions/lnurlpayout/templates/lnurlpayout/index.html create mode 100644 lnbits/extensions/lnurlpayout/views.py create mode 100644 lnbits/extensions/lnurlpayout/views_api.py diff --git a/lnbits/extensions/lnurlpayout/README.md b/lnbits/extensions/lnurlpayout/README.md new file mode 100644 index 000000000..ddf209fe4 --- /dev/null +++ b/lnbits/extensions/lnurlpayout/README.md @@ -0,0 +1,3 @@ +# LNURLPayOut + +## Auto-dump a wallets funds to an LNURLpay diff --git a/lnbits/extensions/lnurlpayout/__init__.py b/lnbits/extensions/lnurlpayout/__init__.py new file mode 100644 index 000000000..e40549dde --- /dev/null +++ b/lnbits/extensions/lnurlpayout/__init__.py @@ -0,0 +1,16 @@ +from fastapi import APIRouter + +from lnbits.db import Database +from lnbits.helpers import template_renderer + +db = Database("ext_lnurlpayout") + +lnurlpayout_ext: APIRouter = APIRouter(prefix="/lnurlpayout", tags=["lnurlpayout"]) + + +def lnurlpayout_renderer(): + return template_renderer(["lnbits/extensions/lnurlpayout/templates"]) + + +from .views_api import * # noqa +from .views import * # noqa diff --git a/lnbits/extensions/lnurlpayout/config.json b/lnbits/extensions/lnurlpayout/config.json new file mode 100644 index 000000000..1e72c0c1e --- /dev/null +++ b/lnbits/extensions/lnurlpayout/config.json @@ -0,0 +1,6 @@ +{ + "name": "LNURLPayout", + "short_description": "Autodump wallet funds to LNURLpay", + "icon": "exit_to_app", + "contributors": ["arcbtc"] +} diff --git a/lnbits/extensions/lnurlpayout/crud.py b/lnbits/extensions/lnurlpayout/crud.py new file mode 100644 index 000000000..ca97c6375 --- /dev/null +++ b/lnbits/extensions/lnurlpayout/crud.py @@ -0,0 +1,42 @@ +from typing import List, Optional, Union + +from lnbits.helpers import urlsafe_short_hash + +from . import db +from .models import lnurlpayout, CreateLnurlPayoutData + + +async def create_lnurlpayout(wallet_id: str, data: CreateLnurlPayoutData) -> lnurlpayout: + lnurlpayout_id = urlsafe_short_hash() + await db.execute( + """ + INSERT INTO lnurlpayout.lnurlpayouts (id, wallet, lnurlpay, threshold) + VALUES (?, ?, ?, ?) + """, + (lnurlpayout_id, wallet_id, data.name, data.currency), + ) + + lnurlpayout = await get_lnurlpayout(lnurlpayout_id) + assert lnurlpayout, "Newly created lnurlpayout couldn't be retrieved" + return lnurlpayout + + +async def get_lnurlpayout(lnurlpayout_id: str) -> Optional[lnurlpayout]: + row = await db.fetchone("SELECT * FROM lnurlpayout.lnurlpayouts WHERE id = ?", (lnurlpayout_id,)) + return lnurlpayout.from_row(row) if row else None + + +async def get_lnurlpayouts(wallet_ids: Union[str, List[str]]) -> List[lnurlpayout]: + if isinstance(wallet_ids, str): + wallet_ids = [wallet_ids] + + q = ",".join(["?"] * len(wallet_ids)) + rows = await db.fetchall( + f"SELECT * FROM lnurlpayout.lnurlpayouts WHERE wallet IN ({q})", (*wallet_ids,) + ) + + return [lnurlpayout.from_row(row) for row in rows] + + +async def delete_lnurlpayout(lnurlpayout_id: str) -> None: + await db.execute("DELETE FROM lnurlpayout.lnurlpayouts WHERE id = ?", (lnurlpayout_id,)) diff --git a/lnbits/extensions/lnurlpayout/migrations.py b/lnbits/extensions/lnurlpayout/migrations.py new file mode 100644 index 000000000..fde01566b --- /dev/null +++ b/lnbits/extensions/lnurlpayout/migrations.py @@ -0,0 +1,14 @@ +async def m001_initial(db): + """ + Initial lnurlpayouts table. + """ + await db.execute( + """ + CREATE TABLE lnurlpayout.lnurlpayouts ( + id TEXT PRIMARY KEY, + wallet TEXT NOT NULL, + lnurlpay TEXT NOT NULL, + threshold INT NOT NULL + ); + """ + ) diff --git a/lnbits/extensions/lnurlpayout/models.py b/lnbits/extensions/lnurlpayout/models.py new file mode 100644 index 000000000..f749ae9d6 --- /dev/null +++ b/lnbits/extensions/lnurlpayout/models.py @@ -0,0 +1,14 @@ +from sqlite3 import Row + +from pydantic import BaseModel + +class CreateLnurlPayoutData(BaseModel): + wallet: str + lnurlpay: str + threshold: int + +class lnurlpayout(BaseModel): + id: str + wallet: str + lnurlpay: str + threshold: str diff --git a/lnbits/extensions/lnurlpayout/templates/lnurlpayout/_api_docs.html b/lnbits/extensions/lnurlpayout/templates/lnurlpayout/_api_docs.html new file mode 100644 index 000000000..1ccc5d0da --- /dev/null +++ b/lnbits/extensions/lnurlpayout/templates/lnurlpayout/_api_docs.html @@ -0,0 +1,90 @@ + + + + + GET + /lnurlpayout/api/v1/lnurlpayouts +
Headers
+ {"X-Api-Key": <invoice_key>}
+
Body (application/json)
+
+ Returns 200 OK (application/json) +
+ [<lnurlpayout_object>, ...] +
Curl example
+ curl -X GET {{ request.base_url }}api/v1/lnurlpayouts -H "X-Api-Key: + <invoice_key>" + +
+
+
+ + + + POST + /lnurlpayout/api/v1/lnurlpayouts +
Headers
+ {"X-Api-Key": <invoice_key>}
+
Body (application/json)
+ {"name": <string>, "currency": <string*ie USD*>} +
+ Returns 201 CREATED (application/json) +
+ {"currency": <string>, "id": <string>, "name": + <string>, "wallet": <string>} +
Curl example
+ curl -X POST {{ request.base_url }}api/v1/lnurlpayouts -d '{"name": + <string>, "currency": <string>}' -H "Content-type: + application/json" -H "X-Api-Key: <admin_key>" + +
+
+
+ + + + + DELETE + /lnurlpayout/api/v1/lnurlpayouts/<lnurlpayout_id> +
Headers
+ {"X-Api-Key": <admin_key>}
+
Returns 204 NO CONTENT
+ +
Curl example
+ curl -X DELETE {{ request.base_url + }}api/v1/lnurlpayouts/<lnurlpayout_id> -H "X-Api-Key: + <admin_key>" + +
+
+
+
diff --git a/lnbits/extensions/lnurlpayout/templates/lnurlpayout/index.html b/lnbits/extensions/lnurlpayout/templates/lnurlpayout/index.html new file mode 100644 index 000000000..977dda060 --- /dev/null +++ b/lnbits/extensions/lnurlpayout/templates/lnurlpayout/index.html @@ -0,0 +1,262 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} +
+
+ + + New LNURLPayout + + + + + +
+
+
LNURLPayout
+
+
+ Export to CSV +
+
+ + {% raw %} + + + + {% endraw %} + +
+
+
+ +
+ + +
+ {{SITE_TITLE}} LNURLPayout extension +
+
+ + + + {% include "lnurlpayout/_api_docs.html" %} + + + +
+
+ + + + + + + +
+ Create LNURLPayout + Cancel +
+
+
+
+
+{% endblock %} {% block scripts %} {{ window_vars(user) }} + +{% endblock %} diff --git a/lnbits/extensions/lnurlpayout/views.py b/lnbits/extensions/lnurlpayout/views.py new file mode 100644 index 000000000..454a33328 --- /dev/null +++ b/lnbits/extensions/lnurlpayout/views.py @@ -0,0 +1,22 @@ +from http import HTTPStatus + +from fastapi import 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 +from lnbits.decorators import check_user_exists + +from . import lnurlpayout_ext, lnurlpayout_renderer +from .crud import get_lnurlpayout + +templates = Jinja2Templates(directory="templates") + + +@lnurlpayout_ext.get("/", response_class=HTMLResponse) +async def index(request: Request, user: User = Depends(check_user_exists)): + return lnurlpayout_renderer().TemplateResponse( + "lnurlpayout/index.html", {"request": request, "user": user.dict()} + ) diff --git a/lnbits/extensions/lnurlpayout/views_api.py b/lnbits/extensions/lnurlpayout/views_api.py new file mode 100644 index 000000000..5d684a1f3 --- /dev/null +++ b/lnbits/extensions/lnurlpayout/views_api.py @@ -0,0 +1,52 @@ +from http import HTTPStatus + +from fastapi import Query +from fastapi.params import Depends +from starlette.exceptions import HTTPException + +from lnbits.core.crud import get_user +from lnbits.core.services import create_invoice +from lnbits.core.views.api import api_payment +from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key + +from . import lnurlpayout_ext +from .crud import create_lnurlpayout, delete_lnurlpayout, get_lnurlpayout, get_lnurlpayouts +from .models import lnurlpayout, CreateLnurlPayoutData + + +@lnurlpayout_ext.get("/api/v1/lnurlpayouts", status_code=HTTPStatus.OK) +async def api_lnurlpayouts( + all_wallets: bool = Query(None), wallet: WalletTypeInfo = Depends(get_key_type) +): + wallet_ids = [wallet.wallet.id] + if all_wallets: + wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids + + return [lnurlpayout.dict() for lnurlpayout in await get_lnurlpayouts(wallet_ids)] + + +@lnurlpayout_ext.post("/api/v1/lnurlpayouts", status_code=HTTPStatus.CREATED) +async def api_lnurlpayout_create( + data: CreateLnurlPayoutData, wallet: WalletTypeInfo = Depends(get_key_type) +): + print("data") + # lnurlpayout = await create_lnurlpayout(wallet_id=wallet.wallet.id, data=data) + return #lnurlpayout.dict() + + +@lnurlpayout_ext.delete("/api/v1/lnurlpayouts/{lnurlpayout_id}") +async def api_lnurlpayout_delete( + lnurlpayout_id: str, wallet: WalletTypeInfo = Depends(require_admin_key) +): + lnurlpayout = await get_lnurlpayout(lnurlpayout_id) + + if not lnurlpayout: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="lnurlpayout does not exist." + ) + + if lnurlpayout.wallet != wallet.wallet.id: + raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your lnurlpayout.") + + await delete_lnurlpayout(lnurlpayout_id) + raise HTTPException(status_code=HTTPStatus.NO_CONTENT) \ No newline at end of file From cacee8d0729f9282b3027307c2969b19ea384560 Mon Sep 17 00:00:00 2001 From: benarc Date: Thu, 2 Dec 2021 22:56:31 +0000 Subject: [PATCH 02/17] Form works, check LNURL is valid url --- lnbits/extensions/lnurlpayout/crud.py | 7 +++---- lnbits/extensions/lnurlpayout/models.py | 1 - lnbits/extensions/lnurlpayout/views_api.py | 10 ++++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lnbits/extensions/lnurlpayout/crud.py b/lnbits/extensions/lnurlpayout/crud.py index ca97c6375..56c6eab74 100644 --- a/lnbits/extensions/lnurlpayout/crud.py +++ b/lnbits/extensions/lnurlpayout/crud.py @@ -13,7 +13,7 @@ async def create_lnurlpayout(wallet_id: str, data: CreateLnurlPayoutData) -> lnu INSERT INTO lnurlpayout.lnurlpayouts (id, wallet, lnurlpay, threshold) VALUES (?, ?, ?, ?) """, - (lnurlpayout_id, wallet_id, data.name, data.currency), + (lnurlpayout_id, wallet_id, data.lnurlpay, data.threshold), ) lnurlpayout = await get_lnurlpayout(lnurlpayout_id) @@ -23,8 +23,7 @@ async def create_lnurlpayout(wallet_id: str, data: CreateLnurlPayoutData) -> lnu async def get_lnurlpayout(lnurlpayout_id: str) -> Optional[lnurlpayout]: row = await db.fetchone("SELECT * FROM lnurlpayout.lnurlpayouts WHERE id = ?", (lnurlpayout_id,)) - return lnurlpayout.from_row(row) if row else None - + return lnurlpayout(**row) if row else None async def get_lnurlpayouts(wallet_ids: Union[str, List[str]]) -> List[lnurlpayout]: if isinstance(wallet_ids, str): @@ -35,7 +34,7 @@ async def get_lnurlpayouts(wallet_ids: Union[str, List[str]]) -> List[lnurlpayou f"SELECT * FROM lnurlpayout.lnurlpayouts WHERE wallet IN ({q})", (*wallet_ids,) ) - return [lnurlpayout.from_row(row) for row in rows] + return [lnurlpayout(**row) if row else None for row in rows] async def delete_lnurlpayout(lnurlpayout_id: str) -> None: diff --git a/lnbits/extensions/lnurlpayout/models.py b/lnbits/extensions/lnurlpayout/models.py index f749ae9d6..246af13a6 100644 --- a/lnbits/extensions/lnurlpayout/models.py +++ b/lnbits/extensions/lnurlpayout/models.py @@ -3,7 +3,6 @@ from sqlite3 import Row from pydantic import BaseModel class CreateLnurlPayoutData(BaseModel): - wallet: str lnurlpay: str threshold: int diff --git a/lnbits/extensions/lnurlpayout/views_api.py b/lnbits/extensions/lnurlpayout/views_api.py index 5d684a1f3..1906cd127 100644 --- a/lnbits/extensions/lnurlpayout/views_api.py +++ b/lnbits/extensions/lnurlpayout/views_api.py @@ -6,7 +6,7 @@ from starlette.exceptions import HTTPException from lnbits.core.crud import get_user from lnbits.core.services import create_invoice -from lnbits.core.views.api import api_payment +from lnbits.core.views.api import api_payment, api_payments_decode from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key from . import lnurlpayout_ext @@ -29,9 +29,11 @@ async def api_lnurlpayouts( async def api_lnurlpayout_create( data: CreateLnurlPayoutData, wallet: WalletTypeInfo = Depends(get_key_type) ): - print("data") - # lnurlpayout = await create_lnurlpayout(wallet_id=wallet.wallet.id, data=data) - return #lnurlpayout.dict() + url = api_payments_decode(data.lnurlpay) + if url[0:4] != "http": + raise PermissionError("Not valid LNURL") + lnurlpayout = await create_lnurlpayout(wallet_id=wallet.wallet.id, data=data) + return lnurlpayout.dict() @lnurlpayout_ext.delete("/api/v1/lnurlpayouts/{lnurlpayout_id}") From fa23be765747d03927e9a113f8a959f324eaa6af Mon Sep 17 00:00:00 2001 From: benarc Date: Fri, 3 Dec 2021 00:17:32 +0000 Subject: [PATCH 03/17] Added check fixed LNURL decode --- lnbits/core/views/api.py | 7 ++++--- .../templates/lnurlpayout/index.html | 14 +------------- lnbits/extensions/lnurlpayout/views_api.py | 17 +++++++++++------ 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index c919821ff..ead178919 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -466,11 +466,12 @@ async def api_lnurlscan(code: str): @core_app.post("/api/v1/payments/decode") async def api_payments_decode(data: str = Query(None)): try: - if g.data["data"][:5] == "LNURL": - url = lnurl.decode(g.data["data"]) + print(data["data"][:5]) + if data["data"][:5] == "LNURL": + url = lnurl.decode(data["data"]) return {"domain": url} else: - invoice = bolt11.decode(g.data["data"]) + invoice = bolt11.decode(data["data"]) return { "payment_hash": invoice.payment_hash, "amount_msat": invoice.amount_msat, diff --git a/lnbits/extensions/lnurlpayout/templates/lnurlpayout/index.html b/lnbits/extensions/lnurlpayout/templates/lnurlpayout/index.html index 977dda060..d8d8de2df 100644 --- a/lnbits/extensions/lnurlpayout/templates/lnurlpayout/index.html +++ b/lnbits/extensions/lnurlpayout/templates/lnurlpayout/index.html @@ -41,18 +41,6 @@