feat(v2): bootstrap consumer task — auto-populate cassette_configs (#29 v1)
Long-running task wired into satmachineadmin_start that subscribes to kind-30078 bitspire-cassettes-state:<atm_pubkey_hex> events from every active machine's ATM and upserts cassette_configs via apply_bootstrap_state on receipt. Pairs with bitspire's one-shot bootstrap publish in aiolabs/lamassu-next#56 — operator's first config publish then validates against a non-empty denomination set. Pattern mirrors wait_for_paid_invoices (try/except per event, never lets the loop die). Uses the same nostr_client.relay_manager singleton that cassette_transport.publish_to_atm uses, just on the subscribe side. Implementation: poll the singleton NostrRouter.received_subscription_events dict keyed by our subscription_id (satmachineadmin-cassette-bootstrap). This is the same drain pattern nostrclient's per-WebSocket NostrRouter uses; since we use a distinct sub_id, no cross-contamination with WebSocket-connected clients of nostrclient. Filter is re-derived from active machines each tick — newly-added machines start receiving bootstrap events without an LNbits restart. Soft-fail surfaces (none crash the listener): - nostrclient extension not installed → log + 30s backoff - inbound event sig-verify fails → log + skip - sender pubkey not in dca_machines → log + skip (relay noise) - operator privkey not on file → log + skip - NIP-44 v2 decrypt / payload validation fails → log + skip - apply_bootstrap_state error → log + skip Per-event handler routes to the right operator's privkey by looking up the machine via get_machine_by_atm_pubkey_hex (O(N) over active machines — fine for small fleets; if fleets grow, normalize machine_npub at write + add an index). CRUD additions: - list_all_active_machines: cross-operator query for the subscription filter - get_machine_by_atm_pubkey_hex: route inbound events to the right machine row + operator account; accepts hex or bech32 storage 14 tests in test_cassette_state_consumer.py covering: - decrypt_and_parse_state_event happy path + 6 negative paths (tamper, wrong privkey, malformed pubkey, missing fields, garbage JSON, wrong-shape payload) - d-tag construction regression guard (REGRESSION GUARD: d-tag uses ATM hex pubkey not internal UUID — pins the load-bearing detail from coord-log 11:50Z) - build_state_d_tags_for_machines + bech32 → hex canonicalisation Full handler dispatch (verify_event → get_machine_by_atm_pubkey_hex → apply_bootstrap_state) needs a live LNbits DB; smoke-tested manually per the existing project convention. Total: 146 passed, 1 skipped (cross-test fixture pending), 1 pre-existing async-plugin failure unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b9d5ea3c57
commit
e57a73083e
4 changed files with 535 additions and 1 deletions
39
crud.py
39
crud.py
|
|
@ -144,6 +144,45 @@ async def get_machines_for_operator(operator_user_id: str) -> List[Machine]:
|
|||
)
|
||||
|
||||
|
||||
async def list_all_active_machines() -> List[Machine]:
|
||||
"""Used by the cassette bootstrap consumer task to build a single
|
||||
cross-operator subscription filter. Each event's pubkey routes to
|
||||
the right operator via get_machine_by_atm_pubkey_hex + the machine's
|
||||
operator_user_id.
|
||||
"""
|
||||
return await db.fetchall(
|
||||
"""
|
||||
SELECT * FROM satoshimachine.dca_machines
|
||||
WHERE is_active = true
|
||||
ORDER BY created_at DESC
|
||||
""",
|
||||
{},
|
||||
Machine,
|
||||
)
|
||||
|
||||
|
||||
async def get_machine_by_atm_pubkey_hex(atm_pubkey_hex: str) -> Optional[Machine]:
|
||||
"""Look up an active machine by its ATM pubkey, accepting hex or bech32
|
||||
in machine_npub. Used by the cassette bootstrap consumer to route an
|
||||
incoming state event to the right machine row (and therefore operator
|
||||
privkey for decryption).
|
||||
|
||||
O(N) over active machines — fine for small fleets. If fleet sizes
|
||||
grow, normalise machine_npub-at-write to hex and add an index.
|
||||
"""
|
||||
from lnbits.utils.nostr import normalize_public_key
|
||||
|
||||
target = atm_pubkey_hex.lower()
|
||||
machines = await list_all_active_machines()
|
||||
for m in machines:
|
||||
try:
|
||||
if normalize_public_key(m.machine_npub).lower() == target:
|
||||
return m
|
||||
except (ValueError, AssertionError):
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
async def update_machine(machine_id: str, data: UpdateMachineData) -> Optional[Machine]:
|
||||
update_data = {k: v for k, v in data.dict().items() if v is not None}
|
||||
if not update_data:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue