feat(machine): use Nostr events for LNURL-withdraw completion notification #24

Open
opened 2026-06-13 22:02:50 +00:00 by padreug · 1 comment
Owner

Migrated from aiolabs/lamassu-next#24 — opened by @padreug on 2026-02-14.\n\n## Current Behavior

The ATM currently polls the Lightning.Pub withdraw extension HTTP API every 2 seconds to detect when an LNURL-withdraw has been claimed:

// lightning.ts:272-300
const pollInterval = setInterval(async () => {
  const response = await fetch(`${CONFIG.extensionApiUrl}/api/v1/lnurl/${uniqueHash}`)
  // ... check if used > 0
}, 2000)

This works but is not "the Lightning.Pub way" - which uses Nostr as the communication layer.

Proposed Solution

Implement Nostr-native withdrawal completion notifications:

  1. Withdraw Extension Changes (Lightning.Pub):

    • When a withdrawal completes, publish a Nostr event to the ATM's pubkey
    • Use NIP-44 encryption for privacy
    • Include: unique_hash, amount_sats, payment_hash, completion timestamp
  2. ATM Changes:

    • When creating withdraw link, include ATM's Nostr pubkey in the request
    • Subscribe to encrypted events from Lightning.Pub
    • Handle withdrawal completion events to trigger state transition

Benefits

  • Real-time notification (no 2-second polling delay)
  • Consistent with ndebit flow architecture
  • Works across network boundaries
  • Reduces HTTP API surface

Implementation Notes

The withdraw extension already has access to ctx.publishNostrEvent() - see src/extensions/context.ts:186-188. The Marketplace extension uses this pattern successfully for product updates.

Suggested event structure:

{
  "kind": 4,  // Encrypted DM or custom kind
  "pubkey": "<lightning-pub-pubkey>",
  "tags": [
    ["p", "<atm-pubkey>"],
    ["e", "<related-event-id>"]
  ],
  "content": "<nip44-encrypted: {unique_hash, amount_sats, payment_hash, status: 'completed'}>"
}
  • Current polling implementation: apps/machine/src/services/lightning.ts:266-307
  • Extension context API: lightning-pub/withdraw/src/extensions/context.ts
> _Migrated from [aiolabs/lamassu-next#24](https://git.atitlan.io/aiolabs/lamassu-next/issues/24) — opened by @padreug on 2026-02-14._\n\n## Current Behavior The ATM currently polls the Lightning.Pub withdraw extension HTTP API every 2 seconds to detect when an LNURL-withdraw has been claimed: ```typescript // lightning.ts:272-300 const pollInterval = setInterval(async () => { const response = await fetch(`${CONFIG.extensionApiUrl}/api/v1/lnurl/${uniqueHash}`) // ... check if used > 0 }, 2000) ``` This works but is not "the Lightning.Pub way" - which uses Nostr as the communication layer. ## Proposed Solution Implement Nostr-native withdrawal completion notifications: 1. **Withdraw Extension Changes** (Lightning.Pub): - When a withdrawal completes, publish a Nostr event to the ATM's pubkey - Use NIP-44 encryption for privacy - Include: `unique_hash`, `amount_sats`, `payment_hash`, completion timestamp 2. **ATM Changes**: - When creating withdraw link, include ATM's Nostr pubkey in the request - Subscribe to encrypted events from Lightning.Pub - Handle withdrawal completion events to trigger state transition ## Benefits - Real-time notification (no 2-second polling delay) - Consistent with ndebit flow architecture - Works across network boundaries - Reduces HTTP API surface ## Implementation Notes The withdraw extension already has access to `ctx.publishNostrEvent()` - see `src/extensions/context.ts:186-188`. The Marketplace extension uses this pattern successfully for product updates. Suggested event structure: ```json { "kind": 4, // Encrypted DM or custom kind "pubkey": "<lightning-pub-pubkey>", "tags": [ ["p", "<atm-pubkey>"], ["e", "<related-event-id>"] ], "content": "<nip44-encrypted: {unique_hash, amount_sats, payment_hash, status: 'completed'}>" } ``` ## Related - Current polling implementation: `apps/machine/src/services/lightning.ts:266-307` - Extension context API: `lightning-pub/withdraw/src/extensions/context.ts`
Author
Owner

@padreug commented on 2026-05-13 (lamassu-next#24):

The exact pain point this issue describes (2-second HTTP polling against LP's withdraw extension for :unique_hash/:id_unique_hash claim status) is what the LNbits nostr-native-transport work has been building toward. Status as of LNbits commit 085fd501 + aiolabs/withdraw commit 82a6d4a:

End-to-end flow, no polling, no HTTP:

  1. ATM creates the link over nostr: lnurlw_create_link({title, min_withdrawable, max_withdrawable, uses, wait_time, is_unique}) → returns the link record including id, unique_hash, uses, is_unique.
  2. ATM subscribes for claim events: subscribe_payments({tag: "withdraw", link_id: <link.id>, max_seconds: 300}) → returns {subscription_id, expires_at}.
  3. Customer wallet redeems normally (the only HTTP touch — intrinsic to the LNURL spec, performed by the customer's wallet, not the ATM).
  4. LNbits pays the customer's bolt11 from the ATM's wallet and stamps Payment.extra = {tag: "withdraw", withdrawal_link_id: <id>}.
  5. The new outgoing-payment fanout (commit 085fd501) routes the settlement through the subscription module → ATM gets an encrypted kind-21000 push event with the full Payment object.

Verified end-to-end with ~/dev/lnbits/nostr-transport/misc-aio/test_lnurlw_flow.py: link create + subscribe ack + customer redemption simulation + push received with matching extra.withdrawal_link_id and status=success. Sub-second delivery from settlement.

Sub-link disambiguation for is_unique=True: the new lnurlw_unique_hashes({id}) RPC (aiolabs/withdraw commit 82a6d4a) returns the canonical list of unredeemed id_unique_hash values for the link — so when a settlement push arrives the ATM can match it back to a specific sub-link if needed (the push itself carries withdrawal_link_id but not the sub-link hash; if you need per-sub-link tracking, mint a separate link for each sub-use, or correlate via amount + timing).

This makes #25 (the unique-mode + id_unique_hash issue) cleanly implementable too.

Net: issue closeable once the ATM's client.ts adapter swaps the polling loop for a subscription. The LNbits surface is in place and stable.

> _@padreug commented on 2026-05-13 ([lamassu-next#24](https://git.atitlan.io/aiolabs/lamassu-next/issues/24#issuecomment-571)):_ ## LNbits side is ready — `subscribe_payments({tag:"withdraw", link_id})` The exact pain point this issue describes (2-second HTTP polling against LP's withdraw extension for `:unique_hash/:id_unique_hash` claim status) is what the LNbits `nostr-native-transport` work has been building toward. Status as of LNbits commit `085fd501` + `aiolabs/withdraw` commit `82a6d4a`: **End-to-end flow, no polling, no HTTP:** 1. ATM creates the link over nostr: `lnurlw_create_link({title, min_withdrawable, max_withdrawable, uses, wait_time, is_unique})` → returns the link record including `id`, `unique_hash`, `uses`, `is_unique`. 2. ATM subscribes for claim events: `subscribe_payments({tag: "withdraw", link_id: <link.id>, max_seconds: 300})` → returns `{subscription_id, expires_at}`. 3. Customer wallet redeems normally (the only HTTP touch — intrinsic to the LNURL spec, performed by the customer's wallet, not the ATM). 4. LNbits pays the customer's bolt11 from the ATM's wallet and stamps `Payment.extra = {tag: "withdraw", withdrawal_link_id: <id>}`. 5. The new outgoing-payment fanout (commit `085fd501`) routes the settlement through the subscription module → ATM gets an encrypted kind-21000 push event with the full `Payment` object. Verified end-to-end with `~/dev/lnbits/nostr-transport/misc-aio/test_lnurlw_flow.py`: link create + subscribe ack + customer redemption simulation + push received with matching `extra.withdrawal_link_id` and `status=success`. Sub-second delivery from settlement. **Sub-link disambiguation for `is_unique=True`:** the new `lnurlw_unique_hashes({id})` RPC (aiolabs/withdraw commit `82a6d4a`) returns the canonical list of unredeemed `id_unique_hash` values for the link — so when a settlement push arrives the ATM can match it back to a specific sub-link if needed (the push itself carries `withdrawal_link_id` but not the sub-link hash; if you need per-sub-link tracking, mint a separate link for each sub-use, or correlate via amount + timing). This makes #25 (the unique-mode + `id_unique_hash` issue) cleanly implementable too. **Net:** issue closeable once the ATM's `client.ts` adapter swaps the polling loop for a subscription. The LNbits surface is in place and stable.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/bitspire#24
No description provided.