diff --git a/README.md b/README.md
index 72fe035..9fcef84 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,13 @@
+
+
+
+
+
+
+
+[](./LICENSE)
+[](https://github.com/lnbits/lnbits)
+
# LNURLp - [LNbits](https://github.com/lnbits/lnbits) extension
For more about LNBits extension check [this tutorial](https://github.com/lnbits/lnbits/wiki/LNbits-Extensions)
@@ -55,3 +65,10 @@ Now you can receive sats to your newly created LN address. You will find this in
[](https://postimg.cc/3WwsXJHP)
+
+## Powered by LNbits
+
+[LNbits](https://lnbits.com) is a free and open-source lightning accounts system.
+
+[](https://shop.lnbits.com/)
+[](https://my.lnbits.com/login)
diff --git a/__init__.py b/__init__.py
index 27ca96e..782315b 100644
--- a/__init__.py
+++ b/__init__.py
@@ -42,6 +42,42 @@ def lnurlp_start():
task = create_permanent_unique_task("lnurlp", wait_for_paid_invoices)
scheduled_tasks.append(task)
+ # Expose lnurlp's CRUD over the LNbits nostr transport so an HTTP-
+ # allergic client (e.g. lamassu-next ATM) can manage PayLinks over
+ # kind-21000 encrypted events. Also wires the link-owner resolver so
+ # `subscribe_payments({tag:"lnurlp", link_id:...})` can verify
+ # ownership of the underlying wallet. No-op if the core transport
+ # module isn't present in the LNbits build.
+ try:
+ from lnbits.core.services.nostr_transport.dispatcher import (
+ AUTH_ACCOUNT,
+ AUTH_WALLET,
+ register_rpc,
+ )
+ from lnbits.core.services.nostr_transport.subscriptions import (
+ register_link_owner_resolver,
+ )
+ except ImportError:
+ return
+
+ from .transport_rpcs import (
+ handle_lnurlp_create,
+ handle_lnurlp_delete,
+ handle_lnurlp_get,
+ handle_lnurlp_list,
+ handle_lnurlp_update,
+ resolve_lnurlp_owner,
+ )
+
+ register_rpc("lnurlp_create", handle_lnurlp_create, AUTH_WALLET)
+ register_rpc("lnurlp_get", handle_lnurlp_get, AUTH_WALLET)
+ register_rpc("lnurlp_list", handle_lnurlp_list, AUTH_ACCOUNT)
+ register_rpc("lnurlp_update", handle_lnurlp_update, AUTH_WALLET)
+ register_rpc("lnurlp_delete", handle_lnurlp_delete, AUTH_WALLET)
+ # lnurlp stamps `extra["link"] = link.id` on settlement
+ # (views_lnurl.py:86), which is the default extras-key, so no override.
+ register_link_owner_resolver("lnurlp", resolve_lnurlp_owner)
+
__all__ = [
"db",
diff --git a/config.json b/config.json
index 41a398f..5742544 100644
--- a/config.json
+++ b/config.json
@@ -1,7 +1,10 @@
{
- "name": "Pay Links",
+ "id": "paylink",
"version": "1.3.0",
- "short_description": "Make reusable LNURL pay links",
+ "name": "Pay Links",
+ "repo": "https://github.com/lnbits/lnurlp",
+ "short_description": "Make static reusable LNURL pay links or lightning addresses",
+ "description": "",
"tile": "/lnurlp/static/image/lnurl-pay.png",
"min_lnbits_version": "1.4.0",
"contributors": [
@@ -51,5 +54,9 @@
],
"description_md": "https://raw.githubusercontent.com/lnbits/lnurlp/main/description.md",
"terms_and_conditions_md": "https://raw.githubusercontent.com/lnbits/lnurlp/main/toc.md",
- "license": "MIT"
+ "license": "MIT",
+ "paid_features": "",
+ "tags": ["Merchant", "Payments"],
+ "donate": "",
+ "hidden": false
}
diff --git a/description.md b/description.md
index 3a81e0d..a4cdd4f 100644
--- a/description.md
+++ b/description.md
@@ -1 +1,10 @@
-Create a static LNURLp or LNaddress people can use to pay.
+Create static LNURL-pay links and Lightning addresses for receiving payments.
+
+Its functions include:
+
+- Generating reusable LNURL-pay QR codes
+- Creating custom Lightning addresses
+- Setting minimum and maximum payment amounts
+- Adding payment descriptions and metadata
+
+A foundational tool for anyone who wants a simple, reusable way to receive Lightning payments with a static QR code or Lightning address.
diff --git a/static/index.js b/static/index.js
index d11f7d7..497d2b6 100644
--- a/static/index.js
+++ b/static/index.js
@@ -150,7 +150,7 @@ window.PageLnurlp = {
username: link.username
}
const domain = link.domain || window.location.host
- this.activeUrl = `https://${domain}/lnurlp//${link.id}`
+ this.activeUrl = `https://${domain}/lnurlp/${link.id}`
this.qrCodeDialog.show = true
},
openUpdateDialog(linkId) {
diff --git a/tasks.py b/tasks.py
index c03e6ce..5c1decd 100644
--- a/tasks.py
+++ b/tasks.py
@@ -144,6 +144,16 @@ async def send_zap(payment: Payment):
async with websockets.connect(relay_url, open_timeout=5) as websocket:
logger.debug(f"Sending zap to {relay_url}")
await websocket.send(event_message)
+ response = await asyncio.wait_for(websocket.recv(), timeout=5)
+ relay_response = json.loads(response)
+ if relay_response[0] != "OK" or not relay_response[2]:
+ logger.debug(
+ f"Relay did not acknowledge zap receipt: {relay_response}"
+ )
+ return
+ logger.debug(f"Zap sent to {relay_url} successfully")
+ except asyncio.TimeoutError:
+ logger.debug(f"Relay did not acknowledge zap receipt: {relay_url}")
except Exception as e:
logger.warning(f"Failed to send zap to {relay_url}: {e}")
diff --git a/transport_rpcs.py b/transport_rpcs.py
new file mode 100644
index 0000000..87f3f59
--- /dev/null
+++ b/transport_rpcs.py
@@ -0,0 +1,151 @@
+"""
+Nostr-transport RPC handlers for the lnurlp (LNURL-pay) extension.
+
+Exposes the same CRUD surface that `views_api.py` exposes via HTTP, but
+encrypted over kind-21000 events through the LNbits nostr transport.
+Mirrors the withdraw extension's `transport_rpcs.py`; both extensions
+hang their handlers off the core dispatcher via their `*_start()` hook.
+
+Auth model (set by the registrations in `__init__.py:lnurlp_start`):
+- *_create / *_get / *_update / *_delete → AUTH_WALLET. The transport
+ resolves the caller's pubkey to a wallet (admin access) before
+ invoking the handler, so we know `auth.wallet.id` and `auth.wallet.user`.
+- *_list → AUTH_ACCOUNT. The caller may list links across all wallets
+ they own, optionally narrowed by `request.wallet_id`.
+
+Ownership: *_get / *_update / *_delete also verify the link's stored
+`wallet` field matches the caller's wallet id — defense in depth, since
+a malicious client could otherwise probe link metadata they don't own.
+
+`resolve_lnurlp_owner` is registered with the core subscription module
+under tag `"lnurlp"` (default link_extra_key `"link"` — that's where
+`views_lnurl.py:86` stamps the link id on settlement). That lets clients
+call `subscribe_payments({tag:"lnurlp", link_id:...})` and stream real-
+time pay events without polling, with ownership enforced server-side.
+"""
+
+from __future__ import annotations
+
+from lnbits.core.crud.wallets import get_wallets
+from lnbits.core.models import Account
+from lnbits.core.models.wallets import WalletTypeInfo
+from lnbits.core.services.nostr_transport.models import NostrRpcRequest
+
+from .crud import (
+ create_pay_link,
+ delete_pay_link,
+ get_pay_link,
+ get_pay_links,
+ update_pay_link,
+)
+from .models import CreatePayLinkData
+
+
+async def handle_lnurlp_create(
+ auth: WalletTypeInfo, request: NostrRpcRequest
+) -> dict:
+ body = request.body or {}
+ body["wallet"] = auth.wallet.id # always create under the calling wallet
+ data = CreatePayLinkData(**body)
+ link = await create_pay_link(data)
+ return _to_dict(link)
+
+
+async def handle_lnurlp_get(
+ auth: WalletTypeInfo, request: NostrRpcRequest
+) -> dict:
+ link_id = _require_id(request)
+ link = await _require_owned_link(link_id, auth.wallet.id)
+ return _to_dict(link)
+
+
+async def handle_lnurlp_list(
+ auth: Account, request: NostrRpcRequest
+) -> list[dict]:
+ """List PayLinks across all wallets owned by the calling account.
+ If `request.wallet_id` is set and is one of those wallets, narrow to
+ just that wallet."""
+ wallets = await get_wallets(auth.id)
+ wallet_ids = [w.id for w in wallets]
+ if not wallet_ids:
+ return []
+ if request.wallet_id and request.wallet_id in wallet_ids:
+ wallet_ids = [request.wallet_id]
+ links = await get_pay_links(wallet_ids)
+ return [_to_dict(link) for link in links]
+
+
+async def handle_lnurlp_update(
+ auth: WalletTypeInfo, request: NostrRpcRequest
+) -> dict:
+ link_id = _require_id(request)
+ link = await _require_owned_link(link_id, auth.wallet.id)
+ body = request.body or {}
+ # Only patchable fields. Identity / counter fields (id, wallet,
+ # served_meta, served_pr, created_at) are not client-mutable.
+ _MUTABLE = {
+ "description",
+ "min",
+ "max",
+ "comment_chars",
+ "currency",
+ "webhook_url",
+ "webhook_headers",
+ "webhook_body",
+ "success_text",
+ "success_url",
+ "fiat_base_multiplier",
+ "username",
+ "zaps",
+ "disposable",
+ "domain",
+ }
+ for k, v in body.items():
+ if k in _MUTABLE:
+ setattr(link, k, v)
+ updated = await update_pay_link(link)
+ return _to_dict(updated)
+
+
+async def handle_lnurlp_delete(
+ auth: WalletTypeInfo, request: NostrRpcRequest
+) -> dict:
+ link_id = _require_id(request)
+ await _require_owned_link(link_id, auth.wallet.id)
+ await delete_pay_link(link_id)
+ return {"ok": True}
+
+
+async def resolve_lnurlp_owner(link_id: str) -> str | None:
+ """For the core subscription module: link_id -> wallet_id (or None)."""
+ link = await get_pay_link(link_id)
+ return link.wallet if link else None
+
+
+# ---------------------------------------------------------------------------
+# helpers
+# ---------------------------------------------------------------------------
+
+
+def _require_id(request: NostrRpcRequest) -> str:
+ body = request.body or {}
+ link_id = body.get("id")
+ if not link_id:
+ raise ValueError("lnurlp: body.id is required")
+ return str(link_id)
+
+
+async def _require_owned_link(link_id: str, wallet_id: str):
+ link = await get_pay_link(link_id)
+ if link is None:
+ raise ValueError(f"lnurlp: link not found: {link_id}")
+ if link.wallet != wallet_id:
+ raise PermissionError(
+ "lnurlp: link does not belong to caller's wallet"
+ )
+ return link
+
+
+def _to_dict(link) -> dict:
+ import json
+ return json.loads(link.json())