diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py
index c919821ff..7d91038f6 100644
--- a/lnbits/core/views/api.py
+++ b/lnbits/core/views/api.py
@@ -466,11 +466,11 @@ 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"])
+ 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/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..ad14dd370
--- /dev/null
+++ b/lnbits/extensions/lnurlpayout/__init__.py
@@ -0,0 +1,22 @@
+import asyncio
+from fastapi import APIRouter
+
+from lnbits.db import Database
+from lnbits.helpers import template_renderer
+from lnbits.tasks import catch_everything_and_restart
+
+db = Database("ext_lnurlpayout")
+
+lnurlpayout_ext: APIRouter = APIRouter(prefix="/lnurlpayout", tags=["lnurlpayout"])
+
+
+def lnurlpayout_renderer():
+ return template_renderer(["lnbits/extensions/lnurlpayout/templates"])
+
+from .tasks import wait_for_paid_invoices
+from .views import * # noqa
+from .views_api import * # noqa
+
+def lnurlpayout_start():
+ loop = asyncio.get_event_loop()
+ loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
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..4a09a2230
--- /dev/null
+++ b/lnbits/extensions/lnurlpayout/crud.py
@@ -0,0 +1,45 @@
+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, admin_key: str, data: CreateLnurlPayoutData) -> lnurlpayout:
+ lnurlpayout_id = urlsafe_short_hash()
+ await db.execute(
+ """
+ INSERT INTO lnurlpayout.lnurlpayouts (id, title, wallet, admin_key, lnurlpay, threshold)
+ VALUES (?, ?, ?, ?, ?, ?)
+ """,
+ (lnurlpayout_id, data.title, wallet_id, admin_key, data.lnurlpay, data.threshold),
+ )
+
+ 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(**row) if row else None
+
+async def get_lnurlpayout_from_wallet(wallet_id: str) -> Optional[lnurlpayout]:
+ row = await db.fetchone("SELECT * FROM lnurlpayout.lnurlpayouts WHERE wallet = ?", (wallet_id,))
+ return lnurlpayout(**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(**row) if row else None 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..6af047910
--- /dev/null
+++ b/lnbits/extensions/lnurlpayout/migrations.py
@@ -0,0 +1,16 @@
+async def m001_initial(db):
+ """
+ Initial lnurlpayouts table.
+ """
+ await db.execute(
+ """
+ CREATE TABLE lnurlpayout.lnurlpayouts (
+ id TEXT PRIMARY KEY,
+ title TEXT NOT NULL,
+ wallet TEXT NOT NULL,
+ admin_key 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..3d6a14011
--- /dev/null
+++ b/lnbits/extensions/lnurlpayout/models.py
@@ -0,0 +1,16 @@
+from sqlite3 import Row
+
+from pydantic import BaseModel
+
+class CreateLnurlPayoutData(BaseModel):
+ title: str
+ lnurlpay: str
+ threshold: int
+
+class lnurlpayout(BaseModel):
+ id: str
+ title: str
+ wallet: str
+ admin_key: str
+ lnurlpay: str
+ threshold: int
diff --git a/lnbits/extensions/lnurlpayout/tasks.py b/lnbits/extensions/lnurlpayout/tasks.py
new file mode 100644
index 000000000..deb287382
--- /dev/null
+++ b/lnbits/extensions/lnurlpayout/tasks.py
@@ -0,0 +1,70 @@
+import asyncio
+import json
+import httpx
+
+from lnbits.core import db as core_db
+from lnbits.core.models import Payment
+from lnbits.tasks import register_invoice_listener
+from lnbits.core.views.api import api_wallet
+from lnbits.core.crud import get_wallet
+from lnbits.core.views.api import api_payment, api_payments_decode, pay_invoice
+
+from .crud import get_lnurlpayout, get_lnurlpayout_from_wallet
+
+
+async def wait_for_paid_invoices():
+ invoice_queue = asyncio.Queue()
+ register_invoice_listener(invoice_queue)
+
+ while True:
+ payment = await invoice_queue.get()
+ await on_invoice_paid(payment)
+
+
+async def on_invoice_paid(payment: Payment) -> None:
+ try:
+ # Check its got a payout associated with it
+ lnurlpayout_link = await get_lnurlpayout_from_wallet(payment.wallet_id)
+ if lnurlpayout_link:
+
+ # Check the wallet balance is more than the threshold
+
+ wallet = await get_wallet(lnurlpayout_link.wallet)
+ if wallet.balance < lnurlpayout_link.threshold + (lnurlpayout_link.threshold*0.02):
+ return
+
+ # Get the invoice from the LNURL to pay
+ async with httpx.AsyncClient() as client:
+ try:
+ url = await api_payments_decode({"data": lnurlpayout_link.lnurlpay})
+ if str(url["domain"])[0:4] != "http":
+ raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="LNURL broken")
+ try:
+ r = await client.get(
+ str(url["domain"]),
+ timeout=40,
+ )
+ res = r.json()
+ try:
+ r = await client.get(
+ res["callback"] + "?amount=" + str(int((wallet.balance - wallet.balance*0.02) * 1000)),
+ timeout=40,
+ )
+ res = r.json()
+ try:
+ await pay_invoice(
+ wallet_id=payment.wallet_id,
+ payment_request=res["pr"],
+ extra={"tag": "lnurlpayout"},
+ )
+ return
+ except:
+ pass
+ except:
+ return
+ except (httpx.ConnectError, httpx.RequestError):
+ return
+ except Exception:
+ raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Failed to save LNURLPayout")
+ except:
+ return
\ No newline at end of file
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..7febea44c
--- /dev/null
+++ b/lnbits/extensions/lnurlpayout/templates/lnurlpayout/_api_docs.html
@@ -0,0 +1,118 @@
+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 }}lnurlpayout/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 }}lnurlpayout/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
+ }}lnurlpayout/api/v1/lnurlpayouts/<lnurlpayout_id> -H
+ "X-Api-Key: <admin_key>"
+
+ GET
+ /lnurlpayout/api/v1/lnurlpayouts/<lnurlpayout_id>
+ Headers
+ {"X-Api-Key": <invoice_key>}
+ Body (application/json)
+
+ Returns 200 OK (application/json)
+
+ [<lnurlpayout_object>, ...]
+ Curl example
+ curl -X GET {{ request.base_url
+ }}lnurlpayout/api/v1/lnurlpayouts/<lnurlpayout_id> -H
+ "X-Api-Key: <invoice_key>"
+
+