From 7defc104448f96a03cb598521ecf92f2348fa2a7 Mon Sep 17 00:00:00 2001 From: Patrick Mulligan Date: Mon, 26 Jan 2026 10:39:55 -0500 Subject: [PATCH] fix: use GetLiveUserOperations for incoming invoice payment detection Lightning.Pub's GetPaymentState RPC is for checking OUTGOING payments (invoices you've paid), not incoming payments (invoices you've created). This caused invoice payment detection to fail with "invoice not found" even when payments succeeded. Solution: Subscribe to GetLiveUserOperations events which Lightning.Pub sends in real-time when invoices are paid. These events have: - requestId: "GetLiveUserOperations" - operation.type: "INCOMING_INVOICE" - operation.identifier: the full invoice string Key changes: - packages/lightning/src/client.ts: Rewrote watchInvoice() to use subscription-based detection instead of GetPaymentState polling - apps/machine/src/services/lightning.ts: Updated to use full invoice string instead of payment hash - packages/lightning/TROUBLESHOOTING.md: Added Issue #9 documenting this gotcha (GetPaymentState vs GetLiveUserOperations) Also includes ATM-driven cash-out flow implementation with: - Invoice generation via NewInvoice RPC - Real-time payment detection - State machine updates for cash-out states Co-Authored-By: Claude Opus 4.5 --- .../apps/machine/src/services/lightning.ts | 62 +++- lamassu-next/apps/machine/src/stores/atm.ts | 34 +++ .../apps/machine/src/views/CashOutView.vue | 237 ++++++++++------ .../packages/lightning/TROUBLESHOOTING.md | 192 ++++++++++++- lamassu-next/packages/lightning/src/client.ts | 264 +++++++++++++++--- lamassu-next/packages/lightning/src/types.ts | 12 +- .../src/__tests__/machine.test.ts | 118 +++++--- .../packages/state-machine/src/machine.ts | 208 ++++++++------ .../packages/state-machine/src/types.ts | 21 ++ 9 files changed, 878 insertions(+), 270 deletions(-) diff --git a/lamassu-next/apps/machine/src/services/lightning.ts b/lamassu-next/apps/machine/src/services/lightning.ts index f4b68ef..f8a6dec 100644 --- a/lamassu-next/apps/machine/src/services/lightning.ts +++ b/lamassu-next/apps/machine/src/services/lightning.ts @@ -140,8 +140,9 @@ export async function initializeLightningServices(): Promise console.log('[CLINK] Invoice created:', invoice.paymentRequest.slice(0, 32) + '...') - // Watch for payment - lightningPub.watchInvoice(invoice.paymentHash, (paidInvoice) => { + // Watch for payment using the full invoice string (not payment hash) + // Lightning.Pub sends LiveUserOperation events when invoices are paid + lightningPub.watchInvoice(invoice.paymentRequest, (paidInvoice) => { console.log('[CLINK] Payment received! Preimage:', paidInvoice.preimage) if (paidInvoice.preimage && paymentReceivedCallback) { paymentReceivedCallback(paidInvoice.preimage) @@ -285,6 +286,7 @@ function createATMServices( console.log('[ATM Service] Generating invoice for', amountMsat, 'msats') const amountSats = Math.floor(amountMsat / 1000) + console.log('[ATM Service] Amount in sats:', amountSats) try { const invoice = await lightningPub.createInvoice({ @@ -292,6 +294,12 @@ function createATMServices( description: 'Lamassu ATM - Cash Out', }) + console.log('[ATM Service] createInvoice response:', invoice) + + if (!invoice || !invoice.paymentRequest) { + throw new Error('Invoice creation returned empty response') + } + console.log('[ATM Service] Generated invoice:', invoice.paymentRequest.slice(0, 32) + '...') return invoice.paymentRequest } catch (error) { @@ -438,6 +446,56 @@ function createATMServices( console.log('[ATM Service] Validated amount:', fiatCents, 'cents') return fiatCents }, + + /** + * Watch an invoice for payment (cash-out flow) + * + * Uses Lightning.Pub's GetLiveUserOperations subscription to detect payment. + * Returns a cleanup function to stop watching. + * + * Note: Lightning.Pub sends LiveUserOperation events when invoices are paid. + * We subscribe to kind 21000 events and filter for INCOMING_INVOICE operations. + */ + watchInvoice: (invoice: string, callback: (preimage: string) => void): (() => void) => { + console.log('[ATM Service] Watching invoice for payment:', invoice.slice(0, 32) + '...') + + // Validate it looks like a BOLT11 invoice + if (!invoice.toLowerCase().startsWith('ln')) { + console.error('[ATM Service] Invalid invoice format - expected BOLT11') + return () => {} + } + + // Lightning.Pub's watchInvoice subscribes to LiveUserOperation events + // and returns a cleanup function + const cleanup = lightningPub.watchInvoice(invoice, (paidInvoice) => { + console.log('[ATM Service] Invoice paid!') + // LiveUserOperation doesn't include preimage, so we use a placeholder + // In a real scenario, we'd need to get preimage from another source + callback(paidInvoice.preimage || 'payment-confirmed') + }) + + // Return the cleanup function from Lightning.Pub + return cleanup + }, + + /** + * Get available inventory (denominations and counts) + * + * In production, this would query the actual dispenser hardware. + * For Phase 1, returns mock inventory. + */ + getInventory: async (): Promise> => { + console.log('[ATM Service] Getting inventory') + + // Mock inventory for Phase 1: only $20 bills + // In production, this would interface with the Rust HAL + const inventory: Record = { + 20: 50, // 50 x $20 bills = $1000 capacity + } + + console.log('[ATM Service] Inventory:', inventory) + return inventory + }, } } diff --git a/lamassu-next/apps/machine/src/stores/atm.ts b/lamassu-next/apps/machine/src/stores/atm.ts index 9428c3f..7b2de95 100644 --- a/lamassu-next/apps/machine/src/stores/atm.ts +++ b/lamassu-next/apps/machine/src/stores/atm.ts @@ -71,6 +71,19 @@ const mockServices: ATMServices = { } return fiatCents }, + + watchInvoice: (paymentHash, _callback) => { + console.log('[Mock] Watching invoice for payment:', paymentHash.slice(0, 20) + '...') + // Note: Auto-pay disabled for testing real invoice monitoring + // Use the "Simulate Payment Now" button in the UI to manually trigger payment + return () => {} + }, + + getInventory: async () => { + console.log('[Mock] Getting inventory') + // Mock inventory: only $20 bills for Phase 1 + return { 20: 50 } + }, } export const useAtmStore = defineStore('atm', () => { @@ -343,6 +356,23 @@ export const useAtmStore = defineStore('atm', () => { send({ type: 'CASH_DISPENSED' }) } + // Cash-out selection methods + function addDenomination(denomination: number) { + send({ type: 'ADD_DENOMINATION', denomination }) + } + + function removeDenomination(denomination: number) { + send({ type: 'REMOVE_DENOMINATION', denomination }) + } + + function clearSelection() { + send({ type: 'CLEAR_SELECTION' }) + } + + function confirmAmount() { + send({ type: 'CONFIRM_AMOUNT' }) + } + function toggleDebug() { debugMode.value = !debugMode.value } @@ -381,6 +411,10 @@ export const useAtmStore = defineStore('atm', () => { requestDebit, skipReceipt, cashTaken, + addDenomination, + removeDenomination, + clearSelection, + confirmAmount, toggleDebug, } }) diff --git a/lamassu-next/apps/machine/src/views/CashOutView.vue b/lamassu-next/apps/machine/src/views/CashOutView.vue index 5d4eb70..a586be6 100644 --- a/lamassu-next/apps/machine/src/views/CashOutView.vue +++ b/lamassu-next/apps/machine/src/views/CashOutView.vue @@ -1,5 +1,5 @@