feat(machine): operator-config consumer over kind-30078 (#56 v1)
Wires the ATM-side consumer of operator-driven cassette config per aiolabs/lamassu-next#56 v1. Operator → ATM only, with a one-shot ATM bootstrap hello-event so satmachineadmin can auto-populate `cassette_configs` rows on first boot. Transport (decision rationale in coordination log 2026-05-30 entries): - kind=30078 (NIP-78 replaceable), ["p", atm_npub]-tagged, ["d", "bitspire-cassettes:<machine_id>"], NIP-44 v2 encrypted content, authored by operator. Subscribed via filter {kinds:[30078], "#p":[my_npub], "#d":[...], authors:OPERATOR_PUBKEYS} - machine_id = ATM hex pubkey (no extra provisioning step) Wire payload is denomination-keyed (per satmachineadmin's 06:40Z audit of the ATM stack — every layer beneath the wire keys on denomination, position is a sortable display column): { "denominations": { "<denom>": { "position": N, "count": M } } } Validation: - event signature + author in VITE_OPERATOR_PUBKEYS allowlist - replay protection via meta.lastKnownConfigCreatedAt (drops events re-delivered on relay reconnect or after restart) - clock-skew defense: reject created_at > now + 60s - denomination key set EXACTLY equal to state.db denominations (no add/remove cassettes from the dashboard) - per-row position positive int, count non-negative int Apply in a single SQLite transaction (cassettes upsert by denomination PK + meta watermark update), then hot-reload HAL via new IPC `hal:reload-cassettes` so dispense math picks up the new layout without restarting the bitspire service. Bootstrap hello-event (one-shot): - on init, if meta.bootstrapPublishedAt IS NULL AND cassettes non-empty, publish kind=30078 with d=bitspire-cassettes-state:<id>, encrypted to operator pubkey, signed by ATM - on success set meta.bootstrapPublishedAt; on failure leave null and retry next boot (best-effort; doesn't block service startup) Schema v7 → v8: adds meta rows lastKnownConfigCreatedAt + bootstrap- PublishedAt. Fresh installs at v8 seed via INSERT OR IGNORE. HAL service grows setCassettes(cassettes) — closes + re-inits the dispenser, rebuilds the inventory map + cassetteDenominations index. Exposed as `hal:reload-cassettes` IPC + window.electronAPI.halReload- Cassettes for the renderer. Out of scope (v2 / separate issue): - continuous ATM-state reverse-channel publish (dashboard reconciliation + ✅/⏳ apply confirmation + safe "Add N bills" UX) 12/12 typecheck + 18/18 state-machine + 11/11 clink + 11/11 lnbits suites pass. refs: aiolabs/lamassu-next#56, aiolabs/satmachineadmin#29, ~/dev/coordination/log.md 2026-05-30 entries (06:30Z, 06:40Z, 07:30Z, 07:50Z, 07:55Z), ~/dev/CLAUDE.md (Nostr architecture → "Respect protocol semantics over friction reduction") Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e4079cb787
commit
4612ff2155
8 changed files with 563 additions and 7 deletions
|
|
@ -95,6 +95,21 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||
remediateTransaction: (txid: string, remediatedByTxid: string): Promise<boolean> =>
|
||||
ipcRenderer.invoke('state:remediate-transaction', txid, remediatedByTxid),
|
||||
|
||||
// Operator-config consumer (aiolabs/lamassu-next#56)
|
||||
getLastKnownConfigCreatedAt: (): Promise<number> =>
|
||||
ipcRenderer.invoke('state:get-last-known-config-created-at'),
|
||||
getBootstrapPublishedAt: (): Promise<number | null> =>
|
||||
ipcRenderer.invoke('state:get-bootstrap-published-at'),
|
||||
markBootstrapPublished: (unixTimestamp: number): Promise<void> =>
|
||||
ipcRenderer.invoke('state:mark-bootstrap-published', unixTimestamp),
|
||||
applyOperatorCassettesConfig: (
|
||||
payload: {
|
||||
denominations: Record<string, { position: number; count: number }>
|
||||
},
|
||||
eventCreatedAt: number
|
||||
): Promise<{ applied: true } | { applied: false; reason: string }> =>
|
||||
ipcRenderer.invoke('state:apply-operator-cassettes-config', payload, eventCreatedAt),
|
||||
|
||||
// Support pages
|
||||
getSupportPages: (): Promise<{ id: string; title: string; content: string }[]> =>
|
||||
ipcRenderer.invoke('support:get-pages'),
|
||||
|
|
@ -108,6 +123,10 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||
halStackBill: (): Promise<void> => ipcRenderer.invoke('hal:stack-bill'),
|
||||
halRejectBill: (): Promise<void> => ipcRenderer.invoke('hal:reject-bill'),
|
||||
halGetInventory: (): Promise<Record<number, number>> => ipcRenderer.invoke('hal:get-inventory'),
|
||||
halReloadCassettes: (
|
||||
cassettes: { denomination: number; count?: number }[]
|
||||
): Promise<{ ok: boolean; error?: string }> =>
|
||||
ipcRenderer.invoke('hal:reload-cassettes', cassettes),
|
||||
halCleanup: (): Promise<void> => ipcRenderer.invoke('hal:cleanup'),
|
||||
|
||||
// HAL event listeners (main process → renderer)
|
||||
|
|
@ -141,7 +160,7 @@ declare global {
|
|||
getVersion: () => Promise<string>
|
||||
getConfig: () => Promise<RuntimeConfig>
|
||||
getAtmSecrets: () => Promise<AtmSecrets>
|
||||
loadCassettes: () => Promise<{ denomination: number; count: number }[]>
|
||||
loadCassettes: () => Promise<{ denomination: number; count: number; position: number }[]>
|
||||
setCassettes: (cassettes: { denomination: number; count: number }[]) => Promise<void>
|
||||
getInventory: () => Promise<Record<number, number>>
|
||||
getCashbox: () => Promise<{
|
||||
|
|
@ -172,6 +191,13 @@ declare global {
|
|||
}) => Promise<void>
|
||||
emptyCashbox: () => Promise<void>
|
||||
remediateTransaction: (txid: string, remediatedByTxid: string) => Promise<boolean>
|
||||
getLastKnownConfigCreatedAt: () => Promise<number>
|
||||
getBootstrapPublishedAt: () => Promise<number | null>
|
||||
markBootstrapPublished: (unixTimestamp: number) => Promise<void>
|
||||
applyOperatorCassettesConfig: (
|
||||
payload: { denominations: Record<string, { position: number; count: number }> },
|
||||
eventCreatedAt: number
|
||||
) => Promise<{ applied: true } | { applied: false; reason: string }>
|
||||
getSupportPages: () => Promise<{ id: string; title: string; content: string }[]>
|
||||
// HAL hardware IPC
|
||||
halInit: (config: any) => Promise<{ success: boolean; error?: string }>
|
||||
|
|
@ -181,6 +207,9 @@ declare global {
|
|||
halStackBill: () => Promise<void>
|
||||
halRejectBill: () => Promise<void>
|
||||
halGetInventory: () => Promise<Record<number, number>>
|
||||
halReloadCassettes: (
|
||||
cassettes: { denomination: number; count?: number }[]
|
||||
) => Promise<{ ok: boolean; error?: string }>
|
||||
halCleanup: () => Promise<void>
|
||||
onHalBillRead: (callback: (denomination: number) => void) => void
|
||||
onHalBillInserted: (callback: (denomination: number) => void) => void
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue