feat(cassettes): publish operations to the ATM
The v2 operator to ATM wire. Same kind-30078 document and the same d-tag the counts wire used, because the machine subscribes by that tag and the document is addressable, so v2 replaces v1 in place. Sends a WINDOW of recent operations, oldest-first, not just the newest change. Each publish replaces the last, so a machine that was offline for one of them would otherwise never see that operation again; carrying the recent history means the channel heals itself without anyone noticing it broke. Re-delivery costs nothing because every op carries an id the machine dedups on. Tests pin the contract rather than the implementation: the d-tag, that the payload declares v2 and carries no positions key, that window order survives the publisher untouched, that an empty window still ships a well-formed document so a machine can tell "no operations" from "operator still on v1", and that an npub entered in the UI is normalised to hex — get that last one wrong and the machine's subscription filter silently never matches. Additive. The endpoints still publish counts until the next commit, so the tree is not left half-switched. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
b2157db219
commit
90bd43d6da
2 changed files with 138 additions and 3 deletions
|
|
@ -17,8 +17,14 @@ publishes position-keyed cassette config to a target ATM via:
|
|||
The ATM-side consumer (lamassu-next#56) subscribes by the d-tag + its own
|
||||
npub, decrypts, validates, applies, hot-reloads HAL.
|
||||
|
||||
Reverse direction (ATM → operator, v1 = one-shot bootstrap on first boot,
|
||||
v2 = continuous reverse channel for reconciliation):
|
||||
The operator → ATM direction carries OPERATIONS as of v2 (bitspire ADR-004):
|
||||
refill, empty, recount, set_denomination, each with an id the machine dedups
|
||||
on. It used to carry absolute counts, which meant the operator and the machine
|
||||
both wrote the same value over a transport that never tells a writer it lost —
|
||||
so a form loaded before a dispense discarded that dispense when published.
|
||||
|
||||
Reverse direction (ATM → operator, continuous: the machine publishes on
|
||||
startup, after every change to its bays, and on a heartbeat):
|
||||
|
||||
kind = 30078
|
||||
tags = [
|
||||
|
|
@ -52,7 +58,12 @@ from lnbits.core.signers.base import (
|
|||
)
|
||||
from lnbits.utils.nostr import normalize_public_key
|
||||
|
||||
from .models import Machine, PublishCassettesPayload
|
||||
from .models import (
|
||||
CassetteOp,
|
||||
Machine,
|
||||
PublishCassetteOpsPayload,
|
||||
PublishCassettesPayload,
|
||||
)
|
||||
from .nip44 import Nip44Error
|
||||
from .nostr_publish import (
|
||||
NostrPublishError,
|
||||
|
|
@ -179,6 +190,39 @@ async def publish_to_atm(
|
|||
return signed
|
||||
|
||||
|
||||
async def publish_ops_to_atm(
|
||||
machine: Machine,
|
||||
ops: list[CassetteOp],
|
||||
operator_user_id: str,
|
||||
) -> dict:
|
||||
"""Publish the operator's recent cassette OPERATIONS to the target ATM.
|
||||
|
||||
The v2 wire (bitspire ADR-004). Replaces sending absolute counts, which
|
||||
let a dashboard form loaded before a dispense silently discard that
|
||||
dispense — the operator and the machine were both writing the same value
|
||||
over a transport that never tells a writer it lost.
|
||||
|
||||
`ops` is a WINDOW, oldest-first, not just the newest change. The event is
|
||||
addressable, so each publish replaces the last, and a machine that was
|
||||
offline for one of them would otherwise never see that operation again.
|
||||
Carrying the recent history means the channel heals itself without anyone
|
||||
noticing it broke. Re-delivery is harmless because each op carries an id
|
||||
the machine dedups on.
|
||||
"""
|
||||
atm_pubkey_hex = _atm_hex_pubkey(machine)
|
||||
payload = PublishCassetteOpsPayload(ops=ops)
|
||||
signed = await publish_encrypted_kind_30078(
|
||||
operator_user_id=operator_user_id,
|
||||
recipient_pubkey_hex=atm_pubkey_hex,
|
||||
d_tag=_config_d_tag(atm_pubkey_hex),
|
||||
payload=payload.to_wire_dict(),
|
||||
log_context=(
|
||||
f"cassette ops (machine={machine.id}, ops={[o.op_type for o in ops]})"
|
||||
),
|
||||
)
|
||||
return signed
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Consume — ATM → operator (the bootstrap consumer task)
|
||||
# =============================================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue