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

View file

@ -94,7 +94,7 @@ class SettlementMetadataError(ValueError):
Raised by `parse_settlement`. Caller records the settlement as
'rejected' with the exception message in `error_message`. Operator
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
graceful-degradation case. Pre-v2 reverse-derivation from the
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
# §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.
# Phase 2 (settlement-reject) lands after observability data.
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 `<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)`
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",
"tile": "/satmachineadmin/static/image/aio.png",
"tile": "/spirekeeper/static/image/aio.png",
"min_lnbits_version": "1.0.0",
"contributors": [
{
@ -26,7 +26,7 @@
}
],
"images": [],
"description_md": "/satmachineadmin/description.md",
"terms_and_conditions_md": "/satmachineadmin/toc.md",
"description_md": "/spirekeeper/description.md",
"terms_and_conditions_md": "/spirekeeper/toc.md",
"license": "MIT"
}

156
crud.py
View file

@ -38,7 +38,7 @@ from .models import (
UpsertDcaLpData,
)
db = Database("ext_satoshimachine")
db = Database("ext_spirekeeper")
# =============================================================================
@ -48,7 +48,7 @@ db = Database("ext_satoshimachine")
async def get_super_config() -> SuperConfig | None:
return await db.fetchone(
"SELECT * FROM satoshimachine.super_config WHERE id = :id",
"SELECT * FROM spirekeeper.super_config WHERE id = :id",
{"id": "default"},
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)
update_data["id"] = "default"
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,
)
return await get_super_config()
@ -78,7 +78,7 @@ async def create_machine(operator_user_id: str, data: CreateMachineData) -> Mach
now = datetime.now()
await db.execute(
"""
INSERT INTO satoshimachine.dca_machines
INSERT INTO spirekeeper.dca_machines
(id, operator_user_id, machine_npub, wallet_id, name, location,
fiat_code, is_active,
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:
return await db.fetchone(
"SELECT * FROM satoshimachine.dca_machines WHERE id = :id",
"SELECT * FROM spirekeeper.dca_machines WHERE id = :id",
{"id": machine_id},
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:
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},
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."""
return await db.fetchone(
"""
SELECT * FROM satoshimachine.dca_machines
SELECT * FROM spirekeeper.dca_machines
WHERE wallet_id = :wid AND is_active = true
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]:
return await db.fetchall(
"""
SELECT * FROM satoshimachine.dca_machines
SELECT * FROM spirekeeper.dca_machines
WHERE operator_user_id = :uid
ORDER BY created_at DESC
""",
@ -157,7 +157,7 @@ async def list_all_active_machines() -> list[Machine]:
"""
return await db.fetchall(
"""
SELECT * FROM satoshimachine.dca_machines
SELECT * FROM spirekeeper.dca_machines
WHERE is_active = true
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)
update_data["id"] = machine_id
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,
)
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:
await db.execute(
"DELETE FROM satoshimachine.dca_machines WHERE id = :id",
"DELETE FROM spirekeeper.dca_machines WHERE id = :id",
{"id": machine_id},
)
@ -226,7 +226,7 @@ async def create_dca_client(data: CreateDcaClientData) -> DcaClient:
now = datetime.now()
await db.execute(
"""
INSERT INTO satoshimachine.dca_clients
INSERT INTO spirekeeper.dca_clients
(id, machine_id, user_id, username, status, created_at, updated_at)
VALUES (:id, :machine_id, :user_id, :username, :status,
:created_at, :updated_at)
@ -255,8 +255,8 @@ _CLIENT_SELECT = """
(lp.user_id IS NOT NULL) AS lp_onboarded
"""
_CLIENT_FROM = (
"satoshimachine.dca_clients c "
"LEFT JOIN satoshimachine.dca_lp lp ON lp.user_id = c.user_id"
"spirekeeper.dca_clients c "
"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"""
SELECT {_CLIENT_SELECT}
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
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(
"""
SELECT c.*
FROM satoshimachine.dca_clients c
JOIN satoshimachine.dca_lp lp ON lp.user_id = c.user_id
FROM spirekeeper.dca_clients c
JOIN spirekeeper.dca_lp lp ON lp.user_id = c.user_id
WHERE c.machine_id = :machine_id
AND lp.default_dca_mode = 'flow'
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
via satmachineclient yet."""
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},
DcaLpPreferences,
)
@ -362,7 +362,7 @@ async def get_dca_lp(user_id: str) -> DcaLpPreferences | None:
async def lp_is_onboarded(user_id: str) -> bool:
"""Cheap existence check used by the deposit-creation gate."""
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},
)
return row is not None
@ -392,7 +392,7 @@ async def upsert_dca_lp(
)
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,
autoforward_ln_address, autoforward_enabled,
created_at, updated_at)
@ -417,7 +417,7 @@ async def upsert_dca_lp(
set_clause = ", ".join(f"{k} = :{k}" for k in update_data)
update_data["uid"] = user_id
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,
)
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)
update_data["id"] = client_id
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,
)
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:
await db.execute(
"DELETE FROM satoshimachine.dca_clients WHERE id = :id",
"DELETE FROM spirekeeper.dca_clients WHERE id = :id",
{"id": client_id},
)
@ -466,7 +466,7 @@ async def create_deposit(
deposit_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO satoshimachine.dca_deposits
INSERT INTO spirekeeper.dca_deposits
(id, client_id, machine_id, creator_user_id, amount, currency,
status, notes, created_at)
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:
return await db.fetchone(
"SELECT * FROM satoshimachine.dca_deposits WHERE id = :id",
"SELECT * FROM spirekeeper.dca_deposits WHERE id = :id",
{"id": deposit_id},
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]:
return await db.fetchall(
"""
SELECT * FROM satoshimachine.dca_deposits
SELECT * FROM spirekeeper.dca_deposits
WHERE client_id = :client_id
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(
"""
SELECT d.*
FROM satoshimachine.dca_deposits d
JOIN satoshimachine.dca_machines m ON m.id = d.machine_id
FROM spirekeeper.dca_deposits d
JOIN spirekeeper.dca_machines m ON m.id = d.machine_id
WHERE m.operator_user_id = :uid
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)
update_data["id"] = deposit_id
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,
)
return await get_deposit(deposit_id)
@ -549,7 +549,7 @@ async def update_deposit_status(
}
await db.execute(
"""
UPDATE satoshimachine.dca_deposits
UPDATE spirekeeper.dca_deposits
SET status = :status,
notes = COALESCE(:notes, notes),
confirmed_at = COALESCE(:confirmed_at, confirmed_at)
@ -562,7 +562,7 @@ async def update_deposit_status(
async def delete_deposit(deposit_id: str) -> None:
await db.execute(
"DELETE FROM satoshimachine.dca_deposits WHERE id = :id",
"DELETE FROM spirekeeper.dca_deposits WHERE id = :id",
{"id": deposit_id},
)
@ -598,7 +598,7 @@ async def create_settlement_idempotent(
settlement_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO satoshimachine.dca_settlements
INSERT INTO spirekeeper.dca_settlements
(id, machine_id, payment_hash, bitspire_event_id, bitspire_txid,
wire_sats, fiat_amount, fiat_code, exchange_rate, principal_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:
return await db.fetchone(
"SELECT * FROM satoshimachine.dca_settlements WHERE id = :id",
"SELECT * FROM spirekeeper.dca_settlements WHERE id = :id",
{"id": settlement_id},
DcaSettlement,
)
@ -650,7 +650,7 @@ async def get_settlement_by_payment_hash(
) -> DcaSettlement | None:
return await db.fetchone(
"""
SELECT * FROM satoshimachine.dca_settlements
SELECT * FROM spirekeeper.dca_settlements
WHERE payment_hash = :hash
""",
{"hash": payment_hash},
@ -663,7 +663,7 @@ async def get_settlements_for_machine(
) -> list[DcaSettlement]:
return await db.fetchall(
"""
SELECT * FROM satoshimachine.dca_settlements
SELECT * FROM spirekeeper.dca_settlements
WHERE machine_id = :machine_id
ORDER BY created_at DESC
LIMIT :lim
@ -698,8 +698,8 @@ async def get_stuck_settlements_for_operator(
rejected = await db.fetchall(
"""
SELECT s.*
FROM satoshimachine.dca_settlements s
JOIN satoshimachine.dca_machines m ON m.id = s.machine_id
FROM spirekeeper.dca_settlements s
JOIN spirekeeper.dca_machines m ON m.id = s.machine_id
WHERE m.operator_user_id = :uid AND s.status = 'rejected'
ORDER BY s.created_at DESC
""",
@ -709,8 +709,8 @@ async def get_stuck_settlements_for_operator(
errored = await db.fetchall(
"""
SELECT s.*
FROM satoshimachine.dca_settlements s
JOIN satoshimachine.dca_machines m ON m.id = s.machine_id
FROM spirekeeper.dca_settlements s
JOIN spirekeeper.dca_machines m ON m.id = s.machine_id
WHERE m.operator_user_id = :uid AND s.status = 'errored'
ORDER BY s.created_at DESC
""",
@ -720,8 +720,8 @@ async def get_stuck_settlements_for_operator(
stuck_pending = await db.fetchall(
"""
SELECT s.*
FROM satoshimachine.dca_settlements s
JOIN satoshimachine.dca_machines m ON m.id = s.machine_id
FROM spirekeeper.dca_settlements s
JOIN spirekeeper.dca_machines m ON m.id = s.machine_id
WHERE m.operator_user_id = :uid
AND s.status = 'pending'
AND s.created_at < :threshold
@ -733,8 +733,8 @@ async def get_stuck_settlements_for_operator(
stuck_processing = await db.fetchall(
"""
SELECT s.*
FROM satoshimachine.dca_settlements s
JOIN satoshimachine.dca_machines m ON m.id = s.machine_id
FROM spirekeeper.dca_settlements s
JOIN spirekeeper.dca_machines m ON m.id = s.machine_id
WHERE m.operator_user_id = :uid
AND s.status = 'processing'
AND s.created_at < :threshold
@ -763,7 +763,7 @@ async def force_reset_stuck_settlement(
decision."""
await db.execute(
"""
UPDATE satoshimachine.dca_settlements
UPDATE spirekeeper.dca_settlements
SET status = 'errored',
processing_claim = NULL,
error_message = 'force-reset by operator (was stuck)'
@ -780,8 +780,8 @@ async def get_settlements_for_operator(
return await db.fetchall(
"""
SELECT s.*
FROM satoshimachine.dca_settlements s
JOIN satoshimachine.dca_machines m ON m.id = s.machine_id
FROM spirekeeper.dca_settlements s
JOIN spirekeeper.dca_machines m ON m.id = s.machine_id
WHERE m.operator_user_id = :uid
ORDER BY s.created_at DESC
LIMIT :lim
@ -801,7 +801,7 @@ async def mark_settlement_status(
fresh claim attempt won't see a stale token."""
await db.execute(
"""
UPDATE satoshimachine.dca_settlements
UPDATE spirekeeper.dca_settlements
SET status = :status,
error_message = :err,
processed_at = CASE
@ -840,7 +840,7 @@ async def claim_settlement_for_processing(
token = urlsafe_short_hash()
await db.execute(
"""
UPDATE satoshimachine.dca_settlements
UPDATE spirekeeper.dca_settlements
SET status = 'processing', processing_claim = :token
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."""
await db.execute(
"""
UPDATE satoshimachine.dca_payments
UPDATE spirekeeper.dca_payments
SET status = 'voided'
WHERE settlement_id = :sid AND status = 'failed'
""",
@ -870,7 +870,7 @@ async def reset_settlement_for_retry(
)
await db.execute(
"""
UPDATE satoshimachine.dca_settlements
UPDATE spirekeeper.dca_settlements
SET status = 'pending',
error_message = NULL,
processing_claim = NULL,
@ -902,7 +902,7 @@ async def apply_partial_dispense(
can re-distribute via the existing idempotent path."""
await db.execute(
"""
UPDATE satoshimachine.dca_settlements
UPDATE spirekeeper.dca_settlements
SET wire_sats = :gross,
principal_sats = :principal,
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)."""
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'
""",
{"sid": settlement_id},
@ -957,7 +957,7 @@ async def append_settlement_note(
formatted = f"[{ts} by {author_user_id}] {note}"
await db.execute(
"""
UPDATE satoshimachine.dca_settlements
UPDATE spirekeeper.dca_settlements
SET notes = CASE
WHEN notes IS NULL OR notes = '' THEN :note
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."""
await db.execute(
"""
UPDATE satoshimachine.dca_payments
UPDATE spirekeeper.dca_payments
SET status = 'voided'
WHERE settlement_id = :sid
AND status IN ('pending', 'failed', 'skipped')
@ -1002,7 +1002,7 @@ async def get_commission_splits(
if machine_id is None:
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
ORDER BY sort_order ASC
""",
@ -1011,7 +1011,7 @@ async def get_commission_splits(
)
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
ORDER BY sort_order ASC
""",
@ -1040,7 +1040,7 @@ async def replace_commission_splits(
if machine_id is None:
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
""",
{"uid": operator_user_id},
@ -1048,7 +1048,7 @@ async def replace_commission_splits(
else:
await db.execute(
"""
DELETE FROM satoshimachine.dca_commission_splits
DELETE FROM spirekeeper.dca_commission_splits
WHERE operator_user_id = :uid AND machine_id = :mid
""",
{"uid": operator_user_id, "mid": machine_id},
@ -1057,7 +1057,7 @@ async def replace_commission_splits(
for leg in legs:
await db.execute(
"""
INSERT INTO satoshimachine.dca_commission_splits
INSERT INTO spirekeeper.dca_commission_splits
(id, machine_id, operator_user_id, target, label, fraction,
sort_order, created_at)
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()
await db.execute(
"""
INSERT INTO satoshimachine.dca_payments
INSERT INTO spirekeeper.dca_payments
(id, settlement_id, client_id, machine_id, operator_user_id,
leg_type, destination_wallet_id, destination_ln_address,
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:
return await db.fetchone(
"SELECT * FROM satoshimachine.dca_payments WHERE id = :id",
"SELECT * FROM spirekeeper.dca_payments WHERE id = :id",
{"id": payment_id},
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]:
return await db.fetchall(
"""
SELECT * FROM satoshimachine.dca_payments
SELECT * FROM spirekeeper.dca_payments
WHERE settlement_id = :sid
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]:
return await db.fetchall(
"""
SELECT * FROM satoshimachine.dca_payments
SELECT * FROM spirekeeper.dca_payments
WHERE client_id = :cid
ORDER BY created_at DESC
""",
@ -1158,7 +1158,7 @@ async def get_payments_for_operator(
if leg_type is None:
return await db.fetchall(
"""
SELECT * FROM satoshimachine.dca_payments
SELECT * FROM spirekeeper.dca_payments
WHERE operator_user_id = :uid
ORDER BY created_at DESC
LIMIT :lim
@ -1168,7 +1168,7 @@ async def get_payments_for_operator(
)
return await db.fetchall(
"""
SELECT * FROM satoshimachine.dca_payments
SELECT * FROM spirekeeper.dca_payments
WHERE operator_user_id = :uid AND leg_type = :leg
ORDER BY created_at DESC
LIMIT :lim
@ -1186,7 +1186,7 @@ async def update_payment_status(
) -> DcaPayment | None:
await db.execute(
"""
UPDATE satoshimachine.dca_payments
UPDATE spirekeeper.dca_payments
SET status = :status,
external_payment_hash = COALESCE(:hash, external_payment_hash),
error_message = :err
@ -1221,7 +1221,7 @@ async def get_client_balance_summary(
deposits_row = await db.fetchone(
"""
SELECT COALESCE(SUM(amount), 0) AS total
FROM satoshimachine.dca_deposits
FROM spirekeeper.dca_deposits
WHERE client_id = :cid AND status = 'confirmed'
""",
{"cid": client_id},
@ -1231,7 +1231,7 @@ async def get_client_balance_summary(
payments_row = await db.fetchone(
"""
SELECT COALESCE(SUM(amount_fiat), 0) AS total
FROM satoshimachine.dca_payments
FROM spirekeeper.dca_payments
WHERE client_id = :cid
AND leg_type IN ('dca', 'settlement')
AND status = 'completed'
@ -1260,7 +1260,7 @@ async def get_client_balance_summary(
async def get_telemetry(machine_id: str) -> TelemetrySnapshot | None:
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},
TelemetrySnapshot,
)
@ -1290,7 +1290,7 @@ async def upsert_beacon_snapshot(
if existing is None:
await db.execute(
"""
INSERT INTO satoshimachine.dca_telemetry
INSERT INTO spirekeeper.dca_telemetry
(machine_id, beacon_cash_in, beacon_cash_out, beacon_cash_level,
beacon_fiat, beacon_model, beacon_name, beacon_location,
beacon_geo, beacon_fees_json, beacon_limits_json,
@ -1319,7 +1319,7 @@ async def upsert_beacon_snapshot(
else:
await db.execute(
"""
UPDATE satoshimachine.dca_telemetry SET
UPDATE spirekeeper.dca_telemetry SET
beacon_cash_in = COALESCE(:cash_in, beacon_cash_in),
beacon_cash_out = COALESCE(:cash_out, beacon_cash_out),
beacon_cash_level = COALESCE(:cash_level, beacon_cash_level),
@ -1366,7 +1366,7 @@ async def upsert_fleet_snapshot(
if existing is None:
await db.execute(
"""
INSERT INTO satoshimachine.dca_telemetry
INSERT INTO spirekeeper.dca_telemetry
(machine_id, telemetry_json, telemetry_received_at)
VALUES (:mid, :json, :now)
""",
@ -1375,7 +1375,7 @@ async def upsert_fleet_snapshot(
else:
await db.execute(
"""
UPDATE satoshimachine.dca_telemetry
UPDATE spirekeeper.dca_telemetry
SET telemetry_json = :json, telemetry_received_at = :now
WHERE machine_id = :mid
""",
@ -1416,7 +1416,7 @@ async def get_cassette_config(
machine_id: str, position: int
) -> CassetteConfig | None:
return await db.fetchone(
"SELECT * FROM satoshimachine.cassette_configs "
"SELECT * FROM spirekeeper.cassette_configs "
"WHERE machine_id = :mid AND position = :pos",
{"mid": machine_id, "pos": position},
CassetteConfig,
@ -1427,7 +1427,7 @@ async def list_cassette_configs_for_machine(
machine_id: str,
) -> list[CassetteConfig]:
return await db.fetchall(
"SELECT * FROM satoshimachine.cassette_configs "
"SELECT * FROM spirekeeper.cassette_configs "
"WHERE machine_id = :mid ORDER BY position",
{"mid": machine_id},
CassetteConfig,
@ -1459,7 +1459,7 @@ async def update_cassette_config(
update_data["mid"] = machine_id
update_data["pos"] = position
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",
update_data,
)
@ -1487,7 +1487,7 @@ async def apply_bootstrap_state(
events land + the operator subsequently edits.
"""
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",
{"mid": machine_id},
)
@ -1505,7 +1505,7 @@ async def apply_bootstrap_state(
for pos, row in payload.positions.items():
await db.execute(
"""
INSERT INTO satoshimachine.cassette_configs
INSERT INTO spirekeeper.cassette_configs
(machine_id, position, denomination, count, updated_at,
updated_by, state_denomination, state_count, state_at,
state_event_id)

View file

@ -132,7 +132,7 @@ async def publish_fee_config(
)
except (SignerUnavailable, RelayUnavailable) as exc:
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"{type(exc).__name__}: {exc}. Underlying CRUD operation "
"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
# (next machine edit / next super edit) will retry.
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}"
)
return None

View file

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

View file

@ -56,11 +56,11 @@ async def m001_satmachine_v2_initial(db):
"dca_deposits",
"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.
await db.execute(f"""
CREATE TABLE IF NOT EXISTS satoshimachine.super_config (
CREATE TABLE IF NOT EXISTS spirekeeper.super_config (
id TEXT PRIMARY KEY,
super_fee_fraction DECIMAL(10,4) NOT NULL DEFAULT 0.0000,
super_fee_wallet_id TEXT,
@ -68,18 +68,18 @@ async def m001_satmachine_v2_initial(db):
);
""")
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:
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)"
)
# 3. dca_machines — one row per bitSpire ATM, owned by exactly one
# operator. wallet_id UNIQUE prevents the IDOR funds-theft vector.
await db.execute(f"""
CREATE TABLE IF NOT EXISTS satoshimachine.dca_machines (
CREATE TABLE IF NOT EXISTS spirekeeper.dca_machines (
id TEXT PRIMARY KEY,
operator_user_id TEXT NOT NULL,
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
# delivery-related is the LP's own preference.
await db.execute(f"""
CREATE TABLE IF NOT EXISTS satoshimachine.dca_clients (
CREATE TABLE IF NOT EXISTS spirekeeper.dca_clients (
id TEXT PRIMARY KEY,
machine_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
# how many machines they're enrolled at. Owned by the LP (writes
# 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?"
#
# 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
# satmachineclient first (which auto-creates the row with their
# default LNbits wallet on first dashboard visit). Forces every
# LP through a "yes, I am here and this is where I want my sats"
# gesture before any fiat starts accumulating against them.
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,
dca_wallet_id TEXT NOT NULL,
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
# at a machine. creator_user_id preserves audit trail.
await db.execute(f"""
CREATE TABLE IF NOT EXISTS satoshimachine.dca_deposits (
CREATE TABLE IF NOT EXISTS spirekeeper.dca_deposits (
id TEXT PRIMARY KEY,
client_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
# fee_fraction. See plan section "Customer discounts" and #10.
await db.execute(f"""
CREATE TABLE IF NOT EXISTS satoshimachine.dca_settlements (
CREATE TABLE IF NOT EXISTS spirekeeper.dca_settlements (
id TEXT PRIMARY KEY,
machine_id TEXT NOT NULL,
payment_hash TEXT NOT NULL UNIQUE,
@ -227,7 +227,7 @@ async def m001_satmachine_v2_initial(db):
# - LNURL string (bech32 LNURL...)
# Resolution lives in distribution._pay_one_split_leg.
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,
machine_id TEXT,
operator_user_id TEXT NOT NULL,
@ -248,7 +248,7 @@ async def m001_satmachine_v2_initial(db):
# autoforward | refund. status enum: pending | completed | failed |
# voided | skipped | refunded.
await db.execute(f"""
CREATE TABLE IF NOT EXISTS satoshimachine.dca_payments (
CREATE TABLE IF NOT EXISTS spirekeeper.dca_payments (
id TEXT PRIMARY KEY,
settlement_id TEXT,
client_id TEXT,
@ -287,7 +287,7 @@ async def m001_satmachine_v2_initial(db):
# (name, location, geo, fees, limits, denominations, version) are
# nullable until that upstream issue lands. Ingest opportunistically.
await db.execute("""
CREATE TABLE IF NOT EXISTS satoshimachine.dca_telemetry (
CREATE TABLE IF NOT EXISTS spirekeeper.dca_telemetry (
machine_id TEXT PRIMARY KEY,
beacon_cash_in 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
schema drift survives the documented uninstall + reinstall workflow
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
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:
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:
# wallet_id column doesn't exist; either m001 produced the correct
# schema on a fresh install or the rename already landed.
return
await db.execute(
"ALTER TABLE satoshimachine.dca_commission_splits "
"ALTER TABLE spirekeeper.dca_commission_splits "
"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.
"""
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:
return
await db.execute(
"ALTER TABLE satoshimachine.dca_settlements "
"ALTER TABLE spirekeeper.dca_settlements "
"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.
"""
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:
return
# 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.
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,
dca_wallet_id TEXT NOT NULL,
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
# recent intent. ROW_NUMBER() OVER (...) requires SQLite 3.25+ (2018).
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,
autoforward_ln_address, autoforward_enabled,
created_at, updated_at)
@ -419,7 +419,7 @@ async def m004_introduce_dca_lp_table(db):
PARTITION BY user_id
ORDER BY updated_at DESC, created_at DESC
) AS rn
FROM satoshimachine.dca_clients
FROM spirekeeper.dca_clients
) ranked
WHERE rn = 1
""")
@ -434,7 +434,7 @@ async def m004_introduce_dca_lp_table(db):
"autoforward_ln_address",
"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):
@ -474,13 +474,13 @@ async def m006_rename_to_canonical_sat_vocabulary(db):
]
for table, old_col, new_col in renames:
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:
# old column doesn't exist; either rename already landed or
# m001 produced the canonical schema directly on fresh install.
continue
await db.execute(
f"ALTER TABLE satoshimachine.{table} "
f"ALTER TABLE spirekeeper.{table} "
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:
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:
# column doesn't exist; either already dropped or never present.
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):
@ -518,15 +518,15 @@ async def m005_lock_deposit_currency_to_machine_fiat_code(db):
no mismatches it's a no-op UPDATE.
"""
await db.execute("""
UPDATE satoshimachine.dca_deposits AS d
UPDATE spirekeeper.dca_deposits AS d
SET currency = (
SELECT m.fiat_code
FROM satoshimachine.dca_machines m
FROM spirekeeper.dca_machines m
WHERE m.id = d.machine_id
)
WHERE EXISTS (
SELECT 1
FROM satoshimachine.dca_machines m
FROM spirekeeper.dca_machines m
WHERE m.id = d.machine_id
AND m.fiat_code IS NOT NULL
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.
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.
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.
"""
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,
denomination 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
# column already exists, m008 already ran — no-op.
await db.fetchone(
"SELECT state_denomination FROM satoshimachine.cassette_configs " "LIMIT 1"
"SELECT state_denomination FROM spirekeeper.cassette_configs " "LIMIT 1"
)
return
except Exception:
pass
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,
position 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
# event re-populates the state_* columns authoritatively.
await db.execute("""
INSERT INTO satoshimachine.cassette_configs_new
INSERT INTO spirekeeper.cassette_configs_new
(machine_id, position, denomination, count,
updated_at, updated_by,
state_denomination, state_count, state_at, state_event_id)
SELECT machine_id, position, denomination, count,
updated_at, updated_by,
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(
"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).
- dca_settlements gains fee_mismatch_sats BIGINT NULL records
bitspire-reported fee minus expected per
satmachineadmin's principal-based recompute.
spirekeeper's principal-based recompute.
Phase 1 observability: log + record, never
reject (per coord-log §2026-06-01T07:00Z
lnbits advisory; option A locked).
@ -697,13 +697,13 @@ async def m009_split_fee_fractions_by_direction(db):
]
for table, col, coltype in additions:
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
continue
except Exception:
pass
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
@ -711,7 +711,7 @@ async def m009_split_fee_fractions_by_direction(db):
# steps cleanly.
try:
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
except Exception:
@ -724,7 +724,7 @@ async def m009_split_fee_fractions_by_direction(db):
# still at DEFAULT 0).
await db.execute(
"""
UPDATE satoshimachine.super_config
UPDATE spirekeeper.super_config
SET super_cash_in_fee_fraction = super_fee_fraction,
super_cash_out_fee_fraction = super_fee_fraction
WHERE super_cash_in_fee_fraction = 0
@ -733,5 +733,5 @@ async def m009_split_fee_fractions_by_direction(db):
"""
)
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)
prefix = f"{log_context}: " if log_context else ""
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"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
time via `register_with_lnbits()`. Until the lnbits side ships
`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.
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
_SOURCE_EXTENSION = "satmachineadmin"
_SOURCE_EXTENSION = "spirekeeper"
@dataclass(frozen=True)
@ -111,11 +111,11 @@ def register_with_lnbits() -> bool:
Returns True if the registration landed (lnbits surface available
+ call succeeded), False if soft-failed because lnbits hasn't
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
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
reload replaces cleanly").
"""
@ -125,7 +125,7 @@ def register_with_lnbits() -> bool:
)
except ImportError:
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 "
"routing falls through to lnbits' default auto-account-from-"
"npub behaviour. See aiolabs/satmachineadmin#20 / coord-log "
@ -134,7 +134,7 @@ def register_with_lnbits() -> bool:
return False
register_roster_resolver(_SOURCE_EXTENSION, resolve)
logger.info(
f"satmachineadmin: registered '{_SOURCE_EXTENSION}' roster "
f"spirekeeper: registered '{_SOURCE_EXTENSION}' roster "
"resolver with lnbits nostr-transport — inbound kind-21000 "
"from a registered ATM npub will route to the operator's wallet "
"directly. (Behavior gated server-side by "

View file

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

View file

@ -1,6 +1,6 @@
// 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 /
// 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
// 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 MACHINES_PATH = `${API}/machines`
const SETTLEMENTS_PATH = `${API}/settlements`
@ -198,7 +198,7 @@ window.app = Vue.createApp({
settlements: [],
// Cassettes sub-tab state (#29 v1) — see openCassettePublishConfirm /
// submitCassettePublish methods + the cassettes panel in
// templates/satmachineadmin/index.html.
// templates/spirekeeper/index.html.
activeTab: 'settlements',
cassetteEdits: [], // editable working copy of cassette_configs rows
cassettesPristine: [], // last-known-clean snapshot for revert

View file

@ -45,7 +45,7 @@ from .crud import (
from .distribution import process_settlement
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
# 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()
register_invoice_listener(invoice_queue, LISTENER_NAME)
logger.info(
"satmachineadmin v2: invoice listener registered as "
"spirekeeper v2: invoice listener registered as "
f"`{LISTENER_NAME}` — waiting for bitSpire settlements."
)
while True:
@ -67,7 +67,7 @@ async def wait_for_paid_invoices() -> None:
await _handle_payment(payment)
except Exception as exc: # listener must never die
logger.error(
f"satmachineadmin: error handling payment "
f"spirekeeper: error handling payment "
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")
if settlement is None:
logger.error(
f"satmachineadmin: failed to insert settlement for "
f"spirekeeper: failed to insert settlement for "
f"payment_hash={payment.payment_hash[:12]}..."
)
return
logger.info(
f"satmachineadmin: landed settlement {settlement.id} for "
f"spirekeeper: landed settlement {settlement.id} for "
f"machine={machine.machine_npub[:12]}... "
f"wire={data.wire_sats}sats principal={data.principal_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:
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]}..."
)
return
logger.error(
f"satmachineadmin: rejected settlement {rejected.id} "
f"spirekeeper: rejected settlement {rejected.id} "
f"(machine={machine.machine_npub[:12]}..., "
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.
#
# 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).
#
# 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,
# 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_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
"""
logger.info(
"satmachineadmin v2: cassette bootstrap consumer starting "
"spirekeeper v2: cassette bootstrap consumer starting "
f"(sub_id={CASSETTE_BOOTSTRAP_SUB_ID})"
)
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)
except _NostrclientUnavailable:
logger.warning(
"satmachineadmin: nostrclient extension not installed; "
"spirekeeper: nostrclient extension not installed; "
f"cassette bootstrap consumer sleeping {_CASSETTE_BACKOFF_S}s "
"before retry. Install + activate nostrclient on this "
"LNbits instance."
@ -299,7 +299,7 @@ async def wait_for_cassette_state_events() -> None:
await asyncio.sleep(_CASSETTE_BACKOFF_S)
except Exception as exc: # listener must never die
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)
@ -346,13 +346,13 @@ async def _cassette_consumer_tick(current_filter_key: str | None) -> str:
CASSETTE_BOOTSTRAP_SUB_ID, filters # type: ignore[arg-type]
)
logger.info(
"satmachineadmin: (re)registered cassette bootstrap "
"spirekeeper: (re)registered cassette bootstrap "
f"subscription with {len(d_tags)} d-tag(s)"
)
else:
nostr_client.relay_manager.close_subscription(CASSETTE_BOOTSTRAP_SUB_ID)
logger.info(
"satmachineadmin: no active machines; closed cassette "
"spirekeeper: no active machines; closed cassette "
"bootstrap subscription"
)
@ -368,7 +368,7 @@ async def _cassette_consumer_tick(current_filter_key: str | None) -> str:
)
except Exception as exc:
logger.warning(
f"satmachineadmin: cassette state event handler "
f"spirekeeper: cassette state event handler "
f"failed (skipping): {exc}"
)
@ -419,14 +419,14 @@ async def _handle_cassette_state_event(
event_obj = event_raw
else:
logger.warning(
f"satmachineadmin: cassette event of unexpected type "
f"spirekeeper: cassette event of unexpected type "
f"{type(event_raw).__name__}; skipping"
)
return
if not verify_event(event_obj):
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]}...)"
)
return
@ -437,7 +437,7 @@ async def _handle_cassette_state_event(
# Unknown sender — could be relay noise or an attacker. Don't
# treat as our problem.
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); "
"skipping"
)
@ -448,7 +448,7 @@ async def _handle_cassette_state_event(
except CassetteTransportError as exc:
# OperatorIdentityMissing / SignerUnavailable — log + skip.
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"{exc}"
)
@ -458,13 +458,13 @@ async def _handle_cassette_state_event(
payload = await decrypt_and_parse_state_event(event_obj, account, signer)
except CassetteEventTransientError as exc:
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}"
)
return
except CassetteEventDecodeError as exc:
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"{exc}"
)
@ -479,12 +479,12 @@ async def _handle_cassette_state_event(
)
if applied:
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)"
)
else:
# Replay: event_id already on file. Normal on relay reconnect.
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)"
)

View file

@ -3,7 +3,7 @@
{% block scripts %}
{{ 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 %}
{% 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
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):
"""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)."""
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
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
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)
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.
Zero = exact match.
@ -116,7 +116,7 @@ class TestFeeMismatchSatsRecording:
"""Real-world Phase-1 scenario before Layer 3 (lamassu-next#57)
ships: ATM hardcodes 7.77% cash-out; operator configures 5%
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."""
machine = _machine(op_out=0.05)
super_cfg = _super_config(out_frac=0.03)

View file

@ -1,6 +1,6 @@
"""
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 /
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.operator_user_id == "op-123"
assert result.wallet_id == "wallet-abc"
assert result.source_extension == "satmachineadmin"
assert result.source_extension == "spirekeeper"
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):
"""Until the lnbits-side path-B PR lands, the registration call
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 = (
__builtins__["__import__"]
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.helpers import template_renderer
satmachineadmin_generic_router = APIRouter()
spirekeeper_generic_router = APIRouter()
def satmachineadmin_renderer():
return template_renderer(["satmachineadmin/templates"])
def spirekeeper_renderer():
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)):
return satmachineadmin_renderer().TemplateResponse(
"satmachineadmin/index.html",
return spirekeeper_renderer().TemplateResponse(
"spirekeeper/index.html",
{"request": req, "user": user.json()},
)

View file

@ -94,7 +94,7 @@ from .models import (
UpsertCassetteConfigData,
)
satmachineadmin_api_router = APIRouter()
spirekeeper_api_router = APIRouter()
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(
data: CreateMachineData, user: User = Depends(check_user_exists)
) -> Machine:
@ -274,14 +274,14 @@ async def api_create_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(
user: User = Depends(check_user_exists),
) -> list[Machine]:
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
)
async def api_get_machine(
@ -293,7 +293,7 @@ async def api_get_machine(
return machine
@satmachineadmin_api_router.put(
@spirekeeper_api_router.put(
"/api/v1/dca/machines/{machine_id}", response_model=Machine
)
async def api_update_machine(
@ -341,7 +341,7 @@ async def api_update_machine(
return updated
@satmachineadmin_api_router.delete(
@spirekeeper_api_router.delete(
"/api/v1/dca/machines/{machine_id}", status_code=HTTPStatus.NO_CONTENT
)
async def api_delete_machine(
@ -379,7 +379,7 @@ async def _client_owned_by(client_id: str, user_id: str) -> DcaClient:
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(
data: CreateDcaClientData, user: User = Depends(check_user_exists)
) -> DcaClient:
@ -388,7 +388,7 @@ async def api_create_client(
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(
machine_id: str | None = None,
user: User = Depends(check_user_exists),
@ -402,7 +402,7 @@ async def api_list_clients(
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
)
async def api_get_client(
@ -411,7 +411,7 @@ async def api_get_client(
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
)
async def api_update_client(
@ -426,7 +426,7 @@ async def api_update_client(
return updated
@satmachineadmin_api_router.delete(
@spirekeeper_api_router.delete(
"/api/v1/dca/clients/{client_id}", status_code=HTTPStatus.NO_CONTENT
)
async def api_delete_client(
@ -436,7 +436,7 @@ async def api_delete_client(
await delete_dca_client(client_id)
@satmachineadmin_api_router.get(
@spirekeeper_api_router.get(
"/api/v1/dca/clients/{client_id}/balance",
response_model=ClientBalanceSummary,
)
@ -450,7 +450,7 @@ async def api_get_client_balance(
return summary
@satmachineadmin_api_router.post(
@spirekeeper_api_router.post(
"/api/v1/dca/clients/{client_id}/settle", response_model=DcaPayment
)
async def api_settle_client_balance(
@ -498,7 +498,7 @@ async def _deposit_owned_by(deposit_id: str, user_id: str) -> DcaDeposit:
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(
data: CreateDepositData, user: User = Depends(check_user_exists)
) -> DcaDeposit:
@ -531,7 +531,7 @@ async def api_create_deposit(
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(
client_id: str | None = None,
user: User = Depends(check_user_exists),
@ -544,7 +544,7 @@ async def api_list_deposits(
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
)
async def api_get_deposit(
@ -553,7 +553,7 @@ async def api_get_deposit(
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
)
async def api_update_deposit(
@ -573,7 +573,7 @@ async def api_update_deposit(
return updated
@satmachineadmin_api_router.put(
@spirekeeper_api_router.put(
"/api/v1/dca/deposits/{deposit_id}/status", response_model=DcaDeposit
)
async def api_update_deposit_status(
@ -588,7 +588,7 @@ async def api_update_deposit_status(
return updated
@satmachineadmin_api_router.delete(
@spirekeeper_api_router.delete(
"/api/v1/dca/deposits/{deposit_id}", status_code=HTTPStatus.NO_CONTENT
)
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]
)
async def api_list_settlements(
@ -617,7 +617,7 @@ async def api_list_settlements(
return await get_settlements_for_operator(user.id)
@satmachineadmin_api_router.get(
@spirekeeper_api_router.get(
"/api/v1/dca/machines/{machine_id}/settlements",
response_model=list[DcaSettlement],
)
@ -637,7 +637,7 @@ async def api_list_settlements_for_machine(
# order).
@satmachineadmin_api_router.get(
@spirekeeper_api_router.get(
"/api/v1/dca/settlements/stuck", response_model=StuckSettlementsResponse
)
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
)
async def api_get_settlement(
@ -683,7 +683,7 @@ async def api_get_settlement(
return settlement
@satmachineadmin_api_router.post(
@spirekeeper_api_router.post(
"/api/v1/dca/settlements/{settlement_id}/partial-dispense",
response_model=DcaSettlement,
)
@ -718,7 +718,7 @@ async def api_partial_dispense(
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",
response_model=DcaSettlement,
)
@ -768,7 +768,7 @@ async def api_force_reset_settlement(
return updated
@satmachineadmin_api_router.post(
@spirekeeper_api_router.post(
"/api/v1/dca/settlements/{settlement_id}/retry",
response_model=DcaSettlement,
)
@ -821,7 +821,7 @@ async def api_retry_settlement(
return after if after is not None else updated
@satmachineadmin_api_router.post(
@spirekeeper_api_router.post(
"/api/v1/dca/settlements/{settlement_id}/notes",
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(
leg_type: str | None = None,
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]
)
async def api_get_commission_splits(
@ -889,7 +889,7 @@ async def api_get_commission_splits(
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]
)
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)
@satmachineadmin_api_router.delete(
@spirekeeper_api_router.delete(
"/api/v1/dca/commission-splits",
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(
_user: User = Depends(check_user_exists),
) -> SuperConfig:
@ -940,7 +940,7 @@ async def api_get_super_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(
data: UpdateSuperConfigData,
_user: User = Depends(check_super_user),
@ -988,7 +988,7 @@ async def api_update_super_config(
# fields per row are denomination and count.
@satmachineadmin_api_router.get(
@spirekeeper_api_router.get(
"/api/v1/dca/machines/{machine_id}/cassettes",
response_model=list[CassetteConfig],
)
@ -1003,7 +1003,7 @@ async def api_list_machine_cassettes(
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",
response_model=list[CassetteConfig],
)
@ -1049,7 +1049,7 @@ async def api_publish_machine_cassettes(
"No cassette_configs rows exist for this machine yet — "
"waiting for the ATM's bootstrap state event. Power on the "
"ATM and confirm it has reached the configured relay; "
"satmachineadmin will auto-populate cassette_configs on "
"spirekeeper will auto-populate cassette_configs on "
"receipt."
),
)
@ -1068,7 +1068,7 @@ async def api_publish_machine_cassettes(
)
# 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.
for pos, row in payload.positions.items():
updated = await update_cassette_config(