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>
This commit is contained in:
parent
cbbb3c743b
commit
d29d4dbec9
3 changed files with 67 additions and 61 deletions
|
|
@ -15,11 +15,13 @@ itself has no awareness of festivals.
|
|||
|
||||
Signing
|
||||
-------
|
||||
Events are signed with the *restaurant's* keypair if `restaurant.nostr_pubkey`
|
||||
is set, otherwise with the LNbits Account keypair of the wallet's owner.
|
||||
This lets a single LNbits account host multiple restaurants under
|
||||
distinct Nostr identities, while keeping a sane default for owners
|
||||
who don't care about identity separation.
|
||||
Events are signed via the core `NostrSigner` abstraction
|
||||
(LocalSigner / RemoteBunkerSigner / ClientSideOnlySigner). The caller
|
||||
resolves a signer for the restaurant's effective Nostr identity and
|
||||
hands it to `publish_event` — the signer fills in `id`/`pubkey`/`sig`
|
||||
per NIP-01. This lets a single LNbits account host multiple
|
||||
restaurants under distinct Nostr identities while keeping a sane
|
||||
default for owners who don't care about identity separation.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
|
@ -27,7 +29,7 @@ import re
|
|||
import time
|
||||
from typing import Optional
|
||||
|
||||
import coincurve
|
||||
from lnbits.core.signers import NostrSigner
|
||||
from loguru import logger
|
||||
|
||||
from .models import MenuItem, Restaurant
|
||||
|
|
@ -202,25 +204,33 @@ def build_delete_event(
|
|||
# --------------------------------------------------------------------- #
|
||||
|
||||
|
||||
def sign_nostr_event(nostr_event: NostrEvent, private_key_hex: str) -> None:
|
||||
"""Schnorr-sign a NostrEvent in place (BIP-340)."""
|
||||
privkey = coincurve.PrivateKey(bytes.fromhex(private_key_hex))
|
||||
sig = privkey.sign_schnorr(bytes.fromhex(nostr_event.id))
|
||||
nostr_event.sig = sig.hex()
|
||||
|
||||
|
||||
async def publish_event(
|
||||
nostr_client,
|
||||
nostr_event: NostrEvent,
|
||||
private_key_hex: str,
|
||||
signer: NostrSigner,
|
||||
) -> Optional[NostrEvent]:
|
||||
"""Sign and publish a built NostrEvent. Returns the event on success
|
||||
so callers can persist its id + created_at, or None on failure."""
|
||||
so callers can persist its id + created_at, or None on failure.
|
||||
|
||||
The unsigned event dict (`kind`/`created_at`/`tags`/`content`) is
|
||||
handed to the signer, which fills in `id`/`pubkey`/`sig` — same
|
||||
NIP-01 serialization rules as our local `event_id` property uses,
|
||||
so the returned id matches what we'd have computed locally. The
|
||||
signer backend (LocalSigner / RemoteBunkerSigner) is transparent."""
|
||||
if not nostr_client:
|
||||
logger.debug("[RESTAURANT] No NostrClient; skipping publish")
|
||||
return None
|
||||
try:
|
||||
sign_nostr_event(nostr_event, private_key_hex)
|
||||
unsigned = {
|
||||
"kind": nostr_event.kind,
|
||||
"created_at": nostr_event.created_at,
|
||||
"tags": nostr_event.tags,
|
||||
"content": nostr_event.content,
|
||||
}
|
||||
signed = await signer.sign_event(unsigned)
|
||||
nostr_event.id = signed["id"]
|
||||
nostr_event.pubkey = signed["pubkey"]
|
||||
nostr_event.sig = signed["sig"]
|
||||
await nostr_client.publish_nostr_event(nostr_event)
|
||||
logger.info(
|
||||
f"[RESTAURANT] Published kind {nostr_event.kind} "
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue