feat(v2): wire fee-config publish into machine + super-config triggers (#39 3/3)
Some checks failed
ci.yml / feat(v2): wire fee-config publish into machine + super-config triggers (#39 3/3) (pull_request) Failing after 0s

Three trigger points wire fee_transport.publish_fee_config into the
satmachineadmin API endpoints per the #39 spec. All three soft-fail on
transport errors — the underlying CRUD operation (machine create /
update / super-config save) succeeds even when the publish couldn't
reach the relay or the signer, and the operator can re-trigger by
editing again.

views_api.py:
- api_create_machine — publishes always after create, even when
  operator fees default to 0/0 (the resulting super-only payload is
  what unblocks the ATM past its `awaiting-fees` maintenance gate).
  Reads super_config singleton; if absent (m001 should have inserted
  it, so this is an impossible state), skips the publish to avoid
  crashing create.
- api_update_machine — publishes only when either
  operator_cash_*_fee_fraction is in the patch payload. Skip on
  name/location/wallet_id/is_active/fiat_code edits since those don't
  affect the fee model the ATM enforces (avoids unnecessary relay
  churn).
- api_update_super_config — publishes to every active machine when
  either super fraction changes. Per-machine: that machine's
  operator_user_id is the signer (machines owned by different
  operators sign with different keys); each soft-fail is independent.
  Skip if only super_fee_wallet_id changed (no fee-model impact).

Tests (9 cases, all green):
- 3 create-machine triggers: default 0/0 operator fees still publishes
  super-only payload, nonzero operator fees publish full payload,
  None super_config short-circuits without crashing
- 4 update-machine triggers: publishes on cash_in change, publishes on
  cash_out change, skips on name-only, skips on is_active-only
- 2 super-config triggers: publishes per-active-machine signed by
  each machine's operator on fraction change, skips entirely on
  wallet-id-only change (with an assertion that list_all_active_machines
  is never called, proving the short-circuit path)

191/191 tests green. Layer 2 (#39) complete; ready for joint smoke
once bitspire fixes the three deploy gaps from coord-log §2026-06-01T18:30Z
(`relay.aiolabs.dev` default, `VITE_LNBITS_HTTP_URL` dead echo,
operator-fees subscriber not running in maintenance state).

Refs: aiolabs/satmachineadmin#37 (parent), #39 (closes Layer 2),
aiolabs/lamassu-next#57 (Layer 3 consumer — blocked on bitspire-side
gaps).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-06-01 20:07:56 +02:00
commit 794d7e5395
2 changed files with 424 additions and 0 deletions

View file

@ -22,6 +22,7 @@ from .cassette_transport import (
SignerUnavailable,
publish_to_atm,
)
from .fee_transport import publish_fee_config
from .crud import (
append_settlement_note,
count_completed_legs_for_settlement,
@ -264,6 +265,12 @@ async def api_create_machine(
data.operator_cash_out_fee_fraction,
)
machine = await create_machine(user.id, data)
# Layer 2 (#39): publish initial fee config to the ATM so it can
# unblock past its `awaiting-fees` maintenance gate. Soft-fails on
# transport errors — machine creation has already succeeded.
super_config = await get_super_config()
if super_config is not None:
await publish_fee_config(machine, super_config, user.id)
return machine
@ -319,6 +326,18 @@ async def api_update_machine(
updated = await update_machine(machine_id, data)
if updated is None:
raise HTTPException(HTTPStatus.NOT_FOUND, "Machine not found")
# Layer 2 (#39): if either operator fee fraction changed, publish a
# fresh kind-30078 to the ATM so it picks up the new total. Skip
# otherwise — name/location/wallet_id/is_active edits don't change
# the fee model the ATM enforces.
fees_changed = (
data.operator_cash_in_fee_fraction is not None
or data.operator_cash_out_fee_fraction is not None
)
if fees_changed:
super_config = await get_super_config()
if super_config is not None:
await publish_fee_config(updated, super_config, user.id)
return updated
@ -939,6 +958,20 @@ async def api_update_super_config(
raise HTTPException(
HTTPStatus.INTERNAL_SERVER_ERROR, "Failed to update super config"
)
# Layer 2 (#39): a super-fee change ripples to every active machine
# since each machine's total = super + machine.operator. Republish
# per-machine with that machine's operator as the signer.
# Soft-fails per machine independently; partial success is acceptable
# (the operator whose publish failed can re-trigger via a machine
# edit). Skip if neither directional fraction was touched in this
# update (e.g. caller only changed super_fee_wallet_id).
super_fractions_changed = (
data.super_cash_in_fee_fraction is not None
or data.super_cash_out_fee_fraction is not None
)
if super_fractions_changed:
for machine in await list_all_active_machines():
await publish_fee_config(machine, config, machine.operator_user_id)
return config