feat(extensions): add LNURL-withdraw extension #6

Merged
padreug merged 8 commits from feature/withdraw into dev 2026-04-02 19:05:20 +00:00
Owner

Summary

  • Implements LNURL-withdraw (LUD-03) for creating withdraw links via extensions
  • Routes extension RPC methods through Nostr middleware (NIP-44 encrypted)
  • Pays from the caller's balance (not app owner) via PayAppUserInvoice
  • Sends real-time balance updates via LiveUserOperation Nostr events
  • Fixes stale balance in payment notifications

Key Changes

Nostr RPC Routing

Extension methods (e.g. withdraw.createLink) are intercepted in nostrMiddleware and routed to the extension loader before falling through to standard nostrTransport. Extension system is initialized before nostrMiddleware so methods are available.

Creator Pubkey Tracking

Withdraw links created via Nostr RPC store the creator_pubkey so the LNURL callback debits the correct user's balance — not the app owner's (which may have 0 sats).

PayAppUserInvoice Integration

When userPubkey is provided, the adapter resolves the ApplicationUser and calls applicationManager.PayAppUserInvoice() instead of paymentManager.PayInvoice() directly. This ensures notifyAppUserPayment fires, sending LiveUserOperation events for real-time balance updates.

Balance Notification Fix

notifyAppUserPayment was sending stale balance_sats from the cached entity loaded before PayInvoice decremented it. Now updates the entity from the PayInvoice response before notification.

RPC Methods

Method Description
withdraw.createLink Create a withdraw link (stores creator pubkey)
withdraw.createVouchers Batch create single-use vouchers
withdraw.getLink Get link details and status
withdraw.listLinks List withdraw links
withdraw.updateLink Update link settings
withdraw.deleteLink Delete a link
withdraw.listWithdrawals List completed withdrawals
withdraw.getStats Get withdrawal statistics

HTTP Endpoints (LNURL Protocol)

GET /api/v1/lnurl/:unique_hash          # Initial LNURL request
GET /api/v1/lnurl/:unique_hash/:use_id  # Unique link request
GET /api/v1/lnurl/cb/:unique_hash       # Callback (pay invoice)

Companion Changes (lamassu-next)

ATM-side changes pushed to aiolabs/lamassu-next main:

  • Bill insertion balance limit: State machine billWithinBalance guard prevents inserting more cash than the ATM's available sats. UI disables denomination buttons and shows informational message when limit reached.
  • BitcoinIcon component: SVGO-optimized (62% smaller) reusable Vue component with configurable size. Grayscale+fade treatment for depleted state.
  • Relay connection tracking: nostrClient connect/disconnect events wired to UI status badge.
  • Real-time balance display: Hybrid Nostr subscription + polling for live balance updates via LiveUserOperation events.
  • Color theme system: 6 switchable palettes (Gruvbox, Catppuccin, Dracula, Nord, Tokyo Night, Cyberpunk) with light/dark variants. Tailwind v4 @theme inline + [data-theme] architecture, persisted to localStorage.
  • Branding: Pyramid logo with float animation and theme-aware glow, "Bitcoinmat" title, BitcoinIcon on Buy Bitcoin card.
  • Regtest miner fix: Reduced MINE_INTERVAL from 120s to 30s to keep LND synced_to_chain: true.
  • Repo cleanup: Removed unused express dependency, moved 9 dev scripts to packages/nostr-client/dev/.

Test Results

Tested end-to-end with ATM UI:

  • Create withdraw link via Nostr RPC
  • Wallet scans LNURL QR and claims withdrawal
  • Correct user's balance debited (not app owner)
  • Balance updates in real-time via LiveUserOperation events
  • Multiple withdrawals work correctly
  • Bill insertion blocked when balance limit reached
  • UI shows depleted Bitcoin icon and disables buttons at limit
  • Theme switching works across all 6 palettes (dark mode)
  • Regtest miner keeps LND synced at 30s interval
## Summary - Implements LNURL-withdraw (LUD-03) for creating withdraw links via extensions - Routes extension RPC methods through Nostr middleware (NIP-44 encrypted) - Pays from the **caller's balance** (not app owner) via `PayAppUserInvoice` - Sends real-time balance updates via `LiveUserOperation` Nostr events - Fixes stale balance in payment notifications ## Key Changes ### Nostr RPC Routing Extension methods (e.g. `withdraw.createLink`) are intercepted in `nostrMiddleware` and routed to the extension loader before falling through to standard `nostrTransport`. Extension system is initialized **before** nostrMiddleware so methods are available. ### Creator Pubkey Tracking Withdraw links created via Nostr RPC store the `creator_pubkey` so the LNURL callback debits the correct user's balance — not the app owner's (which may have 0 sats). ### PayAppUserInvoice Integration When `userPubkey` is provided, the adapter resolves the `ApplicationUser` and calls `applicationManager.PayAppUserInvoice()` instead of `paymentManager.PayInvoice()` directly. This ensures `notifyAppUserPayment` fires, sending `LiveUserOperation` events for real-time balance updates. ### Balance Notification Fix `notifyAppUserPayment` was sending stale `balance_sats` from the cached entity loaded before `PayInvoice` decremented it. Now updates the entity from the `PayInvoice` response before notification. ## RPC Methods | Method | Description | |--------|-------------| | `withdraw.createLink` | Create a withdraw link (stores creator pubkey) | | `withdraw.createVouchers` | Batch create single-use vouchers | | `withdraw.getLink` | Get link details and status | | `withdraw.listLinks` | List withdraw links | | `withdraw.updateLink` | Update link settings | | `withdraw.deleteLink` | Delete a link | | `withdraw.listWithdrawals` | List completed withdrawals | | `withdraw.getStats` | Get withdrawal statistics | ## HTTP Endpoints (LNURL Protocol) ``` GET /api/v1/lnurl/:unique_hash # Initial LNURL request GET /api/v1/lnurl/:unique_hash/:use_id # Unique link request GET /api/v1/lnurl/cb/:unique_hash # Callback (pay invoice) ``` ## Companion Changes (lamassu-next) ATM-side changes pushed to `aiolabs/lamassu-next` main: - **Bill insertion balance limit**: State machine `billWithinBalance` guard prevents inserting more cash than the ATM's available sats. UI disables denomination buttons and shows informational message when limit reached. - **BitcoinIcon component**: SVGO-optimized (62% smaller) reusable Vue component with configurable size. Grayscale+fade treatment for depleted state. - **Relay connection tracking**: `nostrClient` connect/disconnect events wired to UI status badge. - **Real-time balance display**: Hybrid Nostr subscription + polling for live balance updates via `LiveUserOperation` events. - **Color theme system**: 6 switchable palettes (Gruvbox, Catppuccin, Dracula, Nord, Tokyo Night, Cyberpunk) with light/dark variants. Tailwind v4 `@theme inline` + `[data-theme]` architecture, persisted to localStorage. - **Branding**: Pyramid logo with float animation and theme-aware glow, "Bitcoinmat" title, BitcoinIcon on Buy Bitcoin card. - **Regtest miner fix**: Reduced `MINE_INTERVAL` from 120s to 30s to keep LND `synced_to_chain: true`. - **Repo cleanup**: Removed unused `express` dependency, moved 9 dev scripts to `packages/nostr-client/dev/`. ## Test Results Tested end-to-end with ATM UI: - [x] Create withdraw link via Nostr RPC - [x] Wallet scans LNURL QR and claims withdrawal - [x] Correct user's balance debited (not app owner) - [x] Balance updates in real-time via LiveUserOperation events - [x] Multiple withdrawals work correctly - [x] Bill insertion blocked when balance limit reached - [x] UI shows depleted Bitcoin icon and disables buttons at limit - [x] Theme switching works across all 6 palettes (dark mode) - [x] Regtest miner keeps LND synced at 30s interval
padreug added 3 commits 2026-02-13 19:32:27 +00:00
feat(extensions): add LNURL-withdraw extension
Some checks are pending
Docker Compose Actions Workflow / test (push) Waiting to run
0eede24cad
Implements LUD-03 (LNURL-withdraw) for creating withdraw links
that allow anyone to pull funds from a Lightning wallet.

Features:
- Create withdraw links with min/max amounts
- Quick vouchers: batch creation of single-use codes
- Multi-use links with wait time between uses
- Unique QR codes per use (prevents sharing exploits)
- Webhook notifications on successful withdrawals
- Full LNURL protocol compliance for wallet compatibility

Use cases:
- Faucets
- Gift cards / prepaid cards
- Tips / donations
- User onboarding

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
feat(extensions): add NIP-05 identity extension
Some checks are pending
Docker Compose Actions Workflow / test (push) Waiting to run
e0bf3ba8ff
Implements Nostr NIP-05 for human-readable identity verification:
- Username claiming and management (username@domain)
- /.well-known/nostr.json endpoint per spec
- Optional relay hints in JSON response
- Admin controls for identity management

RPC methods:
- nip05.claim - Claim a username
- nip05.release - Release your username
- nip05.updateRelays - Update relay hints
- nip05.getMyIdentity - Get your identity
- nip05.lookup - Look up by username
- nip05.lookupByPubkey - Look up by pubkey
- nip05.listIdentities - List all (admin)
- nip05.deactivate/reactivate - Admin controls

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Revert "feat(extensions): add NIP-05 identity extension"
Some checks are pending
Docker Compose Actions Workflow / test (push) Waiting to run
2b14b06fd8
This reverts commit e0bf3ba8ff.
padreug force-pushed feature/withdraw from 2b14b06fd8 to 22e1a348f2 2026-02-13 19:35:25 +00:00 Compare
padreug changed target branch from feature/marketplace to feature/extension-loader 2026-02-13 19:35:32 +00:00
padreug force-pushed feature/withdraw from 22e1a348f2 to 401578b72b 2026-02-13 20:06:58 +00:00 Compare
padreug added 9 commits 2026-02-16 21:59:04 +00:00
The @noble/curves secp256k1.getSharedSecret expects Uint8Array arguments,
not hex strings. Use hex.decode() to convert the private and public keys.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add extension loader initialization to startup
- Create mainHandlerAdapter to bridge mainHandler with extension context
- Mount extension HTTP routes on separate port (main port + 1)
- Configure EXTENSION_SERVICE_URL for LNURL link generation

The withdraw extension provides LUD-03 LNURL-withdraw support for
creating withdraw links that allow users to pull funds.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Enable CORS on the extension HTTP server to allow cross-origin requests
from ATM apps and other web-based clients.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add POST /api/v1/withdraw/create endpoint to allow external apps (ATM,
web clients) to create LNURL-withdraw links via HTTP instead of RPC.

Changes:
- Add handleCreateWithdrawLink HTTP handler
- Fix route ordering: callback routes before wildcard :unique_hash
- Extract app_id from Authorization header (Bearer app_<id>)
- Use is_unique=false for simple single-use ATM links

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add .dockerignore for runtime state files (sqlite, logs, secrets)
- Bump Node.js base image from 18 to 20
- Add @types/better-sqlite3 dev dependency

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Initialize extension system before nostrMiddleware so registered
RPC methods are available. Extension methods (e.g. withdraw.createLink)
are intercepted and routed to the extension loader before falling
through to the standard nostrTransport.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Store the Nostr pubkey of the user who creates a withdraw link so the
LNURL callback debits the correct user's balance instead of the app
owner's. Pass userPubkey through from RPC handler to WithdrawManager.

- Add creator_pubkey column (migration v4)
- Store creatorPubkey on link creation
- Pass creator_pubkey to payInvoice on LNURL callback

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When userPubkey is provided, resolve the ApplicationUser and call
applicationManager.PayAppUserInvoice instead of paymentManager.PayInvoice
directly. This ensures notifyAppUserPayment fires, sending
LiveUserOperation events via Nostr for real-time balance updates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: use fresh balance in PayAppUserInvoice notification
Some checks failed
Docker Compose Actions Workflow / test (push) Has been cancelled
80d1117370
notifyAppUserPayment was sending the stale cached balance from the
entity loaded before PayInvoice decremented it. Update the entity's
balance_sats from the PayInvoice response so LiveUserOperation events
contain the correct post-payment balance.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
padreug force-pushed feature/withdraw from 80d1117370 to dcb7dca9b5 2026-02-28 12:54:16 +00:00 Compare
padreug added 1 commit 2026-02-28 15:56:47 +00:00
fix(lnd): allow self-payments for LNURL-withdraw
Some checks failed
Docker Compose Actions Workflow / test (push) Has been cancelled
a933ba2770
When the user's wallet (e.g. Zeus) is connected to the same LND node
that LP uses, LNURL-withdraw fails because LND rejects the payment
with "no self-payments allowed". This is safe because LP always
decrements the user's balance before paying and refunds on failure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
padreug force-pushed feature/withdraw from a933ba2770 to 0332a5a34f 2026-03-01 22:13:00 +00:00 Compare
padreug force-pushed feature/withdraw from 0332a5a34f to 46ea632a87 2026-03-04 17:25:24 +00:00 Compare
padreug force-pushed feature/withdraw from 46ea632a87 to 8c8f2d4d3d 2026-03-04 20:23:05 +00:00 Compare
padreug force-pushed feature/withdraw from 8c8f2d4d3d to c08bf4b849 2026-03-26 22:32:12 +00:00 Compare
padreug force-pushed feature/withdraw from c08bf4b849 to 4c69a4d390 2026-04-01 17:27:03 +00:00 Compare
padreug force-pushed feature/withdraw from 4c69a4d390 to 68c71599f8 2026-04-02 18:49:15 +00:00 Compare
padreug changed target branch from feature/extension-loader to dev 2026-04-02 19:04:54 +00:00
padreug merged commit 68c71599f8 into dev 2026-04-02 19:05:20 +00:00
Sign in to join this conversation.
No reviewers
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/lightning-pub#6
No description provided.