bug(cash-out): unwatched-payment windows — no invoice expiry, silent watchInvoice setup failure #78
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
From the 2026-07 dev-branch money-path review (sibling of #58/PR #77). Two related gaps let a customer pay a cash-out invoice that no one is watching — funds land in the operator wallet with no dispense and no machine-side transaction record. Only spirekeeper-side reconciliation would ever notice (the
extra.txidexists but matches no machine row).Gap 1 — no expiry on the cash-out invoice
generateInvoice(apps/machine/src/services/lightning.ts) callslnbits.createInvoicewithoutexpiry, even thoughCreateInvoiceRequestsupports it (packages/lnbits/src/types.ts). The machine abandonsdisplayingInvoiceatINVOICE_TIMEOUT(5 min), but the invoice stays payable for the backend default (~1h). A slow wallet or routing retry paying at minute 7 is money received, no cash dispensed, no record.Fix: set
expiry≈INVOICE_TIMEOUT(plus small grace) so the invoice dies when the machine gives up. Single source for the timeout value so they can't drift.Gap 2 — watcher setup failure is silent
watchInvoice(lightning.ts): ifdecodePaymentorsubscribePaymentsthrows (relay hiccup at exactly QR time), itconsole.errors and returns — noPAYMENT_FAILEDis sent to the machine. The QR stays displayed, fully payable, with no active settlement subscription, until the 5-min timeout bounces toselectingAmount.Fix: on watcher-setup failure,
sendBack({type: 'PAYMENT_FAILED', ...})so the machine leavesdisplayingInvoiceimmediately instead of showing an unwatched QR. (ThefromCallbackactor already routesPAYMENT_FAILED→selectingAmountwith error display.)Related edge (lower priority)
subscribePaymentsusesmax_seconds: 600; combined with Gap 1's expiry fix the sub always outlives the payable window, closing the pay-after-timeout race entirely.