refactor: rename extension identity to spirekeeper

Fork of satmachineadmin's v2-bitspire line into its own repo. Renames
both identifiers so this extension is fully independent of the original
satmachineadmin install (which remains in service):

  - extension id   satmachineadmin -> spirekeeper
    (router prefix, static path/static_url_for, module symbols, task
     names, templates dir, config/manifest paths)
  - database name  satoshimachine  -> spirekeeper
    (Database(ext_spirekeeper), all schema-qualified table refs)

Also resets versioning to 0.1.0, sets the display name + manifest to
spirekeeper/aiolabs, and fixes the placeholder pyproject description.
Historical aiolabs/satmachineadmin#N issue references in comments are
left pointing at the original repo where those issues live.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-06-13 22:30:05 +02:00
commit a059e3f596
22 changed files with 242 additions and 242 deletions

View file

@ -7,29 +7,29 @@ from loguru import logger
from .crud import db from .crud import db
from .nostr_transport_roster import register_with_lnbits as register_roster_with_lnbits from .nostr_transport_roster import register_with_lnbits as register_roster_with_lnbits
from .tasks import wait_for_cassette_state_events, wait_for_paid_invoices from .tasks import wait_for_cassette_state_events, wait_for_paid_invoices
from .views import satmachineadmin_generic_router from .views import spirekeeper_generic_router
from .views_api import satmachineadmin_api_router from .views_api import spirekeeper_api_router
logger.info("satmachineadmin v2 loaded") logger.info("spirekeeper v2 loaded")
satmachineadmin_ext: APIRouter = APIRouter( spirekeeper_ext: APIRouter = APIRouter(
prefix="/satmachineadmin", tags=["DCA Admin"] prefix="/spirekeeper", tags=["DCA Admin"]
) )
satmachineadmin_ext.include_router(satmachineadmin_generic_router) spirekeeper_ext.include_router(spirekeeper_generic_router)
satmachineadmin_ext.include_router(satmachineadmin_api_router) spirekeeper_ext.include_router(spirekeeper_api_router)
satmachineadmin_static_files = [ spirekeeper_static_files = [
{ {
"path": "/satmachineadmin/static", "path": "/spirekeeper/static",
"name": "satmachineadmin_static", "name": "spirekeeper_static",
} }
] ]
scheduled_tasks: list[asyncio.Task] = [] scheduled_tasks: list[asyncio.Task] = []
def satmachineadmin_stop(): def spirekeeper_stop():
for task in scheduled_tasks: for task in scheduled_tasks:
try: try:
task.cancel() task.cancel()
@ -37,10 +37,10 @@ def satmachineadmin_stop():
logger.warning(ex) logger.warning(ex)
def satmachineadmin_start(): def spirekeeper_start():
# bitSpire invoice listener — replaces the v1 SSH/PostgreSQL poller. # bitSpire invoice listener — replaces the v1 SSH/PostgreSQL poller.
invoice_task = create_permanent_unique_task( invoice_task = create_permanent_unique_task(
"ext_satmachineadmin", wait_for_paid_invoices "ext_spirekeeper", wait_for_paid_invoices
) )
scheduled_tasks.append(invoice_task) scheduled_tasks.append(invoice_task)
# Cassette bootstrap consumer (#29 v1) — subscribes to # Cassette bootstrap consumer (#29 v1) — subscribes to
@ -48,7 +48,7 @@ def satmachineadmin_start():
# cassette_configs on receipt. Soft-fails if nostrclient isn't # cassette_configs on receipt. Soft-fails if nostrclient isn't
# installed (logs + backs off, never crashes). # installed (logs + backs off, never crashes).
cassette_task = create_permanent_unique_task( cassette_task = create_permanent_unique_task(
"ext_satmachineadmin_cassette_bootstrap", wait_for_cassette_state_events "ext_spirekeeper_cassette_bootstrap", wait_for_cassette_state_events
) )
scheduled_tasks.append(cassette_task) scheduled_tasks.append(cassette_task)
# Path-B wallet-routing hook (#20 / coord-log 2026-05-31T15:25Z): # Path-B wallet-routing hook (#20 / coord-log 2026-05-31T15:25Z):
@ -61,8 +61,8 @@ def satmachineadmin_start():
__all__ = [ __all__ = [
"db", "db",
"satmachineadmin_ext", "spirekeeper_ext",
"satmachineadmin_start", "spirekeeper_start",
"satmachineadmin_static_files", "spirekeeper_static_files",
"satmachineadmin_stop", "spirekeeper_stop",
] ]

View file

@ -94,7 +94,7 @@ class SettlementMetadataError(ValueError):
Raised by `parse_settlement`. Caller records the settlement as Raised by `parse_settlement`. Caller records the settlement as
'rejected' with the exception message in `error_message`. Operator 'rejected' with the exception message in `error_message`. Operator
investigates the ATM that issued the invoice a bitSpire ATM that investigates the ATM that issued the invoice a bitSpire ATM that
landed on a satmachineadmin-managed wallet without stamping the landed on a spirekeeper-managed wallet without stamping the
canonical fields is a real upstream bug (lamassu-next side), not a canonical fields is a real upstream bug (lamassu-next side), not a
graceful-degradation case. Pre-v2 reverse-derivation from the graceful-degradation case. Pre-v2 reverse-derivation from the
wire amount + a machine-level fallback rate is no longer supported: wire amount + a machine-level fallback rate is no longer supported:
@ -279,7 +279,7 @@ def parse_settlement(
) )
# Phase-1 observability per aiolabs/satmachineadmin#38 + coord-log # Phase-1 observability per aiolabs/satmachineadmin#38 + coord-log
# §2026-06-01T07:00Z (option A locked): compare bitspire's reported # §2026-06-01T07:00Z (option A locked): compare bitspire's reported
# fee_sats against satmachineadmin's recompute, log on out-of- # fee_sats against spirekeeper's recompute, log on out-of-
# tolerance drift, record the delta unconditionally for triage. # tolerance drift, record the delta unconditionally for triage.
# Phase 2 (settlement-reject) lands after observability data. # Phase 2 (settlement-reject) lands after observability data.
fee_mismatch_sats = fee_sats - (platform_fee_sats + operator_fee_sats) fee_mismatch_sats = fee_sats - (platform_fee_sats + operator_fee_sats)

View file

@ -33,7 +33,7 @@ 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_to_atm` per operator submit.
The `<m>` placeholder semantics (load-bearing per the 2026-05-30T11:50Z The `<m>` placeholder semantics (load-bearing per the 2026-05-30T11:50Z
coord-log entry): always the ATM's hex pubkey, NEVER satmachineadmin's coord-log entry): always the ATM's hex pubkey, NEVER spirekeeper's
internal dca_machines.id UUID. Helper `_atm_hex_pubkey(machine)` internal dca_machines.id UUID. Helper `_atm_hex_pubkey(machine)`
centralises the canonicalisation via lnbits.utils.nostr.normalize_public_key. centralises the canonicalisation via lnbits.utils.nostr.normalize_public_key.
""" """
@ -146,7 +146,7 @@ def build_state_d_tags_for_machines(machines: list[Machine]) -> list[str]:
# ============================================================================= # =============================================================================
# Publish — operator → ATM (the satmachineadmin API path) # Publish — operator → ATM (the spirekeeper API path)
# ============================================================================= # =============================================================================

View file

@ -1,7 +1,7 @@
{ {
"name": "DCA Admin", "name": "spirekeeper",
"short_description": "Dollar Cost Averaging administration for Lamassu ATM integration", "short_description": "Dollar Cost Averaging administration for Lamassu ATM integration",
"tile": "/satmachineadmin/static/image/aio.png", "tile": "/spirekeeper/static/image/aio.png",
"min_lnbits_version": "1.0.0", "min_lnbits_version": "1.0.0",
"contributors": [ "contributors": [
{ {
@ -26,7 +26,7 @@
} }
], ],
"images": [], "images": [],
"description_md": "/satmachineadmin/description.md", "description_md": "/spirekeeper/description.md",
"terms_and_conditions_md": "/satmachineadmin/toc.md", "terms_and_conditions_md": "/spirekeeper/toc.md",
"license": "MIT" "license": "MIT"
} }

156
crud.py
View file

@ -38,7 +38,7 @@ from .models import (
UpsertDcaLpData, UpsertDcaLpData,
) )
db = Database("ext_satoshimachine") db = Database("ext_spirekeeper")
# ============================================================================= # =============================================================================
@ -48,7 +48,7 @@ db = Database("ext_satoshimachine")
async def get_super_config() -> SuperConfig | None: async def get_super_config() -> SuperConfig | None:
return await db.fetchone( return await db.fetchone(
"SELECT * FROM satoshimachine.super_config WHERE id = :id", "SELECT * FROM spirekeeper.super_config WHERE id = :id",
{"id": "default"}, {"id": "default"},
SuperConfig, SuperConfig,
) )
@ -62,7 +62,7 @@ async def update_super_config(data: UpdateSuperConfigData) -> SuperConfig | None
set_clause = ", ".join(f"{k} = :{k}" for k in update_data) set_clause = ", ".join(f"{k} = :{k}" for k in update_data)
update_data["id"] = "default" update_data["id"] = "default"
await db.execute( await db.execute(
f"UPDATE satoshimachine.super_config SET {set_clause} WHERE id = :id", f"UPDATE spirekeeper.super_config SET {set_clause} WHERE id = :id",
update_data, update_data,
) )
return await get_super_config() return await get_super_config()
@ -78,7 +78,7 @@ async def create_machine(operator_user_id: str, data: CreateMachineData) -> Mach
now = datetime.now() now = datetime.now()
await db.execute( await db.execute(
""" """
INSERT INTO satoshimachine.dca_machines INSERT INTO spirekeeper.dca_machines
(id, operator_user_id, machine_npub, wallet_id, name, location, (id, operator_user_id, machine_npub, wallet_id, name, location,
fiat_code, is_active, fiat_code, is_active,
operator_cash_in_fee_fraction, operator_cash_out_fee_fraction, operator_cash_in_fee_fraction, operator_cash_out_fee_fraction,
@ -110,7 +110,7 @@ async def create_machine(operator_user_id: str, data: CreateMachineData) -> Mach
async def get_machine(machine_id: str) -> Machine | None: async def get_machine(machine_id: str) -> Machine | None:
return await db.fetchone( return await db.fetchone(
"SELECT * FROM satoshimachine.dca_machines WHERE id = :id", "SELECT * FROM spirekeeper.dca_machines WHERE id = :id",
{"id": machine_id}, {"id": machine_id},
Machine, Machine,
) )
@ -118,7 +118,7 @@ async def get_machine(machine_id: str) -> Machine | None:
async def get_machine_by_npub(machine_npub: str) -> Machine | None: async def get_machine_by_npub(machine_npub: str) -> Machine | None:
return await db.fetchone( return await db.fetchone(
"SELECT * FROM satoshimachine.dca_machines WHERE machine_npub = :npub", "SELECT * FROM spirekeeper.dca_machines WHERE machine_npub = :npub",
{"npub": machine_npub}, {"npub": machine_npub},
Machine, Machine,
) )
@ -128,7 +128,7 @@ async def get_active_machine_by_wallet_id(wallet_id: str) -> Machine | None:
"""Used by the invoice listener to route an incoming payment to a machine.""" """Used by the invoice listener to route an incoming payment to a machine."""
return await db.fetchone( return await db.fetchone(
""" """
SELECT * FROM satoshimachine.dca_machines SELECT * FROM spirekeeper.dca_machines
WHERE wallet_id = :wid AND is_active = true WHERE wallet_id = :wid AND is_active = true
LIMIT 1 LIMIT 1
""", """,
@ -140,7 +140,7 @@ async def get_active_machine_by_wallet_id(wallet_id: str) -> Machine | None:
async def get_machines_for_operator(operator_user_id: str) -> list[Machine]: async def get_machines_for_operator(operator_user_id: str) -> list[Machine]:
return await db.fetchall( return await db.fetchall(
""" """
SELECT * FROM satoshimachine.dca_machines SELECT * FROM spirekeeper.dca_machines
WHERE operator_user_id = :uid WHERE operator_user_id = :uid
ORDER BY created_at DESC ORDER BY created_at DESC
""", """,
@ -157,7 +157,7 @@ async def list_all_active_machines() -> list[Machine]:
""" """
return await db.fetchall( return await db.fetchall(
""" """
SELECT * FROM satoshimachine.dca_machines SELECT * FROM spirekeeper.dca_machines
WHERE is_active = true WHERE is_active = true
ORDER BY created_at DESC ORDER BY created_at DESC
""", """,
@ -196,7 +196,7 @@ async def update_machine(machine_id: str, data: UpdateMachineData) -> Machine |
set_clause = ", ".join(f"{k} = :{k}" for k in update_data) set_clause = ", ".join(f"{k} = :{k}" for k in update_data)
update_data["id"] = machine_id update_data["id"] = machine_id
await db.execute( await db.execute(
f"UPDATE satoshimachine.dca_machines SET {set_clause} WHERE id = :id", f"UPDATE spirekeeper.dca_machines SET {set_clause} WHERE id = :id",
update_data, update_data,
) )
return await get_machine(machine_id) return await get_machine(machine_id)
@ -204,7 +204,7 @@ async def update_machine(machine_id: str, data: UpdateMachineData) -> Machine |
async def delete_machine(machine_id: str) -> None: async def delete_machine(machine_id: str) -> None:
await db.execute( await db.execute(
"DELETE FROM satoshimachine.dca_machines WHERE id = :id", "DELETE FROM spirekeeper.dca_machines WHERE id = :id",
{"id": machine_id}, {"id": machine_id},
) )
@ -226,7 +226,7 @@ async def create_dca_client(data: CreateDcaClientData) -> DcaClient:
now = datetime.now() now = datetime.now()
await db.execute( await db.execute(
""" """
INSERT INTO satoshimachine.dca_clients INSERT INTO spirekeeper.dca_clients
(id, machine_id, user_id, username, status, created_at, updated_at) (id, machine_id, user_id, username, status, created_at, updated_at)
VALUES (:id, :machine_id, :user_id, :username, :status, VALUES (:id, :machine_id, :user_id, :username, :status,
:created_at, :updated_at) :created_at, :updated_at)
@ -255,8 +255,8 @@ _CLIENT_SELECT = """
(lp.user_id IS NOT NULL) AS lp_onboarded (lp.user_id IS NOT NULL) AS lp_onboarded
""" """
_CLIENT_FROM = ( _CLIENT_FROM = (
"satoshimachine.dca_clients c " "spirekeeper.dca_clients c "
"LEFT JOIN satoshimachine.dca_lp lp ON lp.user_id = c.user_id" "LEFT JOIN spirekeeper.dca_lp lp ON lp.user_id = c.user_id"
) )
@ -299,7 +299,7 @@ async def get_dca_clients_for_operator(operator_user_id: str) -> list[DcaClient]
f""" f"""
SELECT {_CLIENT_SELECT} SELECT {_CLIENT_SELECT}
FROM {_CLIENT_FROM} FROM {_CLIENT_FROM}
JOIN satoshimachine.dca_machines m ON m.id = c.machine_id JOIN spirekeeper.dca_machines m ON m.id = c.machine_id
WHERE m.operator_user_id = :uid WHERE m.operator_user_id = :uid
ORDER BY c.created_at DESC ORDER BY c.created_at DESC
""", """,
@ -332,8 +332,8 @@ async def get_flow_mode_clients_for_machine(machine_id: str) -> list[DcaClient]:
return await db.fetchall( return await db.fetchall(
""" """
SELECT c.* SELECT c.*
FROM satoshimachine.dca_clients c FROM spirekeeper.dca_clients c
JOIN satoshimachine.dca_lp lp ON lp.user_id = c.user_id JOIN spirekeeper.dca_lp lp ON lp.user_id = c.user_id
WHERE c.machine_id = :machine_id WHERE c.machine_id = :machine_id
AND lp.default_dca_mode = 'flow' AND lp.default_dca_mode = 'flow'
AND c.status = 'active' AND c.status = 'active'
@ -353,7 +353,7 @@ async def get_dca_lp(user_id: str) -> DcaLpPreferences | None:
"""Return the LP's preferences row, or None if they haven't onboarded """Return the LP's preferences row, or None if they haven't onboarded
via satmachineclient yet.""" via satmachineclient yet."""
return await db.fetchone( return await db.fetchone(
"SELECT * FROM satoshimachine.dca_lp WHERE user_id = :uid", "SELECT * FROM spirekeeper.dca_lp WHERE user_id = :uid",
{"uid": user_id}, {"uid": user_id},
DcaLpPreferences, DcaLpPreferences,
) )
@ -362,7 +362,7 @@ async def get_dca_lp(user_id: str) -> DcaLpPreferences | None:
async def lp_is_onboarded(user_id: str) -> bool: async def lp_is_onboarded(user_id: str) -> bool:
"""Cheap existence check used by the deposit-creation gate.""" """Cheap existence check used by the deposit-creation gate."""
row = await db.fetchone( row = await db.fetchone(
"SELECT user_id FROM satoshimachine.dca_lp WHERE user_id = :uid", "SELECT user_id FROM spirekeeper.dca_lp WHERE user_id = :uid",
{"uid": user_id}, {"uid": user_id},
) )
return row is not None return row is not None
@ -392,7 +392,7 @@ async def upsert_dca_lp(
) )
await db.execute( await db.execute(
""" """
INSERT INTO satoshimachine.dca_lp INSERT INTO spirekeeper.dca_lp
(user_id, dca_wallet_id, default_dca_mode, fixed_mode_daily_limit, (user_id, dca_wallet_id, default_dca_mode, fixed_mode_daily_limit,
autoforward_ln_address, autoforward_enabled, autoforward_ln_address, autoforward_enabled,
created_at, updated_at) created_at, updated_at)
@ -417,7 +417,7 @@ async def upsert_dca_lp(
set_clause = ", ".join(f"{k} = :{k}" for k in update_data) set_clause = ", ".join(f"{k} = :{k}" for k in update_data)
update_data["uid"] = user_id update_data["uid"] = user_id
await db.execute( await db.execute(
f"UPDATE satoshimachine.dca_lp SET {set_clause} WHERE user_id = :uid", f"UPDATE spirekeeper.dca_lp SET {set_clause} WHERE user_id = :uid",
update_data, update_data,
) )
refreshed = await get_dca_lp(user_id) refreshed = await get_dca_lp(user_id)
@ -435,7 +435,7 @@ async def update_dca_client(
set_clause = ", ".join(f"{k} = :{k}" for k in update_data) set_clause = ", ".join(f"{k} = :{k}" for k in update_data)
update_data["id"] = client_id update_data["id"] = client_id
await db.execute( await db.execute(
f"UPDATE satoshimachine.dca_clients SET {set_clause} WHERE id = :id", f"UPDATE spirekeeper.dca_clients SET {set_clause} WHERE id = :id",
update_data, update_data,
) )
return await get_dca_client(client_id) return await get_dca_client(client_id)
@ -443,7 +443,7 @@ async def update_dca_client(
async def delete_dca_client(client_id: str) -> None: async def delete_dca_client(client_id: str) -> None:
await db.execute( await db.execute(
"DELETE FROM satoshimachine.dca_clients WHERE id = :id", "DELETE FROM spirekeeper.dca_clients WHERE id = :id",
{"id": client_id}, {"id": client_id},
) )
@ -466,7 +466,7 @@ async def create_deposit(
deposit_id = urlsafe_short_hash() deposit_id = urlsafe_short_hash()
await db.execute( await db.execute(
""" """
INSERT INTO satoshimachine.dca_deposits INSERT INTO spirekeeper.dca_deposits
(id, client_id, machine_id, creator_user_id, amount, currency, (id, client_id, machine_id, creator_user_id, amount, currency,
status, notes, created_at) status, notes, created_at)
VALUES (:id, :client_id, :machine_id, :creator_user_id, :amount, VALUES (:id, :client_id, :machine_id, :creator_user_id, :amount,
@ -491,7 +491,7 @@ async def create_deposit(
async def get_deposit(deposit_id: str) -> DcaDeposit | None: async def get_deposit(deposit_id: str) -> DcaDeposit | None:
return await db.fetchone( return await db.fetchone(
"SELECT * FROM satoshimachine.dca_deposits WHERE id = :id", "SELECT * FROM spirekeeper.dca_deposits WHERE id = :id",
{"id": deposit_id}, {"id": deposit_id},
DcaDeposit, DcaDeposit,
) )
@ -500,7 +500,7 @@ async def get_deposit(deposit_id: str) -> DcaDeposit | None:
async def get_deposits_for_client(client_id: str) -> list[DcaDeposit]: async def get_deposits_for_client(client_id: str) -> list[DcaDeposit]:
return await db.fetchall( return await db.fetchall(
""" """
SELECT * FROM satoshimachine.dca_deposits SELECT * FROM spirekeeper.dca_deposits
WHERE client_id = :client_id WHERE client_id = :client_id
ORDER BY created_at DESC ORDER BY created_at DESC
""", """,
@ -513,8 +513,8 @@ async def get_deposits_for_operator(operator_user_id: str) -> list[DcaDeposit]:
return await db.fetchall( return await db.fetchall(
""" """
SELECT d.* SELECT d.*
FROM satoshimachine.dca_deposits d FROM spirekeeper.dca_deposits d
JOIN satoshimachine.dca_machines m ON m.id = d.machine_id JOIN spirekeeper.dca_machines m ON m.id = d.machine_id
WHERE m.operator_user_id = :uid WHERE m.operator_user_id = :uid
ORDER BY d.created_at DESC ORDER BY d.created_at DESC
""", """,
@ -532,7 +532,7 @@ async def update_deposit(
set_clause = ", ".join(f"{k} = :{k}" for k in update_data) set_clause = ", ".join(f"{k} = :{k}" for k in update_data)
update_data["id"] = deposit_id update_data["id"] = deposit_id
await db.execute( await db.execute(
f"UPDATE satoshimachine.dca_deposits SET {set_clause} WHERE id = :id", f"UPDATE spirekeeper.dca_deposits SET {set_clause} WHERE id = :id",
update_data, update_data,
) )
return await get_deposit(deposit_id) return await get_deposit(deposit_id)
@ -549,7 +549,7 @@ async def update_deposit_status(
} }
await db.execute( await db.execute(
""" """
UPDATE satoshimachine.dca_deposits UPDATE spirekeeper.dca_deposits
SET status = :status, SET status = :status,
notes = COALESCE(:notes, notes), notes = COALESCE(:notes, notes),
confirmed_at = COALESCE(:confirmed_at, confirmed_at) confirmed_at = COALESCE(:confirmed_at, confirmed_at)
@ -562,7 +562,7 @@ async def update_deposit_status(
async def delete_deposit(deposit_id: str) -> None: async def delete_deposit(deposit_id: str) -> None:
await db.execute( await db.execute(
"DELETE FROM satoshimachine.dca_deposits WHERE id = :id", "DELETE FROM spirekeeper.dca_deposits WHERE id = :id",
{"id": deposit_id}, {"id": deposit_id},
) )
@ -598,7 +598,7 @@ async def create_settlement_idempotent(
settlement_id = urlsafe_short_hash() settlement_id = urlsafe_short_hash()
await db.execute( await db.execute(
""" """
INSERT INTO satoshimachine.dca_settlements INSERT INTO spirekeeper.dca_settlements
(id, machine_id, payment_hash, bitspire_event_id, bitspire_txid, (id, machine_id, payment_hash, bitspire_event_id, bitspire_txid,
wire_sats, fiat_amount, fiat_code, exchange_rate, principal_sats, wire_sats, fiat_amount, fiat_code, exchange_rate, principal_sats,
fee_sats, platform_fee_sats, operator_fee_sats, fee_mismatch_sats, fee_sats, platform_fee_sats, operator_fee_sats, fee_mismatch_sats,
@ -639,7 +639,7 @@ async def create_settlement_idempotent(
async def get_settlement(settlement_id: str) -> DcaSettlement | None: async def get_settlement(settlement_id: str) -> DcaSettlement | None:
return await db.fetchone( return await db.fetchone(
"SELECT * FROM satoshimachine.dca_settlements WHERE id = :id", "SELECT * FROM spirekeeper.dca_settlements WHERE id = :id",
{"id": settlement_id}, {"id": settlement_id},
DcaSettlement, DcaSettlement,
) )
@ -650,7 +650,7 @@ async def get_settlement_by_payment_hash(
) -> DcaSettlement | None: ) -> DcaSettlement | None:
return await db.fetchone( return await db.fetchone(
""" """
SELECT * FROM satoshimachine.dca_settlements SELECT * FROM spirekeeper.dca_settlements
WHERE payment_hash = :hash WHERE payment_hash = :hash
""", """,
{"hash": payment_hash}, {"hash": payment_hash},
@ -663,7 +663,7 @@ async def get_settlements_for_machine(
) -> list[DcaSettlement]: ) -> list[DcaSettlement]:
return await db.fetchall( return await db.fetchall(
""" """
SELECT * FROM satoshimachine.dca_settlements SELECT * FROM spirekeeper.dca_settlements
WHERE machine_id = :machine_id WHERE machine_id = :machine_id
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT :lim LIMIT :lim
@ -698,8 +698,8 @@ async def get_stuck_settlements_for_operator(
rejected = await db.fetchall( rejected = await db.fetchall(
""" """
SELECT s.* SELECT s.*
FROM satoshimachine.dca_settlements s FROM spirekeeper.dca_settlements s
JOIN satoshimachine.dca_machines m ON m.id = s.machine_id JOIN spirekeeper.dca_machines m ON m.id = s.machine_id
WHERE m.operator_user_id = :uid AND s.status = 'rejected' WHERE m.operator_user_id = :uid AND s.status = 'rejected'
ORDER BY s.created_at DESC ORDER BY s.created_at DESC
""", """,
@ -709,8 +709,8 @@ async def get_stuck_settlements_for_operator(
errored = await db.fetchall( errored = await db.fetchall(
""" """
SELECT s.* SELECT s.*
FROM satoshimachine.dca_settlements s FROM spirekeeper.dca_settlements s
JOIN satoshimachine.dca_machines m ON m.id = s.machine_id JOIN spirekeeper.dca_machines m ON m.id = s.machine_id
WHERE m.operator_user_id = :uid AND s.status = 'errored' WHERE m.operator_user_id = :uid AND s.status = 'errored'
ORDER BY s.created_at DESC ORDER BY s.created_at DESC
""", """,
@ -720,8 +720,8 @@ async def get_stuck_settlements_for_operator(
stuck_pending = await db.fetchall( stuck_pending = await db.fetchall(
""" """
SELECT s.* SELECT s.*
FROM satoshimachine.dca_settlements s FROM spirekeeper.dca_settlements s
JOIN satoshimachine.dca_machines m ON m.id = s.machine_id JOIN spirekeeper.dca_machines m ON m.id = s.machine_id
WHERE m.operator_user_id = :uid WHERE m.operator_user_id = :uid
AND s.status = 'pending' AND s.status = 'pending'
AND s.created_at < :threshold AND s.created_at < :threshold
@ -733,8 +733,8 @@ async def get_stuck_settlements_for_operator(
stuck_processing = await db.fetchall( stuck_processing = await db.fetchall(
""" """
SELECT s.* SELECT s.*
FROM satoshimachine.dca_settlements s FROM spirekeeper.dca_settlements s
JOIN satoshimachine.dca_machines m ON m.id = s.machine_id JOIN spirekeeper.dca_machines m ON m.id = s.machine_id
WHERE m.operator_user_id = :uid WHERE m.operator_user_id = :uid
AND s.status = 'processing' AND s.status = 'processing'
AND s.created_at < :threshold AND s.created_at < :threshold
@ -763,7 +763,7 @@ async def force_reset_stuck_settlement(
decision.""" decision."""
await db.execute( await db.execute(
""" """
UPDATE satoshimachine.dca_settlements UPDATE spirekeeper.dca_settlements
SET status = 'errored', SET status = 'errored',
processing_claim = NULL, processing_claim = NULL,
error_message = 'force-reset by operator (was stuck)' error_message = 'force-reset by operator (was stuck)'
@ -780,8 +780,8 @@ async def get_settlements_for_operator(
return await db.fetchall( return await db.fetchall(
""" """
SELECT s.* SELECT s.*
FROM satoshimachine.dca_settlements s FROM spirekeeper.dca_settlements s
JOIN satoshimachine.dca_machines m ON m.id = s.machine_id JOIN spirekeeper.dca_machines m ON m.id = s.machine_id
WHERE m.operator_user_id = :uid WHERE m.operator_user_id = :uid
ORDER BY s.created_at DESC ORDER BY s.created_at DESC
LIMIT :lim LIMIT :lim
@ -801,7 +801,7 @@ async def mark_settlement_status(
fresh claim attempt won't see a stale token.""" fresh claim attempt won't see a stale token."""
await db.execute( await db.execute(
""" """
UPDATE satoshimachine.dca_settlements UPDATE spirekeeper.dca_settlements
SET status = :status, SET status = :status,
error_message = :err, error_message = :err,
processed_at = CASE processed_at = CASE
@ -840,7 +840,7 @@ async def claim_settlement_for_processing(
token = urlsafe_short_hash() token = urlsafe_short_hash()
await db.execute( await db.execute(
""" """
UPDATE satoshimachine.dca_settlements UPDATE spirekeeper.dca_settlements
SET status = 'processing', processing_claim = :token SET status = 'processing', processing_claim = :token
WHERE id = :id AND status = 'pending' WHERE id = :id AND status = 'pending'
""", """,
@ -862,7 +862,7 @@ async def reset_settlement_for_retry(
are left in place we never re-pay sats that already moved.""" are left in place we never re-pay sats that already moved."""
await db.execute( await db.execute(
""" """
UPDATE satoshimachine.dca_payments UPDATE spirekeeper.dca_payments
SET status = 'voided' SET status = 'voided'
WHERE settlement_id = :sid AND status = 'failed' WHERE settlement_id = :sid AND status = 'failed'
""", """,
@ -870,7 +870,7 @@ async def reset_settlement_for_retry(
) )
await db.execute( await db.execute(
""" """
UPDATE satoshimachine.dca_settlements UPDATE spirekeeper.dca_settlements
SET status = 'pending', SET status = 'pending',
error_message = NULL, error_message = NULL,
processing_claim = NULL, processing_claim = NULL,
@ -902,7 +902,7 @@ async def apply_partial_dispense(
can re-distribute via the existing idempotent path.""" can re-distribute via the existing idempotent path."""
await db.execute( await db.execute(
""" """
UPDATE satoshimachine.dca_settlements UPDATE spirekeeper.dca_settlements
SET wire_sats = :gross, SET wire_sats = :gross,
principal_sats = :principal, principal_sats = :principal,
fee_sats = :commission, fee_sats = :commission,
@ -937,7 +937,7 @@ async def count_completed_legs_for_settlement(settlement_id: str) -> int:
successfully moved sats (Lightning payments can't be clawed back).""" successfully moved sats (Lightning payments can't be clawed back)."""
row = await db.fetchone( row = await db.fetchone(
""" """
SELECT COUNT(*) AS n FROM satoshimachine.dca_payments SELECT COUNT(*) AS n FROM spirekeeper.dca_payments
WHERE settlement_id = :sid AND status = 'completed' WHERE settlement_id = :sid AND status = 'completed'
""", """,
{"sid": settlement_id}, {"sid": settlement_id},
@ -957,7 +957,7 @@ async def append_settlement_note(
formatted = f"[{ts} by {author_user_id}] {note}" formatted = f"[{ts} by {author_user_id}] {note}"
await db.execute( await db.execute(
""" """
UPDATE satoshimachine.dca_settlements UPDATE spirekeeper.dca_settlements
SET notes = CASE SET notes = CASE
WHEN notes IS NULL OR notes = '' THEN :note WHEN notes IS NULL OR notes = '' THEN :note
ELSE :note || char(10) || char(10) || notes ELSE :note || char(10) || char(10) || notes
@ -977,7 +977,7 @@ async def void_open_legs_for_settlement(settlement_id: str) -> None:
writes its own (possibly different) skipped reasons.""" writes its own (possibly different) skipped reasons."""
await db.execute( await db.execute(
""" """
UPDATE satoshimachine.dca_payments UPDATE spirekeeper.dca_payments
SET status = 'voided' SET status = 'voided'
WHERE settlement_id = :sid WHERE settlement_id = :sid
AND status IN ('pending', 'failed', 'skipped') AND status IN ('pending', 'failed', 'skipped')
@ -1002,7 +1002,7 @@ async def get_commission_splits(
if machine_id is None: if machine_id is None:
return await db.fetchall( return await db.fetchall(
""" """
SELECT * FROM satoshimachine.dca_commission_splits SELECT * FROM spirekeeper.dca_commission_splits
WHERE operator_user_id = :uid AND machine_id IS NULL WHERE operator_user_id = :uid AND machine_id IS NULL
ORDER BY sort_order ASC ORDER BY sort_order ASC
""", """,
@ -1011,7 +1011,7 @@ async def get_commission_splits(
) )
return await db.fetchall( return await db.fetchall(
""" """
SELECT * FROM satoshimachine.dca_commission_splits SELECT * FROM spirekeeper.dca_commission_splits
WHERE operator_user_id = :uid AND machine_id = :mid WHERE operator_user_id = :uid AND machine_id = :mid
ORDER BY sort_order ASC ORDER BY sort_order ASC
""", """,
@ -1040,7 +1040,7 @@ async def replace_commission_splits(
if machine_id is None: if machine_id is None:
await db.execute( await db.execute(
""" """
DELETE FROM satoshimachine.dca_commission_splits DELETE FROM spirekeeper.dca_commission_splits
WHERE operator_user_id = :uid AND machine_id IS NULL WHERE operator_user_id = :uid AND machine_id IS NULL
""", """,
{"uid": operator_user_id}, {"uid": operator_user_id},
@ -1048,7 +1048,7 @@ async def replace_commission_splits(
else: else:
await db.execute( await db.execute(
""" """
DELETE FROM satoshimachine.dca_commission_splits DELETE FROM spirekeeper.dca_commission_splits
WHERE operator_user_id = :uid AND machine_id = :mid WHERE operator_user_id = :uid AND machine_id = :mid
""", """,
{"uid": operator_user_id, "mid": machine_id}, {"uid": operator_user_id, "mid": machine_id},
@ -1057,7 +1057,7 @@ async def replace_commission_splits(
for leg in legs: for leg in legs:
await db.execute( await db.execute(
""" """
INSERT INTO satoshimachine.dca_commission_splits INSERT INTO spirekeeper.dca_commission_splits
(id, machine_id, operator_user_id, target, label, fraction, (id, machine_id, operator_user_id, target, label, fraction,
sort_order, created_at) sort_order, created_at)
VALUES (:id, :machine_id, :uid, :target, :label, :fraction, VALUES (:id, :machine_id, :uid, :target, :label, :fraction,
@ -1086,7 +1086,7 @@ async def create_dca_payment(data: CreateDcaPaymentData) -> DcaPayment:
payment_id = urlsafe_short_hash() payment_id = urlsafe_short_hash()
await db.execute( await db.execute(
""" """
INSERT INTO satoshimachine.dca_payments INSERT INTO spirekeeper.dca_payments
(id, settlement_id, client_id, machine_id, operator_user_id, (id, settlement_id, client_id, machine_id, operator_user_id,
leg_type, destination_wallet_id, destination_ln_address, leg_type, destination_wallet_id, destination_ln_address,
amount_sats, amount_fiat, exchange_rate, transaction_time, amount_sats, amount_fiat, exchange_rate, transaction_time,
@ -1122,7 +1122,7 @@ async def create_dca_payment(data: CreateDcaPaymentData) -> DcaPayment:
async def get_dca_payment(payment_id: str) -> DcaPayment | None: async def get_dca_payment(payment_id: str) -> DcaPayment | None:
return await db.fetchone( return await db.fetchone(
"SELECT * FROM satoshimachine.dca_payments WHERE id = :id", "SELECT * FROM spirekeeper.dca_payments WHERE id = :id",
{"id": payment_id}, {"id": payment_id},
DcaPayment, DcaPayment,
) )
@ -1131,7 +1131,7 @@ async def get_dca_payment(payment_id: str) -> DcaPayment | None:
async def get_payments_for_settlement(settlement_id: str) -> list[DcaPayment]: async def get_payments_for_settlement(settlement_id: str) -> list[DcaPayment]:
return await db.fetchall( return await db.fetchall(
""" """
SELECT * FROM satoshimachine.dca_payments SELECT * FROM spirekeeper.dca_payments
WHERE settlement_id = :sid WHERE settlement_id = :sid
ORDER BY created_at ASC ORDER BY created_at ASC
""", """,
@ -1143,7 +1143,7 @@ async def get_payments_for_settlement(settlement_id: str) -> list[DcaPayment]:
async def get_payments_for_client(client_id: str) -> list[DcaPayment]: async def get_payments_for_client(client_id: str) -> list[DcaPayment]:
return await db.fetchall( return await db.fetchall(
""" """
SELECT * FROM satoshimachine.dca_payments SELECT * FROM spirekeeper.dca_payments
WHERE client_id = :cid WHERE client_id = :cid
ORDER BY created_at DESC ORDER BY created_at DESC
""", """,
@ -1158,7 +1158,7 @@ async def get_payments_for_operator(
if leg_type is None: if leg_type is None:
return await db.fetchall( return await db.fetchall(
""" """
SELECT * FROM satoshimachine.dca_payments SELECT * FROM spirekeeper.dca_payments
WHERE operator_user_id = :uid WHERE operator_user_id = :uid
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT :lim LIMIT :lim
@ -1168,7 +1168,7 @@ async def get_payments_for_operator(
) )
return await db.fetchall( return await db.fetchall(
""" """
SELECT * FROM satoshimachine.dca_payments SELECT * FROM spirekeeper.dca_payments
WHERE operator_user_id = :uid AND leg_type = :leg WHERE operator_user_id = :uid AND leg_type = :leg
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT :lim LIMIT :lim
@ -1186,7 +1186,7 @@ async def update_payment_status(
) -> DcaPayment | None: ) -> DcaPayment | None:
await db.execute( await db.execute(
""" """
UPDATE satoshimachine.dca_payments UPDATE spirekeeper.dca_payments
SET status = :status, SET status = :status,
external_payment_hash = COALESCE(:hash, external_payment_hash), external_payment_hash = COALESCE(:hash, external_payment_hash),
error_message = :err error_message = :err
@ -1221,7 +1221,7 @@ async def get_client_balance_summary(
deposits_row = await db.fetchone( deposits_row = await db.fetchone(
""" """
SELECT COALESCE(SUM(amount), 0) AS total SELECT COALESCE(SUM(amount), 0) AS total
FROM satoshimachine.dca_deposits FROM spirekeeper.dca_deposits
WHERE client_id = :cid AND status = 'confirmed' WHERE client_id = :cid AND status = 'confirmed'
""", """,
{"cid": client_id}, {"cid": client_id},
@ -1231,7 +1231,7 @@ async def get_client_balance_summary(
payments_row = await db.fetchone( payments_row = await db.fetchone(
""" """
SELECT COALESCE(SUM(amount_fiat), 0) AS total SELECT COALESCE(SUM(amount_fiat), 0) AS total
FROM satoshimachine.dca_payments FROM spirekeeper.dca_payments
WHERE client_id = :cid WHERE client_id = :cid
AND leg_type IN ('dca', 'settlement') AND leg_type IN ('dca', 'settlement')
AND status = 'completed' AND status = 'completed'
@ -1260,7 +1260,7 @@ async def get_client_balance_summary(
async def get_telemetry(machine_id: str) -> TelemetrySnapshot | None: async def get_telemetry(machine_id: str) -> TelemetrySnapshot | None:
return await db.fetchone( return await db.fetchone(
"SELECT * FROM satoshimachine.dca_telemetry WHERE machine_id = :mid", "SELECT * FROM spirekeeper.dca_telemetry WHERE machine_id = :mid",
{"mid": machine_id}, {"mid": machine_id},
TelemetrySnapshot, TelemetrySnapshot,
) )
@ -1290,7 +1290,7 @@ async def upsert_beacon_snapshot(
if existing is None: if existing is None:
await db.execute( await db.execute(
""" """
INSERT INTO satoshimachine.dca_telemetry INSERT INTO spirekeeper.dca_telemetry
(machine_id, beacon_cash_in, beacon_cash_out, beacon_cash_level, (machine_id, beacon_cash_in, beacon_cash_out, beacon_cash_level,
beacon_fiat, beacon_model, beacon_name, beacon_location, beacon_fiat, beacon_model, beacon_name, beacon_location,
beacon_geo, beacon_fees_json, beacon_limits_json, beacon_geo, beacon_fees_json, beacon_limits_json,
@ -1319,7 +1319,7 @@ async def upsert_beacon_snapshot(
else: else:
await db.execute( await db.execute(
""" """
UPDATE satoshimachine.dca_telemetry SET UPDATE spirekeeper.dca_telemetry SET
beacon_cash_in = COALESCE(:cash_in, beacon_cash_in), beacon_cash_in = COALESCE(:cash_in, beacon_cash_in),
beacon_cash_out = COALESCE(:cash_out, beacon_cash_out), beacon_cash_out = COALESCE(:cash_out, beacon_cash_out),
beacon_cash_level = COALESCE(:cash_level, beacon_cash_level), beacon_cash_level = COALESCE(:cash_level, beacon_cash_level),
@ -1366,7 +1366,7 @@ async def upsert_fleet_snapshot(
if existing is None: if existing is None:
await db.execute( await db.execute(
""" """
INSERT INTO satoshimachine.dca_telemetry INSERT INTO spirekeeper.dca_telemetry
(machine_id, telemetry_json, telemetry_received_at) (machine_id, telemetry_json, telemetry_received_at)
VALUES (:mid, :json, :now) VALUES (:mid, :json, :now)
""", """,
@ -1375,7 +1375,7 @@ async def upsert_fleet_snapshot(
else: else:
await db.execute( await db.execute(
""" """
UPDATE satoshimachine.dca_telemetry UPDATE spirekeeper.dca_telemetry
SET telemetry_json = :json, telemetry_received_at = :now SET telemetry_json = :json, telemetry_received_at = :now
WHERE machine_id = :mid WHERE machine_id = :mid
""", """,
@ -1416,7 +1416,7 @@ async def get_cassette_config(
machine_id: str, position: int machine_id: str, position: int
) -> CassetteConfig | None: ) -> CassetteConfig | None:
return await db.fetchone( return await db.fetchone(
"SELECT * FROM satoshimachine.cassette_configs " "SELECT * FROM spirekeeper.cassette_configs "
"WHERE machine_id = :mid AND position = :pos", "WHERE machine_id = :mid AND position = :pos",
{"mid": machine_id, "pos": position}, {"mid": machine_id, "pos": position},
CassetteConfig, CassetteConfig,
@ -1427,7 +1427,7 @@ async def list_cassette_configs_for_machine(
machine_id: str, machine_id: str,
) -> list[CassetteConfig]: ) -> list[CassetteConfig]:
return await db.fetchall( return await db.fetchall(
"SELECT * FROM satoshimachine.cassette_configs " "SELECT * FROM spirekeeper.cassette_configs "
"WHERE machine_id = :mid ORDER BY position", "WHERE machine_id = :mid ORDER BY position",
{"mid": machine_id}, {"mid": machine_id},
CassetteConfig, CassetteConfig,
@ -1459,7 +1459,7 @@ async def update_cassette_config(
update_data["mid"] = machine_id update_data["mid"] = machine_id
update_data["pos"] = position update_data["pos"] = position
await db.execute( await db.execute(
f"UPDATE satoshimachine.cassette_configs SET {set_clause} " f"UPDATE spirekeeper.cassette_configs SET {set_clause} "
"WHERE machine_id = :mid AND position = :pos", "WHERE machine_id = :mid AND position = :pos",
update_data, update_data,
) )
@ -1487,7 +1487,7 @@ async def apply_bootstrap_state(
events land + the operator subsequently edits. events land + the operator subsequently edits.
""" """
existing_first: dict | None = await db.fetchone( existing_first: dict | None = await db.fetchone(
"SELECT state_event_id FROM satoshimachine.cassette_configs " "SELECT state_event_id FROM spirekeeper.cassette_configs "
"WHERE machine_id = :mid LIMIT 1", "WHERE machine_id = :mid LIMIT 1",
{"mid": machine_id}, {"mid": machine_id},
) )
@ -1505,7 +1505,7 @@ async def apply_bootstrap_state(
for pos, row in payload.positions.items(): for pos, row in payload.positions.items():
await db.execute( await db.execute(
""" """
INSERT INTO satoshimachine.cassette_configs INSERT INTO spirekeeper.cassette_configs
(machine_id, position, denomination, count, updated_at, (machine_id, position, denomination, count, updated_at,
updated_by, state_denomination, state_count, state_at, updated_by, state_denomination, state_count, state_at,
state_event_id) state_event_id)

View file

@ -132,7 +132,7 @@ async def publish_fee_config(
) )
except (SignerUnavailable, RelayUnavailable) as exc: except (SignerUnavailable, RelayUnavailable) as exc:
logger.warning( logger.warning(
f"satmachineadmin: fee-config publish soft-fail for machine " f"spirekeeper: fee-config publish soft-fail for machine "
f"{machine.id} ({machine.name or machine.machine_npub[:12]}): " f"{machine.id} ({machine.name or machine.machine_npub[:12]}): "
f"{type(exc).__name__}: {exc}. Underlying CRUD operation " f"{type(exc).__name__}: {exc}. Underlying CRUD operation "
"succeeded; operator can re-trigger publish via the next " "succeeded; operator can re-trigger publish via the next "
@ -144,7 +144,7 @@ async def publish_fee_config(
# don't break the caller's CRUD path; a future publish attempt # don't break the caller's CRUD path; a future publish attempt
# (next machine edit / next super edit) will retry. # (next machine edit / next super edit) will retry.
logger.warning( logger.warning(
f"satmachineadmin: fee-config publish unexpected transport " f"spirekeeper: fee-config publish unexpected transport "
f"error for machine {machine.id}: {type(exc).__name__}: {exc}" f"error for machine {machine.id}: {type(exc).__name__}: {exc}"
) )
return None return None

View file

@ -1,9 +1,9 @@
{ {
"repos": [ "repos": [
{ {
"id": "satmachineadmin", "id": "spirekeeper",
"organisation": "atitlanio", "organisation": "aiolabs",
"repository": "satmachineadmin" "repository": "spirekeeper"
} }
] ]
} }

View file

@ -56,11 +56,11 @@ async def m001_satmachine_v2_initial(db):
"dca_deposits", "dca_deposits",
"dca_clients", "dca_clients",
): ):
await db.execute(f"DROP TABLE IF EXISTS satoshimachine.{table}") await db.execute(f"DROP TABLE IF EXISTS spirekeeper.{table}")
# 2. super_config — singleton (id='default') with platform-fee config. # 2. super_config — singleton (id='default') with platform-fee config.
await db.execute(f""" await db.execute(f"""
CREATE TABLE IF NOT EXISTS satoshimachine.super_config ( CREATE TABLE IF NOT EXISTS spirekeeper.super_config (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
super_fee_fraction DECIMAL(10,4) NOT NULL DEFAULT 0.0000, super_fee_fraction DECIMAL(10,4) NOT NULL DEFAULT 0.0000,
super_fee_wallet_id TEXT, super_fee_wallet_id TEXT,
@ -68,18 +68,18 @@ async def m001_satmachine_v2_initial(db):
); );
""") """)
existing = await db.fetchone( existing = await db.fetchone(
"SELECT id FROM satoshimachine.super_config WHERE id = 'default'" "SELECT id FROM spirekeeper.super_config WHERE id = 'default'"
) )
if not existing: if not existing:
await db.execute( await db.execute(
"INSERT INTO satoshimachine.super_config (id, super_fee_fraction) " "INSERT INTO spirekeeper.super_config (id, super_fee_fraction) "
"VALUES ('default', 0.0000)" "VALUES ('default', 0.0000)"
) )
# 3. dca_machines — one row per bitSpire ATM, owned by exactly one # 3. dca_machines — one row per bitSpire ATM, owned by exactly one
# operator. wallet_id UNIQUE prevents the IDOR funds-theft vector. # operator. wallet_id UNIQUE prevents the IDOR funds-theft vector.
await db.execute(f""" await db.execute(f"""
CREATE TABLE IF NOT EXISTS satoshimachine.dca_machines ( CREATE TABLE IF NOT EXISTS spirekeeper.dca_machines (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
operator_user_id TEXT NOT NULL, operator_user_id TEXT NOT NULL,
machine_npub TEXT NOT NULL UNIQUE, machine_npub TEXT NOT NULL UNIQUE,
@ -107,7 +107,7 @@ async def m001_satmachine_v2_initial(db):
# just decides "this LP is enrolled at my machine"; everything # just decides "this LP is enrolled at my machine"; everything
# delivery-related is the LP's own preference. # delivery-related is the LP's own preference.
await db.execute(f""" await db.execute(f"""
CREATE TABLE IF NOT EXISTS satoshimachine.dca_clients ( CREATE TABLE IF NOT EXISTS spirekeeper.dca_clients (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
machine_id TEXT NOT NULL, machine_id TEXT NOT NULL,
user_id TEXT NOT NULL, user_id TEXT NOT NULL,
@ -129,17 +129,17 @@ async def m001_satmachine_v2_initial(db):
# user that has onboarded as a Liquidity Provider, regardless of # user that has onboarded as a Liquidity Provider, regardless of
# how many machines they're enrolled at. Owned by the LP (writes # how many machines they're enrolled at. Owned by the LP (writes
# come from the satmachineclient extension under the LP's session), # come from the satmachineclient extension under the LP's session),
# read by satmachineadmin during distribution to resolve "where do # read by spirekeeper during distribution to resolve "where do
# DCA payouts for this LP go?" # DCA payouts for this LP go?"
# #
# Gating: satmachineadmin refuses to create deposits for an LP who # Gating: spirekeeper refuses to create deposits for an LP who
# doesn't have a dca_lp row yet. The LP must onboard via # doesn't have a dca_lp row yet. The LP must onboard via
# satmachineclient first (which auto-creates the row with their # satmachineclient first (which auto-creates the row with their
# default LNbits wallet on first dashboard visit). Forces every # default LNbits wallet on first dashboard visit). Forces every
# LP through a "yes, I am here and this is where I want my sats" # LP through a "yes, I am here and this is where I want my sats"
# gesture before any fiat starts accumulating against them. # gesture before any fiat starts accumulating against them.
await db.execute(f""" await db.execute(f"""
CREATE TABLE IF NOT EXISTS satoshimachine.dca_lp ( CREATE TABLE IF NOT EXISTS spirekeeper.dca_lp (
user_id TEXT PRIMARY KEY, user_id TEXT PRIMARY KEY,
dca_wallet_id TEXT NOT NULL, dca_wallet_id TEXT NOT NULL,
default_dca_mode TEXT NOT NULL DEFAULT 'flow', default_dca_mode TEXT NOT NULL DEFAULT 'flow',
@ -154,7 +154,7 @@ async def m001_satmachine_v2_initial(db):
# 5. dca_deposits — fiat the operator (or super) records against an LP # 5. dca_deposits — fiat the operator (or super) records against an LP
# at a machine. creator_user_id preserves audit trail. # at a machine. creator_user_id preserves audit trail.
await db.execute(f""" await db.execute(f"""
CREATE TABLE IF NOT EXISTS satoshimachine.dca_deposits ( CREATE TABLE IF NOT EXISTS spirekeeper.dca_deposits (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
client_id TEXT NOT NULL, client_id TEXT NOT NULL,
machine_id TEXT NOT NULL, machine_id TEXT NOT NULL,
@ -184,7 +184,7 @@ async def m001_satmachine_v2_initial(db):
# forgave what per transaction. Do not collapse them into a single # forgave what per transaction. Do not collapse them into a single
# fee_fraction. See plan section "Customer discounts" and #10. # fee_fraction. See plan section "Customer discounts" and #10.
await db.execute(f""" await db.execute(f"""
CREATE TABLE IF NOT EXISTS satoshimachine.dca_settlements ( CREATE TABLE IF NOT EXISTS spirekeeper.dca_settlements (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
machine_id TEXT NOT NULL, machine_id TEXT NOT NULL,
payment_hash TEXT NOT NULL UNIQUE, payment_hash TEXT NOT NULL UNIQUE,
@ -227,7 +227,7 @@ async def m001_satmachine_v2_initial(db):
# - LNURL string (bech32 LNURL...) # - LNURL string (bech32 LNURL...)
# Resolution lives in distribution._pay_one_split_leg. # Resolution lives in distribution._pay_one_split_leg.
await db.execute(f""" await db.execute(f"""
CREATE TABLE IF NOT EXISTS satoshimachine.dca_commission_splits ( CREATE TABLE IF NOT EXISTS spirekeeper.dca_commission_splits (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
machine_id TEXT, machine_id TEXT,
operator_user_id TEXT NOT NULL, operator_user_id TEXT NOT NULL,
@ -248,7 +248,7 @@ async def m001_satmachine_v2_initial(db):
# autoforward | refund. status enum: pending | completed | failed | # autoforward | refund. status enum: pending | completed | failed |
# voided | skipped | refunded. # voided | skipped | refunded.
await db.execute(f""" await db.execute(f"""
CREATE TABLE IF NOT EXISTS satoshimachine.dca_payments ( CREATE TABLE IF NOT EXISTS spirekeeper.dca_payments (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
settlement_id TEXT, settlement_id TEXT,
client_id TEXT, client_id TEXT,
@ -287,7 +287,7 @@ async def m001_satmachine_v2_initial(db):
# (name, location, geo, fees, limits, denominations, version) are # (name, location, geo, fees, limits, denominations, version) are
# nullable until that upstream issue lands. Ingest opportunistically. # nullable until that upstream issue lands. Ingest opportunistically.
await db.execute(""" await db.execute("""
CREATE TABLE IF NOT EXISTS satoshimachine.dca_telemetry ( CREATE TABLE IF NOT EXISTS spirekeeper.dca_telemetry (
machine_id TEXT PRIMARY KEY, machine_id TEXT PRIMARY KEY,
beacon_cash_in BOOLEAN, beacon_cash_in BOOLEAN,
beacon_cash_out BOOLEAN, beacon_cash_out BOOLEAN,
@ -315,7 +315,7 @@ async def m002_rename_commission_split_wallet_id_to_target(db):
EXISTS`, which is a no-op when the table already exists so the EXISTS`, which is a no-op when the table already exists so the
schema drift survives the documented uninstall + reinstall workflow schema drift survives the documented uninstall + reinstall workflow
because LNbits' uninstall wipes the dbversions tracker but NOT the because LNbits' uninstall wipes the dbversions tracker but NOT the
satoshimachine.sqlite3 file on disk. spirekeeper.sqlite3 file on disk.
Idempotent: probes for the `wallet_id` column via a SELECT. If the Idempotent: probes for the `wallet_id` column via a SELECT. If the
probe succeeds the column still exists and we RENAME it; otherwise probe succeeds the column still exists and we RENAME it; otherwise
@ -326,14 +326,14 @@ async def m002_rename_commission_split_wallet_id_to_target(db):
""" """
try: try:
await db.fetchone( await db.fetchone(
"SELECT wallet_id FROM satoshimachine.dca_commission_splits LIMIT 1" "SELECT wallet_id FROM spirekeeper.dca_commission_splits LIMIT 1"
) )
except Exception: except Exception:
# wallet_id column doesn't exist; either m001 produced the correct # wallet_id column doesn't exist; either m001 produced the correct
# schema on a fresh install or the rename already landed. # schema on a fresh install or the rename already landed.
return return
await db.execute( await db.execute(
"ALTER TABLE satoshimachine.dca_commission_splits " "ALTER TABLE spirekeeper.dca_commission_splits "
"RENAME COLUMN wallet_id TO target" "RENAME COLUMN wallet_id TO target"
) )
@ -350,11 +350,11 @@ async def m003_rename_settlements_net_sats_to_principal_sats(db):
Idempotent: probes for the old `net_sats` column. If present, rename. Idempotent: probes for the old `net_sats` column. If present, rename.
""" """
try: try:
await db.fetchone("SELECT net_sats FROM satoshimachine.dca_settlements LIMIT 1") await db.fetchone("SELECT net_sats FROM spirekeeper.dca_settlements LIMIT 1")
except Exception: except Exception:
return return
await db.execute( await db.execute(
"ALTER TABLE satoshimachine.dca_settlements " "ALTER TABLE spirekeeper.dca_settlements "
"RENAME COLUMN net_sats TO principal_sats" "RENAME COLUMN net_sats TO principal_sats"
) )
@ -383,14 +383,14 @@ async def m004_introduce_dca_lp_table(db):
absent the install already on the new shape; no-op. absent the install already on the new shape; no-op.
""" """
try: try:
await db.fetchone("SELECT wallet_id FROM satoshimachine.dca_clients LIMIT 1") await db.fetchone("SELECT wallet_id FROM spirekeeper.dca_clients LIMIT 1")
except Exception: except Exception:
return return
# Step 1: create dca_lp if it doesn't exist yet. m001 on a fresh install # Step 1: create dca_lp if it doesn't exist yet. m001 on a fresh install
# already created it; on a pre-m004 install we're creating it here. # already created it; on a pre-m004 install we're creating it here.
await db.execute(f""" await db.execute(f"""
CREATE TABLE IF NOT EXISTS satoshimachine.dca_lp ( CREATE TABLE IF NOT EXISTS spirekeeper.dca_lp (
user_id TEXT PRIMARY KEY, user_id TEXT PRIMARY KEY,
dca_wallet_id TEXT NOT NULL, dca_wallet_id TEXT NOT NULL,
default_dca_mode TEXT NOT NULL DEFAULT 'flow', default_dca_mode TEXT NOT NULL DEFAULT 'flow',
@ -407,7 +407,7 @@ async def m004_introduce_dca_lp_table(db):
# enrolled at multiple machines — that row reflects their most # enrolled at multiple machines — that row reflects their most
# recent intent. ROW_NUMBER() OVER (...) requires SQLite 3.25+ (2018). # recent intent. ROW_NUMBER() OVER (...) requires SQLite 3.25+ (2018).
await db.execute(""" await db.execute("""
INSERT OR IGNORE INTO satoshimachine.dca_lp INSERT OR IGNORE INTO spirekeeper.dca_lp
(user_id, dca_wallet_id, default_dca_mode, fixed_mode_daily_limit, (user_id, dca_wallet_id, default_dca_mode, fixed_mode_daily_limit,
autoforward_ln_address, autoforward_enabled, autoforward_ln_address, autoforward_enabled,
created_at, updated_at) created_at, updated_at)
@ -419,7 +419,7 @@ async def m004_introduce_dca_lp_table(db):
PARTITION BY user_id PARTITION BY user_id
ORDER BY updated_at DESC, created_at DESC ORDER BY updated_at DESC, created_at DESC
) AS rn ) AS rn
FROM satoshimachine.dca_clients FROM spirekeeper.dca_clients
) ranked ) ranked
WHERE rn = 1 WHERE rn = 1
""") """)
@ -434,7 +434,7 @@ async def m004_introduce_dca_lp_table(db):
"autoforward_ln_address", "autoforward_ln_address",
"autoforward_enabled", "autoforward_enabled",
): ):
await db.execute(f"ALTER TABLE satoshimachine.dca_clients DROP COLUMN {col}") await db.execute(f"ALTER TABLE spirekeeper.dca_clients DROP COLUMN {col}")
async def m006_rename_to_canonical_sat_vocabulary(db): async def m006_rename_to_canonical_sat_vocabulary(db):
@ -474,13 +474,13 @@ async def m006_rename_to_canonical_sat_vocabulary(db):
] ]
for table, old_col, new_col in renames: for table, old_col, new_col in renames:
try: try:
await db.fetchone(f"SELECT {old_col} FROM satoshimachine.{table} LIMIT 1") await db.fetchone(f"SELECT {old_col} FROM spirekeeper.{table} LIMIT 1")
except Exception: except Exception:
# old column doesn't exist; either rename already landed or # old column doesn't exist; either rename already landed or
# m001 produced the canonical schema directly on fresh install. # m001 produced the canonical schema directly on fresh install.
continue continue
await db.execute( await db.execute(
f"ALTER TABLE satoshimachine.{table} " f"ALTER TABLE spirekeeper.{table} "
f"RENAME COLUMN {old_col} TO {new_col}" f"RENAME COLUMN {old_col} TO {new_col}"
) )
@ -494,11 +494,11 @@ async def m006_rename_to_canonical_sat_vocabulary(db):
] ]
for table, col in drops: for table, col in drops:
try: try:
await db.fetchone(f"SELECT {col} FROM satoshimachine.{table} LIMIT 1") await db.fetchone(f"SELECT {col} FROM spirekeeper.{table} LIMIT 1")
except Exception: except Exception:
# column doesn't exist; either already dropped or never present. # column doesn't exist; either already dropped or never present.
continue continue
await db.execute(f"ALTER TABLE satoshimachine.{table} DROP COLUMN {col}") await db.execute(f"ALTER TABLE spirekeeper.{table} DROP COLUMN {col}")
async def m005_lock_deposit_currency_to_machine_fiat_code(db): async def m005_lock_deposit_currency_to_machine_fiat_code(db):
@ -518,15 +518,15 @@ async def m005_lock_deposit_currency_to_machine_fiat_code(db):
no mismatches it's a no-op UPDATE. no mismatches it's a no-op UPDATE.
""" """
await db.execute(""" await db.execute("""
UPDATE satoshimachine.dca_deposits AS d UPDATE spirekeeper.dca_deposits AS d
SET currency = ( SET currency = (
SELECT m.fiat_code SELECT m.fiat_code
FROM satoshimachine.dca_machines m FROM spirekeeper.dca_machines m
WHERE m.id = d.machine_id WHERE m.id = d.machine_id
) )
WHERE EXISTS ( WHERE EXISTS (
SELECT 1 SELECT 1
FROM satoshimachine.dca_machines m FROM spirekeeper.dca_machines m
WHERE m.id = d.machine_id WHERE m.id = d.machine_id
AND m.fiat_code IS NOT NULL AND m.fiat_code IS NOT NULL
AND m.fiat_code != d.currency AND m.fiat_code != d.currency
@ -538,7 +538,7 @@ async def m007_add_cassette_configs(db):
"""Add cassette_configs table for operator-driven ATM cassette inventory. """Add cassette_configs table for operator-driven ATM cassette inventory.
Tracks per-machine cassette state (denomination, count, position) editable Tracks per-machine cassette state (denomination, count, position) editable
via the satmachineadmin dashboard and published to the ATM as encrypted via the spirekeeper dashboard and published to the ATM as encrypted
kind-30078 events. See aiolabs/satmachineadmin#29 + lamassu-next#56. kind-30078 events. See aiolabs/satmachineadmin#29 + lamassu-next#56.
Schema choice: PK (machine_id, denomination) mirrors the ATM-side Schema choice: PK (machine_id, denomination) mirrors the ATM-side
@ -556,7 +556,7 @@ async def m007_add_cassette_configs(db):
render them; v2 reconciliation UI consumes them without a migration. render them; v2 reconciliation UI consumes them without a migration.
""" """
await db.execute(f""" await db.execute(f"""
CREATE TABLE IF NOT EXISTS satoshimachine.cassette_configs ( CREATE TABLE IF NOT EXISTS spirekeeper.cassette_configs (
machine_id TEXT NOT NULL, machine_id TEXT NOT NULL,
denomination INTEGER NOT NULL, denomination INTEGER NOT NULL,
count INTEGER NOT NULL, count INTEGER NOT NULL,
@ -600,14 +600,14 @@ async def m008_flip_cassette_configs_pk_to_position(db):
# Probe: does the old PK shape still exist? If state_denomination # Probe: does the old PK shape still exist? If state_denomination
# column already exists, m008 already ran — no-op. # column already exists, m008 already ran — no-op.
await db.fetchone( await db.fetchone(
"SELECT state_denomination FROM satoshimachine.cassette_configs " "LIMIT 1" "SELECT state_denomination FROM spirekeeper.cassette_configs " "LIMIT 1"
) )
return return
except Exception: except Exception:
pass pass
await db.execute(f""" await db.execute(f"""
CREATE TABLE IF NOT EXISTS satoshimachine.cassette_configs_new ( CREATE TABLE IF NOT EXISTS spirekeeper.cassette_configs_new (
machine_id TEXT NOT NULL, machine_id TEXT NOT NULL,
position INTEGER NOT NULL, position INTEGER NOT NULL,
denomination INTEGER NOT NULL, denomination INTEGER NOT NULL,
@ -630,19 +630,19 @@ async def m008_flip_cassette_configs_pk_to_position(db):
# = current denomination as a best-guess baseline; the next bootstrap # = current denomination as a best-guess baseline; the next bootstrap
# event re-populates the state_* columns authoritatively. # event re-populates the state_* columns authoritatively.
await db.execute(""" await db.execute("""
INSERT INTO satoshimachine.cassette_configs_new INSERT INTO spirekeeper.cassette_configs_new
(machine_id, position, denomination, count, (machine_id, position, denomination, count,
updated_at, updated_by, updated_at, updated_by,
state_denomination, state_count, state_at, state_event_id) state_denomination, state_count, state_at, state_event_id)
SELECT machine_id, position, denomination, count, SELECT machine_id, position, denomination, count,
updated_at, updated_by, updated_at, updated_by,
denomination, state_count, state_at, state_event_id denomination, state_count, state_at, state_event_id
FROM satoshimachine.cassette_configs FROM spirekeeper.cassette_configs
""") """)
await db.execute("DROP TABLE satoshimachine.cassette_configs") await db.execute("DROP TABLE spirekeeper.cassette_configs")
await db.execute( await db.execute(
"ALTER TABLE satoshimachine.cassette_configs_new " "RENAME TO cassette_configs" "ALTER TABLE spirekeeper.cassette_configs_new " "RENAME TO cassette_configs"
) )
@ -676,7 +676,7 @@ async def m009_split_fee_fractions_by_direction(db):
operators set via the new UI surface). operators set via the new UI surface).
- dca_settlements gains fee_mismatch_sats BIGINT NULL records - dca_settlements gains fee_mismatch_sats BIGINT NULL records
bitspire-reported fee minus expected per bitspire-reported fee minus expected per
satmachineadmin's principal-based recompute. spirekeeper's principal-based recompute.
Phase 1 observability: log + record, never Phase 1 observability: log + record, never
reject (per coord-log §2026-06-01T07:00Z reject (per coord-log §2026-06-01T07:00Z
lnbits advisory; option A locked). lnbits advisory; option A locked).
@ -697,13 +697,13 @@ async def m009_split_fee_fractions_by_direction(db):
] ]
for table, col, coltype in additions: for table, col, coltype in additions:
try: try:
await db.fetchone(f"SELECT {col} FROM satoshimachine.{table} LIMIT 1") await db.fetchone(f"SELECT {col} FROM spirekeeper.{table} LIMIT 1")
# column already present — migration partially-ran previously, skip # column already present — migration partially-ran previously, skip
continue continue
except Exception: except Exception:
pass pass
await db.execute( await db.execute(
f"ALTER TABLE satoshimachine.{table} ADD COLUMN {col} {coltype}" f"ALTER TABLE spirekeeper.{table} ADD COLUMN {col} {coltype}"
) )
# Backfill + drop the legacy singleton, gated on the column still # Backfill + drop the legacy singleton, gated on the column still
@ -711,7 +711,7 @@ async def m009_split_fee_fractions_by_direction(db):
# steps cleanly. # steps cleanly.
try: try:
await db.fetchone( await db.fetchone(
"SELECT super_fee_fraction FROM satoshimachine.super_config LIMIT 1" "SELECT super_fee_fraction FROM spirekeeper.super_config LIMIT 1"
) )
legacy_present = True legacy_present = True
except Exception: except Exception:
@ -724,7 +724,7 @@ async def m009_split_fee_fractions_by_direction(db):
# still at DEFAULT 0). # still at DEFAULT 0).
await db.execute( await db.execute(
""" """
UPDATE satoshimachine.super_config UPDATE spirekeeper.super_config
SET super_cash_in_fee_fraction = super_fee_fraction, SET super_cash_in_fee_fraction = super_fee_fraction,
super_cash_out_fee_fraction = super_fee_fraction super_cash_out_fee_fraction = super_fee_fraction
WHERE super_cash_in_fee_fraction = 0 WHERE super_cash_in_fee_fraction = 0
@ -733,5 +733,5 @@ async def m009_split_fee_fractions_by_direction(db):
""" """
) )
await db.execute( await db.execute(
"ALTER TABLE satoshimachine.super_config DROP COLUMN super_fee_fraction" "ALTER TABLE spirekeeper.super_config DROP COLUMN super_fee_fraction"
) )

View file

@ -288,7 +288,7 @@ async def publish_encrypted_kind_30078(
await publish_signed_event(signed) await publish_signed_event(signed)
prefix = f"{log_context}: " if log_context else "" prefix = f"{log_context}: " if log_context else ""
logger.info( logger.info(
f"satmachineadmin: {prefix}published kind-30078 to ATM " f"spirekeeper: {prefix}published kind-30078 to ATM "
f"{recipient_pubkey_hex[:12]}... d-tag={d_tag} " f"{recipient_pubkey_hex[:12]}... d-tag={d_tag} "
f"event_id={signed['id'][:12]}..." f"event_id={signed['id'][:12]}..."
) )

View file

@ -11,7 +11,7 @@ on a match.
The hook is registered with lnbits' `nostr_transport` at extension-init The hook is registered with lnbits' `nostr_transport` at extension-init
time via `register_with_lnbits()`. Until the lnbits side ships time via `register_with_lnbits()`. Until the lnbits side ships
`lnbits.core.services.nostr_transport.register_roster_resolver`, the `lnbits.core.services.nostr_transport.register_roster_resolver`, the
registration call lazily-imports + soft-fails so satmachineadmin keeps registration call lazily-imports + soft-fails so spirekeeper keeps
loading cleanly on any lnbits version. loading cleanly on any lnbits version.
When the lnbits implementation lands + the satmachine instance has When the lnbits implementation lands + the satmachine instance has
@ -37,7 +37,7 @@ from loguru import logger
from .crud import get_machine_by_atm_pubkey_hex from .crud import get_machine_by_atm_pubkey_hex
_SOURCE_EXTENSION = "satmachineadmin" _SOURCE_EXTENSION = "spirekeeper"
@dataclass(frozen=True) @dataclass(frozen=True)
@ -111,11 +111,11 @@ def register_with_lnbits() -> bool:
Returns True if the registration landed (lnbits surface available Returns True if the registration landed (lnbits surface available
+ call succeeded), False if soft-failed because lnbits hasn't + call succeeded), False if soft-failed because lnbits hasn't
shipped `register_roster_resolver` yet that's the expected shipped `register_roster_resolver` yet that's the expected
state until the path-B lnbits PR lands. Either way satmachineadmin state until the path-B lnbits PR lands. Either way spirekeeper
boots cleanly; only the routing-via-roster behavior is gated on boots cleanly; only the routing-via-roster behavior is gated on
the lnbits side being present. the lnbits side being present.
Called once from `satmachineadmin_start()`. Idempotent on the Called once from `spirekeeper_start()`. Idempotent on the
lnbits side per their 15:15Z spec ("re-registration on extension lnbits side per their 15:15Z spec ("re-registration on extension
reload replaces cleanly"). reload replaces cleanly").
""" """
@ -125,7 +125,7 @@ def register_with_lnbits() -> bool:
) )
except ImportError: except ImportError:
logger.info( logger.info(
"satmachineadmin: nostr-transport roster-resolver hook not " "spirekeeper: nostr-transport roster-resolver hook not "
"available on this lnbits version (pre-path-B); ATM-npub " "available on this lnbits version (pre-path-B); ATM-npub "
"routing falls through to lnbits' default auto-account-from-" "routing falls through to lnbits' default auto-account-from-"
"npub behaviour. See aiolabs/satmachineadmin#20 / coord-log " "npub behaviour. See aiolabs/satmachineadmin#20 / coord-log "
@ -134,7 +134,7 @@ def register_with_lnbits() -> bool:
return False return False
register_roster_resolver(_SOURCE_EXTENSION, resolve) register_roster_resolver(_SOURCE_EXTENSION, resolve)
logger.info( logger.info(
f"satmachineadmin: registered '{_SOURCE_EXTENSION}' roster " f"spirekeeper: registered '{_SOURCE_EXTENSION}' roster "
"resolver with lnbits nostr-transport — inbound kind-21000 " "resolver with lnbits nostr-transport — inbound kind-21000 "
"from a registered ATM npub will route to the operator's wallet " "from a registered ATM npub will route to the operator's wallet "
"directly. (Behavior gated server-side by " "directly. (Behavior gated server-side by "

View file

@ -1,7 +1,7 @@
[tool.poetry] [tool.poetry]
name = "satmachineadmin" name = "spirekeeper"
version = "0.0.4" version = "0.1.0"
description = "Eightball is a simple API that allows you to create a random number generator." description = "bitSpire admin — Lightning DCA + Lamassu ATM operator administration for LNbits"
authors = ["benarc", "dni <dni@lnbits.com>"] authors = ["benarc", "dni <dni@lnbits.com>"]
[tool.poetry.dependencies] [tool.poetry.dependencies]

View file

@ -1,6 +1,6 @@
// Satoshi Machine v2 — operator dashboard (P9a foundation). // Satoshi Machine v2 — operator dashboard (P9a foundation).
// //
// Vue 3 + Quasar UMD app. Talks to the v2 satmachineadmin REST surface // Vue 3 + Quasar UMD app. Talks to the v2 spirekeeper REST surface
// (machines / clients / deposits / settlements / commission-splits / // (machines / clients / deposits / settlements / commission-splits /
// super-config). All endpoints are operator-scoped via the LNbits session. // super-config). All endpoints are operator-scoped via the LNbits session.
// //
@ -12,7 +12,7 @@
// - For pale backgrounds (bg-*-1), pair with explicit dark text class // - For pale backgrounds (bg-*-1), pair with explicit dark text class
// so dark-mode users don't get unreadable white-on-cream. // so dark-mode users don't get unreadable white-on-cream.
const API = '/satmachineadmin/api/v1/dca' const API = '/spirekeeper/api/v1/dca'
const SUPER_FEE_PATH = `${API}/super-config` const SUPER_FEE_PATH = `${API}/super-config`
const MACHINES_PATH = `${API}/machines` const MACHINES_PATH = `${API}/machines`
const SETTLEMENTS_PATH = `${API}/settlements` const SETTLEMENTS_PATH = `${API}/settlements`
@ -198,7 +198,7 @@ window.app = Vue.createApp({
settlements: [], settlements: [],
// Cassettes sub-tab state (#29 v1) — see openCassettePublishConfirm / // Cassettes sub-tab state (#29 v1) — see openCassettePublishConfirm /
// submitCassettePublish methods + the cassettes panel in // submitCassettePublish methods + the cassettes panel in
// templates/satmachineadmin/index.html. // templates/spirekeeper/index.html.
activeTab: 'settlements', activeTab: 'settlements',
cassetteEdits: [], // editable working copy of cassette_configs rows cassetteEdits: [], // editable working copy of cassette_configs rows
cassettesPristine: [], // last-known-clean snapshot for revert cassettesPristine: [], // last-known-clean snapshot for revert

View file

@ -45,7 +45,7 @@ from .crud import (
from .distribution import process_settlement from .distribution import process_settlement
from .models import CreateDcaSettlementData, Machine from .models import CreateDcaSettlementData, Machine
LISTENER_NAME = "ext_satmachineadmin" LISTENER_NAME = "ext_spirekeeper"
# Holds strong refs to in-flight distribution tasks so Python's GC doesn't # Holds strong refs to in-flight distribution tasks so Python's GC doesn't
# collect them mid-flight (asyncio.create_task only weakly references its # collect them mid-flight (asyncio.create_task only weakly references its
@ -58,7 +58,7 @@ async def wait_for_paid_invoices() -> None:
invoice_queue: asyncio.Queue = asyncio.Queue() invoice_queue: asyncio.Queue = asyncio.Queue()
register_invoice_listener(invoice_queue, LISTENER_NAME) register_invoice_listener(invoice_queue, LISTENER_NAME)
logger.info( logger.info(
"satmachineadmin v2: invoice listener registered as " "spirekeeper v2: invoice listener registered as "
f"`{LISTENER_NAME}` — waiting for bitSpire settlements." f"`{LISTENER_NAME}` — waiting for bitSpire settlements."
) )
while True: while True:
@ -67,7 +67,7 @@ async def wait_for_paid_invoices() -> None:
await _handle_payment(payment) await _handle_payment(payment)
except Exception as exc: # listener must never die except Exception as exc: # listener must never die
logger.error( logger.error(
f"satmachineadmin: error handling payment " f"spirekeeper: error handling payment "
f"{payment.payment_hash[:12]}...: {exc}" f"{payment.payment_hash[:12]}...: {exc}"
) )
@ -169,12 +169,12 @@ async def _handle_payment(payment: Payment) -> None:
settlement = await create_settlement_idempotent(data, initial_status="pending") settlement = await create_settlement_idempotent(data, initial_status="pending")
if settlement is None: if settlement is None:
logger.error( logger.error(
f"satmachineadmin: failed to insert settlement for " f"spirekeeper: failed to insert settlement for "
f"payment_hash={payment.payment_hash[:12]}..." f"payment_hash={payment.payment_hash[:12]}..."
) )
return return
logger.info( logger.info(
f"satmachineadmin: landed settlement {settlement.id} for " f"spirekeeper: landed settlement {settlement.id} for "
f"machine={machine.machine_npub[:12]}... " f"machine={machine.machine_npub[:12]}... "
f"wire={data.wire_sats}sats principal={data.principal_sats}sats " f"wire={data.wire_sats}sats principal={data.principal_sats}sats "
f"fee={data.fee_sats}sats " f"fee={data.fee_sats}sats "
@ -224,12 +224,12 @@ async def _record_rejected(payment: Payment, machine: Machine, exc: Exception) -
) )
if rejected is None: if rejected is None:
logger.error( logger.error(
f"satmachineadmin: failed to insert rejected settlement for " f"spirekeeper: failed to insert rejected settlement for "
f"payment_hash={payment.payment_hash[:12]}..." f"payment_hash={payment.payment_hash[:12]}..."
) )
return return
logger.error( logger.error(
f"satmachineadmin: rejected settlement {rejected.id} " f"spirekeeper: rejected settlement {rejected.id} "
f"(machine={machine.machine_npub[:12]}..., " f"(machine={machine.machine_npub[:12]}..., "
f"payment_hash={payment.payment_hash[:12]}...): {exc}" f"payment_hash={payment.payment_hash[:12]}...): {exc}"
) )
@ -245,7 +245,7 @@ async def _record_rejected(payment: Payment, machine: Machine, exc: Exception) -
# upserts cassette_configs via apply_bootstrap_state. # upserts cassette_configs via apply_bootstrap_state.
# #
# v1 = one-shot per machine (ATM's meta.bootstrapPublishedAt makes the # v1 = one-shot per machine (ATM's meta.bootstrapPublishedAt makes the
# publish idempotent on ATM-side restart; satmachineadmin's apply_bootstrap_ # publish idempotent on ATM-side restart; spirekeeper's apply_bootstrap_
# state dedups on state_event_id for relay re-delivery). # state dedups on state_event_id for relay re-delivery).
# #
# v2 (separate issue) = continuous reverse-channel consumer with a # v2 (separate issue) = continuous reverse-channel consumer with a
@ -258,7 +258,7 @@ async def _record_rejected(payment: Payment, machine: Machine, exc: Exception) -
# websocket. The relay manager is the same singleton publish_to_atm uses, # websocket. The relay manager is the same singleton publish_to_atm uses,
# so add_subscription registers a filter against the same relay pool. # so add_subscription registers a filter against the same relay pool.
CASSETTE_BOOTSTRAP_SUB_ID = "satmachineadmin-cassette-bootstrap" CASSETTE_BOOTSTRAP_SUB_ID = "spirekeeper-cassette-bootstrap"
_CASSETTE_POLL_INTERVAL_S = 2.0 _CASSETTE_POLL_INTERVAL_S = 2.0
_CASSETTE_BACKOFF_S = 30.0 # when nostrclient isn't installed yet _CASSETTE_BACKOFF_S = 30.0 # when nostrclient isn't installed yet
@ -280,7 +280,7 @@ async def wait_for_cassette_state_events() -> None:
- apply_bootstrap_state errors log + skip - apply_bootstrap_state errors log + skip
""" """
logger.info( logger.info(
"satmachineadmin v2: cassette bootstrap consumer starting " "spirekeeper v2: cassette bootstrap consumer starting "
f"(sub_id={CASSETTE_BOOTSTRAP_SUB_ID})" f"(sub_id={CASSETTE_BOOTSTRAP_SUB_ID})"
) )
current_filter_key: str | None = None current_filter_key: str | None = None
@ -290,7 +290,7 @@ async def wait_for_cassette_state_events() -> None:
await asyncio.sleep(_CASSETTE_POLL_INTERVAL_S) await asyncio.sleep(_CASSETTE_POLL_INTERVAL_S)
except _NostrclientUnavailable: except _NostrclientUnavailable:
logger.warning( logger.warning(
"satmachineadmin: nostrclient extension not installed; " "spirekeeper: nostrclient extension not installed; "
f"cassette bootstrap consumer sleeping {_CASSETTE_BACKOFF_S}s " f"cassette bootstrap consumer sleeping {_CASSETTE_BACKOFF_S}s "
"before retry. Install + activate nostrclient on this " "before retry. Install + activate nostrclient on this "
"LNbits instance." "LNbits instance."
@ -299,7 +299,7 @@ async def wait_for_cassette_state_events() -> None:
await asyncio.sleep(_CASSETTE_BACKOFF_S) await asyncio.sleep(_CASSETTE_BACKOFF_S)
except Exception as exc: # listener must never die except Exception as exc: # listener must never die
logger.error( logger.error(
f"satmachineadmin: cassette consumer loop error (continuing): " f"{exc}" f"spirekeeper: cassette consumer loop error (continuing): " f"{exc}"
) )
await asyncio.sleep(_CASSETTE_POLL_INTERVAL_S) await asyncio.sleep(_CASSETTE_POLL_INTERVAL_S)
@ -346,13 +346,13 @@ async def _cassette_consumer_tick(current_filter_key: str | None) -> str:
CASSETTE_BOOTSTRAP_SUB_ID, filters # type: ignore[arg-type] CASSETTE_BOOTSTRAP_SUB_ID, filters # type: ignore[arg-type]
) )
logger.info( logger.info(
"satmachineadmin: (re)registered cassette bootstrap " "spirekeeper: (re)registered cassette bootstrap "
f"subscription with {len(d_tags)} d-tag(s)" f"subscription with {len(d_tags)} d-tag(s)"
) )
else: else:
nostr_client.relay_manager.close_subscription(CASSETTE_BOOTSTRAP_SUB_ID) nostr_client.relay_manager.close_subscription(CASSETTE_BOOTSTRAP_SUB_ID)
logger.info( logger.info(
"satmachineadmin: no active machines; closed cassette " "spirekeeper: no active machines; closed cassette "
"bootstrap subscription" "bootstrap subscription"
) )
@ -368,7 +368,7 @@ async def _cassette_consumer_tick(current_filter_key: str | None) -> str:
) )
except Exception as exc: except Exception as exc:
logger.warning( logger.warning(
f"satmachineadmin: cassette state event handler " f"spirekeeper: cassette state event handler "
f"failed (skipping): {exc}" f"failed (skipping): {exc}"
) )
@ -419,14 +419,14 @@ async def _handle_cassette_state_event(
event_obj = event_raw event_obj = event_raw
else: else:
logger.warning( logger.warning(
f"satmachineadmin: cassette event of unexpected type " f"spirekeeper: cassette event of unexpected type "
f"{type(event_raw).__name__}; skipping" f"{type(event_raw).__name__}; skipping"
) )
return return
if not verify_event(event_obj): if not verify_event(event_obj):
logger.warning( logger.warning(
f"satmachineadmin: cassette state event sig verify failed " f"spirekeeper: cassette state event sig verify failed "
f"(id={event_obj.get('id', '?')[:12]}...)" f"(id={event_obj.get('id', '?')[:12]}...)"
) )
return return
@ -437,7 +437,7 @@ async def _handle_cassette_state_event(
# Unknown sender — could be relay noise or an attacker. Don't # Unknown sender — could be relay noise or an attacker. Don't
# treat as our problem. # treat as our problem.
logger.warning( logger.warning(
f"satmachineadmin: cassette state event from unknown ATM " f"spirekeeper: cassette state event from unknown ATM "
f"pubkey {sender_pubkey[:12]}... (not in dca_machines); " f"pubkey {sender_pubkey[:12]}... (not in dca_machines); "
"skipping" "skipping"
) )
@ -448,7 +448,7 @@ async def _handle_cassette_state_event(
except CassetteTransportError as exc: except CassetteTransportError as exc:
# OperatorIdentityMissing / SignerUnavailable — log + skip. # OperatorIdentityMissing / SignerUnavailable — log + skip.
logger.warning( logger.warning(
f"satmachineadmin: can't resolve signer for operator " f"spirekeeper: can't resolve signer for operator "
f"{machine.operator_user_id[:8]}... (machine {machine.id}): " f"{machine.operator_user_id[:8]}... (machine {machine.id}): "
f"{exc}" f"{exc}"
) )
@ -458,13 +458,13 @@ async def _handle_cassette_state_event(
payload = await decrypt_and_parse_state_event(event_obj, account, signer) payload = await decrypt_and_parse_state_event(event_obj, account, signer)
except CassetteEventTransientError as exc: except CassetteEventTransientError as exc:
logger.info( logger.info(
f"satmachineadmin: cassette state event for machine {machine.id} " f"spirekeeper: cassette state event for machine {machine.id} "
f"hit a transient signer error (will retry next poll): {exc}" f"hit a transient signer error (will retry next poll): {exc}"
) )
return return
except CassetteEventDecodeError as exc: except CassetteEventDecodeError as exc:
logger.warning( logger.warning(
f"satmachineadmin: cassette state event decode failed for " f"spirekeeper: cassette state event decode failed for "
f"machine {machine.id} (id={event_obj.get('id', '?')[:12]}...): " f"machine {machine.id} (id={event_obj.get('id', '?')[:12]}...): "
f"{exc}" f"{exc}"
) )
@ -479,12 +479,12 @@ async def _handle_cassette_state_event(
) )
if applied: if applied:
logger.info( logger.info(
f"satmachineadmin: applied bootstrap state event {event_id[:12]}... " f"spirekeeper: applied bootstrap state event {event_id[:12]}... "
f"to machine {machine.id} ({len(payload.positions)} cassettes)" f"to machine {machine.id} ({len(payload.positions)} cassettes)"
) )
else: else:
# Replay: event_id already on file. Normal on relay reconnect. # Replay: event_id already on file. Normal on relay reconnect.
logger.debug( logger.debug(
f"satmachineadmin: cassette state event {event_id[:12]}... " f"spirekeeper: cassette state event {event_id[:12]}... "
f"already applied to machine {machine.id} (replay no-op)" f"already applied to machine {machine.id} (replay no-op)"
) )

View file

@ -3,7 +3,7 @@
{% block scripts %} {% block scripts %}
{{ window_vars(user) }} {{ window_vars(user) }}
<script src="{{ static_url_for('satmachineadmin/static', path='js/index.js') }}"></script> <script src="{{ static_url_for('spirekeeper/static', path='js/index.js') }}"></script>
{% endblock %} {% endblock %}
{% block page %} {% block page %}

View file

@ -1,5 +1,5 @@
""" """
Pytest configuration for the satmachineadmin extension test suite. Pytest configuration for the spirekeeper extension test suite.
Provides a `loguru_capture` fixture for tests that need to verify Provides a `loguru_capture` fixture for tests that need to verify
loguru WARN/ERROR side-effects. Loguru attaches its default sink to loguru WARN/ERROR side-effects. Loguru attaches its default sink to

View file

@ -210,7 +210,7 @@ class TestShouldApplyBootstrapState:
def test_skips_when_existing_event_id_matches(self): def test_skips_when_existing_event_id_matches(self):
"""The same bootstrap event re-delivered after a relay reconnect """The same bootstrap event re-delivered after a relay reconnect
or satmachineadmin restart should no-op, not re-upsert the same or spirekeeper restart should no-op, not re-upsert the same
rows (which would clobber any operator edits since).""" rows (which would clobber any operator edits since)."""
assert _should_apply_bootstrap_state("same-event", "same-event") is False assert _should_apply_bootstrap_state("same-event", "same-event") is False

View file

@ -10,7 +10,7 @@ auto-account wallet.
The guard refuses to register a machine whose npub matches any LNbits The guard refuses to register a machine whose npub matches any LNbits
operator account's `accounts.pubkey`, so this state cannot be entered operator account's `accounts.pubkey`, so this state cannot be entered
through the satmachineadmin UI in the first place. through the spirekeeper UI in the first place.
Monkeypatches `views_api.get_account_by_pubkey` to avoid needing a live Monkeypatches `views_api.get_account_by_pubkey` to avoid needing a live
LNbits DB; this matches the assertion-style of tests/test_nostr_attribution LNbits DB; this matches the assertion-style of tests/test_nostr_attribution

View file

@ -7,7 +7,7 @@ Each settlement records:
fee_mismatch_sats = bitspire_fee_sats - (platform_fee_sats + operator_fee_sats) fee_mismatch_sats = bitspire_fee_sats - (platform_fee_sats + operator_fee_sats)
Positive = bitspire over-reported (claimed more fee than satmachineadmin Positive = bitspire over-reported (claimed more fee than spirekeeper
recomputed against principal). Negative = bitspire under-reported. recomputed against principal). Negative = bitspire under-reported.
Zero = exact match. Zero = exact match.
@ -116,7 +116,7 @@ class TestFeeMismatchSatsRecording:
"""Real-world Phase-1 scenario before Layer 3 (lamassu-next#57) """Real-world Phase-1 scenario before Layer 3 (lamassu-next#57)
ships: ATM hardcodes 7.77% cash-out; operator configures 5% ships: ATM hardcodes 7.77% cash-out; operator configures 5%
operator + 3% super = 8% total. Bitspire reports operator + 3% super = 8% total. Bitspire reports
100_000 * 0.0777 = 7_770 sats; satmachineadmin recomputes 8_000. 100_000 * 0.0777 = 7_770 sats; spirekeeper recomputes 8_000.
Delta is large and visible for triage; behavior unchanged.""" Delta is large and visible for triage; behavior unchanged."""
machine = _machine(op_out=0.05) machine = _machine(op_out=0.05)
super_cfg = _super_config(out_frac=0.03) super_cfg = _super_config(out_frac=0.03)

View file

@ -1,6 +1,6 @@
""" """
Tests for `nostr_transport_roster.resolve` the lookup function Tests for `nostr_transport_roster.resolve` the lookup function
satmachineadmin hands lnbits' nostr-transport via spirekeeper hands lnbits' nostr-transport via
`register_roster_resolver` (path-B wallet-routing fix, #20 / `register_roster_resolver` (path-B wallet-routing fix, #20 /
coord-log 2026-05-31T15:25Z). coord-log 2026-05-31T15:25Z).
@ -73,7 +73,7 @@ def test_resolve_known_atm_returns_route_hit(monkeypatch):
assert result is not None assert result is not None
assert result.operator_user_id == "op-123" assert result.operator_user_id == "op-123"
assert result.wallet_id == "wallet-abc" assert result.wallet_id == "wallet-abc"
assert result.source_extension == "satmachineadmin" assert result.source_extension == "spirekeeper"
assert captured["pubkey_hex"] == _ATM_PUB_HEX assert captured["pubkey_hex"] == _ATM_PUB_HEX
@ -141,7 +141,7 @@ def test_resolve_raises_on_malformed_input(monkeypatch):
def test_register_with_lnbits_soft_fails_without_hook(monkeypatch): def test_register_with_lnbits_soft_fails_without_hook(monkeypatch):
"""Until the lnbits-side path-B PR lands, the registration call """Until the lnbits-side path-B PR lands, the registration call
must soft-fail cleanly (returns False, no exception) so must soft-fail cleanly (returns False, no exception) so
satmachineadmin keeps booting on every lnbits version.""" spirekeeper keeps booting on every lnbits version."""
real_import = ( real_import = (
__builtins__["__import__"] __builtins__["__import__"]
if isinstance(__builtins__, dict) if isinstance(__builtins__, dict)

View file

@ -10,16 +10,16 @@ from lnbits.core.models import User
from lnbits.decorators import check_user_exists from lnbits.decorators import check_user_exists
from lnbits.helpers import template_renderer from lnbits.helpers import template_renderer
satmachineadmin_generic_router = APIRouter() spirekeeper_generic_router = APIRouter()
def satmachineadmin_renderer(): def spirekeeper_renderer():
return template_renderer(["satmachineadmin/templates"]) return template_renderer(["spirekeeper/templates"])
@satmachineadmin_generic_router.get("/", response_class=HTMLResponse) @spirekeeper_generic_router.get("/", response_class=HTMLResponse)
async def index(req: Request, user: User = Depends(check_user_exists)): async def index(req: Request, user: User = Depends(check_user_exists)):
return satmachineadmin_renderer().TemplateResponse( return spirekeeper_renderer().TemplateResponse(
"satmachineadmin/index.html", "spirekeeper/index.html",
{"request": req, "user": user.json()}, {"request": req, "user": user.json()},
) )

View file

@ -94,7 +94,7 @@ from .models import (
UpsertCassetteConfigData, UpsertCassetteConfigData,
) )
satmachineadmin_api_router = APIRouter() spirekeeper_api_router = APIRouter()
async def _assert_wallet_owned_by(wallet_id: str, user_id: str) -> None: async def _assert_wallet_owned_by(wallet_id: str, user_id: str) -> None:
@ -254,7 +254,7 @@ async def _assert_super_config_cap_safe(
# ============================================================================= # =============================================================================
@satmachineadmin_api_router.post("/api/v1/dca/machines", response_model=Machine) @spirekeeper_api_router.post("/api/v1/dca/machines", response_model=Machine)
async def api_create_machine( async def api_create_machine(
data: CreateMachineData, user: User = Depends(check_user_exists) data: CreateMachineData, user: User = Depends(check_user_exists)
) -> Machine: ) -> Machine:
@ -274,14 +274,14 @@ async def api_create_machine(
return machine return machine
@satmachineadmin_api_router.get("/api/v1/dca/machines", response_model=list[Machine]) @spirekeeper_api_router.get("/api/v1/dca/machines", response_model=list[Machine])
async def api_list_machines( async def api_list_machines(
user: User = Depends(check_user_exists), user: User = Depends(check_user_exists),
) -> list[Machine]: ) -> list[Machine]:
return await get_machines_for_operator(user.id) return await get_machines_for_operator(user.id)
@satmachineadmin_api_router.get( @spirekeeper_api_router.get(
"/api/v1/dca/machines/{machine_id}", response_model=Machine "/api/v1/dca/machines/{machine_id}", response_model=Machine
) )
async def api_get_machine( async def api_get_machine(
@ -293,7 +293,7 @@ async def api_get_machine(
return machine return machine
@satmachineadmin_api_router.put( @spirekeeper_api_router.put(
"/api/v1/dca/machines/{machine_id}", response_model=Machine "/api/v1/dca/machines/{machine_id}", response_model=Machine
) )
async def api_update_machine( async def api_update_machine(
@ -341,7 +341,7 @@ async def api_update_machine(
return updated return updated
@satmachineadmin_api_router.delete( @spirekeeper_api_router.delete(
"/api/v1/dca/machines/{machine_id}", status_code=HTTPStatus.NO_CONTENT "/api/v1/dca/machines/{machine_id}", status_code=HTTPStatus.NO_CONTENT
) )
async def api_delete_machine( async def api_delete_machine(
@ -379,7 +379,7 @@ async def _client_owned_by(client_id: str, user_id: str) -> DcaClient:
return client return client
@satmachineadmin_api_router.post("/api/v1/dca/clients", response_model=DcaClient) @spirekeeper_api_router.post("/api/v1/dca/clients", response_model=DcaClient)
async def api_create_client( async def api_create_client(
data: CreateDcaClientData, user: User = Depends(check_user_exists) data: CreateDcaClientData, user: User = Depends(check_user_exists)
) -> DcaClient: ) -> DcaClient:
@ -388,7 +388,7 @@ async def api_create_client(
return await create_dca_client(data) return await create_dca_client(data)
@satmachineadmin_api_router.get("/api/v1/dca/clients", response_model=list[DcaClient]) @spirekeeper_api_router.get("/api/v1/dca/clients", response_model=list[DcaClient])
async def api_list_clients( async def api_list_clients(
machine_id: str | None = None, machine_id: str | None = None,
user: User = Depends(check_user_exists), user: User = Depends(check_user_exists),
@ -402,7 +402,7 @@ async def api_list_clients(
return await get_dca_clients_for_machine(machine_id) return await get_dca_clients_for_machine(machine_id)
@satmachineadmin_api_router.get( @spirekeeper_api_router.get(
"/api/v1/dca/clients/{client_id}", response_model=DcaClient "/api/v1/dca/clients/{client_id}", response_model=DcaClient
) )
async def api_get_client( async def api_get_client(
@ -411,7 +411,7 @@ async def api_get_client(
return await _client_owned_by(client_id, user.id) return await _client_owned_by(client_id, user.id)
@satmachineadmin_api_router.put( @spirekeeper_api_router.put(
"/api/v1/dca/clients/{client_id}", response_model=DcaClient "/api/v1/dca/clients/{client_id}", response_model=DcaClient
) )
async def api_update_client( async def api_update_client(
@ -426,7 +426,7 @@ async def api_update_client(
return updated return updated
@satmachineadmin_api_router.delete( @spirekeeper_api_router.delete(
"/api/v1/dca/clients/{client_id}", status_code=HTTPStatus.NO_CONTENT "/api/v1/dca/clients/{client_id}", status_code=HTTPStatus.NO_CONTENT
) )
async def api_delete_client( async def api_delete_client(
@ -436,7 +436,7 @@ async def api_delete_client(
await delete_dca_client(client_id) await delete_dca_client(client_id)
@satmachineadmin_api_router.get( @spirekeeper_api_router.get(
"/api/v1/dca/clients/{client_id}/balance", "/api/v1/dca/clients/{client_id}/balance",
response_model=ClientBalanceSummary, response_model=ClientBalanceSummary,
) )
@ -450,7 +450,7 @@ async def api_get_client_balance(
return summary return summary
@satmachineadmin_api_router.post( @spirekeeper_api_router.post(
"/api/v1/dca/clients/{client_id}/settle", response_model=DcaPayment "/api/v1/dca/clients/{client_id}/settle", response_model=DcaPayment
) )
async def api_settle_client_balance( async def api_settle_client_balance(
@ -498,7 +498,7 @@ async def _deposit_owned_by(deposit_id: str, user_id: str) -> DcaDeposit:
return deposit return deposit
@satmachineadmin_api_router.post("/api/v1/dca/deposits", response_model=DcaDeposit) @spirekeeper_api_router.post("/api/v1/dca/deposits", response_model=DcaDeposit)
async def api_create_deposit( async def api_create_deposit(
data: CreateDepositData, user: User = Depends(check_user_exists) data: CreateDepositData, user: User = Depends(check_user_exists)
) -> DcaDeposit: ) -> DcaDeposit:
@ -531,7 +531,7 @@ async def api_create_deposit(
return await create_deposit(user.id, data, currency=machine.fiat_code) return await create_deposit(user.id, data, currency=machine.fiat_code)
@satmachineadmin_api_router.get("/api/v1/dca/deposits", response_model=list[DcaDeposit]) @spirekeeper_api_router.get("/api/v1/dca/deposits", response_model=list[DcaDeposit])
async def api_list_deposits( async def api_list_deposits(
client_id: str | None = None, client_id: str | None = None,
user: User = Depends(check_user_exists), user: User = Depends(check_user_exists),
@ -544,7 +544,7 @@ async def api_list_deposits(
return await get_deposits_for_operator(user.id) return await get_deposits_for_operator(user.id)
@satmachineadmin_api_router.get( @spirekeeper_api_router.get(
"/api/v1/dca/deposits/{deposit_id}", response_model=DcaDeposit "/api/v1/dca/deposits/{deposit_id}", response_model=DcaDeposit
) )
async def api_get_deposit( async def api_get_deposit(
@ -553,7 +553,7 @@ async def api_get_deposit(
return await _deposit_owned_by(deposit_id, user.id) return await _deposit_owned_by(deposit_id, user.id)
@satmachineadmin_api_router.put( @spirekeeper_api_router.put(
"/api/v1/dca/deposits/{deposit_id}", response_model=DcaDeposit "/api/v1/dca/deposits/{deposit_id}", response_model=DcaDeposit
) )
async def api_update_deposit( async def api_update_deposit(
@ -573,7 +573,7 @@ async def api_update_deposit(
return updated return updated
@satmachineadmin_api_router.put( @spirekeeper_api_router.put(
"/api/v1/dca/deposits/{deposit_id}/status", response_model=DcaDeposit "/api/v1/dca/deposits/{deposit_id}/status", response_model=DcaDeposit
) )
async def api_update_deposit_status( async def api_update_deposit_status(
@ -588,7 +588,7 @@ async def api_update_deposit_status(
return updated return updated
@satmachineadmin_api_router.delete( @spirekeeper_api_router.delete(
"/api/v1/dca/deposits/{deposit_id}", status_code=HTTPStatus.NO_CONTENT "/api/v1/dca/deposits/{deposit_id}", status_code=HTTPStatus.NO_CONTENT
) )
async def api_delete_deposit( async def api_delete_deposit(
@ -608,7 +608,7 @@ async def api_delete_deposit(
# ============================================================================= # =============================================================================
@satmachineadmin_api_router.get( @spirekeeper_api_router.get(
"/api/v1/dca/settlements", response_model=list[DcaSettlement] "/api/v1/dca/settlements", response_model=list[DcaSettlement]
) )
async def api_list_settlements( async def api_list_settlements(
@ -617,7 +617,7 @@ async def api_list_settlements(
return await get_settlements_for_operator(user.id) return await get_settlements_for_operator(user.id)
@satmachineadmin_api_router.get( @spirekeeper_api_router.get(
"/api/v1/dca/machines/{machine_id}/settlements", "/api/v1/dca/machines/{machine_id}/settlements",
response_model=list[DcaSettlement], response_model=list[DcaSettlement],
) )
@ -637,7 +637,7 @@ async def api_list_settlements_for_machine(
# order). # order).
@satmachineadmin_api_router.get( @spirekeeper_api_router.get(
"/api/v1/dca/settlements/stuck", response_model=StuckSettlementsResponse "/api/v1/dca/settlements/stuck", response_model=StuckSettlementsResponse
) )
async def api_list_stuck_settlements( async def api_list_stuck_settlements(
@ -668,7 +668,7 @@ async def api_list_stuck_settlements(
) )
@satmachineadmin_api_router.get( @spirekeeper_api_router.get(
"/api/v1/dca/settlements/{settlement_id}", response_model=DcaSettlement "/api/v1/dca/settlements/{settlement_id}", response_model=DcaSettlement
) )
async def api_get_settlement( async def api_get_settlement(
@ -683,7 +683,7 @@ async def api_get_settlement(
return settlement return settlement
@satmachineadmin_api_router.post( @spirekeeper_api_router.post(
"/api/v1/dca/settlements/{settlement_id}/partial-dispense", "/api/v1/dca/settlements/{settlement_id}/partial-dispense",
response_model=DcaSettlement, response_model=DcaSettlement,
) )
@ -718,7 +718,7 @@ async def api_partial_dispense(
raise HTTPException(HTTPStatus.BAD_REQUEST, str(exc)) from exc raise HTTPException(HTTPStatus.BAD_REQUEST, str(exc)) from exc
@satmachineadmin_api_router.post( @spirekeeper_api_router.post(
"/api/v1/dca/settlements/{settlement_id}/force-reset", "/api/v1/dca/settlements/{settlement_id}/force-reset",
response_model=DcaSettlement, response_model=DcaSettlement,
) )
@ -768,7 +768,7 @@ async def api_force_reset_settlement(
return updated return updated
@satmachineadmin_api_router.post( @spirekeeper_api_router.post(
"/api/v1/dca/settlements/{settlement_id}/retry", "/api/v1/dca/settlements/{settlement_id}/retry",
response_model=DcaSettlement, response_model=DcaSettlement,
) )
@ -821,7 +821,7 @@ async def api_retry_settlement(
return after if after is not None else updated return after if after is not None else updated
@satmachineadmin_api_router.post( @spirekeeper_api_router.post(
"/api/v1/dca/settlements/{settlement_id}/notes", "/api/v1/dca/settlements/{settlement_id}/notes",
response_model=DcaSettlement, response_model=DcaSettlement,
) )
@ -854,7 +854,7 @@ async def api_append_settlement_note(
# ============================================================================= # =============================================================================
@satmachineadmin_api_router.get("/api/v1/dca/payments", response_model=list[DcaPayment]) @spirekeeper_api_router.get("/api/v1/dca/payments", response_model=list[DcaPayment])
async def api_list_payments( async def api_list_payments(
leg_type: str | None = None, leg_type: str | None = None,
user: User = Depends(check_user_exists), user: User = Depends(check_user_exists),
@ -869,7 +869,7 @@ async def api_list_payments(
# ============================================================================= # =============================================================================
@satmachineadmin_api_router.get( @spirekeeper_api_router.get(
"/api/v1/dca/commission-splits", response_model=list[CommissionSplit] "/api/v1/dca/commission-splits", response_model=list[CommissionSplit]
) )
async def api_get_commission_splits( async def api_get_commission_splits(
@ -889,7 +889,7 @@ async def api_get_commission_splits(
return await get_commission_splits(user.id, None) return await get_commission_splits(user.id, None)
@satmachineadmin_api_router.put( @spirekeeper_api_router.put(
"/api/v1/dca/commission-splits", response_model=list[CommissionSplit] "/api/v1/dca/commission-splits", response_model=list[CommissionSplit]
) )
async def api_replace_commission_splits( async def api_replace_commission_splits(
@ -905,7 +905,7 @@ async def api_replace_commission_splits(
return await replace_commission_splits(user.id, data.machine_id, data.legs) return await replace_commission_splits(user.id, data.machine_id, data.legs)
@satmachineadmin_api_router.delete( @spirekeeper_api_router.delete(
"/api/v1/dca/commission-splits", "/api/v1/dca/commission-splits",
status_code=HTTPStatus.NO_CONTENT, status_code=HTTPStatus.NO_CONTENT,
) )
@ -927,7 +927,7 @@ async def api_delete_commission_splits(
# ============================================================================= # =============================================================================
@satmachineadmin_api_router.get("/api/v1/dca/super-config", response_model=SuperConfig) @spirekeeper_api_router.get("/api/v1/dca/super-config", response_model=SuperConfig)
async def api_get_super_config( async def api_get_super_config(
_user: User = Depends(check_user_exists), _user: User = Depends(check_user_exists),
) -> SuperConfig: ) -> SuperConfig:
@ -940,7 +940,7 @@ async def api_get_super_config(
return config return config
@satmachineadmin_api_router.put("/api/v1/dca/super-config", response_model=SuperConfig) @spirekeeper_api_router.put("/api/v1/dca/super-config", response_model=SuperConfig)
async def api_update_super_config( async def api_update_super_config(
data: UpdateSuperConfigData, data: UpdateSuperConfigData,
_user: User = Depends(check_super_user), _user: User = Depends(check_super_user),
@ -988,7 +988,7 @@ async def api_update_super_config(
# fields per row are denomination and count. # fields per row are denomination and count.
@satmachineadmin_api_router.get( @spirekeeper_api_router.get(
"/api/v1/dca/machines/{machine_id}/cassettes", "/api/v1/dca/machines/{machine_id}/cassettes",
response_model=list[CassetteConfig], response_model=list[CassetteConfig],
) )
@ -1003,7 +1003,7 @@ async def api_list_machine_cassettes(
return await list_cassette_configs_for_machine(machine_id) return await list_cassette_configs_for_machine(machine_id)
@satmachineadmin_api_router.post( @spirekeeper_api_router.post(
"/api/v1/dca/machines/{machine_id}/cassettes/publish", "/api/v1/dca/machines/{machine_id}/cassettes/publish",
response_model=list[CassetteConfig], response_model=list[CassetteConfig],
) )
@ -1049,7 +1049,7 @@ async def api_publish_machine_cassettes(
"No cassette_configs rows exist for this machine yet — " "No cassette_configs rows exist for this machine yet — "
"waiting for the ATM's bootstrap state event. Power on the " "waiting for the ATM's bootstrap state event. Power on the "
"ATM and confirm it has reached the configured relay; " "ATM and confirm it has reached the configured relay; "
"satmachineadmin will auto-populate cassette_configs on " "spirekeeper will auto-populate cassette_configs on "
"receipt." "receipt."
), ),
) )
@ -1068,7 +1068,7 @@ async def api_publish_machine_cassettes(
) )
# Apply each per-row edit so the operator-believed state on # Apply each per-row edit so the operator-believed state on
# satmachineadmin reflects the published payload, even if the ATM # spirekeeper reflects the published payload, even if the ATM
# ack lands later (v2). updated_by audit-stamps the operator user id. # ack lands later (v2). updated_by audit-stamps the operator user id.
for pos, row in payload.positions.items(): for pos, row in payload.positions.items():
updated = await update_cassette_config( updated = await update_cassette_config(