feat: publish ticket counts in NIP-52 tags + republish on sale
Some checks failed
lint.yml / feat: publish ticket counts in NIP-52 tags + republish on sale (pull_request) Failing after 0s

Inventory sync over Nostr, mirroring how nostrmarket republishes
kind 30018 product events when stock changes. Connected webapp /
other-client subscriptions pick up the new state via their existing
relay subscription — no REST polling needed.

build_nip52_event grows four AIO custom tags on every published
kind 31922/31923 event:
- tickets_available — current remaining (omitted when amount_tickets
  is 0, the schema's "unlimited" sentinel, so clients can tell the
  difference between unlimited and sold-out)
- tickets_sold — running count, always emitted (clients derive
  original_capacity = available + sold for progress bars)
- tickets_price — price_per_ticket (0 means free)
- tickets_currency — the currency string

Tags are AIO additions outside the NIP-52 spec; spec-compliant
clients MUST ignore unknown tags so this stays backwards-compatible.

set_ticket_paid calls publish_or_delete_nostr_event after the
counter update so the new state lands on relays. The whole sequence
(counter update + republish) is wrapped in a per-event-id asyncio
lock to address the existing # todo: lock and to ensure two paid
invoices for the same event can't reorder the published state.

Failures inside the Nostr publish are logged + swallowed by the
existing wrapper, so a relay outage can never break the payment
flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-23 20:31:56 +02:00
commit edf1493e0c
2 changed files with 56 additions and 11 deletions

View file

@ -1,5 +1,6 @@
from __future__ import annotations
import asyncio
from asyncio.tasks import create_task
from lnbits.core.models.users import UserNotifications
@ -21,6 +22,7 @@ from .crud import (
update_ticket,
)
from .models import Event, Ticket
from .nostr_hooks import publish_or_delete_nostr_event
DEFAULT_NOSTR_RELAYS = [
"wss://relay.damus.io",
@ -28,19 +30,42 @@ DEFAULT_NOSTR_RELAYS = [
"wss://relay.nostr.band",
]
# Per-event lock: serializes the counter-update + Nostr republish for a
# single event_id so two paid invoices landing on the listener queue back-
# to-back can't reorder the published state. Lazy-populated; entries are
# left in memory for the lifetime of the process (cheap — one asyncio.Lock
# object per event ever sold).
_event_paid_locks: dict[str, asyncio.Lock] = {}
def _event_paid_lock(event_id: str) -> asyncio.Lock:
lock = _event_paid_locks.get(event_id)
if lock is None:
lock = asyncio.Lock()
_event_paid_locks[event_id] = lock
return lock
async def set_ticket_paid(ticket: Ticket) -> Ticket:
if ticket.paid:
return ticket
ticket.paid = True
await update_ticket(ticket)
async with _event_paid_lock(ticket.event):
ticket.paid = True
await update_ticket(ticket)
event = await get_event(ticket.event)
assert event, "Couldn't get event from ticket being paid"
event.sold += 1
event.amount_tickets -= 1
await update_event(event)
event = await get_event(ticket.event)
assert event, "Couldn't get event from ticket being paid"
event.sold += 1
event.amount_tickets -= 1
await update_event(event)
# Republish the NIP-52 calendar event so connected clients see
# the new tickets_available / tickets_sold counters via their
# existing relay subscription. Failures are logged + swallowed
# inside publish_or_delete_nostr_event so a Nostr outage doesn't
# break the payment flow.
await publish_or_delete_nostr_event(event)
return ticket