From 9281cb74fb8f7a70a32e16e1020f94ca635c90c1 Mon Sep 17 00:00:00 2001 From: DoktorShift <106493492+DoktorShift@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:36:59 +0100 Subject: [PATCH 1/4] doc: Changes to more pages (#125) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Changes to more pages --------- Co-authored-by: dni ⚡ --- README.md | 17 +++++++++++++++++ config.json | 13 ++++++++++--- description.md | 11 ++++++++++- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 72fe035..9fcef84 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,13 @@ + + + + LNbits + + + +[![License: MIT](https://img.shields.io/badge/License-MIT-success?logo=open-source-initiative&logoColor=white)](./LICENSE) +[![Built for LNbits](https://img.shields.io/badge/Built%20for-LNbits-4D4DFF?logo=lightning&logoColor=white)](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 [![lnurl-details.jpg](https://i.postimg.cc/zDwq1V2X/lnurl-details.jpg)](https://postimg.cc/3WwsXJHP) + +## Powered by LNbits + +[LNbits](https://lnbits.com) is a free and open-source lightning accounts system. + +[![Visit LNbits Shop](https://img.shields.io/badge/Visit-LNbits%20Shop-7C3AED?logo=shopping-cart&logoColor=white&labelColor=5B21B6)](https://shop.lnbits.com/) +[![Try myLNbits SaaS](https://img.shields.io/badge/Try-myLNbits%20SaaS-2563EB?logo=lightning&logoColor=white&labelColor=1E40AF)](https://my.lnbits.com/login) 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. From dc37e259ba4bd157e936da8b5c42b88e43325909 Mon Sep 17 00:00:00 2001 From: DoktorShift <106493492+DoktorShift@users.noreply.github.com> Date: Thu, 16 Apr 2026 13:12:29 +0200 Subject: [PATCH 2/4] remove double slash in LNURL pay endpoint URL (#129) --- static/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) { From d299e15c2fee294ee6dcd7e5c8f788879c359aa4 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Fri, 8 May 2026 05:32:48 +0100 Subject: [PATCH 3/4] wait for zap receipt (#133) --- tasks.py | 10 ++++++++++ 1 file changed, 10 insertions(+) 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}") From 31cf2eb164c3513fc49511efb3c54b366701e37a Mon Sep 17 00:00:00 2001 From: Padreug Date: Wed, 13 May 2026 08:46:17 +0200 Subject: [PATCH 4/4] feat: register transport RPCs over LNbits nostr transport MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors what aiolabs/withdraw did — hooks lnurlp's existing CRUD into the LNbits nostr transport layer so an HTTP-allergic client (e.g. lamassu-next ATM) can manage PayLinks over kind-21000 encrypted events instead of HTTP. Extends the existing `lnurlp_start()` lifecycle hook (auto-invoked by the LNbits extension manager) to import the transport's `register_rpc` and register five RPCs: lnurlp_create AUTH_WALLET lnurlp_get AUTH_WALLET lnurlp_list AUTH_ACCOUNT lnurlp_update AUTH_WALLET lnurlp_delete AUTH_WALLET All handlers are thin shims around the existing crud.py functions — no business logic duplication. *_get / *_update / *_delete verify that the link's stored wallet matches the caller's wallet id. Also registers a link-owner resolver with the core subscriptions module (tag "lnurlp", extras-key "link" — the default, matching 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. The transport import is guarded by try/except ImportError so this extension still loads cleanly against an LNbits build that doesn't have nostr_transport. Co-Authored-By: Claude Opus 4.7 (1M context) --- __init__.py | 36 +++++++++++ transport_rpcs.py | 151 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 transport_rpcs.py 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/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())