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