Compare commits
1 commit
762b0def5c
...
8b7098a4b0
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b7098a4b0 |
2 changed files with 76 additions and 22 deletions
|
|
@ -56,6 +56,17 @@ export interface OperatorConfigServiceConfig {
|
|||
export interface OperatorConfigService {
|
||||
/** Unsubscribe from operator events and free resources. */
|
||||
stop(): void
|
||||
/**
|
||||
* Republish the current cassette state (kind-30078, replaceable). Call after
|
||||
* a dispense and on a cassette reload so the operator's view tracks reality.
|
||||
* Best-effort — logs and swallows errors.
|
||||
*/
|
||||
publishCassettesState(): Promise<void>
|
||||
}
|
||||
|
||||
const NOOP_SERVICE: OperatorConfigService = {
|
||||
stop: () => {},
|
||||
publishCassettesState: async () => {},
|
||||
}
|
||||
|
||||
export async function startOperatorConfigService(
|
||||
|
|
@ -63,11 +74,11 @@ export async function startOperatorConfigService(
|
|||
): Promise<OperatorConfigService> {
|
||||
if (cfg.operatorPubkeys.length === 0) {
|
||||
console.log('[OperatorConfig] No operator pubkeys configured — service disabled')
|
||||
return { stop: () => {} }
|
||||
return NOOP_SERVICE
|
||||
}
|
||||
if (!isElectron || !window.electronAPI) {
|
||||
console.log('[OperatorConfig] Not in Electron — service disabled (browser dev mode)')
|
||||
return { stop: () => {} }
|
||||
return NOOP_SERVICE
|
||||
}
|
||||
const api = window.electronAPI
|
||||
const machineId = cfg.machineId ?? cfg.signer.pubkey
|
||||
|
|
@ -103,6 +114,12 @@ export async function startOperatorConfigService(
|
|||
|
||||
return {
|
||||
stop: () => cfg.nostrClient.unsubscribe(subscriptionId),
|
||||
publishCassettesState: () =>
|
||||
publishCassettesState(cfg, api, machineId)
|
||||
.then(() => {})
|
||||
.catch((err) => {
|
||||
console.warn('[OperatorConfig] cassettes-state republish failed:', err)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -193,29 +210,35 @@ async function handleOperatorConfigEvent(
|
|||
console.log(
|
||||
`[OperatorConfig] Applied — created_at=${event.created_at}, positions=${Object.keys(parsed.positions).join(',')}`
|
||||
)
|
||||
|
||||
// Republish our resulting cassette state so the operator's view reflects the
|
||||
// applied config (the "on cassette reload" case). Different d-tag from the
|
||||
// operator's config event, so no echo loop. Best-effort.
|
||||
const machineId = cfg.machineId ?? cfg.signer.pubkey
|
||||
await publishCassettesState(cfg, api, machineId).catch((err) =>
|
||||
console.warn('[OperatorConfig] post-apply cassettes-state republish failed:', err)
|
||||
)
|
||||
}
|
||||
|
||||
async function maybePublishBootstrap(
|
||||
/**
|
||||
* Publish the ATM's current cassette state as a replaceable kind-30078 event
|
||||
* (`bitspire-cassettes-state:<machineId>`), NIP-44-encrypted to the operator.
|
||||
* Replaceable → latest wins; the operator consumes every update. Call after a
|
||||
* dispense and on a cassette reload so the operator view tracks reality, not
|
||||
* the frozen bootstrap snapshot (coord 2026-06-21 / lamassu-next#56).
|
||||
*
|
||||
* NOT gated on the bootstrap flag — this is the live update. Returns whether an
|
||||
* event was published (false when there are no cassettes / no operator).
|
||||
*/
|
||||
async function publishCassettesState(
|
||||
cfg: OperatorConfigServiceConfig,
|
||||
api: NonNullable<typeof window.electronAPI>,
|
||||
machineId: string
|
||||
): Promise<void> {
|
||||
const already = await api.getBootstrapPublishedAt()
|
||||
if (already !== null) {
|
||||
console.log('[OperatorConfig] Bootstrap already published at unix', already)
|
||||
return
|
||||
}
|
||||
): Promise<boolean> {
|
||||
const cassettes = await api.loadCassettes()
|
||||
if (cassettes.length === 0) {
|
||||
console.log('[OperatorConfig] state.db.cassettes empty — skipping bootstrap')
|
||||
return
|
||||
}
|
||||
|
||||
if (cassettes.length === 0) return false
|
||||
const operatorPubkey = cfg.operatorPubkeys[0]
|
||||
if (!operatorPubkey) {
|
||||
console.log('[OperatorConfig] No operator pubkey — skipping bootstrap')
|
||||
return
|
||||
}
|
||||
if (!operatorPubkey) return false
|
||||
|
||||
const positions: Record<string, { denomination: number; count: number }> = {}
|
||||
for (const c of cassettes) {
|
||||
|
|
@ -235,6 +258,30 @@ async function maybePublishBootstrap(
|
|||
})
|
||||
|
||||
await cfg.nostrClient.publish(event)
|
||||
await api.markBootstrapPublished(Math.floor(Date.now() / 1000))
|
||||
console.log('[OperatorConfig] Bootstrap hello-event published:', { dTag, eventId: event.id })
|
||||
console.log('[OperatorConfig] cassettes-state published:', { dTag, eventId: event.id })
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* First-boot hello: publish the cassette state once and mark the gate. The
|
||||
* gate (lamassu-next#56) prevents re-emitting the *bootstrap* on every boot;
|
||||
* live updates after dispenses go through `publishCassettesState` directly.
|
||||
*/
|
||||
async function maybePublishBootstrap(
|
||||
cfg: OperatorConfigServiceConfig,
|
||||
api: NonNullable<typeof window.electronAPI>,
|
||||
machineId: string
|
||||
): Promise<void> {
|
||||
const already = await api.getBootstrapPublishedAt()
|
||||
if (already !== null) {
|
||||
console.log('[OperatorConfig] Bootstrap already published at unix', already)
|
||||
return
|
||||
}
|
||||
const published = await publishCassettesState(cfg, api, machineId)
|
||||
if (published) {
|
||||
await api.markBootstrapPublished(Math.floor(Date.now() / 1000))
|
||||
console.log('[OperatorConfig] Bootstrap hello-event published')
|
||||
} else {
|
||||
console.log('[OperatorConfig] No cassettes/operator — skipping bootstrap')
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -521,7 +521,10 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
bills,
|
||||
cassettes: dr?.cassettes,
|
||||
error: dr?.error ?? ctx.error,
|
||||
}).then(() => reloadPersistedInventory())
|
||||
})
|
||||
.then(() => reloadPersistedInventory())
|
||||
// Republish cassette state — a partial dispense changed counts.
|
||||
.then(() => operatorConfigSvc?.publishCassettesState())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -551,7 +554,11 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
bills,
|
||||
cassettes: dr?.cassettes,
|
||||
error: dr?.error,
|
||||
}).then(() => reloadPersistedInventory())
|
||||
})
|
||||
.then(() => reloadPersistedInventory())
|
||||
// Republish cassette state after a cash-out dispense (counts
|
||||
// decremented); harmless no-op echo for a cash-in complete.
|
||||
.then(() => (isCashInTx ? undefined : operatorConfigSvc?.publishCassettesState()))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue