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
63
views_api.py
63
views_api.py
|
|
@ -22,9 +22,8 @@ from pydantic import BaseModel
|
|||
from starlette.exceptions import HTTPException
|
||||
|
||||
from lnbits.core.crud import get_user
|
||||
from lnbits.core.crud.users import get_account
|
||||
from lnbits.core.crud.wallets import get_wallet
|
||||
from lnbits.core.models import Account, WalletTypeInfo
|
||||
from lnbits.core.signers import NostrSigner, resolve_for_wallet
|
||||
from lnbits.decorators import (
|
||||
check_admin,
|
||||
check_user_exists,
|
||||
|
|
@ -108,53 +107,47 @@ restaurant_api_router = APIRouter()
|
|||
# --------------------------------------------------------------------- #
|
||||
|
||||
|
||||
async def _resolve_signing_keypair(
|
||||
async def _resolve_signer(
|
||||
restaurant: Restaurant,
|
||||
) -> Optional[tuple[str, str]]:
|
||||
) -> Optional[NostrSigner]:
|
||||
"""
|
||||
Resolve the (pubkey, prvkey) pair for signing Nostr events on behalf
|
||||
of a restaurant.
|
||||
Resolve a `NostrSigner` for signing events on behalf of a restaurant.
|
||||
|
||||
Order of precedence:
|
||||
1. restaurant.nostr_pubkey is set → use a per-restaurant key.
|
||||
(Storage of the corresponding prvkey is intentionally out of
|
||||
scope here; for now this branch is a no-op until we ship a
|
||||
secret-management approach. Returns None.)
|
||||
2. Otherwise → fall back to the LNbits Account keypair of the
|
||||
wallet owner.
|
||||
1. restaurant.nostr_pubkey is set → per-restaurant identity.
|
||||
(A per-restaurant signer/vault is out of scope here; until
|
||||
that lands this branch is a no-op. Returns None.)
|
||||
2. Otherwise → fall back to the wallet owner's signer via
|
||||
`resolve_for_wallet` (wallet → account → signer with a
|
||||
can_sign-check; soft-fails to None on missing wallet, missing
|
||||
account, unclassified row, or ClientSideOnlySigner accounts
|
||||
where the server has no signing authority).
|
||||
"""
|
||||
if restaurant.nostr_pubkey:
|
||||
# TODO: per-restaurant secret key vault.
|
||||
# TODO: per-restaurant signer / secret vault.
|
||||
return None
|
||||
wallet_obj = await get_wallet(restaurant.wallet)
|
||||
if not wallet_obj:
|
||||
return None
|
||||
account = await get_account(wallet_obj.user)
|
||||
if not account or not account.pubkey or not account.prvkey:
|
||||
return None
|
||||
return account.pubkey, account.prvkey
|
||||
return await resolve_for_wallet(restaurant.wallet)
|
||||
|
||||
|
||||
async def _publish_restaurant(restaurant: Restaurant) -> None:
|
||||
settings = await get_settings()
|
||||
if not settings.nostr_publish_enabled:
|
||||
return
|
||||
keypair = await _resolve_signing_keypair(restaurant)
|
||||
if not keypair:
|
||||
signer = await _resolve_signer(restaurant)
|
||||
if signer is None:
|
||||
return
|
||||
pubkey, prvkey = keypair
|
||||
|
||||
from . import nostr_client
|
||||
|
||||
event = build_restaurant_metadata_event(restaurant, pubkey)
|
||||
published = await publish_event(nostr_client, event, prvkey)
|
||||
event = build_restaurant_metadata_event(restaurant, signer.pubkey)
|
||||
published = await publish_event(nostr_client, event, signer)
|
||||
if published:
|
||||
restaurant.nostr_event_id = published.id
|
||||
restaurant.nostr_event_created_at = published.created_at
|
||||
if not restaurant.nostr_pubkey:
|
||||
# Echo back the resolved pubkey so the row carries it for
|
||||
# discovery (e.g. webapp follows this pubkey).
|
||||
restaurant.nostr_pubkey = pubkey
|
||||
restaurant.nostr_pubkey = signer.pubkey
|
||||
await update_restaurant(restaurant)
|
||||
|
||||
|
||||
|
|
@ -188,18 +181,17 @@ async def _publish_menu_item(item: MenuItem) -> None:
|
|||
restaurant = await get_restaurant(item.restaurant_id)
|
||||
if not restaurant:
|
||||
return
|
||||
keypair = await _resolve_signing_keypair(restaurant)
|
||||
if not keypair:
|
||||
signer = await _resolve_signer(restaurant)
|
||||
if signer is None:
|
||||
return
|
||||
pubkey, prvkey = keypair
|
||||
|
||||
from . import nostr_client
|
||||
|
||||
ancestors = await _ancestor_names_for_node(item.node_id)
|
||||
event = build_menu_item_event(
|
||||
item, restaurant, pubkey, ancestor_names=ancestors
|
||||
item, restaurant, signer.pubkey, ancestor_names=ancestors
|
||||
)
|
||||
published = await publish_event(nostr_client, event, prvkey)
|
||||
published = await publish_event(nostr_client, event, signer)
|
||||
if published:
|
||||
item.nostr_event_id = published.id
|
||||
item.nostr_event_created_at = published.created_at
|
||||
|
|
@ -213,15 +205,14 @@ async def _publish_menu_item_delete(item: MenuItem) -> None:
|
|||
restaurant = await get_restaurant(item.restaurant_id)
|
||||
if not restaurant:
|
||||
return
|
||||
keypair = await _resolve_signing_keypair(restaurant)
|
||||
if not keypair:
|
||||
signer = await _resolve_signer(restaurant)
|
||||
if signer is None:
|
||||
return
|
||||
pubkey, prvkey = keypair
|
||||
|
||||
from . import nostr_client
|
||||
|
||||
event = build_delete_event(30402, item.id, pubkey, "Menu item removed")
|
||||
await publish_event(nostr_client, event, prvkey)
|
||||
event = build_delete_event(30402, item.id, signer.pubkey, "Menu item removed")
|
||||
await publish_event(nostr_client, event, signer)
|
||||
|
||||
|
||||
def _require_owner(restaurant: Restaurant, wallet: WalletTypeInfo) -> None:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue