diff --git a/cassette_transport.py b/cassette_transport.py index 4bae8b1..5862e11 100644 --- a/cassette_transport.py +++ b/cassette_transport.py @@ -3,7 +3,7 @@ Cassette-config Nostr transport — operator ↔ ATM kind-30078 publish + consum Per the locked design at aiolabs/satmachineadmin#29 (paired with lamassu-next#56) and the dcd0874 privacy-by-default pivot, the operator -publishes denomination-keyed cassette config to a target ATM via: +publishes position-keyed cassette config to a target ATM via: kind = 30078 (NIP-78, replaceable) tags = [ @@ -304,7 +304,7 @@ async def publish_to_atm( logger.info( f"satmachineadmin: published kind-30078 cassette config to ATM " f"{atm_pubkey_hex[:12]}... (event_id={signed['id'][:12]}..., " - f"machine_id={machine.id}, denominations={list(payload.denominations.keys())})" + f"machine_id={machine.id}, positions={sorted(payload.positions.keys())})" ) return signed diff --git a/crud.py b/crud.py index 9da14c8..01ee876 100644 --- a/crud.py +++ b/crud.py @@ -1379,14 +1379,14 @@ async def upsert_fleet_snapshot( # ============================================================================= -# Cassette configs — operator-driven ATM cassette inventory (#29). +# Cassette configs — operator-driven ATM cassette inventory (#29 v1.1). # ============================================================================= # Row lifecycle per #29: -# - First population for a (machine_id, denomination) pair → apply_bootstrap_state +# - First population for a (machine_id, position) pair → apply_bootstrap_state # (consumer reading the ATM's one-shot bitspire-cassettes-state event) -# - Operator edit of count or position → update_cassette_config (refuses to -# create new rows; the denomination set is hardware-determined) -# - Row creation/deletion for a new denomination → admin only, via ATM +# - Operator edit of denomination or count → update_cassette_config +# (refuses to create new rows; the slot count is hardware-determined) +# - Row creation/deletion for a new position → admin only, via ATM # re-provisioning + new bootstrap event (not exposed in v1 here) @@ -1407,12 +1407,12 @@ def _should_apply_bootstrap_state( async def get_cassette_config( - machine_id: str, denomination: int + machine_id: str, position: int ) -> Optional[CassetteConfig]: return await db.fetchone( "SELECT * FROM satoshimachine.cassette_configs " - "WHERE machine_id = :mid AND denomination = :denom", - {"mid": machine_id, "denom": denomination}, + "WHERE machine_id = :mid AND position = :pos", + {"mid": machine_id, "pos": position}, CassetteConfig, ) @@ -1422,7 +1422,7 @@ async def list_cassette_configs_for_machine( ) -> List[CassetteConfig]: return await db.fetchall( "SELECT * FROM satoshimachine.cassette_configs " - "WHERE machine_id = :mid ORDER BY position, denomination", + "WHERE machine_id = :mid ORDER BY position", {"mid": machine_id}, CassetteConfig, ) @@ -1430,18 +1430,18 @@ async def list_cassette_configs_for_machine( async def update_cassette_config( machine_id: str, - denomination: int, + position: int, data: UpsertCassetteConfigData, *, updated_by: Optional[str] = None, ) -> Optional[CassetteConfig]: - """Operator-driven row update: change count and/or position for a single - cassette. Refuses to create new rows — those only land via + """Operator-driven row update: change denomination and/or count for a + single cassette slot. Refuses to create new rows — those only land via apply_bootstrap_state() consuming an ATM bootstrap event (per #29 row - lifecycle: hardware-determined denomination set, not operator-creatable). - Returns None if the (machine_id, denomination) row doesn't exist. + lifecycle: hardware-determined slot count, not operator-creatable). + Returns None if the (machine_id, position) row doesn't exist. """ - existing = await get_cassette_config(machine_id, denomination) + existing = await get_cassette_config(machine_id, position) if existing is None: return None update_data: dict = {k: v for k, v in data.dict().items() if v is not None} @@ -1451,13 +1451,13 @@ async def update_cassette_config( update_data["updated_by"] = updated_by set_clause = ", ".join(f"{k} = :{k}" for k in update_data) update_data["mid"] = machine_id - update_data["denom"] = denomination + update_data["pos"] = position await db.execute( f"UPDATE satoshimachine.cassette_configs SET {set_clause} " - "WHERE machine_id = :mid AND denomination = :denom", + "WHERE machine_id = :mid AND position = :pos", update_data, ) - return await get_cassette_config(machine_id, denomination) + return await get_cassette_config(machine_id, position) async def apply_bootstrap_state( @@ -1467,17 +1467,18 @@ async def apply_bootstrap_state( payload: PublishCassettesPayload, ) -> bool: """Consume an ATM-published kind-30078 bitspire-cassettes-state: event - and upsert one cassette_configs row per denomination in the payload. + and upsert one cassette_configs row per position in the payload. Returns True if the upsert ran; False if any existing row for this machine already references this event_id (idempotent on relay re-delivery / restart). - Populates both the operator-believed columns (count, position, + Populates both the operator-believed columns (denomination, count, updated_at, updated_by='atm-bootstrap') AND the v2 reverse-channel - columns (state_count, state_at, state_event_id) so the operator's - initial view matches the ATM's reported state. v2 reconciliation UI - will diverge them when continuous reverse-channel events land. + columns (state_denomination, state_count, state_at, state_event_id) + so the operator's initial view matches the ATM's reported state. v2 + reconciliation UI will diverge them when continuous reverse-channel + events land + the operator subsequently edits. """ existing_first = await db.fetchone( "SELECT state_event_id FROM satoshimachine.cassette_configs " @@ -1495,30 +1496,33 @@ async def apply_bootstrap_state( return False now = datetime.now() - for denom, row in payload.denominations.items(): + for pos, row in payload.positions.items(): await db.execute( """ INSERT INTO satoshimachine.cassette_configs - (machine_id, denomination, count, position, updated_at, - updated_by, state_count, state_at, state_event_id) - VALUES (:mid, :denom, :count, :pos, :now, :by, - :state_count, :state_at, :event_id) - ON CONFLICT (machine_id, denomination) DO UPDATE SET + (machine_id, position, denomination, count, updated_at, + updated_by, state_denomination, state_count, state_at, + state_event_id) + VALUES (:mid, :pos, :denom, :count, :now, :by, + :state_denom, :state_count, :state_at, :event_id) + ON CONFLICT (machine_id, position) DO UPDATE SET + denomination = excluded.denomination, count = excluded.count, - position = excluded.position, updated_at = excluded.updated_at, updated_by = excluded.updated_by, + state_denomination = excluded.state_denomination, state_count = excluded.state_count, state_at = excluded.state_at, state_event_id = excluded.state_event_id """, { "mid": machine_id, - "denom": denom, + "pos": pos, + "denom": row.denomination, "count": row.count, - "pos": row.position, "now": now, "by": "atm-bootstrap", + "state_denom": row.denomination, "state_count": row.count, "state_at": event_created_at, "event_id": event_id,