fix(machine): reactive unblock from 'awaiting-fees' maintenance (#57)
Fixes gap-3 from coord log 2026-06-01T18:30Z: the operator-fees subscriber wasn't running during the 'awaiting-fees' maintenance state, so the maintenance state had no path to clear. Every restart found empty state.db, entered maintenance, never subscribed, never wrote. Forever stuck. Root cause: `initializeForProduction` bailed via early `return` when the persisted fee config was null. The subscriber starts inside `initializeWithHalIpc`, which was never reached. Fix has three pieces: 1. Remove the early return. HAL + Lightning + operator-fees subscriber all init even when `initError = 'awaiting-fees'` is set. The maintenance card UI still blocks user interaction (no router-view renders), and the state machine starts with zero fractions until the first event lands. 2. New `UPDATE_FEE_CONFIG` event on the state machine, handled at the root level — assigns `cashInFeeFraction` / `cashOutFeeFraction` onto context so subsequent cashIn/cashOut entries pick them up via setCashInFee / setCashOutFee actions. No actor restart needed. 3. `applyFeeConfig` (the operator-fees subscriber's onApply callback) now dispatches UPDATE_FEE_CONFIG into the running actor AND clears `initError` when it was 'awaiting-fees'. Operator publishes the first event → ATM auto-unblocks → UI flips from maintenance card to IdleView showing the new fee%. No `systemctl restart bitspire` needed. Adds three tests covering the new UPDATE_FEE_CONFIG handler: - updates context fractions - does not leave idle state - propagates to context.feeFraction on next cashIn entry (the load-bearing chain: subscriber → context → setCashInFee → fee math is correct for the next transaction) Total state-machine tests: 21 (was 18); apps/machine tests unchanged at 24. All 12 workspace packages typecheck. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
a8e984e12b
commit
9bdb9333fd
4 changed files with 125 additions and 17 deletions
|
|
@ -334,14 +334,40 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
|
||||
/**
|
||||
* Push a freshly-applied fee config from the operator-fees consumer
|
||||
* into the renderer's reactive refs. XState's per-flow context-snapshot
|
||||
* boundary defers the effective change to the next transaction (the
|
||||
* in-flight flow keeps using its captured fractions). See operator-
|
||||
* fees.ts file header for the full reasoning.
|
||||
* into the renderer's reactive refs + the running state machine.
|
||||
*
|
||||
* Three responsibilities (see coord log 2026-06-01T18:30Z for the
|
||||
* gap-3 reactive unblock):
|
||||
*
|
||||
* 1. Update the pinia refs so future `createATMMachine()` calls bake
|
||||
* in the right fractions.
|
||||
* 2. Dispatch UPDATE_FEE_CONFIG into the running actor so the in-memory
|
||||
* machine picks up the new fees on its next cashIn/cashOut entry
|
||||
* (no service restart needed; XState's per-flow context-snapshot
|
||||
* boundary still defers the in-flight transaction).
|
||||
* 3. If we're sitting in the 'awaiting-fees' maintenance state, clear
|
||||
* `initError` so the UI flips from maintenance card → IdleView.
|
||||
* Operator just published the first config; ATM goes live.
|
||||
*/
|
||||
function applyFeeConfig(fees: { cashInFeeFraction: number; cashOutFeeFraction: number }) {
|
||||
cashInFeeFraction.value = fees.cashInFeeFraction
|
||||
cashOutFeeFraction.value = fees.cashOutFeeFraction
|
||||
|
||||
if (actor.value) {
|
||||
actor.value.send({
|
||||
type: 'UPDATE_FEE_CONFIG',
|
||||
cashInFeeFraction: fees.cashInFeeFraction,
|
||||
cashOutFeeFraction: fees.cashOutFeeFraction,
|
||||
})
|
||||
}
|
||||
|
||||
if (initError.value === 'awaiting-fees') {
|
||||
console.log(
|
||||
`[ATM] Operator fee config received — clearing 'awaiting-fees' maintenance state ` +
|
||||
`(cash_in=${fees.cashInFeeFraction} cash_out=${fees.cashOutFeeFraction})`
|
||||
)
|
||||
initError.value = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1026,26 +1052,34 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
|
||||
// Load persisted operator fee config (aiolabs/lamassu-next#57). If no
|
||||
// config has ever been applied (fresh ATM, pre-operator-publish),
|
||||
// fail-closed into maintenance screen. The operator-fees subscriber
|
||||
// started below will unblock this once it receives a valid event from
|
||||
// the operator's satmachineadmin (publish triggered by machine create
|
||||
// / update / super-config change per aiolabs/satmachineadmin#39).
|
||||
// enter the 'awaiting-fees' maintenance state — UI shows the operator
|
||||
// a directive to publish initial fee config, transactions are blocked
|
||||
// (no user-facing route renders).
|
||||
//
|
||||
// Crucially, we DO NOT bail out of init here — HAL + Lightning + the
|
||||
// operator-fees subscriber still need to start so the first inbound
|
||||
// kind-30078 `bitspire-fees:<atm_pubkey>` event can land. `applyFeeConfig`
|
||||
// (the subscriber's onApply callback) clears `initError` reactively
|
||||
// when that first event arrives, flipping the UI live without a
|
||||
// service restart. See coord log 2026-06-01T18:30Z for the gap-3
|
||||
// dead-end this fixes.
|
||||
const persistedFees = await api.getFeeConfig()
|
||||
if (persistedFees === null) {
|
||||
initError.value = 'awaiting-fees'
|
||||
console.warn(
|
||||
'[ATM] No persisted fee config and no inbound event yet — entering maintenance state. ' +
|
||||
'Operator must publish initial fee config via satmachineadmin.'
|
||||
'Operator must publish initial fee config via satmachineadmin. ' +
|
||||
'Subscriber is starting; ATM will unblock reactively on first valid event.'
|
||||
)
|
||||
} else {
|
||||
cashInFeeFraction.value = persistedFees.cashInFeeFraction
|
||||
cashOutFeeFraction.value = persistedFees.cashOutFeeFraction
|
||||
console.log(
|
||||
`[ATM] Restored fee config from state.db: ` +
|
||||
`cash_in=${persistedFees.cashInFeeFraction} cash_out=${persistedFees.cashOutFeeFraction} ` +
|
||||
`(event_created_at=${persistedFees.eventCreatedAt})`
|
||||
)
|
||||
return
|
||||
}
|
||||
cashInFeeFraction.value = persistedFees.cashInFeeFraction
|
||||
cashOutFeeFraction.value = persistedFees.cashOutFeeFraction
|
||||
console.log(
|
||||
`[ATM] Restored fee config from state.db: ` +
|
||||
`cash_in=${persistedFees.cashInFeeFraction} cash_out=${persistedFees.cashOutFeeFraction} ` +
|
||||
`(event_created_at=${persistedFees.eventCreatedAt})`
|
||||
)
|
||||
|
||||
// Build device config from runtime values
|
||||
const { getDeviceConfig, toHalConfig, MACHINE_PRESETS } = await import('@/config')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue