Compare commits
No commits in common. "main" and "v1.3.0-aio.1" have entirely different histories.
main
...
v1.3.0-aio
7 changed files with 5 additions and 235 deletions
17
README.md
17
README.md
|
|
@ -1,13 +1,3 @@
|
||||||
<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)
|
|
||||||
[](https://github.com/lnbits/lnbits)
|
|
||||||
|
|
||||||
# LNURLp - <small>[LNbits](https://github.com/lnbits/lnbits) extension</small>
|
# 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>
|
<small>For more about LNBits extension check [this tutorial](https://github.com/lnbits/lnbits/wiki/LNbits-Extensions)</small>
|
||||||
|
|
@ -65,10 +55,3 @@ Now you can receive sats to your newly created LN address. You will find this in
|
||||||
[](https://postimg.cc/3WwsXJHP)
|
[](https://postimg.cc/3WwsXJHP)
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
## 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)
|
|
||||||
|
|
|
||||||
36
__init__.py
36
__init__.py
|
|
@ -42,42 +42,6 @@ def lnurlp_start():
|
||||||
task = create_permanent_unique_task("lnurlp", wait_for_paid_invoices)
|
task = create_permanent_unique_task("lnurlp", wait_for_paid_invoices)
|
||||||
scheduled_tasks.append(task)
|
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__ = [
|
__all__ = [
|
||||||
"db",
|
"db",
|
||||||
|
|
|
||||||
13
config.json
13
config.json
|
|
@ -1,10 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "paylink",
|
|
||||||
"version": "1.3.0",
|
|
||||||
"name": "Pay Links",
|
"name": "Pay Links",
|
||||||
"repo": "https://github.com/lnbits/lnurlp",
|
"version": "1.3.0",
|
||||||
"short_description": "Make static reusable LNURL pay links or lightning addresses",
|
"short_description": "Make reusable LNURL pay links",
|
||||||
"description": "",
|
|
||||||
"tile": "/lnurlp/static/image/lnurl-pay.png",
|
"tile": "/lnurlp/static/image/lnurl-pay.png",
|
||||||
"min_lnbits_version": "1.4.0",
|
"min_lnbits_version": "1.4.0",
|
||||||
"contributors": [
|
"contributors": [
|
||||||
|
|
@ -54,9 +51,5 @@
|
||||||
],
|
],
|
||||||
"description_md": "https://raw.githubusercontent.com/lnbits/lnurlp/main/description.md",
|
"description_md": "https://raw.githubusercontent.com/lnbits/lnurlp/main/description.md",
|
||||||
"terms_and_conditions_md": "https://raw.githubusercontent.com/lnbits/lnurlp/main/toc.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
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1 @@
|
||||||
Create static LNURL-pay links and Lightning addresses for receiving payments.
|
Create a static LNURLp or LNaddress people can use to pay.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,7 @@ window.PageLnurlp = {
|
||||||
username: link.username
|
username: link.username
|
||||||
}
|
}
|
||||||
const domain = link.domain || window.location.host
|
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
|
this.qrCodeDialog.show = true
|
||||||
},
|
},
|
||||||
openUpdateDialog(linkId) {
|
openUpdateDialog(linkId) {
|
||||||
|
|
|
||||||
10
tasks.py
10
tasks.py
|
|
@ -144,16 +144,6 @@ async def send_zap(payment: Payment):
|
||||||
async with websockets.connect(relay_url, open_timeout=5) as websocket:
|
async with websockets.connect(relay_url, open_timeout=5) as websocket:
|
||||||
logger.debug(f"Sending zap to {relay_url}")
|
logger.debug(f"Sending zap to {relay_url}")
|
||||||
await websocket.send(event_message)
|
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:
|
except Exception as e:
|
||||||
logger.warning(f"Failed to send zap to {relay_url}: {e}")
|
logger.warning(f"Failed to send zap to {relay_url}: {e}")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,151 +0,0 @@
|
||||||
"""
|
|
||||||
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())
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue