Compare commits

..

4 commits

Author SHA1 Message Date
31cf2eb164 feat: register transport RPCs over LNbits nostr transport
Some checks failed
lint.yml / feat: register transport RPCs over LNbits nostr transport (push) Failing after 0s
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) <noreply@anthropic.com>
2026-05-13 08:46:17 +02:00
Tiago Vasconcelos
d299e15c2f
wait for zap receipt (#133)
Some checks failed
lint.yml / wait for zap receipt (#133) (push) Failing after 0s
2026-05-08 06:32:48 +02:00
DoktorShift
dc37e259ba
remove double slash in LNURL pay endpoint URL (#129) 2026-04-16 13:12:29 +02:00
DoktorShift
9281cb74fb
doc: Changes to more pages (#125)
* Changes to more pages

---------

Co-authored-by: dni  <office@dnilabs.com>
2026-01-27 11:36:59 +01:00
7 changed files with 235 additions and 5 deletions

View file

@ -1,3 +1,13 @@
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
<img src="https://i.imgur.com/fyKPgVT.png" alt="LNbits" style="width:280px">
</picture>
</a>
[![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 - <small>[LNbits](https://github.com/lnbits/lnbits) extension</small>
<small>For more about LNBits extension check [this tutorial](https://github.com/lnbits/lnbits/wiki/LNbits-Extensions)</small>
@ -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)
</details>
## 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)

View file

@ -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",

View file

@ -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
}

View file

@ -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.

View file

@ -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) {

View file

@ -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}")

151
transport_rpcs.py Normal file
View file

@ -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())