fix(machine): persist transactions stuck in waitingForCashTaken

The IPC HAL path (production) never set halServices.value, so the
auto-advance from waitingForCashTaken → complete never fired. The
machine hung on "Cash Ready!" indefinitely — no transaction persisted.

Three fixes:
- Auto-advance now checks `isElectron` (covers IPC path)
- waitingForCashTaken has a 30s after-timeout as safety net
- Fix unscoped lightningPub refs in LNURL session helpers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-03-07 21:26:58 -05:00
commit d64f6cd9ab
3 changed files with 15 additions and 5 deletions

View file

@ -246,7 +246,8 @@ function registerLnurlSession(
sessionId: string, sessionId: string,
linkId: string, linkId: string,
uniqueHash: string, uniqueHash: string,
satsAmount: number satsAmount: number,
lightningPub: LightningPubClient
): void { ): void {
console.log('[LNURL Session] Registering:', uniqueHash, 'for', satsAmount, 'sats') console.log('[LNURL Session] Registering:', uniqueHash, 'for', satsAmount, 'sats')
@ -331,7 +332,10 @@ function startLnurlCompletionPolling(
* Invalidate an active LNURL session by cash-in sessionId. * Invalidate an active LNURL session by cash-in sessionId.
* Stops polling and deletes the link on the server. * Stops polling and deletes the link on the server.
*/ */
function invalidateLnurlSessionBySessionId(sessionId: string): void { function invalidateLnurlSessionBySessionId(
sessionId: string,
lightningPub: LightningPubClient
): void {
for (const [hash, session] of lnurlSessions.entries()) { for (const [hash, session] of lnurlSessions.entries()) {
if (session.sessionId === sessionId && session.status === 'active') { if (session.sessionId === sessionId && session.status === 'active') {
console.log('[LNURL Session] Invalidating previous session:', hash) console.log('[LNURL Session] Invalidating previous session:', hash)
@ -1094,7 +1098,7 @@ function createATMServices(
// Invalidate any previous LNURL session for this cash-in session // Invalidate any previous LNURL session for this cash-in session
// (shouldn't happen — LNURL is generated once — but guard against it) // (shouldn't happen — LNURL is generated once — but guard against it)
if (context.cashInSessionId) { if (context.cashInSessionId) {
invalidateLnurlSessionBySessionId(context.cashInSessionId) invalidateLnurlSessionBySessionId(context.cashInSessionId, lightningPub)
} }
// Call the withdraw extension via Nostr RPC (encrypted) // Call the withdraw extension via Nostr RPC (encrypted)
@ -1124,7 +1128,8 @@ function createATMServices(
context.cashInSessionId, context.cashInSessionId,
response.link.id || '', response.link.id || '',
response.link.unique_hash, response.link.unique_hash,
context.satsAmount context.satsAmount,
lightningPub
) )
// Start polling for completion (still uses HTTP - no RPC alternative yet) // Start polling for completion (still uses HTTP - no RPC alternative yet)

View file

@ -251,10 +251,11 @@ export const useAtmStore = defineStore('atm', () => {
// Auto-advance past waitingForCashTaken when HAL is connected, // Auto-advance past waitingForCashTaken when HAL is connected,
// because HAL's dispenseCash already calls waitForBillsRemoved() // because HAL's dispenseCash already calls waitForBillsRemoved()
// before resolving (both direct HAL and IPC paths).
if ( if (
currentNested === 'waitingForCashTaken' && currentNested === 'waitingForCashTaken' &&
prevNestedState !== 'waitingForCashTaken' && prevNestedState !== 'waitingForCashTaken' &&
halServices.value (halServices.value || isElectron)
) { ) {
console.log('[ATM] HAL active — bills already removed, auto-advancing') console.log('[ATM] HAL active — bills already removed, auto-advancing')
send({ type: 'CASH_DISPENSED' }) send({ type: 'CASH_DISPENSED' })

View file

@ -703,6 +703,10 @@ export function createATMMachine(
}, },
}, },
waitingForCashTaken: { waitingForCashTaken: {
// Auto-advance after 30s — bills are already out, assume taken.
after: {
30000: 'complete',
},
on: { on: {
CASH_DISPENSED: 'complete', CASH_DISPENSED: 'complete',
TIMEOUT: 'complete', // Assume taken TIMEOUT: 'complete', // Assume taken