feat(v2): operator-scoped CRUD + stub legacy entry points

Replaces v1's super-only single-config CRUD with the v2 operator-scoped data
layer that matches the m005 schema:

- Machines: create/get/get_by_npub/list_for_operator/update/delete
- Clients: scoped per (machine, user). Adds list_for_operator (across an
  operator's fleet) and list_for_user (LP cross-operator view), plus
  get_flow_mode_clients_for_machine for the distribution algorithm.
- Deposits: now carry machine_id and creator_user_id; per-operator listing.
- Settlements: create_settlement_idempotent treats bitspire_event_id as the
  uniqueness key, returning the existing row on replay so subscription
  re-delivery is safe by construction. mark_settlement_status drives the
  pending → processed/partial/refunded/errored lifecycle.
- Commission splits: replace_commission_splits is an atomic per-scope
  replace; the SetCommissionSplitsData model already validates legs sum
  to 1.0 at the boundary. get_effective_commission_splits handles the
  per-machine-override-or-operator-default precedence.
- Payments: leg-typed (dca / super_fee / operator_split / settlement /
  autoforward / refund) with helpers for settlement/client/operator scopes.
- Balance summary: sums confirmed deposits minus completed dca legs.
- Telemetry: upsert_beacon_snapshot uses COALESCE so today's sparse
  kind-30078 payload doesn't clobber post-#43 fields when they start
  arriving. upsert_fleet_snapshot stores raw JSON until lamassu-next#42
  fixes the kind-30079 schema.
- Super config: singleton get/update.

Also stubs three legacy entry points so __init__.py imports cleanly while
the rest of P0/P1 is in flight:

- tasks.py: no-op stubs for wait_for_paid_invoices + hourly_transaction_polling.
  Real Nostr subscription manager lands in P1.
- views_api.py: a single /api/v1/dca/{...} catch-all returns 503 with a
  precise message. v2 endpoints land in P1+.
- views.py: drops the super-only check on the index page (v2 is
  operator-installable); platform-fee config moves to a super-only API in P1.

transaction_processor.py is left untouched but is now orphaned (no one
imports it) — gets a full rewrite in P1.

Refs: plan at ~/.claude/plans/snug-gliding-shamir.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-14 14:37:48 +02:00
commit 937749f149
4 changed files with 798 additions and 969 deletions

View file

@ -1,53 +1,32 @@
import asyncio
from datetime import datetime
# Satoshi Machine v2 — task placeholders.
#
# The v1 SSH/PostgreSQL polling + invoice listener are intentionally absent.
# They will be replaced in P1 (Nostr subscription manager: subscribes via
# lnbits.core.services.nostr_transport to kind-21000 settlements + kind-30078
# beacons + kind-30079 telemetry per registered machine, with auto-reconnect).
#
# These no-op stubs keep __init__.py importable in the interim so the
# extension can be activated even before P1 lands.
import asyncio
from lnbits.core.models import Payment
from lnbits.core.services import websocket_updater
from lnbits.tasks import register_invoice_listener
from loguru import logger
from .transaction_processor import poll_lamassu_transactions
#######################################
########## RUN YOUR TASKS HERE ########
#######################################
# The usual task is to listen to invoices related to this extension
async def wait_for_paid_invoices():
"""Invoice listener for DCA-related payments"""
invoice_queue = asyncio.Queue()
register_invoice_listener(invoice_queue, "ext_satmachineadmin")
async def wait_for_paid_invoices() -> None:
"""No-op placeholder pending P1 Nostr subscription manager."""
logger.debug(
"satmachineadmin v2: invoice listener stub running. "
"Real Nostr-transport subscription pending P1."
)
# Sleep forever; the task system expects a long-lived coroutine.
while True:
payment = await invoice_queue.get()
await on_invoice_paid(payment)
await asyncio.sleep(3600)
async def hourly_transaction_polling():
"""Background task that polls Lamassu database every hour for new transactions"""
logger.info("Starting hourly Lamassu transaction polling task")
async def hourly_transaction_polling() -> None:
"""No-op placeholder. The v1 Lamassu PostgreSQL poller is gone — bitSpire
settlements arrive push-based via Nostr kind-21000 in v2."""
logger.debug("satmachineadmin v2: legacy polling stub (no-op).")
while True:
try:
logger.info(f"Running Lamassu transaction poll at {datetime.now()}")
await poll_lamassu_transactions()
logger.info("Completed Lamassu transaction poll, sleeping for 1 hour")
# Sleep for 1 hour (3600 seconds)
await asyncio.sleep(3600)
except Exception as e:
logger.error(f"Error in hourly polling task: {e}")
# Sleep for 5 minutes before retrying on error
await asyncio.sleep(300)
async def on_invoice_paid(payment: Payment) -> None:
"""Handle DCA-related invoice payments"""
# DCA payments are handled internally by the transaction processor
# This function can be extended if needed for additional payment processing
if payment.extra.get("tag") in ["dca_distribution", "dca_commission"]:
logger.info(f"DCA payment processed: {payment.checking_id} - {payment.amount} sats")
# Could add websocket notifications here if needed
pass
await asyncio.sleep(3600)