fix(machine): subscribe to single-invoice settlement by payment_hash only

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 <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-31 21:05:00 +02:00
commit cb8ad3d813
2 changed files with 25 additions and 5 deletions

View file

@ -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<LnbitsPayment> {
// 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) => {