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
Some checks failed
lint.yml / feat: register transport RPCs over LNbits nostr transport (push) Failing after 0s
Hooks the existing withdraw CRUD into the LNbits nostr transport layer
so an HTTP-allergic client (e.g. lamassu-next ATM) can manage LNURL-
withdraw links over kind-21000 encrypted events instead of HTTP.
New `withdraw_start()` lifecycle hook (auto-invoked by the LNbits
extension manager) imports the transport's `register_rpc` and registers
four RPCs mirroring the Lightning.Pub `withdraw.*` contract exactly so
lamassu-next's adapter can be a pure name-translation layer:
lnurlw_create_link AUTH_WALLET
lnurlw_get_link AUTH_WALLET
lnurlw_update_link AUTH_WALLET
lnurlw_delete_link 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 (under tag "withdraw", extras-key "withdrawal_link_id" — the
exact field name views_lnurl.py:144 stamps on payment.extra when a
withdraw settles). That lets clients call
`subscribe_payments({tag:"withdraw", link_id:...})` and stream real-
time claim 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>
This commit is contained in:
parent
2e52400f52
commit
95ed17754d
2 changed files with 162 additions and 1 deletions
119
transport_rpcs.py
Normal file
119
transport_rpcs.py
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
"""
|
||||
Nostr-transport RPC handlers for the withdraw (LNURL-withdraw) extension.
|
||||
|
||||
Names mirror the Lightning.Pub `withdraw.*` contract that the lamassu-next
|
||||
ATM consumes (see ~/dev/shocknet/lamassu-next/packages/lightning/src/client.ts
|
||||
lines ~301–351). That keeps the lamassu-next-side adapter a pure name
|
||||
translation — no semantic reshaping.
|
||||
|
||||
Auth model (set in `__init__.py:withdraw_start`):
|
||||
- create / get / update / delete → AUTH_WALLET; the calling pubkey must
|
||||
own the wallet the link is scoped to. *_get / *_update / *_delete also
|
||||
verify the link's stored `wallet` matches the caller's wallet id.
|
||||
|
||||
`resolve_withdraw_owner` is registered with the core subscription module
|
||||
under tag `"withdraw"` and extras-key `"withdrawal_link_id"` (matching
|
||||
where the extension stamps the link id on settlement — see
|
||||
`views_lnurl.py:144`). That lets `subscribe_payments({tag:"withdraw",
|
||||
link_id:...})` enforce ownership without core importing this module.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from lnbits.core.models.wallets import WalletTypeInfo
|
||||
from lnbits.core.services.nostr_transport.models import NostrRpcRequest
|
||||
|
||||
from .crud import (
|
||||
create_withdraw_link,
|
||||
delete_withdraw_link,
|
||||
get_withdraw_link,
|
||||
update_withdraw_link,
|
||||
)
|
||||
from .models import CreateWithdrawData
|
||||
|
||||
|
||||
async def handle_lnurlw_create_link(
|
||||
auth: WalletTypeInfo, request: NostrRpcRequest
|
||||
) -> dict:
|
||||
body = request.body or {}
|
||||
data = CreateWithdrawData(**body)
|
||||
link = await create_withdraw_link(data, auth.wallet.id)
|
||||
return _to_dict(link)
|
||||
|
||||
|
||||
async def handle_lnurlw_get_link(
|
||||
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_lnurlw_update_link(
|
||||
auth: WalletTypeInfo, request: NostrRpcRequest
|
||||
) -> dict:
|
||||
link_id = _require_id(request)
|
||||
link = await _require_owned_link(link_id, auth.wallet.id)
|
||||
body = request.body or {}
|
||||
_MUTABLE = {
|
||||
"title",
|
||||
"min_withdrawable",
|
||||
"max_withdrawable",
|
||||
"uses",
|
||||
"wait_time",
|
||||
"is_unique",
|
||||
"webhook_url",
|
||||
"webhook_headers",
|
||||
"webhook_body",
|
||||
"custom_url",
|
||||
"enabled",
|
||||
}
|
||||
for k, v in body.items():
|
||||
if k in _MUTABLE:
|
||||
setattr(link, k, v)
|
||||
updated = await update_withdraw_link(link)
|
||||
return _to_dict(updated)
|
||||
|
||||
|
||||
async def handle_lnurlw_delete_link(
|
||||
auth: WalletTypeInfo, request: NostrRpcRequest
|
||||
) -> dict:
|
||||
link_id = _require_id(request)
|
||||
await _require_owned_link(link_id, auth.wallet.id)
|
||||
await delete_withdraw_link(link_id)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
async def resolve_withdraw_owner(link_id: str) -> str | None:
|
||||
"""For the core subscription module: link_id -> wallet_id (or None)."""
|
||||
link = await get_withdraw_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("withdraw: body.id is required")
|
||||
return str(link_id)
|
||||
|
||||
|
||||
async def _require_owned_link(link_id: str, wallet_id: str):
|
||||
link = await get_withdraw_link(link_id)
|
||||
if link is None:
|
||||
raise ValueError(f"withdraw: link not found: {link_id}")
|
||||
if link.wallet != wallet_id:
|
||||
raise PermissionError(
|
||||
"withdraw: 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