feat(v2): publish operator-signed kind:30078 fleet roster + per-machine config (S4)
Closes aiolabs/satmachineadmin#18 (S4 — NIP-78 per-machine config +
fleet roster). On every machine create/update/delete, publish two
operator-signed kind:30078 (NIP-78 addressable) events via the
`nostrclient` LNbits extension:
- `bitspire-config:<machine_id>` — per-machine config event, one
per machine. Tagged with `p=<atm_npub>` so external observers
can filter by ATM pubkey: `{"#p": ["<atm_npub>"]}`.
- `bitspire-fleet` — aggregate roster across the operator's
active fleet. Lists every machine's atm_pubkey + display fields.
Tagged with `p=<atm_npub>` per active machine.
Delete path tombstones the per-machine config (replaceable kind:30078
with `content.deleted=true`) and re-publishes the roster without the
machine — external readers see the tombstone OR the absence from the
roster.
Implementation choice — direct in-process singleton import (path b
from the pre-flight check, not the WebSocket path a):
from nostrclient.router import nostr_client
nostr_client.relay_manager.publish_message(json.dumps(["EVENT", e]))
Bypasses the public/private WebSocket entirely. Cleaner than going
through `wss://localhost/nostrclient/api/v1/<encrypted_ws_id>`. Same
cross-extension import pattern lnbits core uses for
nostrmarket.services + nostrrelay.crud (guarded by try/except).
Soft-failure throughout:
- nostrclient extension not installed → log warning + skip.
- Operator account has no Nostr keypair on file (account never went
through Nostr-login flow, or post-bunker future where nsec is
moved off-disk per lnbits#18) → log warning + skip.
- The settlement / distribution path does NOT depend on the publish
— these events exist for external observers, not internal flow
control.
Out of scope (intentionally):
- ATM-side consumer in lamassu-next (forward-looking, will read
`#p=<atm_npub>` to learn its operator's config).
- LNbits-server-side roster-gating in the nostr-transport handler
(S6 / lnbits#14 Item 3 — needs satmachineadmin to publish first;
this commit lays the groundwork).
- Operator's NIP-65 relay list as the publish target (today we use
whatever nostrclient is configured with; future per-operator
relay lists can live on accounts.relays or similar).
m006 (the canonical-vocabulary rename migration shipped at d717a6e)
ran cleanly against the regtest container on lnbits restart.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d717a6e214
commit
131ff92aa8
2 changed files with 251 additions and 1 deletions
25
views_api.py
25
views_api.py
|
|
@ -53,6 +53,11 @@ from .distribution import (
|
|||
process_settlement,
|
||||
settle_lp_balance,
|
||||
)
|
||||
from .nostr_publish import (
|
||||
publish_fleet_roster,
|
||||
publish_machine_config,
|
||||
tombstone_machine_config,
|
||||
)
|
||||
from .models import (
|
||||
AppendSettlementNoteData,
|
||||
ClientBalanceSummary,
|
||||
|
|
@ -103,7 +108,14 @@ async def api_create_machine(
|
|||
data: CreateMachineData, user: User = Depends(check_user_exists)
|
||||
) -> Machine:
|
||||
await _assert_wallet_owned_by(data.wallet_id, user.id)
|
||||
return await create_machine(user.id, data)
|
||||
machine = await create_machine(user.id, data)
|
||||
# NIP-78 (kind:30078) publish — operator-signed per-machine config +
|
||||
# refreshed fleet roster so external observers see the new machine.
|
||||
# Soft-failure: best-effort, log + skip if nostrclient or operator key
|
||||
# not available (see nostr_publish module docstring).
|
||||
await publish_machine_config(machine)
|
||||
await publish_fleet_roster(user.id)
|
||||
return machine
|
||||
|
||||
|
||||
@satmachineadmin_api_router.get("/api/v1/dca/machines", response_model=list[Machine])
|
||||
|
|
@ -141,6 +153,11 @@ async def api_update_machine(
|
|||
updated = await update_machine(machine_id, data)
|
||||
if updated is None:
|
||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Machine not found")
|
||||
# Re-publish: per-machine config picks up the changed fields, fleet
|
||||
# roster picks up any is_active flip (deactivated machines drop out
|
||||
# of the roster but their per-machine config stays — replaceable).
|
||||
await publish_machine_config(updated)
|
||||
await publish_fleet_roster(user.id)
|
||||
return updated
|
||||
|
||||
|
||||
|
|
@ -154,6 +171,12 @@ async def api_delete_machine(
|
|||
if machine is None or machine.operator_user_id != user.id:
|
||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Machine not found")
|
||||
await delete_machine(machine_id)
|
||||
# Tombstone the per-machine config (replaceable event with
|
||||
# `content.deleted=true`) + refresh the roster without the machine.
|
||||
# External readers see the tombstone OR the absence from the roster
|
||||
# and treat it as gone.
|
||||
await tombstone_machine_config(user.id, machine_id, machine.machine_npub)
|
||||
await publish_fleet_roster(user.id)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue