feat(cassettes): swap the count-publish endpoint for operation endpoints
The operator can no longer write a count. POST .../cassettes/ops records one operation — refill, empty, recount, set_denomination — and publishes the machine's recent window; GET .../cassettes/ops lists them newest first with acked_at, so the dashboard can tell a delivered operation from one merely sent. POST .../cassettes/publish is gone, along with update_cassette_config and UpsertCassetteConfigData. Nothing in the operator can now set a count, which is the point: a value with one writer cannot be clobbered. Under the old endpoint a dashboard form loaded before a dispense silently discarded that dispense on publish, and neither side could detect it — addressable events order by created_at at second granularity and a relay returns OK for an event it then drops, so the losing writer is never told. The op is recorded before the publish and is deliberately not rolled back when the publish fails. It records something that physically happened; notes went into a bay whether or not a relay was reachable. The window carries recent operations rather than just the newest, so an op that missed its own publish rides out with the next one. Validation rejects an unpaired machine and a position the machine has not reported. Bay count stays hardware-determined.
This commit is contained in:
parent
3d8368bcc4
commit
2ac3e2064e
5 changed files with 89 additions and 214 deletions
|
|
@ -36,7 +36,7 @@ startup, after every change to its bays, and on a heartbeat):
|
|||
|
||||
This module owns the wire-format side of both directions. The consumer
|
||||
task (tasks.py) calls `decrypt_and_parse_state_event` per incoming event;
|
||||
the API endpoint (views_api.py) calls `publish_to_atm` per operator submit.
|
||||
the API endpoint (views_api.py) calls `publish_ops_to_atm` per operation.
|
||||
|
||||
The `<m>` placeholder semantics (load-bearing per the 2026-05-30T11:50Z
|
||||
coord-log entry): always the ATM's hex pubkey, NEVER spirekeeper's
|
||||
|
|
@ -86,7 +86,7 @@ __all__ = [
|
|||
"RelayUnavailable",
|
||||
"build_state_d_tags_for_machines",
|
||||
"decrypt_and_parse_state_event",
|
||||
"publish_to_atm",
|
||||
"publish_ops_to_atm",
|
||||
]
|
||||
|
||||
_D_TAG_CONFIG_PREFIX = "bitspire-cassettes:" # operator → ATM
|
||||
|
|
@ -163,33 +163,6 @@ def build_state_d_tags_for_machines(machines: list[Machine]) -> list[str]:
|
|||
# =============================================================================
|
||||
|
||||
|
||||
async def publish_to_atm(
|
||||
machine: Machine,
|
||||
payload: PublishCassettesPayload,
|
||||
operator_user_id: str,
|
||||
) -> dict:
|
||||
"""Build, encrypt, sign, and publish a kind-30078 cassette config event
|
||||
from the operator to the target ATM.
|
||||
|
||||
Returns the signed event dict on success (caller may log event.id for
|
||||
audit). Raises NostrPublishError subclasses (re-exported here as
|
||||
CassetteTransportError, OperatorIdentityMissing, SignerUnavailable,
|
||||
RelayUnavailable) on hard failures.
|
||||
"""
|
||||
atm_pubkey_hex = _atm_hex_pubkey(machine)
|
||||
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 config (machine={machine.id}, "
|
||||
f"positions={sorted(payload.positions.keys())})"
|
||||
),
|
||||
)
|
||||
return signed
|
||||
|
||||
|
||||
async def publish_ops_to_atm(
|
||||
machine: Machine,
|
||||
ops: list[CassetteOp],
|
||||
|
|
@ -224,7 +197,7 @@ async def publish_ops_to_atm(
|
|||
|
||||
|
||||
# =============================================================================
|
||||
# Consume — ATM → operator (the bootstrap consumer task)
|
||||
# Consume — ATM → operator (the machine's state reports)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue