diff --git a/migrations.py b/migrations.py index b8e6ec0..da8ba07 100644 --- a/migrations.py +++ b/migrations.py @@ -735,3 +735,44 @@ async def m009_split_fee_fractions_by_direction(db): await db.execute( "ALTER TABLE spirekeeper.super_config DROP COLUMN super_fee_fraction" ) + + +async def m010_add_machine_bunker_pairing(db): + """Add NIP-46 bunker-pairing columns to dca_machines for seed-URL + pairing (S0 / aiolabs/spirekeeper#9; spire-side aiolabs/bitspire#52). + + Under the chosen model (A1, decided 2026-06-16), the spire's signing + key lives inside the operator's nsecbunkerd rather than on the spire's + disk. `pair_machine` mints a per-spire key in the bunker, issues a + scoped NIP-46 connect token, and hands the spire a one-shot seed URL + embedding a `bunker://` connection. The spire then self-signs all its + events (kind-21000 RPC, kind-30078 beacon/cassette-state) as its own + bunker-held key; lnbits' path-B roster routes that npub to the + operator's wallet. + + ("spire" = a bitSpire machine; the legacy Lamassu term was "ATM".) + + - bunker_spire_key_name — the spire's key name inside the bunker + (`spire-`). Used to re-issue a connect token on + re-pair and (once the admin client grows a revoke RPC) to revoke + spire access. + - paired_at — timestamp of the last successful pair. NULL = the + machine row exists but no bunker key has been minted yet. + + Both nullable: machines created before this migration, and registered + -but-never-paired machines, carry NULL until first pair. Idempotent + column-probe pattern (same shape as m009). + """ + additions = [ + ("dca_machines", "bunker_spire_key_name", "TEXT"), + ("dca_machines", "paired_at", "TIMESTAMP"), + ] + for table, col, coltype in additions: + try: + await db.fetchone(f"SELECT {col} FROM spirekeeper.{table} LIMIT 1") + continue + except Exception: + pass + await db.execute( + f"ALTER TABLE spirekeeper.{table} ADD COLUMN {col} {coltype}" + ) diff --git a/models.py b/models.py index c158fba..90df810 100644 --- a/models.py +++ b/models.py @@ -56,6 +56,9 @@ class Machine(BaseModel): is_active: bool operator_cash_in_fee_fraction: float = 0.0 operator_cash_out_fee_fraction: float = 0.0 + # NIP-46 bunker pairing (S0 / #9). NULL until the spire is first paired. + bunker_spire_key_name: str | None = None + paired_at: datetime | None = None created_at: datetime updated_at: datetime