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
44
__init__.py
44
__init__.py
|
|
@ -17,4 +17,46 @@ withdraw_ext.include_router(withdraw_ext_generic)
|
|||
withdraw_ext.include_router(withdraw_ext_api)
|
||||
withdraw_ext.include_router(withdraw_ext_lnurl)
|
||||
|
||||
__all__ = ["db", "withdraw_ext", "withdraw_static_files"]
|
||||
|
||||
def withdraw_start() -> None:
|
||||
"""
|
||||
Register this extension's RPCs with the LNbits nostr transport so an
|
||||
HTTP-allergic client (e.g. lamassu-next ATM) can manage LNURL-withdraw
|
||||
links without touching the HTTP API. Also wires the link-owner
|
||||
resolver so subscribe_payments({tag:"withdraw", link_id:...}) can
|
||||
verify ownership.
|
||||
|
||||
No-op if the core transport module isn't present in the LNbits build.
|
||||
No runtime `if nostr_transport_enabled` guard is needed — when
|
||||
disabled, the relay pool never publishes, so registered RPCs are
|
||||
simply unreachable.
|
||||
"""
|
||||
try:
|
||||
from lnbits.core.services.nostr_transport.dispatcher import (
|
||||
AUTH_WALLET,
|
||||
register_rpc,
|
||||
)
|
||||
from lnbits.core.services.nostr_transport.subscriptions import (
|
||||
register_link_owner_resolver,
|
||||
)
|
||||
except ImportError:
|
||||
return
|
||||
|
||||
from .transport_rpcs import (
|
||||
handle_lnurlw_create_link,
|
||||
handle_lnurlw_delete_link,
|
||||
handle_lnurlw_get_link,
|
||||
handle_lnurlw_update_link,
|
||||
resolve_withdraw_owner,
|
||||
)
|
||||
|
||||
register_rpc("lnurlw_create_link", handle_lnurlw_create_link, AUTH_WALLET)
|
||||
register_rpc("lnurlw_get_link", handle_lnurlw_get_link, AUTH_WALLET)
|
||||
register_rpc("lnurlw_update_link", handle_lnurlw_update_link, AUTH_WALLET)
|
||||
register_rpc("lnurlw_delete_link", handle_lnurlw_delete_link, AUTH_WALLET)
|
||||
register_link_owner_resolver(
|
||||
"withdraw", resolve_withdraw_owner, link_extra_key="withdrawal_link_id"
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["db", "withdraw_ext", "withdraw_start", "withdraw_static_files"]
|
||||
|
|
|
|||
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