Compare commits

..

14 commits

Author SHA1 Message Date
Patrick Mulligan
68c71599f8 fix(lnd): allow self-payments for LNURL-withdraw
Some checks failed
Docker Compose Actions Workflow / test (push) Has been cancelled
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>
2026-04-02 14:48:55 -04:00
Patrick Mulligan
5aaa3bcc23 feat(extensions): pay from caller's balance via PayAppUserInvoice
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>
2026-04-02 14:48:55 -04:00
Patrick Mulligan
cb9fb78eb8 feat(withdraw): track creator pubkey on withdraw links
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>
2026-04-02 14:48:55 -04:00
Patrick Mulligan
1273da9020 feat: route Nostr RPC to extension methods
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>
2026-04-02 14:48:55 -04:00
Patrick Mulligan
3ee8b6b010 feat(withdraw): add HTTP API for creating withdraw links
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>
2026-04-02 14:48:55 -04:00
Patrick Mulligan
f06d50f227 feat(server): add CORS support for extension HTTP routes
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>
2026-04-02 14:48:55 -04:00
Patrick Mulligan
e998762ca7 feat: integrate extension system with withdraw extension support
- 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>
2026-04-02 14:48:55 -04:00
Patrick Mulligan
8de5e4fd3a feat(extensions): add LNURL-withdraw extension
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>
2026-04-02 14:48:55 -04:00
77e5772afd feat(extensions): add extension loader infrastructure (#3)
Some checks are pending
Docker Compose Actions Workflow / test (push) Waiting to run
## Summary

- Adds a modular extension system for Lightning.Pub enabling third-party plugins
- Provides isolated SQLite databases per extension for data safety
- Implements ExtensionContext API for accessing Lightning.Pub services (payments, Nostr, storage)
- Supports RPC method registration with automatic namespacing
- Includes HTTP route handling for protocols like LNURL
- Event routing for payment receipts and Nostr events
- Comprehensive documentation with architecture overview and working examples

## Key Components

- `src/extensions/types.ts` - Core extension interfaces
- `src/extensions/loader.ts` - Extension discovery, loading, and lifecycle management
- `src/extensions/context.ts` - Bridge between extensions and Lightning.Pub services
- `src/extensions/database.ts` - SQLite isolation with WAL mode
- `src/extensions/README.md` - Full documentation with examples

## ExtensionContext API

| Method | Description |
|--------|-------------|
| `getApplication()` | Get application info |
| `createInvoice()` | Create Lightning invoice |
| `payInvoice()` | Pay Lightning invoice |
| `getLnurlPayInfo()` | Get LNURL-pay info for a user (enables Lightning Address/zaps) |
| `sendEncryptedDM()` | Send Nostr DM (NIP-44) |
| `publishNostrEvent()` | Publish Nostr event |
| `registerMethod()` | Register RPC method |
| `onPaymentReceived()` | Subscribe to payment callbacks |
| `onNostrEvent()` | Subscribe to Nostr events |

## Test plan

- [x] Review extension loader code for correctness
- [x] Verify TypeScript compilation succeeds
- [x] Test extension discovery from `src/extensions/` directory
- [x] Test RPC method registration and routing
- [x] Test database isolation between extensions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: boufni95 <boufni95@gmail.com>
Co-authored-by: Patrick Mulligan <patjmulligan@protonmail.com>
Reviewed-on: #3
2026-04-02 18:47:55 +00:00
Patrick Mulligan
72c9872b23 fix(watchdog): handle LND restarts without locking outgoing operations
Some checks failed
Docker Compose Actions Workflow / test (push) Has been cancelled
When the payment index advances (e.g. after an LND restart or external
payment), update the cached offset instead of immediately locking.
Only lock if both a history mismatch AND a balance discrepancy are
detected — indicating a real security concern rather than a benign
LND restart.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 15:19:20 -05:00
Patrick Mulligan
5e5e30c7a2 fix(lnd): wait for chain/graph sync before marking LND ready
Warmup() previously only checked that LND responded to GetInfo(), but
did not verify syncedToChain/syncedToGraph. This caused LP to accept
requests while LND was still syncing, leading to "not synced" errors
on every Health() check. Now waits for full sync with a 10min timeout.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 15:18:06 -05:00
Patrick Mulligan
611eb4fc04 fix(nostr): close SimplePool after publishing to prevent connection leak
Each sendEvent() call created a new SimplePool() but never closed it,
causing relay WebSocket connections to accumulate indefinitely (~20/min).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 15:16:28 -05:00
Patrick Mulligan
6512e10f08 fix(handlers): await NostrSend calls throughout codebase
Update all NostrSend call sites to properly handle the async nature
of the function now that it returns Promise<void>.

Changes:
- handler.ts: Add async to sendResponse, await nostrSend calls
- debitManager.ts: Add logging for Kind 21002 response sending
- nostrMiddleware.ts: Update nostrSend signature
- tlvFilesStorageProcessor.ts: Update nostrSend signature
- webRTC/index.ts: Add async/await for nostrSend calls

This ensures Kind 21002 (ndebit) responses are properly sent to
wallet clients, fixing the "Debit request failed" issue in ShockWallet.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-04 15:16:28 -05:00
Patrick Mulligan
e9b5dacb3b fix(nostr): update NostrSend type to Promise<void> with error handling
The NostrSend type was incorrectly typed as returning void when it actually
returns Promise<void>. This caused async errors to be silently swallowed.

Changes:
- Update NostrSend type signature to return Promise<void>
- Make NostrSender._nostrSend default to async function
- Add .catch() error handling in NostrSender.Send() to log failures
- Add logging to track event publishing status

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-04 15:16:28 -05:00
2 changed files with 17 additions and 8 deletions

View file

@ -142,15 +142,20 @@ export default class {
return new Promise<void>((res, rej) => { return new Promise<void>((res, rej) => {
const interval = setInterval(async () => { const interval = setInterval(async () => {
try { try {
await this.GetInfo() const info = await this.GetInfo()
if (!info.syncedToChain || !info.syncedToGraph) {
this.log("LND responding but not synced yet, waiting...")
return
}
clearInterval(interval) clearInterval(interval)
this.ready = true this.ready = true
res() res()
} catch (err) { } catch (err) {
this.log(INFO, "LND is not ready yet, will try again in 1 second") this.log(INFO, "LND is not ready yet, will try again in 1 second")
if (Date.now() - now > 1000 * 60) { }
rej(new Error("LND not ready after 1 minute")) if (Date.now() - now > 1000 * 60 * 10) {
} clearInterval(interval)
rej(new Error("LND not synced after 10 minutes"))
} }
}, 1000) }, 1000)
}) })

View file

@ -238,13 +238,17 @@ export class Watchdog {
const knownMaxIndex = Math.max(maxFromDb, this.latestPaymentIndexOffset) const knownMaxIndex = Math.max(maxFromDb, this.latestPaymentIndexOffset)
const newLatest = await this.lnd.GetLatestPaymentIndex(knownMaxIndex) const newLatest = await this.lnd.GetLatestPaymentIndex(knownMaxIndex)
const historyMismatch = newLatest > knownMaxIndex const historyMismatch = newLatest > knownMaxIndex
const deny = await this.checkBalanceUpdate(deltaLnd, deltaUsers)
if (historyMismatch) { if (historyMismatch) {
getLogger({ component: 'bark' })("History mismatch detected in absolute update, locking outgoing operations") this.log("Payment index advanced from", knownMaxIndex, "to", newLatest, "- updating offset (likely LND restart or external payment)")
this.lnd.LockOutgoingOperations() this.latestPaymentIndexOffset = newLatest
return
} }
const deny = await this.checkBalanceUpdate(deltaLnd, deltaUsers)
if (deny) { if (deny) {
if (historyMismatch) {
getLogger({ component: 'bark' })("Balance mismatch with unexpected payment history, locking outgoing operations")
this.lnd.LockOutgoingOperations()
return
}
this.log("Balance mismatch detected in absolute update, but history is ok") this.log("Balance mismatch detected in absolute update, but history is ok")
} }
this.lnd.UnlockOutgoingOperations() this.lnd.UnlockOutgoingOperations()