From 454154e528f4f001bb50842014d132eb3dc7046d Mon Sep 17 00:00:00 2001 From: Patrick Mulligan Date: Thu, 2 Apr 2026 12:54:01 -0400 Subject: [PATCH] fix(state-machine): reject bills when available balance is unknown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, if getAvailableBalance returned 0 or exchange rate was missing, all bills were accepted — risking cash-in exceeding the ATM's sats balance. Now rejects bills in that case to protect customers from losing cash. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/state-machine/src/__tests__/machine.test.ts | 1 + packages/state-machine/src/machine.ts | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/state-machine/src/__tests__/machine.test.ts b/packages/state-machine/src/__tests__/machine.test.ts index ff624e3..0d59d12 100644 --- a/packages/state-machine/src/__tests__/machine.test.ts +++ b/packages/state-machine/src/__tests__/machine.test.ts @@ -15,6 +15,7 @@ describe('ATM State Machine', () => { dispensed: true, } satisfies DispenseCashResult), getExchangeRate: vi.fn().mockResolvedValue(2500), // 2500 sats per USD + getAvailableBalance: vi.fn().mockResolvedValue(1_000_000), // 1M sats available // noffer cash-out services (legacy) generateNoffer: vi.fn().mockResolvedValue('noffer1atmtest'), sendOfferResponse: vi.fn().mockResolvedValue(undefined), diff --git a/packages/state-machine/src/machine.ts b/packages/state-machine/src/machine.ts index 4d448b2..3f7e438 100644 --- a/packages/state-machine/src/machine.ts +++ b/packages/state-machine/src/machine.ts @@ -368,7 +368,9 @@ export function createATMMachine( hasOfferRequest: ({ context }) => context.pendingOfferRequest !== null, billWithinBalance: ({ context, event }) => { if (event.type !== 'BILL_INSERTED') return false - if (context.availableBalance <= 0 || context.exchangeRate === 0) return true + // Reject bills if balance or rate is unknown — don't risk accepting + // cash we can't cover with sats + if (context.availableBalance <= 0 || context.exchangeRate === 0) return false const newFiatCents = context.fiatCents + event.denomination * 100 const grossSats = Math.floor((newFiatCents / 100) * context.exchangeRate) const fee = Math.floor(grossSats * context.feePercent)