feat: expose Chatelet over the LNbits nostr transport (#1) #8

Merged
padreug merged 3 commits from feat/nostr-transport-rpcs into main 2026-07-19 14:50:39 +00:00
Owner

Makes Chatelet drivable entirely over the core LNbits nostr transport (kind-21000 RPC), no HTTP needed — the functional half of the "Nostr-native" goal. Closes the code portion of #1.

Commits

  1. refactor: extract booking flow into services.py — behavior-neutral. Availability quoting + check-then-hold + invoice orchestration move out of views_api.py into a transport-agnostic services.py with typed errors (NotFound/Unavailable/ValueError/BookingError). views_api becomes a thin HTTP door mapping those to status codes. This is what lets both doors share one flow.
  2. feat: register RPC handlers on the core dispatcher — mirrors lnurlp/transport_rpcs.py.
  3. docs: reframe kind:22000/22001 as a retained client-agnostic proposal (not redundant) — see finding below.

RPC surface

RPC Auth
chatelet_room_create / _update / _publish, chatelet_block_create wallet operator, ownership-checked
chatelet_room_list_mine account operator's rooms
chatelet_room_list / _get none public discovery (wallet id stripped)
chatelet_availability none free? + quote
chatelet_booking_request / _get none guest booking; identity = signed sender_pubkey

register_link_owner_resolver(tag="chatelet", key="booking_id") lets the operator stream settlements via subscribe_payments. Graceful no-op on pre-#4 LNbits builds.

Scope + transport finding

The core transport pool is kind-21000 RPC only (_publish_event emits nothing else; subscriptions filter only 21000) — the correct tool for #1, but it can't publish public discovery events. So this PR is the RPC booking flow (#1) only; the public NIP-99/52/78 events are #2.

Follow-up research (posted on #2) resolved #2's path and revised my initial take:

  • #2 → publish via the nostrclient extension in-process (spirekeeper template: nostr_client.relay_manager.publish_message(...), sign via resolve_signer). nostrmarket/spirekeeper already do this; the core pool was never the right layer for it.
  • kind:22000/22001 is retained as a client-agnostic proposal, not dropped — per workspace doctrine the availability RPC is the training wheel and the public kind is the destination. Commit 3 reflects this in event-flow.md; issue #6 (register the allocation) stays open.

So nothing here is blocked — #1 stands on its own; #2 is a tracked follow-up with a decided path.

Test (FakeWallet + core transport build)

Send an encrypted kind-21000 chatelet_availability, then chatelet_booking_request → expect a BookingQuote with bolt11; pay it → chatelet_booking_get shows confirmed.

Why PR

Money-handling extension → PR category. Handing off merge to you via the Forgejo UI.

🤖 Generated with Claude Code

Makes Chatelet drivable entirely over the core LNbits nostr transport (kind-21000 RPC), no HTTP needed — the functional half of the "Nostr-native" goal. Closes the code portion of #1. ## Commits 1. **refactor: extract booking flow into `services.py`** — behavior-neutral. Availability quoting + check-then-hold + invoice orchestration move out of `views_api.py` into a transport-agnostic `services.py` with typed errors (`NotFound`/`Unavailable`/`ValueError`/`BookingError`). `views_api` becomes a thin HTTP door mapping those to status codes. This is what lets both doors share one flow. 2. **feat: register RPC handlers on the core dispatcher** — mirrors `lnurlp/transport_rpcs.py`. 3. **docs: reframe `kind:22000/22001`** as a retained client-agnostic proposal (not redundant) — see finding below. ## RPC surface | RPC | Auth | | |---|---|---| | `chatelet_room_create` / `_update` / `_publish`, `chatelet_block_create` | wallet | operator, ownership-checked | | `chatelet_room_list_mine` | account | operator's rooms | | `chatelet_room_list` / `_get` | none | public discovery (wallet id stripped) | | `chatelet_availability` | none | free? + quote | | `chatelet_booking_request` / `_get` | none | guest booking; identity = signed `sender_pubkey` | `register_link_owner_resolver(tag="chatelet", key="booking_id")` lets the **operator** stream settlements via `subscribe_payments`. Graceful no-op on pre-#4 LNbits builds. ## Scope + transport finding The core transport pool is **kind-21000 RPC only** (`_publish_event` emits nothing else; subscriptions filter only 21000) — the correct tool for #1, but it can't publish public discovery events. So this PR is the **RPC booking flow (#1) only**; the public NIP-99/52/78 events are #2. Follow-up research (posted on #2) resolved #2's path and revised my initial take: - **#2 → publish via the `nostrclient` extension in-process** (spirekeeper template: `nostr_client.relay_manager.publish_message(...)`, sign via `resolve_signer`). nostrmarket/spirekeeper already do this; the core pool was never the right layer for it. - **`kind:22000/22001` is retained as a client-agnostic proposal, not dropped** — per workspace doctrine the availability RPC is the training wheel and the public kind is the destination. Commit 3 reflects this in `event-flow.md`; issue #6 (register the allocation) stays open. So nothing here is blocked — #1 stands on its own; #2 is a tracked follow-up with a decided path. ## Test (FakeWallet + core transport build) Send an encrypted kind-21000 `chatelet_availability`, then `chatelet_booking_request` → expect a `BookingQuote` with bolt11; pay it → `chatelet_booking_get` shows `confirmed`. ## Why PR Money-handling extension → PR category. Handing off merge to you via the Forgejo UI. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Move availability quoting + the check-then-hold + invoice orchestration out
of views_api.py into a transport-agnostic services.py, with typed errors
(NotFound/Unavailable/ValueError/BookingError). views_api becomes a thin
HTTP door that maps those to status codes. No behavior change — this is so
the incoming nostr-transport door can share one booking flow instead of
duplicating it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VUQCfdqiLSsFS2jcGnaFD
Register kind-21000 RPC handlers on the core nostr_transport dispatcher so
the booking flow runs over relays with no HTTP, mirroring lnurlp:

- operator (AUTH_WALLET): room create/update/publish, block create — all
  ownership-checked; room_list_mine (AUTH_ACCOUNT).
- public (AUTH_NONE): room_list/get (wallet id stripped), availability,
  booking_request, booking_get. Guest identity is the signed sender_pubkey,
  so no guest_pubkey is trusted from the body.
- register_link_owner_resolver(tag=chatelet, key=booking_id) lets the
  operator stream settlements via subscribe_payments.

Handlers delegate to services.py — no logic duplicated. Graceful no-op if
the core transport module isn't in this LNbits build (pre-#4). Guests can't
subscribe to the operator wallet, so they poll booking_get to confirm.

Note documented in event-flow.md: with availability now an RPC, the custom
kind:22000/22001 pair is redundant for RPC clients (revisit in #2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VUQCfdqiLSsFS2jcGnaFD
Per the client-agnostic doctrine (workspace CLAUDE.md): the availability RPC
is the training wheel, the public kind:22000/22001 is the destination. Keep
the custom kinds as a proposal so a generic Nostr client can one day query
availability without our RPC. Published via the nostrclient relay path (#2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VUQCfdqiLSsFS2jcGnaFD
padreug deleted branch feat/nostr-transport-rpcs 2026-07-19 14:50:39 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/chatelet!8
No description provided.