feat(cassettes): consume operator operations instead of counts
The machine now owns its bay counts outright. The operator publishes what it did — a refill in notes added, an empty, a recount, a denomination change — and this process applies it to the total it already holds. Both sides used to write the same value over a transport that never tells a writer it lost. Addressable events order by created_at at second granularity with ties broken on event id, and a relay returns OK for an event it then discards, so a dashboard form loaded before a dispense silently discarded that dispense and neither side could detect it. A value with one writer cannot be clobbered. Schema v13 adds cassette_ops, the dedup ledger. A delta applied twice is wrong and addressable events are re-delivered on every reconnect, so the operator mints an id per operation and this table records the ones applied. That also retires the created_at watermark on this path: it was the only replay defence under absolute counts, but it drops an out-of-order event whole, operations included, where per-op ids let the unseen ones through and no-op the rest. A window is applied oldest-first by `at`, ties broken by id, in one transaction with the count mutation. A recount then a refill is not the same as the reverse, and a crash mid-apply must roll back to a coherent count rather than a partial one. A malformed op or one naming a bay this machine does not have is neither applied nor recorded, so it stays pending on the operator's dashboard. That is the honest outcome. Recording it as applied would stop the noise by telling the operator their refill landed. The state document gains applied_ops, seq and schema_version. applied_ops is the acknowledgement leg — echoing the ids back is the only way the operator can tell an operation that landed from one merely sent. seq is bumped on every local count change from any cause, so a reader can reject a regression without trusting either clock.
This commit is contained in:
parent
9077f9c299
commit
c889f7f0df
6 changed files with 537 additions and 165 deletions
|
|
@ -163,13 +163,23 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||
}): Promise<{ ok: boolean; bolt11?: string; reason?: string }> =>
|
||||
ipcRenderer.invoke('lnurl:pay-session', args),
|
||||
|
||||
applyOperatorCassettesConfig: (
|
||||
payload: {
|
||||
positions: Record<string, { denomination: number; count: number }>
|
||||
},
|
||||
eventCreatedAt: number
|
||||
): Promise<{ applied: true } | { applied: false; reason: string }> =>
|
||||
ipcRenderer.invoke('state:apply-operator-cassettes-config', payload, eventCreatedAt),
|
||||
applyOperatorCassetteOps: (
|
||||
ops: {
|
||||
id: string
|
||||
at: number
|
||||
type: 'refill' | 'empty' | 'recount' | 'set_denomination'
|
||||
position: number
|
||||
bills?: number
|
||||
count?: number
|
||||
denomination?: number
|
||||
}[]
|
||||
): Promise<{
|
||||
applied: string[]
|
||||
rejected: { id: string; reason: string }[]
|
||||
}> => ipcRenderer.invoke('state:apply-operator-cassette-ops', ops),
|
||||
getAppliedOpIds: (limit?: number): Promise<string[]> =>
|
||||
ipcRenderer.invoke('state:get-applied-op-ids', limit),
|
||||
getCassetteStateSeq: (): Promise<number> => ipcRenderer.invoke('state:get-cassette-state-seq'),
|
||||
|
||||
// Operator-fees consumer (aiolabs/lamassu-next#57)
|
||||
getFeeConfig: (): Promise<{
|
||||
|
|
@ -314,10 +324,22 @@ declare global {
|
|||
lnurlw: string
|
||||
amountMsat: number
|
||||
}) => Promise<{ ok: boolean; bolt11?: string; reason?: string }>
|
||||
applyOperatorCassettesConfig: (
|
||||
payload: { positions: Record<string, { denomination: number; count: number }> },
|
||||
eventCreatedAt: number
|
||||
) => Promise<{ applied: true } | { applied: false; reason: string }>
|
||||
applyOperatorCassetteOps: (
|
||||
ops: {
|
||||
id: string
|
||||
at: number
|
||||
type: 'refill' | 'empty' | 'recount' | 'set_denomination'
|
||||
position: number
|
||||
bills?: number
|
||||
count?: number
|
||||
denomination?: number
|
||||
}[]
|
||||
) => Promise<{
|
||||
applied: string[]
|
||||
rejected: { id: string; reason: string }[]
|
||||
}>
|
||||
getAppliedOpIds: (limit?: number) => Promise<string[]>
|
||||
getCassetteStateSeq: () => Promise<number>
|
||||
getFeeConfig: () => Promise<{
|
||||
cashInFeeFraction: number
|
||||
cashOutFeeFraction: number
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue