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

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
This commit is contained in:
Padreug 2026-07-19 01:30:13 +02:00
commit 5b176e5186
3 changed files with 291 additions and 9 deletions

View file

@ -7,9 +7,45 @@ How rooms, guests, LNbits, and relays interact. The guiding rule:
> Lightning payment is the confirmation.
Two doors lead into the *same* booking flow — the REST API
([`../views_api.py`](../views_api.py)) and the Nostr subscription
([`../nostr/service.py`](../nostr/service.py)). Both funnel through
[`../crud.py`](../crud.py) so arbitration + quoting live in one place.
([`../views_api.py`](../views_api.py)) and the Nostr-transport RPC layer
([`../transport_rpcs.py`](../transport_rpcs.py)). Both delegate to
[`../services.py`](../services.py) (orchestration) over
[`../crud.py`](../crud.py) (persistence) so arbitration + quoting live in one
place and can't drift between doors.
## Door 2: RPC over the core nostr transport
The core LNbits nostr transport (`lnbits.core.services.nostr_transport`) is a
**kind-21000 encrypted RPC bus** (NIP-44), not a general event publisher.
Chatelet registers handlers on it in `chatelet_start()` so the whole booking
flow runs over relays with no HTTP:
| RPC | Auth | Purpose |
|---|---|---|
| `chatelet_room_create` / `_update` / `_publish` | wallet | operator room CRUD (ownership-checked) |
| `chatelet_block_create` | wallet | operator blocks a range |
| `chatelet_room_list_mine` | account | operator's rooms across their wallets |
| `chatelet_room_list` / `_get` | none | public discovery (active rooms, wallet id stripped) |
| `chatelet_availability` | none | is a range free + a quote |
| `chatelet_booking_request` | none | guest requests a stay (guest id = signed `sender_pubkey`) |
| `chatelet_booking_get` | none | guest reads back their booking (ownership by `sender_pubkey`) |
Guest identity is the `sender_pubkey` the dispatcher lifts off the signed
kind-21000 event — unspoofable, and it means no separate `guest_pubkey` is
trusted from the body.
**Payment confirmation over RPC:** `subscribe_payments` is wallet-owner
scoped, so the *operator* can stream booking settlements
(`subscribe_payments({tag:"chatelet", link_id:<booking_id>})`, wired via
`register_link_owner_resolver`). A **guest** can't subscribe to the operator's
wallet, so the guest confirms by polling `chatelet_booking_get` until
`confirmed` (a guest push would need a NIP-17 DM — issue #5).
> **Consequence for the custom kinds:** with availability answered by the
> `chatelet_availability` RPC, the ephemeral `kind:22000/22001` availability
> query/response (ADR-0001) is **redundant for RPC clients**. It is only worth
> keeping if we want non-RPC Nostr clients to query availability by publishing
> an event. Revisit when the public-discovery transport is chosen (issue #2).
## Actors