refactor(v2): cassette CRUD + transport — position-keyed (#29 v1.1)
CRUD layer flips:
- get_cassette_config(machine_id, position) — was (..., denomination)
- list_cassette_configs_for_machine returns ORDER BY position alone
(no secondary denomination ordering — position is the unique key)
- update_cassette_config(machine_id, position, data, updated_by) —
operator edits denomination + count for a fixed slot
- apply_bootstrap_state upserts ON CONFLICT(machine_id, position)
iterating payload.positions; populates new state_denomination
column from row.denomination alongside state_count
cassette_transport.py needs almost no functional change — the wire
shape is implicit via PublishCassettesPayload.to_wire_dict (now emits
{"positions": {...}}) and decrypt_and_parse_state_event accepts what
the model parses. Just the module docstring + the publish log line
get updated to reference positions rather than denominations.
Tests still red until commit f rewrites them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
427cad33de
commit
5dbd7314f4
2 changed files with 38 additions and 34 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
68
crud.py
68
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:<m> 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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue