From cb8ad3d813a50f0f5a17e4a6ba30737fc0423dd5 Mon Sep 17 00:00:00 2001 From: Padreug Date: Sun, 31 May 2026 21:05:00 +0200 Subject: [PATCH] fix(machine): subscribe to single-invoice settlement by payment_hash only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Under path B (NOSTR_TRANSPORT_ROSTER_REQUIRED=true), lnbits's roster-lookup override routes create_invoice to the operator's wallet, but the subsequent subscribe_payments was scoping its filter to the ATM's pre-override wallet_id. The dispatcher AND-filters payment_hash + wallet_id, so the settlement on the operator wallet was invisible to the subscription — bitspire stayed in "Watching invoice" forever, dispense never fired. Omit wallet_id on the single-invoice watcher: lnbits already resolves the wallet from get_standalone_payment(payment_hash) and ownership-checks against the auth'd account. Works pre/post- override; payment_hash is the natural primary key for "wait for THIS invoice" anyway. Cash-out subscription site at services/lightning.ts:1008-1010 (production caller) + watchInvoice convenience helper at packages/lnbits/src/client.ts:286-313 both flipped. LNURL-withdraw subscription at services/lightning.ts:720 (filter: tag+link_id) is the symmetric case but pending lnbits confirmation that the tag+link_id branch of _resolve_owner_wallet_id exists alongside the payment_hash branch. Coordination: ~/dev/coordination/log.md 2026-05-31T18:35Z (joint smoke surfaced the bug), 18:40Z (bitspire diagnosis), 18:50Z (lnbits narrowed the fix shape + confirmed path-2 works against deployed lnbits today). Co-Authored-By: Claude Opus 4.7 --- apps/machine/src/services/lightning.ts | 15 +++++++++++++-- packages/lnbits/src/client.ts | 15 ++++++++++++--- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/apps/machine/src/services/lightning.ts b/apps/machine/src/services/lightning.ts index 92c6af5..82369f3 100644 --- a/apps/machine/src/services/lightning.ts +++ b/apps/machine/src/services/lightning.ts @@ -1005,8 +1005,18 @@ function createATMServices( return } if (cancelled) return + // walletId omitted: payment_hash is the natural primary key for + // "wait for THIS invoice to settle." Under path B + // (NOSTR_TRANSPORT_ROSTER_REQUIRED=true) lnbits routes the payment + // to the operator's wallet, so a subscription scoped to the ATM's + // pre-override wallet_id would AND-filter the settlement out and + // never fire. With wallet_id omitted, lnbits resolves the wallet + // from get_standalone_payment(payment_hash) and ownership-checks + // against the auth'd account — works on both pre/post-override + // wallets. Coordination log 2026-05-31T18:50Z (lnbits) for the + // confirmation, §18:35Z for the joint smoke that surfaced the bug. subId = await lnbits.subscribePayments( - lnbitsWalletId, + undefined, { payment_hash: paymentHash, max_seconds: 600 }, (push) => { if (push.payment_hash !== paymentHash) return @@ -1022,7 +1032,8 @@ function createATMServices( return () => { cancelled = true if (subId) { - void lnbits.unsubscribe(lnbitsWalletId, subId).catch(() => {}) + // wallet_id omitted to match the subscribePayments call above. + void lnbits.unsubscribe(undefined, subId).catch(() => {}) } } }, diff --git a/packages/lnbits/src/client.ts b/packages/lnbits/src/client.ts index 0a18c44..b30f469 100644 --- a/packages/lnbits/src/client.ts +++ b/packages/lnbits/src/client.ts @@ -284,21 +284,30 @@ export class LnbitsClient { * shape so services/lightning.ts can swap mechanically. */ async watchInvoice( - walletId: string, + _walletId: string | undefined, paymentHash: string, timeoutMs = 5 * 60_000, ): Promise { + // wallet_id intentionally NOT propagated to subscribe_payments — + // payment_hash is the natural primary key for "wait for THIS invoice." + // Under path B (NOSTR_TRANSPORT_ROSTER_REQUIRED=true) lnbits routes + // settlements to the operator's wallet, so a subscription pinned to + // the caller's pre-override wallet_id would AND-filter the settlement + // out and never fire. With wallet_id omitted, lnbits resolves the + // wallet from get_standalone_payment(payment_hash) and ownership- + // checks against the auth'd account — works pre/post-override. + // Coordination log 2026-05-31T18:50Z (lnbits) for confirmation. return new Promise((resolve, reject) => { let resolved = false const subId = this.subscribePayments( - walletId, + undefined, { payment_hash: paymentHash, max_seconds: Math.ceil(timeoutMs / 1000) }, (payment) => { if (payment.payment_hash !== paymentHash) return if (payment.status !== 'success') return if (resolved) return resolved = true - void this.unsubscribe(walletId, subIdRef).catch(() => {}) + void this.unsubscribe(undefined, subIdRef).catch(() => {}) resolve(payment) }, (reason) => {