diff --git a/lnbits/extensions/lnurlp/README.md b/lnbits/extensions/lnurlp/README.md
new file mode 100644
index 0000000..34a4bc0
--- /dev/null
+++ b/lnbits/extensions/lnurlp/README.md
@@ -0,0 +1 @@
+# LNURLp
diff --git a/lnbits/extensions/lnurlp/__init__.py b/lnbits/extensions/lnurlp/__init__.py
new file mode 100644
index 0000000..5b41e06
--- /dev/null
+++ b/lnbits/extensions/lnurlp/__init__.py
@@ -0,0 +1,8 @@
+from flask import Blueprint
+
+
+lnurlp_ext: Blueprint = Blueprint("lnurlp", __name__, static_folder="static", template_folder="templates")
+
+
+from .views_api import * # noqa
+from .views import * # noqa
diff --git a/lnbits/extensions/lnurlp/config.json b/lnbits/extensions/lnurlp/config.json
new file mode 100644
index 0000000..294afe7
--- /dev/null
+++ b/lnbits/extensions/lnurlp/config.json
@@ -0,0 +1,10 @@
+{
+ "name": "LNURLp",
+ "short_description": "Make reusable LNURL pay links",
+ "icon": "receipt",
+ "contributors": [
+ "arcbtc",
+ "eillarra",
+ "fiatjaf"
+ ]
+}
diff --git a/lnbits/extensions/lnurlp/crud.py b/lnbits/extensions/lnurlp/crud.py
new file mode 100644
index 0000000..b53ac1b
--- /dev/null
+++ b/lnbits/extensions/lnurlp/crud.py
@@ -0,0 +1,74 @@
+from typing import List, Optional, Union
+
+from lnbits.db import open_ext_db
+
+from .models import PayLink
+
+
+def create_pay_link(*, wallet_id: str, description: str, amount: int) -> PayLink:
+ with open_ext_db("lnurlp") as db:
+ with db.cursor() as c:
+ c.execute(
+ """
+ INSERT INTO pay_links (
+ wallet,
+ description,
+ amount,
+ served_meta,
+ served_pr
+ )
+ VALUES (?, ?, ?, 0, 0)
+ """,
+ (wallet_id, description, amount),
+ )
+ return get_pay_link(c.lastrowid)
+
+
+def get_pay_link(link_id: str) -> Optional[PayLink]:
+ with open_ext_db("lnurlp") as db:
+ row = db.fetchone("SELECT * FROM pay_links WHERE id = ?", (link_id,))
+
+ return PayLink.from_row(row) if row else None
+
+
+def get_pay_link_by_hash(unique_hash: str) -> Optional[PayLink]:
+ with open_ext_db("lnurlp") as db:
+ row = db.fetchone("SELECT * FROM pay_links WHERE unique_hash = ?", (unique_hash,))
+
+ return PayLink.from_row(row) if row else None
+
+
+def get_pay_links(wallet_ids: Union[str, List[str]]) -> List[PayLink]:
+ if isinstance(wallet_ids, str):
+ wallet_ids = [wallet_ids]
+
+ with open_ext_db("lnurlp") as db:
+ q = ",".join(["?"] * len(wallet_ids))
+ rows = db.fetchall(f"SELECT * FROM pay_links WHERE wallet IN ({q})", (*wallet_ids,))
+
+ return [PayLink.from_row(row) for row in rows]
+
+
+def update_pay_link(link_id: str, **kwargs) -> Optional[PayLink]:
+ q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
+
+ with open_ext_db("lnurlp") as db:
+ db.execute(f"UPDATE pay_links SET {q} WHERE id = ?", (*kwargs.values(), link_id))
+ row = db.fetchone("SELECT * FROM pay_links WHERE id = ?", (link_id,))
+
+ return PayLink.from_row(row) if row else None
+
+
+def increment_pay_link(link_id: str, **kwargs) -> Optional[PayLink]:
+ q = ", ".join([f"{field[0]} = {field[0]} + ?" for field in kwargs.items()])
+
+ with open_ext_db("lnurlp") as db:
+ db.execute(f"UPDATE pay_links SET {q} WHERE id = ?", (*kwargs.values(), link_id))
+ row = db.fetchone("SELECT * FROM pay_links WHERE id = ?", (link_id,))
+
+ return PayLink.from_row(row) if row else None
+
+
+def delete_pay_link(link_id: str) -> None:
+ with open_ext_db("lnurlp") as db:
+ db.execute("DELETE FROM pay_links WHERE id = ?", (link_id,))
diff --git a/lnbits/extensions/lnurlp/migrations.py b/lnbits/extensions/lnurlp/migrations.py
new file mode 100644
index 0000000..b1fe152
--- /dev/null
+++ b/lnbits/extensions/lnurlp/migrations.py
@@ -0,0 +1,24 @@
+from lnbits.db import open_ext_db
+
+
+def m001_initial(db):
+ """
+ Initial pay table.
+ """
+ db.execute(
+ """
+ CREATE TABLE IF NOT EXISTS pay_links (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ wallet TEXT NOT NULL,
+ description TEXT NOT NULL,
+ amount INTEGER NOT NULL,
+ served_meta INTEGER NOT NULL,
+ served_pr INTEGER NOT NULL
+ );
+ """
+ )
+
+
+def migrate():
+ with open_ext_db("lnurlp") as db:
+ m001_initial(db)
diff --git a/lnbits/extensions/lnurlp/models.py b/lnbits/extensions/lnurlp/models.py
new file mode 100644
index 0000000..048b02c
--- /dev/null
+++ b/lnbits/extensions/lnurlp/models.py
@@ -0,0 +1,32 @@
+import json
+from flask import url_for
+from lnurl import Lnurl, encode as lnurl_encode
+from lnurl.types import LnurlPayMetadata
+from sqlite3 import Row
+from typing import NamedTuple
+
+from lnbits.settings import FORCE_HTTPS
+
+
+class PayLink(NamedTuple):
+ id: str
+ wallet: str
+ description: str
+ amount: int
+ served_meta: int
+ served_pr: int
+
+ @classmethod
+ def from_row(cls, row: Row) -> "PayLink":
+ data = dict(row)
+ return cls(**data)
+
+ @property
+ def lnurl(self) -> Lnurl:
+ scheme = "https" if FORCE_HTTPS else None
+ url = url_for("lnurlp.api_lnurl_response", link_id=self.id, _external=True, _scheme=scheme)
+ return lnurl_encode(url)
+
+ @property
+ def lnurlpay_metadata(self) -> LnurlPayMetadata:
+ return LnurlPayMetadata(json.dumps([["text/plain", self.description]]))
diff --git a/lnbits/extensions/lnurlp/templates/lnurlp/_api_docs.html b/lnbits/extensions/lnurlp/templates/lnurlp/_api_docs.html
new file mode 100644
index 0000000..bf6176f
--- /dev/null
+++ b/lnbits/extensions/lnurlp/templates/lnurlp/_api_docs.html
@@ -0,0 +1,130 @@
+
+ WARNING: LNURL must be used over https or TOR
+ Exploring LNURL and finding use cases, is really helping inform
+ lightning protocol development, rather than the protocol dictating how
+ lightning-network should be engaged with.
+ GET /pay/api/v1/links
+ Headers
+ {"X-Api-Key": <invoice_key>}
+ Body (application/json)
+
+ Returns 200 OK (application/json)
+
+ [<pay_link_object>, ...]
+ Curl example
+ curl -X GET {{ request.url_root }}pay/api/v1/links -H "X-Api-Key: {{
+ g.user.wallets[0].inkey }}"
+
+ GET
+ /pay/api/v1/links/<pay_id>
+ Headers
+ {"X-Api-Key": <invoice_key>}
+ Body (application/json)
+
+ Returns 201 CREATED (application/json)
+
+ {"lnurl": <string>}
+ Curl example
+ curl -X GET {{ request.url_root }}pay/api/v1/links/<pay_id> -H
+ "X-Api-Key: {{ g.user.wallets[0].inkey }}"
+
+ POST /pay/api/v1/links
+ Headers
+ {"X-Api-Key": <admin_key>}
+ Body (application/json)
+ {"description": <string> "amount": <integer>}
+
+ Returns 201 CREATED (application/json)
+
+ {"lnurl": <string>}
+ Curl example
+ curl -X POST {{ request.url_root }}pay/api/v1/links -d
+ '{"description": <string>, "amount": <integer>}' -H
+ "Content-type: application/json" -H "X-Api-Key: {{
+ g.user.wallets[0].adminkey }}"
+
+ PUT
+ /pay/api/v1/links/<pay_id>
+ Headers
+ {"X-Api-Key": <admin_key>}
+ Body (application/json)
+ {"description": <string>, "amount": <integer>}
+
+ Returns 200 OK (application/json)
+
+ {"lnurl": <string>}
+ Curl example
+ curl -X PUT {{ request.url_root }}pay/api/v1/links/<pay_id> -d
+ '{"description": <string>, "amount": <integer>}' -H
+ "Content-type: application/json" -H "X-Api-Key: {{
+ g.user.wallets[0].adminkey }}"
+
+ DELETE
+ /pay/api/v1/links/<pay_id>
+ Headers
+ {"X-Api-Key": <admin_key>}
+ Returns 204 NO CONTENT
+
+
Curl example
+ curl -X DELETE {{ request.url_root }}pay/api/v1/links/<pay_id>
+ -H "X-Api-Key: {{ g.user.wallets[0].adminkey }}"
+
+
+ LNURL is a range of lightning-network standards that allow us to use
+ lightning-network differently. An LNURL withdraw is the permission for
+ someone to pull a certain amount of funds from a lightning wallet. In
+ this extension time is also added - an amount can be withdraw over a
+ period of time. A typical use case for an LNURL withdraw is a faucet,
+ although it is a very powerful technology, with much further reaching
+ implications. For example, an LNURL withdraw could be minted to pay for
+ a subscription service.
+
+ Use a LNURL compatible bitcoin wallet to claim the sats. +
+
+ ID: {{ qrCodeDialog.data.id }}
+ Amount: {{ qrCodeDialog.data.amount }} sat
+