Commit graph

3 commits

Author SHA1 Message Date
d29d4dbec9 feat(signer): migrate Nostr publishing off account.prvkey → resolve_for_wallet (#11)
Closes aiolabs/restaurant#11. Pre-cascade prerequisite for
aiolabs/lnbits#17 (signer abstraction phase 1), which lands an m002
startup job that NULLs the legacy `accounts.prvkey` column. After
this migration, the restaurant extension reads no plaintext nsec and
works with any NostrSigner backend (LocalSigner / RemoteBunkerSigner
/ ClientSideOnlySigner).

## What changed

### views_api.py — _resolve_signing_keypair → _resolve_signer

Was: `_resolve_signing_keypair(restaurant)` returned `(pubkey, prvkey)`
read directly from `account.pubkey` / `account.prvkey` after walking
wallet → account.

Now: `_resolve_signer(restaurant)` returns `NostrSigner | None`.
Precedence order preserved:

  1. `restaurant.nostr_pubkey` set → per-restaurant identity. Still
     a no-op TODO returning None until a per-restaurant signer /
     vault ships (separate concern, future work).
  2. fallback → `resolve_for_wallet(restaurant.wallet)` (the DRY
     helper from aiolabs/lnbits#23 — wallet → account → signer →
     can_sign-check in one call, returns None on any soft-fail).

Three call sites updated (`_publish_restaurant`, `_publish_menu_item`,
`_publish_menu_item_delete`): each now passes the resolved `signer`
to `publish_event` instead of the keypair tuple, and uses
`signer.pubkey` for tag construction. The discovery-echo line in
`_publish_restaurant` (`restaurant.nostr_pubkey = signer.pubkey`)
preserves prior behavior.

Dropped now-unused imports: `get_account`, `get_wallet`.

### nostr_publisher.py — publish_event

Was: `publish_event(client, event, private_key_hex)` called a local
`sign_nostr_event` helper that signed in place via
`coincurve.PrivateKey.sign_schnorr`.

Now: `publish_event(client, event, signer: NostrSigner)` builds the
unsigned dict (`kind`/`created_at`/`tags`/`content`), hands it to
`await signer.sign_event(...)`, and writes `id`/`pubkey`/`sig` back
onto the local `NostrEvent` model before publishing. The signer
backend (LocalSigner / RemoteBunkerSigner) is transparent.

Removed the `sign_nostr_event` helper entirely — the signer
abstraction handles all signing now.

Dropped the `coincurve` import; no direct crypto in this extension.

### docs/nostr-layer.md — signing prose

Updated the Signing section to reflect the signer-abstraction model:
`resolve_for_wallet` resolves a `NostrSigner`, the extension no
longer touches `account.prvkey` or calls `coincurve.sign_schnorr`
directly. The per-restaurant-identity TODO is preserved.

## Acceptance

- [x] `_resolve_signing_keypair` replaced with `_resolve_signer` returning NostrSigner
- [x] `sign_nostr_event` helper removed (signer handles it internally)
- [x] `publish_event` accepts a NostrSigner instead of private_key_hex
- [x] all three call sites updated to pass the signer
- [x] re-grep `restaurant/`: zero `account.prvkey` references
- [x] coincurve import dropped
- [x] docs/nostr-layer.md updated in the same commit

Manual smoke testing + tag + catalog entry follow the migration
landing; will run against the regtest stack with lnbits on
`issue-18-phase-2.3` (which validates both LocalSigner and
RemoteBunkerSigner signing paths end-to-end).

## Cross-references

- aiolabs/restaurant#11 — issue this commit closes
- aiolabs/lnbits#17 — the cascading signer-abstraction PR
- aiolabs/lnbits#23 — the resolve_for_wallet helper this uses
- aiolabs/lnbits#21 — umbrella audit (5 affected extensions)
- aiolabs/events#23 / aiolabs/tasks#3 — sister migrations (already on signer-abstraction branches)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 22:26:41 +02:00
cedd548963 feat(nostr): ancestor 't' tags on menu listings
When a menu item's NIP-99 kind-30402 listing is published, the
extension now emits one 't' tag per ancestor node name (root-first,
slugified to lowercase ASCII). This lets Nostr clients filter the
global listing stream by category — e.g.
    {"#t": ["hot-beverages"]}
    {"#t": ["coffee-based"]}
without having to know the publisher's pubkey or pull markdown
content. The 'menu' anchor stays first so subscribers can still
get the universal stream. Allergen / ingredient prefixes
(allergen:<x>, ingr:<x>) and dietary tags are unchanged.

nostr_publisher.py:
  - Add _slugify(name) -> str (lowercase, [^a-z0-9]+ -> '-', strip).
  - build_menu_item_event takes ancestor_names: tuple[str, ...] kw
    and emits dedup'd slugs. Stays DB-free; the caller does the
    walk.

views_api.py:
  - _ancestor_names_for_node walks the materialized path of an
    item's node to (root.name, ..., leaf.name).
  - _publish_menu_item passes them to the builder.
  - api_update_menu_node detects a name change and calls
    _republish_subtree_items(node_id), which re-publishes every
    menu_item in the subtree so the new ancestor slug lands on
    each listing. <=50 items per restaurant in practice; eager
    re-publish keeps the relay state consistent without a
    background sync.
2026-05-09 07:11:06 +02:00
b155548036 feat(nostr): NIP-99 menu listings, NIP-01 profile, NIP-17 stub
nostr/event.py
  Bare NIP-01 NostrEvent with canonical id computation.

nostr/nostr_client.py
  Bidirectional WebSocket client (lifted from events ext, kept
  local). Connects to nostrclient ext's internal relay endpoint,
  dedups by event id (LRU 1000).

nostr_publisher.py
  Builders for:
    * kind 0    — restaurant profile (NIP-01 metadata)
    * kind 30402 — menu item (NIP-99 classified listing,
                              parameterized replaceable by item.id)
    * kind 5    — deletion request (NIP-09)
  Schnorr signing via coincurve (BIP-340).

  Menu listings carry structured price tags (["price", n, currency]),
  status (active|sold) so customers see sold-out items, and 't' tags
  for category, dietary, allergens (allergen:<x>) and ingredients
  (ingr:<x>) so webapps can filter without parsing markdown.

  Restaurants can sign with their own keypair (per-restaurant Nostr
  identity) or fall back to the LNbits Account keypair.

nostr_sync.py
  Subscribes to:
    * kind 30402 #t=menu — backfill 200 + live (echo confirmation
      for now; foreign-menu indexing deferred until we settle on a
      federated cache table).
    * kind 1059 — NIP-17 gift-wrapped DMs, only when
      settings.nostr_orders_enabled. Decryption stubbed (needs
      NIP-44 v2 unwrap); REST stays the supported transport
      until that's wired up. _place_order_from_dm is complete and
      ready for the decryption hook.
2026-05-09 07:11:06 +02:00